• 构建配置
    • 服务器配置 (Server Config)
      • 扩展说明 (Externals Caveats)
    • 客户端配置 (Client Config)
      • 生成 clientManifest
      • 手动资源注入(Manual Asset Injection)

    构建配置

    我们假设你已经知道,如何为纯客户端 (client-only) 项目配置 webpack。服务器端渲染 (SSR) 项目的配置大体上与纯客户端项目类似,但是我们建议将配置分为三个文件:base, clientserver。基本配置 (base config) 包含在两个环境共享的配置,例如,输出路径 (output path),别名 (alias) 和 loader。服务器配置 (server config) 和客户端配置 (client config),可以通过使用 webpack-merge 来简单地扩展基本配置。

    服务器配置 (Server Config)

    服务器配置,是用于生成传递给 createBundleRenderer 的 server bundle。它应该是这样的:

    1. const merge = require('webpack-merge')
    2. const nodeExternals = require('webpack-node-externals')
    3. const baseConfig = require('./webpack.base.config.js')
    4. const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    5. module.exports = merge(baseConfig, {
    6. // 将 entry 指向应用程序的 server entry 文件
    7. entry: '/path/to/entry-server.js',
    8. // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
    9. // 并且还会在编译 Vue 组件时,
    10. // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    11. target: 'node',
    12. // 对 bundle renderer 提供 source map 支持
    13. devtool: 'source-map',
    14. // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    15. output: {
    16. libraryTarget: 'commonjs2'
    17. },
    18. // https://webpack.js.org/configuration/externals/#function
    19. // https://github.com/liady/webpack-node-externals
    20. // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    21. // 并生成较小的 bundle 文件。
    22. externals: nodeExternals({
    23. // 不要外置化 webpack 需要处理的依赖模块。
    24. // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    25. // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    26. whitelist: /\.css$/
    27. }),
    28. // 这是将服务器的整个输出
    29. // 构建为单个 JSON 文件的插件。
    30. // 默认文件名为 `vue-ssr-server-bundle.json`
    31. plugins: [
    32. new VueSSRServerPlugin()
    33. ]
    34. })

    在生成 vue-ssr-server-bundle.json 之后,只需将文件路径传递给 createBundleRenderer

    1. const { createBundleRenderer } = require('vue-server-renderer')
    2. const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
    3. // ……renderer 的其他选项
    4. })

    又或者,你还可以将 bundle 作为对象传递给 createBundleRenderer。这对开发过程中的热重载是很有用的 - 具体请查看 HackerNews demo 的参考设置。

    扩展说明 (Externals Caveats)

    请注意,在 externals 选项中,我们将 CSS 文件列入白名单。这是因为从依赖模块导入的 CSS 还应该由 webpack 处理。如果你导入依赖于 webpack 的任何其他类型的文件(例如 *.vue, *.sass),那么你也应该将它们添加到白名单中。

    如果你使用 runInNewContext: 'once'runInNewContext: true,那么你还应该将修改 global 的 polyfill 列入白名单,例如 babel-polyfill。这是因为当使用新的上下文模式时,server bundle 中的代码具有自己的 global 对象。由于在使用 Node 7.6+ 时,在服务器并不真正需要它,所以实际上只需在客户端 entry 导入它。

    客户端配置 (Client Config)

    客户端配置 (client config) 和基本配置 (base config) 大体上相同。显然你需要把 entry 指向你的客户端入口文件。除此之外,如果你使用 CommonsChunkPlugin,请确保仅在客户端配置 (client config) 中使用,因为服务器包需要单独的入口 chunk。

    生成 clientManifest

    需要版本 2.3.0+

    除了 server bundle 之外,我们还可以生成客户端构建清单 (client build manifest)。使用客户端清单 (client manifest) 和服务器 bundle(server bundle),renderer 现在具有了服务器和客户端的构建信息,因此它可以自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 链接 / script 标签到所渲染的 HTML。

    好处是双重的:

    1. 在生成的文件名中有哈希时,可以取代 html-webpack-plugin 来注入正确的资源 URL。

    2. 在通过 webpack 的按需代码分割特性渲染 bundle 时,我们可以确保对 chunk 进行最优化的资源预加载/数据预取,并且还可以将所需的异步 chunk 智能地注入为 <script> 标签,以避免客户端的瀑布式请求 (waterfall request),以及改善可交互时间 (TTI - time-to-interactive)。

    要使用客户端清单 (client manifest),客户端配置 (client config) 将如下所示:

    1. const webpack = require('webpack')
    2. const merge = require('webpack-merge')
    3. const baseConfig = require('./webpack.base.config.js')
    4. const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    5. module.exports = merge(baseConfig, {
    6. entry: '/path/to/entry-client.js',
    7. plugins: [
    8. // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
    9. // 以便可以在之后正确注入异步 chunk。
    10. // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
    11. new webpack.optimize.CommonsChunkPlugin({
    12. name: "manifest",
    13. minChunks: Infinity
    14. }),
    15. // 此插件在输出目录中
    16. // 生成 `vue-ssr-client-manifest.json`。
    17. new VueSSRClientPlugin()
    18. ]
    19. })

    然后,你就可以使用生成的客户端清单 (client manifest) 以及页面模板:

    1. const { createBundleRenderer } = require('vue-server-renderer')
    2. const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
    3. const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
    4. const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
    5. const renderer = createBundleRenderer(serverBundle, {
    6. template,
    7. clientManifest
    8. })

    通过以上设置,使用代码分割特性构建后的服务器渲染的 HTML 代码,将看起来如下(所有都是自动注入):

    1. <html>
    2. <head>
    3. <!-- 用于当前渲染的 chunk 会被资源预加载(preload) -->
    4. <link rel="preload" href="/manifest.js" as="script">
    5. <link rel="preload" href="/main.js" as="script">
    6. <link rel="preload" href="/0.js" as="script">
    7. <!-- 未用到的异步 chunk 会被数据预取(prefetch)(次要优先级) -->
    8. <link rel="prefetch" href="/1.js" as="script">
    9. </head>
    10. <body>
    11. <!-- 应用程序内容 -->
    12. <div data-server-rendered="true"><div>async</div></div>
    13. <!-- manifest chunk 优先 -->
    14. <script src="/manifest.js"></script>
    15. <!-- 在主 chunk 之前注入异步 chunk -->
    16. <script src="/0.js"></script>
    17. <script src="/main.js"></script>
    18. </body>
    19. </html>

    手动资源注入(Manual Asset Injection)

    默认情况下,当提供 template 渲染选项时,资源注入是自动执行的。但是有时候,你可能需要对资源注入的模板进行更细粒度 (finer-grained) 的控制,或者你根本不使用模板。在这种情况下,你可以在创建 renderer 并手动执行资源注入时,传入 inject: false

    renderToString 回调函数中,你传入的 context 对象会暴露以下方法:

    • context.renderStyles()

      这将返回内联 <style> 标签包含所有关键 CSS(critical CSS) ,其中关键 CSS 是在要用到的 *.vue 组件的渲染过程中收集的。有关更多详细信息,请查看 CSS 管理。

      如果提供了 clientManifest,返回的字符串中,也将包含着 <link rel="stylesheet"> 标签内由 webpack 输出(webpack-emitted)的 CSS 文件(例如,使用 extract-text-webpack-plugin 提取的 CSS,或使用 file-loader 导入的 CSS)

    • context.renderState(options?: Object)

      此方法序列化 context.state 并返回一个内联的 script,其中状态被嵌入在 window.__INITIAL_STATE__ 中。

      上下文状态键 (context state key) 和 window 状态键 (window state key),都可以通过传递选项对象进行自定义:

      context.renderState({
        contextKey: 'myCustomState',
        windowKey: '__MY_STATE__'
      })
      
      // -> <script>window.__MY_STATE__={...}</script>
      
    • context.renderScripts()

      • 需要 clientManifest

      此方法返回引导客户端应用程序所需的 <script> 标签。当在应用程序代码中使用异步代码分割 (async code-splitting) 时,此方法将智能地正确的推断需要引入的那些异步 chunk。

    • context.renderResourceHints()

      • 需要 clientManifest

      此方法返回当前要渲染的页面,所需的 <link rel="preload/prefetch"> 资源提示 (resource hint)。默认情况下会:

      • 预加载页面所需的 JavaScript 和 CSS 文件
      • 预取异步 JavaScript chunk,之后可能会用于渲染

      使用 shouldPreload 选项可以进一步自定义要预加载的文件。

    • context.getPreloadFiles()

      • 需要 clientManifest

      此方法不返回字符串 - 相反,它返回一个数组,此数组是由要预加载的资源文件对象所组成。这可以用在以编程方式 (programmatically) 执行 HTTP/2 服务器推送 (HTTP/2 server push)。

    由于传递给 createBundleRenderertemplate 将会使用 context 对象进行插值,你可以(通过传入 inject: false)在模板中使用这些方法:

    <html>
      <head>
        <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
        {{{ renderResourceHints() }}}
        {{{ renderStyles() }}}
      </head>
      <body>
        <!--vue-ssr-outlet-->
        {{{ renderState() }}}
        {{{ renderScripts() }}}
      </body>
    </html>
    

    如果你根本没有使用 template,你可以自己拼接字符串。