# Конфигурация сборки

Конфигурация webpack для проекта с SSR аналогична конфигурации проекта только для клиентской стороны. Если ещё не знакомы с настройкой webpack, более подробную информацию можно найти в документации Vue CLI (opens new window) или настроить Vue Loader вручную (opens new window).

# Ключевые отличия от сборки только для клиента

  1. Необходимо создать манифест webpack (opens new window) для серверной части кода. Это JSON-файл, который webpack держит для отслеживания того, как все исходные модули сопоставляются с получившимися файлами сборки.

  2. Требуется экстернализация зависимостей приложения (opens new window). Это делает сборку сервера намного быстрее и создаёт меньший по размеру файл. При этом необходимо исключить зависимости, которые должны обрабатываться webpack (например, .css или .vue).

  3. Нужно переключить target (opens new window) в webpack на Node.js. Это позволит webpack обрабатывать динамические импорты в соответствии с подходом в Node, а также укажет vue-loader выдавать серверно-ориентированный код при компиляции компонентов Vue.

  4. При создании серверной точки входа, необходимо определить переменную окружения, указывающую что работаем с SSR. Удобно добавить несколько записей в scripts в package.json проекта для этого:

"scripts": {
  "build:client": "vue-cli-service build --dest dist/client",
  "build:server": "SSR=1 vue-cli-service build --dest dist/server",
  "build": "npm run build:client && npm run build:server",
}
1
2
3
4
5

# Пример конфигурации

Ниже приведён пример vue.config.js, который добавляет SSR в проект Vue CLI, но его можно адаптировать для любой сборки webpack.

const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
const nodeExternals = require('webpack-node-externals')
const webpack = require('webpack')

module.exports = {
  chainWebpack: webpackConfig => {
    // Необходимо отключать cache-loader, иначе в сборке для клиента
    // будут использоваться кэшированные компоненты из сборки для сервера
    webpackConfig.module.rule('vue').uses.delete('cache-loader')
    webpackConfig.module.rule('js').uses.delete('cache-loader')
    webpackConfig.module.rule('ts').uses.delete('cache-loader')
    webpackConfig.module.rule('tsx').uses.delete('cache-loader')

    if (!process.env.SSR) {
      // Определяем точку входа клиентской части приложения
      webpackConfig
        .entry('app')
        .clear()
        .add('./src/entry-client.js')
      return
    }

    // Определяем точку входа серверной части приложения
    webpackConfig
      .entry('app')
      .clear()
      .add('./src/entry-server.js')

    // Это позволяет webpack обрабатывать динамические импорты в соответствии
    // с подходом в Node, а также указывает `vue-loader` выдавать
    // серверно-ориентированный код при компиляции компонентов Vue.
    webpackConfig.target('node')
    // Это указывает сборке для сервера использовать экспорты в стиле Node
    webpackConfig.output.libraryTarget('commonjs2')

    webpackConfig
      .plugin('manifest')
      .use(new WebpackManifestPlugin({ fileName: 'ssr-manifest.json' }))

    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // Экстернализация зависимостей приложения. Это сделает сборку для сервера
    // гораздо быстрее и создаст более лёгкий файл итоговой сборки.

    // Не нужно экстернализировать зависимости, которые должны обрабатываться webpack.
    // Также следует внести в белый список зависимости, которые изменяют `global` (например, полифилы)
    webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }))

    webpackConfig.optimization.splitChunks(false).minimize(false)

    webpackConfig.plugins.delete('preload')
    webpackConfig.plugins.delete('prefetch')
    webpackConfig.plugins.delete('progress')
    webpackConfig.plugins.delete('friendly-errors')

    webpackConfig.plugin('limit').use(
      new webpack.optimize.LimitChunkCountPlugin({
        maxChunks: 1
      })
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# Предостережения для externals

Обратите внимание, что в опции externals в белый список добавляются CSS-файлы. Это связано с тем, что CSS импортируемый из зависимостей, всё равно должен обрабатываться webpack. Если импортируете файлы других типов, которые также полагаются на webpack (например, *.vue, *.sass), их тоже следует добавить в белый список.

При использовании runInNewContext: 'once' или runInNewContext: true необходимо добавить в белый список полифилы, изменяющие global (например, babel-polyfill). Это требуется для того, что при использовании режима нового контекста код внутри сборки для сервера будет иметь свой собственный объект global. Поскольку на сервере он не нужен, проще просто импортировать его в файл клиентской точки входа.

# Генерация clientManifest

В дополнение к серверной сборке также можно сгенерировать манифест клиентской сборки. Благодаря клиентскому манифесту и серверной сборке, рендерер будет иметь информацию как о серверной так и о клиентской сборках. Благодаря этому он сможет автоматически определять и внедрять директивы для preload / prefetch (opens new window), теги <link> и <script> в создаваемый HTML.

Выгода от этого двойная:

  1. Он заменяет html-webpack-plugin для внедрения корректных URL ресурсов, когда в именах генерируемых файлов присутствуют хэши.

  2. При генерации сборки, которая использует возможности webpack по разделению кода, можно обеспечить preload / prefetch необходимых фрагментов, а также интеллектуально внедрять теги <script> для требуемых асинхронных фрагментов, чтобы избежать водопада запросов на клиенте, тем самым улучшая TTI (time-to-interactive).