|
| 1 | +# 构建配置 |
| 2 | + |
| 3 | +我们假设你已经知道,如何为纯客户端(client-only)项目配置 webpack。服务器端渲染(SSR)项目的配置大体上与纯客户端项目类似,但是我们建议将配置分为三个文件:*base*, *client* 和 *server*。基本配置(base config)包含在两个环境共享的配置,例如,输出路径(output path),别名(alias)和 loader。服务器配置(server config)和客户端配置(client config),可以通过使用 [webpack-merge](https://github.com/survivejs/webpack-merge) 来简单地扩展基本配置。 |
| 4 | + |
| 5 | +## 服务器配置(Server Config) |
| 6 | + |
| 7 | +服务器配置,是用于生成传递给 `createBundleRenderer` 的 server bundle。它应该是这样的: |
| 8 | + |
| 9 | +```js |
| 10 | +const merge = require('webpack-merge') |
| 11 | +const nodeExternals = require('webpack-node-externals') |
| 12 | +const baseConfig = require('./webpack.base.config.js') |
| 13 | +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') |
| 14 | +module.exports = merge(baseConfig, { |
| 15 | + // 将 entry 指向应用程序的 server entry 文件 |
| 16 | + entry: '/path/to/entry-server.js', |
| 17 | + // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), |
| 18 | + // 并且还会在编译 Vue 组件时, |
| 19 | + // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 |
| 20 | + target: 'node', |
| 21 | + // 对 bundle renderer 提供 source map 支持 |
| 22 | + devtool: 'source-map', |
| 23 | + // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) |
| 24 | + output: { |
| 25 | + libraryTarget: 'commonjs2' |
| 26 | + }, |
| 27 | + // https://webpack.js.org/configuration/externals/#function |
| 28 | + // https://github.com/liady/webpack-node-externals |
| 29 | + // 外置化应用程序依赖模块。可以使服务器构建速度更快, |
| 30 | + // 并生成较小的 bundle 文件。 |
| 31 | + externals: nodeExternals({ |
| 32 | + // 不要外置化 webpack 需要处理的依赖模块。 |
| 33 | + // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, |
| 34 | + // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 |
| 35 | + whitelist: /\.css$/ |
| 36 | + }), |
| 37 | + // 这是将服务器的整个输出 |
| 38 | + // 构建为单个 JSON 文件的插件。 |
| 39 | + // 默认文件名为 `vue-ssr-server-bundle.json` |
| 40 | + plugins: [ |
| 41 | + new VueSSRServerPlugin() |
| 42 | + ] |
| 43 | +}) |
| 44 | +``` |
| 45 | + |
| 46 | +在生成 `vue-ssr-server-bundle.json` 之后,只需将文件路径传递给 `createBundleRenderer`: |
| 47 | + |
| 48 | +```js |
| 49 | +const { createBundleRenderer } = require('vue-server-renderer') |
| 50 | +const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { |
| 51 | + // ……renderer 的其他选项 |
| 52 | +}) |
| 53 | +``` |
| 54 | + |
| 55 | +又或者,你还可以将 bundle 作为对象传递给 `createBundleRenderer`。这对开发过程中的热重新是很有用的 - 具体请查看 HackerNews demo 的[参考设置](https://github.com/vuejs/vue-hackernews-2.0/blob/master/build/setup-dev-server.js)。 |
| 56 | + |
| 57 | +### 扩展说明(Externals Caveats) |
| 58 | + |
| 59 | +请注意,在 `externals` 选项中,我们将 CSS 文件列入白名单。这是因为从依赖模块导入的 CSS 还应该由 webpack 处理。如果你导入依赖于 webpack 的任何其他类型的文件(例如 `*.vue`, `*.sass`),那么你也应该将它们添加到白名单中。 |
| 60 | + |
| 61 | +如果你使用 `runInNewContext: 'once'` 或 `runInNewContext: true`,那么你还应该将修改 `global` 的 polyfill 列入白名单,例如 `babel-polyfill`。这是因为当使用新的上下文模式时,**server bundle 中的代码具有自己的 `global` 对象。**由于在使用 Node 7.6+ 时,在服务器并不真正需要它,所以实际上只需在客户端 entry 导入它。 |
| 62 | + |
| 63 | +## 客户端配置(Client Config) |
| 64 | + |
| 65 | +客户端配置(client config)和基本配置(base config)大体上相同。显然你需要把 `entry` 指向你的客户端入口文件。除此之外,如果你使用 `CommonsChunkPlugin`,请确保仅在客户端配置(client config)中使用,因为服务器包需要单独的入口 chunk。 |
| 66 | + |
| 67 | +### 生成 `clientManifest` |
| 68 | + |
| 69 | +> 需要版本 2.3.0+ |
| 70 | +
|
| 71 | +除了 server bundle 之外,我们还可以生成客户端构建清单(client build manifest)。使用客户端清单(client manifest)和服务器 bundle(server bundle),renderer 现在具有了服务器*和*客户端的构建信息,因此它可以自动推断和注入[资源预加载 / 数据预取指令(preload / prefetch directive)](https://css-tricks.com/prefetching-preloading-prebrowsing/),以及 css 链接 / script 标签到所渲染的 HTML。 |
| 72 | + |
| 73 | +好处是双重的: |
| 74 | + |
| 75 | +1. 在生成的文件名中有哈希时,可以取代 `html-webpack-plugin` 来注入正确的资源 URL。 |
| 76 | +2. 在通过 webpack 的按需代码分割特性渲染 bundle 时,我们可以确保对 chunk 进行最优化的资源预加载/数据预取,并且还可以将所需的异步 chunk 智能地注入为 `<script>` 标签,以避免客户端的瀑布式请求(waterfall request),以及改善可交互时间(TTI - time-to-interactive)。 |
| 77 | + |
| 78 | +要使用客户端清单(client manifest),客户端配置(client config)将如下所示: |
| 79 | + |
| 80 | +```js |
| 81 | +const webpack = require('webpack') |
| 82 | +const merge = require('webpack-merge') |
| 83 | +const baseConfig = require('./webpack.base.config.js') |
| 84 | +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') |
| 85 | +module.exports = merge(baseConfig, { |
| 86 | + entry: '/path/to/entry-client.js', |
| 87 | + plugins: [ |
| 88 | + // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, |
| 89 | + // 以便可以在之后正确注入异步 chunk。 |
| 90 | + // 这也为你的 应用程序/vendor 代码提供了更好的缓存。 |
| 91 | + new webpack.optimize.CommonsChunkPlugin({ |
| 92 | + name: "manifest", |
| 93 | + minChunks: Infinity |
| 94 | + }), |
| 95 | + // 此插件在输出目录中 |
| 96 | + // 生成 `vue-ssr-client-manifest.json`。 |
| 97 | + new VueSSRClientPlugin() |
| 98 | + ] |
| 99 | +}) |
| 100 | +``` |
| 101 | + |
| 102 | +然后,你就可以使用生成的客户端清单(client manifest)以及页面模板: |
| 103 | + |
| 104 | +```js |
| 105 | +const { createBundleRenderer } = require('vue-server-renderer') |
| 106 | +const template = require('fs').readFileSync('/path/to/template.html', 'utf-8') |
| 107 | +const serverBundle = require('/path/to/vue-ssr-server-bundle.json') |
| 108 | +const clientManifest = require('/path/to/vue-ssr-client-manifest.json') |
| 109 | +const renderer = createBundleRenderer(serverBundle, { |
| 110 | + template, |
| 111 | + clientManifest |
| 112 | +}) |
| 113 | +``` |
| 114 | + |
| 115 | +通过以上设置,使用代码分割特性构建后的服务器渲染的 HTML 代码,将看起来如下(所有都是自动注入): |
| 116 | + |
| 117 | +```html |
| 118 | +<html> |
| 119 | + <head> |
| 120 | + <!-- 用于当前渲染的 chunk 会被资源预加载(preload) --> |
| 121 | + <link rel="preload" href="/manifest.js" as="script"> |
| 122 | + <link rel="preload" href="/main.js" as="script"> |
| 123 | + <link rel="preload" href="/0.js" as="script"> |
| 124 | + <!-- 未用到的异步 chunk 会被数据预取(preload)(次要优先级) --> |
| 125 | + <link rel="prefetch" href="/1.js" as="script"> |
| 126 | + </head> |
| 127 | + <body> |
| 128 | + <!-- 应用程序内容 --> |
| 129 | + <div data-server-rendered="true"><div>async</div></div> |
| 130 | + <!-- manifest chunk 优先 --> |
| 131 | + <script src="/manifest.js"></script> |
| 132 | + <!-- 在主 chunk 之前注入异步 chunk --> |
| 133 | + <script src="/0.js"></script> |
| 134 | + <script src="/main.js"></script> |
| 135 | + </body> |
| 136 | +</html>` |
| 137 | +``` |
| 138 | + |
| 139 | +### 手动资源注入(Manual Asset Injection) |
| 140 | + |
| 141 | +默认情况下,当提供 `template` 渲染选项时,资源注入是自动执行的。但是有时候,你可能需要对资源注入的模板进行更细粒度(finer-grained)的控制,或者你根本不使用模板。在这种情况下,你可以在创建 renderer 并手动执行资源注入时,传入 `inject: false`。 |
| 142 | + |
| 143 | +在 `renderToString` 回调函数中,你传入的 `context` 对象会暴露以下方法: |
| 144 | + |
| 145 | +- `context.renderStyles()` |
| 146 | + |
| 147 | +这将返回内联 `<style>` 标签包含所有关键 CSS(critical CSS) ,其中关键 CSS 是在要用到的 `*.vue` 组件的渲染过程中收集的。有关更多详细信息,请查看 [CSS 管理](./css.md)。 |
| 148 | + |
| 149 | + 如果提供了 `clientManifest`,返回的字符串中,也将包含着 `<link rel="stylesheet">` 标签内由 webpack 输出(webpack-emitted)的 CSS 文件(例如,使用 `extract-text-webpack-plugin` 提取的 CSS,或使用 `file-loader` 导入的 CSS) |
| 150 | + |
| 151 | +- `context.renderState(options?: Object)` |
| 152 | + |
| 153 | + 此方法序列化 `context.state` 并返回一个内联的 script,其中状态被嵌入在 `window.__INITIAL_STATE__` 中。 |
| 154 | + |
| 155 | + 上下文状态键(context state key)和 window 状态键(window state key),都可以通过传递选项对象进行自定义: |
| 156 | + |
| 157 | +```js |
| 158 | + context.renderState({ |
| 159 | + contextKey: 'myCustomState', |
| 160 | + windowKey: '__MY_STATE__' |
| 161 | + }) |
| 162 | + // -> <script>window.__MY_STATE__={...}</script> |
| 163 | +``` |
| 164 | + |
| 165 | +- `context.renderScripts()` |
| 166 | + - 需要 `clientManifest` |
| 167 | + |
| 168 | + 此方法返回引导客户端应用程序所需的 `<script>` 标签。当在应用程序代码中使用异步代码分割(async code-splitting)时,此方法将智能地正确的推断需要引入的那些异步 chunk。 |
| 169 | + |
| 170 | +- `context.renderResourceHints()` |
| 171 | + - 需要 `clientManifest` |
| 172 | + |
| 173 | + 此方法返回当前要渲染的页面,所需的 `<link rel="preload/prefetch">` 资源提示(resource hint)。默认情况下会: |
| 174 | + |
| 175 | +- 预加载页面所需的 JavaScript 和 CSS 文件 |
| 176 | +- 预取异步 JavaScript chunk,之后可能会用于渲染 |
| 177 | + |
| 178 | + 使用 [`shouldPreload`](./api.md#shouldpreload) 选项可以进一步自定义要预加载的文件。 |
| 179 | + |
| 180 | +- `context.getPreloadFiles()` |
| 181 | + - 需要 `clientManifest` |
| 182 | + |
| 183 | + 此方法不返回字符串 - 相反,它返回一个数组,此数组是由要预加载的资源文件对象所组成。这可以用在以编程方式(programmatically)执行 HTTP/2 服务器推送(HTTP/2 server push)。 |
| 184 | + |
| 185 | +由于传递给 `createBundleRenderer` 的 `template` 将会使用 `context` 对象进行插值,你可以(通过传入 `inject: false`)在模板中使用这些方法: |
| 186 | + |
| 187 | +```html |
| 188 | +<html> |
| 189 | + <head> |
| 190 | + <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) --> |
| 191 | + {{{ renderResourceHints() }}} |
| 192 | + {{{ renderStyles() }}} |
| 193 | + </head> |
| 194 | + <body> |
| 195 | + <!--vue-ssr-outlet--> |
| 196 | + {{{ renderState() }}} |
| 197 | + {{{ renderScripts() }}} |
| 198 | + </body> |
| 199 | +</html> |
| 200 | +``` |
| 201 | + |
| 202 | +如果你根本没有使用 `template`,你可以自己拼接字符串。 |
0 commit comments