# 构建配置

一个服务端渲染项目的 webpack 配置和一个只有客户端的项目类似。如果你不熟悉 webpack 配置,你可以在 Vue CLI(opens new window) 手动配置 Vue Loader(opens new window) 的文档中找到更多信息。

# 和客户端构建的关键不同

  1. 我们需要为服务端代码创建一个 webpack manifest(opens new window) 。这是一个让 webpack 追踪所有的模块如何对应到生成的包中的 JSON 文件。

  2. 我们应该将应用依赖变为外部扩展(opens new window) 。这使得服务端构建更加快速并生成更小的包文件。做这件事的时候,我们需要把交给 webpack 处理的依赖 (如 .css.vue 文件) 排除在外。

  3. 我们需要将 webpack 的目标(opens new window) 改为 Node.js。这会允许 webpack 以适合于 Node 的方式处理动态导入,同时也告诉 vue-loader 在编译 Vue 组件的时候抛出面向服务端的代码。

  4. 当构建一个服务端入口时,我们需要定义一个环境变量来指明当前的工作是服务端渲染。在工程的 package.json 中加入一些 scripts 会很帮助:

"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 的例子,这个例子向一个 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` 的依赖 (例如各种 polyfill) 整理成一个白名单
    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

# 关于外部依赖的警告

注意在 externals 选项中我们将 CSS 文件列入了白名单。这是因为从依赖导入的 CSS 应该被 webpack 处理。如果你导入其它同样需要 webpack 的类型文件 (如 *.vue*.sass),你应该把它们也加入到白名单中。

如果你使用了 runInNewContext: 'once'runInNewContext: true,那么你也需要把修改 global 的 polyfill (如 babel-polyfill) 也加入这个白名单。这是因为在使用新上下文模式时,**服务端构建内的代码有其自己的 global 对象。**但由于服务端并不真的需要它,所以它从客户端入口被引入更加容易。

# 生成 clientManifest

对于服务端的包,我们还额外生成一个客户端构建单 (manifest)。有了这个客户端构建单和服务端的包,渲染器现在就同时有了服务端客户端构建的信息。这样它就可以自动推断并向渲染出来的 HTML 中注入 preload / prefetch 指令(opens new window) <link><script> 标签。

带来的好处是双向的:

  1. 当生成的文件名有 hash 的时候,它可以替换 html-webpack-plugin 以注入正确的资源 URL。

  2. 当渲染一个基于 webpack 的按需代码分隔特性的包时,我们可以确保优化过的代码块是被 preload / prefetch 的,同时会智能地注入 <script> 标签以避免异步代码块在客户端被瀑布式请求,从而改善可交互时间 (TTI:time-to-interactive)。