From 44582ab1db13ab89b219ed4fc51c45ef53add835 Mon Sep 17 00:00:00 2001 From: lizhihua <275091674@qq.com> Date: Tue, 1 May 2018 13:52:42 +0800 Subject: [PATCH 01/70] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E6=96=87=E6=A1=A3(20180501)=20&=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20localize=20=E5=AF=BC=E8=87=B4=E7=9A=84=20markdown?= =?UTF-8?q?=20=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zh/README.md | 15 +++-- zh/SUMMARY.md | 13 ++-- zh/api.md | 137 +++++++++++++++++++++++------------------- zh/basic.md | 39 ++++++++---- zh/build-config.md | 42 ++++++++----- zh/bundle-renderer.md | 9 ++- zh/caching.md | 14 +++-- zh/css.md | 14 +++-- zh/data.md | 58 +++++++++++++----- zh/head.md | 18 ++++-- zh/hydration.md | 18 +++--- zh/non-node.md | 41 +++++++++++++ zh/routing.md | 45 +++++++++----- zh/streaming.md | 9 ++- zh/structure.md | 25 +++++--- zh/universal.md | 3 +- 16 files changed, 329 insertions(+), 171 deletions(-) create mode 100644 zh/non-node.md diff --git a/zh/README.md b/zh/README.md index 6867fa0e..ed0cb872 100644 --- a/zh/README.md +++ b/zh/README.md @@ -3,7 +3,7 @@ > **注意:** 本指南需要最低为如下版本的 Vue,以及以下 library 支持: > - vue & vue-server-renderer 2.3.0+ > - vue-router 2.5.0+ -> - vue-loader 12.0.0+ & vue-style-loader 3.0.0+ +> - vue-loader 12.0.0+ & vue-style-loader 3.0.0+ > 如果先前已经使用过 Vue 2.2 的服务器端渲染(SSR),你应该注意到,推荐的代码结构现在[略有不同](./structure.md)(使用新的 [runInNewContext](./api.md#runinnewcontext) 选项,并设置为 `false`)。现有的应用程序可以继续运行,但建议你迁移到新的推荐规范。 @@ -11,7 +11,7 @@ Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。 -服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器**和**客户端上运行。 +服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在**服务器**和**客户端**上运行。 ## 为什么使用服务器端渲染(SSR)? @@ -19,7 +19,7 @@ Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏 - 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。 -请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。 + 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。 - 更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)至关重要。 @@ -31,7 +31,7 @@ Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏 - 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源(CPU-intensive - CPU 密集),因此如果你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。 -在对你的应用程序使用服务器端渲染(SSR)之前,你应该问第一个问题是否真的需要它。这主要取决于内容到达时间(time-to-content)对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染(SSR)将是一个小题大作之举。然而,内容到达时间(time-to-content)要求是绝对关键的指标,在这种情况下,服务器端渲染(SSR)可以帮助你实现最佳的初始加载性能。 +在对你的应用程序使用服务器端渲染(SSR)之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间(time-to-content)对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染(SSR)将是一个小题大作之举。然而,内容到达时间(time-to-content)要求是绝对关键的指标,在这种情况下,服务器端渲染(SSR)可以帮助你实现最佳的初始加载性能。 ## 服务器端渲染 vs 预渲染(SSR vs Prerendering) @@ -41,9 +41,12 @@ Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏 ## 关于此指南 -本指南专注于,使用 Node.js server 的服务器端单页面应用程序渲染。将 Vue 服务器端渲染(SSR)与其他后端设置进行混合使用,是其它后端自身的一个主题,本指南不包括在内。 +本指南专注于,使用 Node.js server 的服务器端单页面应用程序渲染。 +将 Vue 服务器端渲染(SSR)与其他后端设置进行混合使用,是其它后端自身的一个主题,本指南不包括在内。 -本指南将会非常深入,并且假设你已经熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。如果你倾向于使用提供了平滑开箱即用体验的更高层次解决方案,你应该去尝试使用 [Nuxt.js](http://nuxtjs.org/)。它建立在同等的 Vue 技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。但是,如果你需要更直接地控制应用程序的结构,Nuxt.js 并不适合这种使用场景。无论如何,阅读本指南将更有助于更好地了解一切如何运行。 +将 Vue 服务器端渲染(SSR)与其他后端设置进行混合使用,是后端自身集成 SSR 的话题,我们会在 [专门章节](./non-node.md) 中进行简要讨论。 + +本指南将会非常深入,并且假设你已经熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。如果你倾向于使用提供了平滑开箱即用体验的更高层次解决方案,你应该去尝试使用 [Nuxt.js](https://nuxtjs.org/)。它建立在同等的 Vue 技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。但是,如果你需要更直接地控制应用程序的结构,Nuxt.js 并不适合这种使用场景。无论如何,阅读本指南将更有助于更好地了解一切如何运行。 当你阅读时,参考官方 [HackerNews Demo](https://github.com/vuejs/vue-hackernews-2.0/) 将会有所帮助,此示例使用了本指南涵盖的大部分技术。 diff --git a/zh/SUMMARY.md b/zh/SUMMARY.md index 1a071fe2..c23fe9a7 100644 --- a/zh/SUMMARY.md +++ b/zh/SUMMARY.md @@ -10,12 +10,13 @@ - [Head 管理](head.md) - [缓存](caching.md) - [流式渲染](streaming.md) +- [在非 Node.js 环境下使用](non-node.md) - [API 参考](api.md) - - [createRenderer](api.md#createrendereroptions) - - [createBundleRenderer](api.md#createbundlerendererbundle-options) - - [Class: Renderer](api.md#class-renderer) - - [Class: BundleRenderer](api.md#class-bundlerenderer) - - [Renderer 选项](api.md#renderer-options) + - [createRenderer](api.md#createrendereroptions) + - [createBundleRenderer](api.md#createbundlerendererbundle-options) + - [Class: Renderer](api.md#class-renderer) + - [Class: BundleRenderer](api.md#class-bundlerenderer) + - [Renderer 选项](api.md#renderer-options) - [template](api.md#template) - [clientManifest](api.md#clientmanifest) - [inject](api.md#inject) @@ -24,4 +25,4 @@ - [basedir](api.md#basedir) - [cache](api.md#cache) - [directives](api.md#directives) - - [webpack 插件](api.md#webpack-plugins) + - [webpack 插件](api.md#webpack-plugins) diff --git a/zh/api.md b/zh/api.md index d3638fa4..701ff75d 100644 --- a/zh/api.md +++ b/zh/api.md @@ -4,7 +4,7 @@ 使用(可选的)[选项](#renderer-options)创建一个 [`Renderer`](#class-renderer) 实例。 -```js +``` js const { createRenderer } = require('vue-server-renderer') const renderer = createRenderer({ ... }) ``` @@ -13,7 +13,7 @@ const renderer = createRenderer({ ... }) 使用 server bundle 和(可选的)[选项](#renderer-options)创建一个 [`BundleRenderer`](#class-bundlerenderer) 实例。 -```js +``` js const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer(serverBundle, { ... }) ``` @@ -30,82 +30,86 @@ const renderer = createBundleRenderer(serverBundle, { ... }) ## `Class: Renderer` -- #### `renderer.renderToString(vm[, context], callback)` +- #### `renderer.renderToString(vm[, context, callback]): ?Promise` - 将 Vue 实例渲染为字符串。上下文对象 (context object) 可选。回调函数是典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。 + 将 Vue 实例渲染为字符串。上下文对象(context object)可选。回调函数是典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。 -- #### `renderer.renderToStream(vm[, context])` + 在 2.5.0+ 版本中,此 callback 回调函数是可选项。在不传递 callback 时,此方法返回一个 Promise 对象,在其 resolve 后返回最终渲染的 HTML。 - 将 Vue 实例渲染为一个 Node.js 流 (stream)。上下文对象 (context object) 可选。更多细节请查看[流式渲染](./streaming.md)。 +- #### `renderer.renderToStream(vm[, context]): stream.Readable` + + 将 Vue 实例渲染为一个 [Node.js 可读流](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams)。上下文对象(context object)可选。更多细节请查看[流式渲染](./streaming.md)。 ## `Class: BundleRenderer` -- #### `bundleRenderer.renderToString([context, ]callback)` +- #### `bundleRenderer.renderToString([context, callback]): ?Promise` + + 将 bundle 渲染为字符串。上下文对象(context object)可选。回调是一个典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。 -将 bundle 渲染为字符串。上下文对象 (context object) 可选。回调是一个典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。 + 在 2.5.0+ 版本中,此 callback 回调函数是可选项。在不传递 callback 时,此方法返回一个 Promise 对象,在其 resolve 后返回最终渲染的 HTML。 -- #### `bundleRenderer.renderToStream([context])` +- #### `bundleRenderer.renderToStream([context]): stream.Readable` - 将 bundle 渲染为一个 Node.js 流 (stream). 上下文对象 (context object) 可选。更多细节请查看[流式渲染](./streaming.md)。 + 将 bundle 渲染为一个 [Node.js 可读流](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams)。上下文对象(context object)可选。更多细节请查看[流式渲染](./streaming.md)。 ## Renderer 选项 - #### `template` - 为整个页面的 HTML 提供一个模板。此模板应包含注释 ``,作为渲染应用内容的占位符。 + 为整个页面的 HTML 提供一个模板。此模板应包含注释 ``,作为渲染应用程序内容的占位符。 -模板还支持使用渲染上下文 (render context) 进行基本插值: + 模板还支持使用渲染上下文(render context)进行基本插值: -- 使用双花括号 (double-mustache) 进行 HTML 转义插值 (HTML-escaped interpolation); -- 使用三花括号 (triple-mustache) 进行 HTML 不转义插值 (non-HTML-escaped interpolation)。 + - 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation); + - 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation)。 -当在渲染上下文 (render context) 上存在一些特定属性时,模板会自动注入对应的内容: + 当在渲染上下文(render context)上存在一些特定属性时,模板会自动注入对应的内容: -- `context.head`:(字符串)将会被作为 HTML 注入到页面的头部 (head) 里。 + - `context.head`:(字符串)将会被作为 HTML 注入到页面的头部(head)里。 -- `context.styles`:(字符串)内联 CSS,将以 style 标签的形式注入到页面头部。注意,如过你使用了 `vue-loader` + `vue-style-loader` 来处理组件 CSS,此属性会在构建过程中被自动生成。 + - `context.styles`:(字符串)内联 CSS,将以 style 标签的形式注入到页面头部。注意,如过你使用了 `vue-loader` + `vue-style-loader` 来处理组件 CSS,此属性会在构建过程中被自动生成。 -- `context.state`:(对象)初始 Vuex store 状态,将以 `window.__INITIAL_STATE__` 的形式内联到页面。内联的 JSON 将使用 [serialize-javascript](https://github.com/yahoo/serialize-javascript) 自动清理,以防止 XSS 攻击。 + - `context.state`:(对象)初始 Vuex store 状态,将以 `window.__INITIAL_STATE__` 的形式内联到页面。内联的 JSON 将使用 [serialize-javascript](https://github.com/yahoo/serialize-javascript) 自动清理,以防止 XSS 攻击。 + + 在 2.5.0+ 版本中,嵌入式 script 也可以也可以在生产模式(production mode)下自行移除。 此外,当提供 `clientManifest` 时,模板会自动注入以下内容: -- 渲染当前页面所需的最优客户端 JavaScript 和 CSS 资源(支持自动推导异步代码分割所需的文件) -- 为要渲染页面提供最佳的 `` 资源提示(resource hints)。 + - 渲染当前页面所需的最优客户端 JavaScript 和 CSS 资源(支持自动推导异步代码分割所需的文件); + - 为要渲染页面提供最佳的 `` 资源提示(resource hints)。 你也可以通过将 `inject: false` 传递给 renderer,来禁用所有自动注入。 -具体查看: + 具体查看: -- [使用一个页面模板](./basic.md#using-a-page-template) + - [使用一个页面模板](./basic.md#using-a-page-template) + - [手动资源注入(Manual Asset Injection)](./build-config.md#manual-asset-injection) -- [手动资源注入(Manual Asset Injection)](./build-config.md#manual-asset-injection) - - #### `clientManifest` +- #### `clientManifest` -- 2.3.0+ + - 2.3.0+ - 通过此选项提供一个由 `vue-server-renderer/client-plugin` 生成的客户端构建 manifest 对象 (client build manifest object)。此对象包含了 webpack 整个构建过程的信息,从而可以让 bundle renderer 自动推导需要在 HTML 模板中注入的内容。更多详细信息,请查看[生成 clientManifest](./build-config.md#generating-clientmanifest)。 + 通过此选项提供一个由 `vue-server-renderer/client-plugin` 生成的客户端构建 manifest 对象(client build manifest object)。此对象包含了 webpack 整个构建过程的信息,从而可以让 bundle renderer 自动推导需要在 HTML 模板中注入的内容。更多详细信息,请查看[生成 clientManifest](./build-config.md#generating-clientmanifest)。 -- -#### `inject` +- #### `inject` - - 2.3.0+ + - 2.3.0+ 控制使用 `template` 时是否执行自动注入。默认是 `true`。 参考:[手动资源注入(Manual Asset Injection)](./build-config.md#manual-asset-injection)。 -- -#### `shouldPreload` +- #### `shouldPreload` - - 2.3.0+ + - 2.3.0+ - 一个函数,用来控制什么文件应该生成 `` 资源预加载提示 (resource hints)。 + 一个函数,用来控制什么文件应该生成 `` 资源预加载提示(resource hints)。 -默认情况下,只有 JavaScript 和 CSS 文件会被预加载,因为它们是启动应用时所必需的。 + 默认情况下,只有 JavaScript 和 CSS 文件会被预加载,因为它们是启动应用时所必需的。 对于其他类型的资源(如图像或字体),预加载过多可能会浪费带宽,甚至损害性能,因此预加载什么资源具体依赖于场景。你可以使用 `shouldPreload` 选项精确控制预加载资源: -```js + ``` js const renderer = createBundleRenderer(bundle, { template, clientManifest, @@ -116,44 +120,50 @@ const renderer = createBundleRenderer(serverBundle, { ... }) return true } if (type === 'font') { - // only preload woff2 fonts + // 只预加载 woff2 字体 return /\.woff2$/.test(file) } if (type === 'image') { - // only preload important images + // 只预加载重要 images return file === 'hero.jpg' } } }) -``` + ``` + +- #### `shouldPrefetch` + + - 2.5.0+ + + 一个函数,用来控制对于哪些文件,是需要生成 `` 资源提示。 -- -#### `runInNewContext` + 默认情况下,异步 chunk 中的所有资源都将被预取,因为这是低优先级指令; 然而,为了更好地控制带宽使用情况,你也可以自定义要预取的资源。此选项具有与 `shouldPreload` 相同的函数签名。 - - 2.3.0+ - - 只用于 `createBundleRenderer` - - Expects: `boolean | 'once'` (`'once'` 仅在 2.3.1+ 中支持) +- #### `runInNewContext` + + - 2.3.0+ + - 只用于 `createBundleRenderer` + - 期望值:`boolean | 'once'`(`'once'` 只在 2.3.1+ 支持) 默认情况下,对于每次渲染,bundle renderer 将创建一个新的 V8 上下文并重新执行整个 bundle。这具有一些好处 - 例如,应用程序代码与服务器进程隔离,我们无需担心文档中提到的[状态单例问题](./structure.md#avoid-stateful-singletons)。然而,这种模式有一些相当大的性能开销,因为重新创建上下文并执行整个 bundle 还是相当昂贵的,特别是当应用很大的时候。 出于向后兼容的考虑,此选项默认为 `true`,但建议你尽可能使用 `runInNewContext: false` 或 `runInNewContext: 'once'`。 -> 在 2.3.0 中,此选项有一个 bug,其中 `runInNewContext: false` 仍然使用独立的全局上下文(separate global context)执行 bundle。以下信息假定版本为 2.3.1+。 + > 在 2.3.0 中,此选项有一个 bug,其中 `runInNewContext: false` 仍然使用独立的全局上下文(separate global context)执行 bundle。以下信息假定版本为 2.3.1+。 使用 `runInNewContext: false`,bundle 代码将与服务器进程在同一个 `global` 上下文中运行,所以请留意在应用程序代码中尽量避免修改 `global`。 - 使用 `runInNewContext: 'once'` (2.3.1+),bundle 将在独立的`全局`上下文 (separate global context) 取值,然而只在启动时取值一次。这提供了一定程度的应用程序代码隔离,因为它能够防止 bundle 中的代码意外污染服务器进程的 `global` 对象。注意事项如下: + 使用 `runInNewContext: 'once'` (2.3.1+),bundle 将在独立的`全局`上下文(separate global context)取值,然而只在启动时取值一次。这提供了一定程度的应用程序代码隔离,因为它能够防止 bundle 中的代码意外污染服务器进程的 `global` 对象。注意事项如下: -1. 在此模式下,修改 `global`(例如,polyfill)的依赖模块必须被打包进 bundle,不能被外部化 (externalize); -2. 从 bundle 执行返回的值将使用不同的全局构造函数,例如,在服务器进程中捕获到 bundle 内部抛出的错误,使用的是 bundle 上下文中的 Error 构造函数,所以它不会是服务器进程中 `Error` 的一个实例。 + 1. 在此模式下,修改 `global`(例如,polyfill)的依赖模块必须被打包进 bundle,不能被外部化 (externalize); + 2. 从 bundle 执行返回的值将使用不同的全局构造函数,例如,在服务器进程中捕获到 bundle 内部抛出的错误,使用的是 bundle 上下文中的 Error 构造函数,所以它不会是服务器进程中 `Error` 的一个实例。 参考:[源码结构](./structure.md) -- -#### `basedir` +- #### `basedir` - - 2.2.0+ - - 只用于 `createBundleRenderer` + - 2.2.0+ + - 只用于 `createBundleRenderer` 显式地声明 server bundle 的运行目录。运行时将会以此目录为基准来解析 `node_modules` 中的依赖模块。只有在所生成的 bundle 文件与外部的 NPM 依赖模块放置在不同位置,或者 `vue-server-renderer` 是通过 NPM link 链接到当前项目中时,才需要配置此选项。 @@ -161,28 +171,29 @@ const renderer = createBundleRenderer(serverBundle, { ... }) 提供[组件缓存](./caching.md#component-level-caching)具体实现。缓存对象必须实现以下接口(使用 Flow 语法表示): -```js + ``` js type RenderCache = { get: (key: string, cb?: Function) => string | void; set: (key: string, val: string) => void; has?: (key: string, cb?: Function) => boolean | void; }; -``` + ``` 典型用法是传入 [lru-cache](https://github.com/isaacs/node-lru-cache): -```js + ``` js const LRU = require('lru-cache') + const renderer = createRenderer({ cache: LRU({ max: 10000 }) }) -``` + ``` - 请注意,缓存对象应至少要实现 `get` 和 `set`。此外,如果 `get` 和 `has` 接收第二个参数作为回调,那 gethas 也可以是可选的异步函数。这允许缓存使用异步 API,例如,一个 Redis 客户端: + 请注意,缓存对象应至少要实现 `get` 和 `set`。此外,如果 `get` 和 `has` 接收第二个参数作为回调,那 `get` 和 `has` 也可以是可选的异步函数。这允许缓存使用异步 API,例如,一个 Redis 客户端: -```js + ``` js const renderer = createRenderer({ cache: { get: (key, cb) => { @@ -196,13 +207,13 @@ const renderer = createBundleRenderer(serverBundle, { ... }) } } }) -``` + ``` - #### `directives` -对于自定义指令,允许提供服务器端实现: + 对于自定义指令,允许提供服务器端实现: -```js + ``` js const renderer = createRenderer({ directives: { example (vnode, directiveMeta) { @@ -210,7 +221,7 @@ const renderer = createBundleRenderer(serverBundle, { ... }) } } }) -``` + ``` 例如,请查看 [`v-show` 的服务器端实现](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js)。 @@ -218,7 +229,7 @@ const renderer = createBundleRenderer(serverBundle, { ... }) webpack 插件作为独立文件提供,并且应当直接 require: -```js +``` js const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') ``` @@ -230,7 +241,7 @@ const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 创建插件实例时可以自定义文件名: -```js +``` js const plugin = new VueSSRServerPlugin({ filename: 'my-server-bundle.json' }) diff --git a/zh/basic.md b/zh/basic.md index 005f1911..742f4485 100644 --- a/zh/basic.md +++ b/zh/basic.md @@ -2,7 +2,7 @@ ## 安装 -```bash +``` bash npm install vue vue-server-renderer --save ``` @@ -16,36 +16,44 @@ npm install vue vue-server-renderer --save ## 渲染一个 Vue 实例 -```js +``` js // 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const app = new Vue({ template: `
Hello World
` }) + // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() + // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // =>
Hello World
}) + +// 在 2.5.0+,如果传入回调函数,则会返回 Promise: +renderer.renderToString(app).then(html => { + console.log(html) +}).catch(err => { + console.error(err) +}) ``` ## 与服务器集成 在 Node.js 服务器中使用时相当简单直接,例如 [Express](https://expressjs.com/): -```bash +``` bash npm install express --save ``` - --- - -```js +``` js const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer() + server.get('*', (req, res) => { const app = new Vue({ data: { @@ -53,6 +61,7 @@ server.get('*', (req, res) => { }, template: `
访问的 URL 是: {{ url }}
` }) + renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') @@ -67,6 +76,7 @@ server.get('*', (req, res) => { `) }) }) + server.listen(8080) ``` @@ -76,7 +86,7 @@ server.listen(8080) 为了简化这些,你可以直接在创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如 `index.template.html`: -```html +``` html Codestin Search App @@ -90,12 +100,13 @@ server.listen(8080) 然后,我们可以读取和传输文件到 Vue renderer 中: -```js +``` js const renderer = createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) + renderer.renderToString(app, (err, html) => { - console.log(html) // will be the full page with app content injected. + console.log(html) // html 将是注入应用程序内容的完整页面 }) ``` @@ -103,11 +114,12 @@ renderer.renderToString(app, (err, html) => { 模板还支持简单插值。给定如下模板: -```html +``` html Codestin Search App + {{{ meta }}} @@ -119,7 +131,7 @@ renderer.renderToString(app, (err, html) => { 我们可以通过传入一个"渲染上下文对象",作为 `renderToString` 函数的第二个参数,来提供插值数据: -```js +``` js const context = { title: 'hello', meta: ` @@ -127,9 +139,10 @@ const context = { ` } + renderer.renderToString(app, context, (err, html) => { - // page title will be "Hello" - // with meta tags injected + // 页面 title 将会是 "Hello" + // meta 标签也会注入 }) ``` diff --git a/zh/build-config.md b/zh/build-config.md index cf7915a5..8a6b75ec 100644 --- a/zh/build-config.md +++ b/zh/build-config.md @@ -6,24 +6,29 @@ 服务器配置,是用于生成传递给 `createBundleRenderer` 的 server bundle。它应该是这样的: -```js +``` js const merge = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const baseConfig = require('./webpack.base.config.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') + module.exports = merge(baseConfig, { // 将 entry 指向应用程序的 server entry 文件 entry: '/path/to/entry-server.js', + // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), // 并且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 target: 'node', + // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', + // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' }, + // https://webpack.js.org/configuration/externals/#function // https://github.com/liady/webpack-node-externals // 外置化应用程序依赖模块。可以使服务器构建速度更快, @@ -34,6 +39,7 @@ module.exports = merge(baseConfig, { // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 whitelist: /\.css$/ }), + // 这是将服务器的整个输出 // 构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` @@ -45,7 +51,7 @@ module.exports = merge(baseConfig, { 在生成 `vue-ssr-server-bundle.json` 之后,只需将文件路径传递给 `createBundleRenderer`: -```js +``` js const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { // ……renderer 的其他选项 @@ -73,15 +79,17 @@ const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { 好处是双重的: 1. 在生成的文件名中有哈希时,可以取代 `html-webpack-plugin` 来注入正确的资源 URL。 + 2. 在通过 webpack 的按需代码分割特性渲染 bundle 时,我们可以确保对 chunk 进行最优化的资源预加载/数据预取,并且还可以将所需的异步 chunk 智能地注入为 ` -``` + ``` - `context.renderScripts()` - - 需要 `clientManifest` + + - 需要 `clientManifest` 此方法返回引导客户端应用程序所需的 `` タグを挿入することができます。そのようにして TTI (time-to-interactive) が改善します。 + +クライアントマニフェストを利用するためには、client 設定はこのようになります: + +```js +const webpack = require('webpack') +const merge = require('webpack-merge') +const baseConfig = require('./webpack.base.config.js') +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') + +module.exports = merge(baseConfig, { + entry: '/path/to/entry-client.js', + plugins: [ + // 重要: webpack ランタイムを先頭のチャンクに分割して、 + // 直後に非同期チャンクを挿入できるようにする + // これにより、アプリ/ベンダーコードのキャッシングも改善される + new webpack.optimize.CommonsChunkPlugin({ + name: "manifest", + minChunks: Infinity + }), + // このプラグインは、出力ディレクトリに + // `vue-ssr-client-manifest.json` を生成する + new VueSSRClientPlugin() + ] +}) +``` + +これで、作成されたクライアントマニフェストをページテンプレートと一緒に利用できるようになります。 + +```js +const { createBundleRenderer } = require('vue-server-renderer') +const template = require('fs').readFileSync('/path/to/template.html', 'utf-8') +const serverBundle = require('/path/to/vue-ssr-server-bundle.json') +const clientManifest = require('/path/to/vue-ssr-client-manifest.json') +const renderer = createBundleRenderer(serverBundle, { + template, + clientManifest +}) +``` + +この設定で、コード分割されたビルドのためにサーバ側で描画される HTML はこのようになります(すべて自動でインジェクトされます): + +```html + + + + + + + + + + + +
async
+ + + + + + + +``` + +### 手動でのアセットインジェクション + +デフォルト設定で、アセットインジェクションはあなたが作成した `template` 描画オプションで自動に行われます。しかし、アセットがどのようにテンプレートにインジェクトされるかをより細かくコントロールしたい時もあるでしょうし、あるいはテンプレートを使わない時もあるかもしれません。そのような場合にはレンダラを作る時に `inject: false` を渡せば、手動でアセットインジェクションを行うことができます。 + +渡した `context` オブジェクトは `renderToString` コールバックで、次のメソッドを持ちます: + +- `context.renderStyles()` + + これは、描画中に使われた `*.vue` コンポーネントから集めた全てのクリティカル CSS を含んだ `` タグを返します。詳細は [CSS の管理](./css.md)の章を見てください。 + + `clientManifest` が提供されたら、返ってきた文字列は webpack が放出した CSS ファイルの `` タグも含みます。(例: `extract-text-webpack-plugin` から抽出された CSS や、`file-loader` でインポートされた CSS) + +- `context.renderState(options?: Object)` + + このメソッドは `context.state` をシリアライズし、`window.__INITIAL_STATE__` ステートとして埋め込まれたインラインスクリプトを返します。 + + context のステートキーと window のステートキーはどちらとも、オプションオブジェクトとして渡すことでカスタマイズできます。 + +```js + context.renderState({ + contextKey: 'myCustomState', + windowKey: '__MY_STATE__' + }) + + // -> +``` + +- `context.renderScripts()` + + - 必須 `clientManifest` + +このメソッドはクライアントアプリケーションを起動するのに必要な `` タグを返します。コードの中に非同期コード分割を使っている時、このメソッドは賢くも、インクルードされるべき正しい非同期チャンクを推論します。 + +- `context.renderResourceHints()` + + - 必須 `clientManifest` + +このメソッドは、現在描画されているページに必要な `` リソースヒントを返します。 デフォルト設定ではこのようになります: + +- ページに必要な JavaScript や CSS ファイルをプリロードする +- あとで必要な非同期 JavaScript チャンクをプリフェッチする + + ファイルのプリロードは [`shouldPreload`](../api/#shouldpreload) オプションによってさらにカスタマイズが可能です。 + +- `context.getPreloadFiles()` + + - 必須 `clientManifest` + + このメソッドは文字列を返さない代わりに、プリロードされるべきアセットを表すファイルオブジェクトの配列を返します。これは HTTP/2 サーバプッシュをプログラムで行うときに使えるでしょう。 + +`createBundleRenderer` に渡された `template` は `context` を使って挿入されるので、これらのメソッドをテンプレート内で(`inject: false`で)使用することができます: + +```html + + + + {{{ renderResourceHints() }}} + {{{ renderStyles() }}} + + + + {{{ renderState() }}} + {{{ renderScripts() }}} + + +``` + +もし `template` を全く使っていないのなら、自分自身で文字列を結合することができます。 diff --git a/docs/ja/guide/bundle-renderer.md b/docs/ja/guide/bundle-renderer.md new file mode 100644 index 00000000..e8c7248f --- /dev/null +++ b/docs/ja/guide/bundle-renderer.md @@ -0,0 +1,52 @@ +# バンドルレンダラの紹介 + +## 根本的なサーバサイドレンダリングの問題 + +今までは、バンドルされたサーバサイドのコードが `require` によって直接使用されることを想定していました: + +```js +const createApp = require('/path/to/built-server-bundle.js') +``` + +これはとても簡単です。しかしながらアプリケーションのソースコードを編集する度に、あなたはサーバを停止し再起動させる必要があるでしょう。この作業は開発の生産性を著しく損ないます。さらに、Node.js はソースマップをネイティブサポートしていません。 + +## バンドルレンダラの追加 + +`vue-server-renderer` はこの問題を解決するために、`createBundleRenderer` という API を提供しています。また、webpack の拡張プラグインを利用することで、サーババンドルはバンドルレンダラに渡すことができる特別な JSON ファイルとして生成されます。バンドルレンダラが1度生成されると、使用方法は通常のレンダラと一緒ですが、次のような利点があります。 + +- ビルトインソースマップのサポート ( webpack の設定オプションに `devtool: 'source-map'` を指定) + +- 開発中、デプロイ中のホットリロード(更新されたバンドルや、再作成されたレンダラのインスタンスを読み込むだけです) + +- クリティカル CSS の評価 (`*.vue` ファイルを利用しているとき): インライン CSS は、描画中に利用されるコンポーネントによって必要とされます。詳細は [CSS](./css.md) を参照してください。 + +- [clientManifest](../api/#clientmanifest) によるアセットの注入: 自動的に最適なディレクティブが推測され、プリロードとプリフェッチを実行します。また、初期描画時にはコード分割チャンクを必要とします。 + +--- + +次のセクションでバンドルレンダラによって必要とされるビルドされたモノを生成するために、webpack 設定する方法について説明しますが、今既に必要なものがあるものとしましょう。これは、バンドルレンダラを使用する方法です: + +```js +const { createBundleRenderer } = require('vue-server-renderer') + +const renderer = createBundleRenderer(serverBundle, { + runInNewContext: false, // 推奨 + template, // (任意) ページテンプレート + clientManifest // (任意) クライアントビルドマニフェスト +}) + +// 内部のサーバ処理 ... +server.get('*', (req, res) => { + const context = { url: req.url } + // バンドルを実行することで自動作成されるため、ここでアプリケーションを渡す必要はありません + // 今、私たちのサーバーはVueアプリから切り離されています! + renderer.renderToString(context, (err, html) => { + // ハンドリングエラー ... + res.end(html) + }) +}) +``` + +バンドルレンダラによって`rendertoString` が呼び出されると、バンドルによってエクスポートされた関数が自動的に実行され、(引数として`context`を渡して)アプリケーションのインスタンスを生成し、描画処理を実行します。 + +`runInNewContext` オプションを `false` または `'once'` に設定することをお勧めします。詳細は [API リファレンス](../api/#runinnewcontext)を参照してください。 diff --git a/docs/ja/guide/caching.md b/docs/ja/guide/caching.md new file mode 100644 index 00000000..7877e12e --- /dev/null +++ b/docs/ja/guide/caching.md @@ -0,0 +1,87 @@ +# キャッシュ + +Vue の SSR は非常に高速ですが、コンポーネントインスタンスや仮想 DOM ノードの作成コストのため純粋な文字列ベースのテンプレートのパフォーマンスにはかないません。 SSR のパフォーマンスが重大である場合、キャッシュ戦略を賢く活用することで、応答時間が大幅に改善され、サーバーの負荷が軽減されます。 + +## ページレベルでのキャッシュ + +ほとんどの場合、サーバで描画されたアプリケーションは外部データに依存するため、コンテンツは本質的には動的であり長期間キャッシュすることはできません。しかしながら、コンテンツがユーザー固有のものでない場合、(すなわち、同一URLが常にすべてのユーザに対して同じコンテンツを描画する場合)、 [マイクロキャッシング](https://www.nginx.com/blog/benefits-of-microcaching-nginx/) という戦略を活用してアプリケーションのトラフィック処理能力を劇的に改善します。 + +これは通常 Nginx レイヤーで行われますが、 Node.js で実装することも可能です: + +```js +const microCache = LRU({ + max: 100, + maxAge: 1000 // 重要: コンテンツの登録内容は1秒後に期限切れになります +}) + +const isCacheable = req => { + // リクエストがユーザー固有のものかどうかチェックするロジックを実装します + // ユーザー固有でないページのみがキャッシュ可能です +} + +server.get('*', (req, res) => { + const cacheable = isCacheable(req) + if (cacheable) { + const hit = microCache.get(req.url) + if (hit) { + return res.end(hit) + } + } + + renderer.renderToString((err, html) => { + res.end(html) + if (cacheable) { + microCache.set(req.url, html) + } + }) +}) +``` + +コンテンツは1秒間だけキャッシュされるため、ユーザーに古いコンテンツが表示されることはありません。ただし、これはサーバーがキャッシュされたページごとに秒間最大1回の完全描画を実行するだけであることを意味します。 + +## コンポーネントレベルでのキャッシュ + +`vue-server-renderer` には、コンポーネントレベルのキャッシュ機能が組み込まれています。それを有効にするにはレンダラを作成する際に[キャッシュ実装](../api/#cache)を有効にする必要があります。代表的な使用例は [lru-cache](https://github.com/isaacs/node-lru-cache) を渡すことです: + +```js +const LRU = require('lru-cache') + +const renderer = createRenderer({ + cache: LRU({ + max: 10000, + maxAge: ... + }) +}) +``` + +次に `serverCacheKey` 関数を実装してコンポーネントをキャッシュすることが出来ます: + +```js +export default { + name: 'item', // required + props: ['item'], + serverCacheKey: props => props.item.id, + render (h) { + return h('div', this.item.id) + } +} +``` + +キャッシュ可能なコンポーネントは **一意の `name` オプションも定義しなければいけない**ことに注意してください。一意の名前を持つと、キャッシュキーがコンポーネントごとに異なるため、同一キーを返す2つのコンポーネントについて心配する必要はありません。 + +`serverCacheKey` から返されるキーは描画結果を表すのに十分な情報を含んでいる必要があります。描画結果が単に `props.item.id` によって決定される場合、上記は良い実装でしょう。しかしながら、同じIDを持つアイテムが時間の経過とともに変わる場合や描画結果が他のプロパティに依存する場合、 他の変数を考慮して `getCacheKey` の実装を修正する必要があります。 + +定数を返すと、コンポーネントは常にキャッシュされ、単なる静的なコンポーネントには効果的です。 + +### いつコンポーネントキャッシュを使うか + +描画中にレンダラがコンポーネントのキャッシュにヒットした場合、キャッシュされた結果をサブツリー全体で直接再利用します。 つまり、次の場合にコンポーネントをキャッシュ **しない** でください。 + +- グローバルな状態に依存する子コンポーネントがあります。 +- 描画 `context` に副作用をもたらす子コンポーネントがあります。 + +したがって、コンポーネントのキャッシングは、パフォーマンスのボトルネックに取り組むために慎重に適用する必要があります。 ほとんどの場合、単一インスタンスのコンポーネントをキャッシュする必要はなく、すべきではありません。キャッシングに適した最も一般的なコンポーネントのタイプは、大きな `v-for` リストで繰り返されるコンポーネントです。 これらのコンポーネントは通常、データベースコレクション内のオブジェクトを元にするため、一意のIDと最後に更新されたタイムスタンプを合わせて使用してキャッシュキーを生成するという単純なキャッシュ戦略を使用できます: + +```js +serverCacheKey: props => props.item.id + '::' + props.item.last_updated +``` diff --git a/docs/ja/guide/css.md b/docs/ja/guide/css.md new file mode 100644 index 00000000..a7ae4c58 --- /dev/null +++ b/docs/ja/guide/css.md @@ -0,0 +1,114 @@ +# CSS の管理 + +CSS を管理するためのおすすめの方法は、シンプルに単一ファイルコンポーネントである `*.vue` の中で `` を使うことができます。 + +もし `import 'foo.css'` のように JavaScriptからCSSをインポートしたいならば、適切な loader の設定が必要です: + +```js +module.exports = { + // ... + module: { + rules: [ + { + test: /\.css$/, + // 重要: style-loader の代わりに vue-style-loader を使用します + use: isProduction + ? ExtractTextPlugin.extract({ + use: 'css-loader', + fallback: 'vue-style-loader' + }) + : ['vue-style-loader', 'css-loader'] + } + ] + }, + // ... +} +``` + +## 依存関係からのスタイルのインポート + +NPM 依存で CSS をインポートするときに気を付けることがいくつかあります: + +1. サーバービルドで外部化しないでください。 + +2. もし CSS抽出 + `CommonsChunkPlugin` でベンダー抽出を使用している場合、抽出されたベンダーのチャンクに抽出された CSS があれば、`extract-text-webpack-plugin` に問題が発生します。この問題を解決するためには、ベンダーのチャンクに CSS ファイルを含めないでください。クライアント側の webpack の設定例です: + +```js + module.exports = { + // ... + plugins: [ + // deps をベンダーのチャンクに抽出してキャッシュを改善するのが一般的です + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module) { + // モジュールはベンダーチャンクに抽出されます... + return ( + // node_modules 内部な場合 + /node_modules/.test(module.context) && + // リクエストが CSS ファイルの場合、抽出しない + !/\.css$/.test(module.request) + ) + } + }), + // webpack ランタイム & マニフェストを抽出する + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest' + }), + // ... + ] + } +``` diff --git a/docs/ja/guide/data.md b/docs/ja/guide/data.md new file mode 100644 index 00000000..304abb21 --- /dev/null +++ b/docs/ja/guide/data.md @@ -0,0 +1,311 @@ +# データのプリフェッチと状態 + +## データストア + +SSR をしているとき、基本的にはアプリケーションの"スナップショット"を描画しています、したがって、アプリケーションがいくつかの非同期データに依存している場合においては、**それらのデータを、描画処理を開始する前にプリフェッチして解決する必要があります**。 + +もうひとつの重要なことは、クライアントサイドでアプリケーションがマウントされる前に、クライアントサイドで同じデータを利用可能である必要があるということです。そうしないと、クライアントサイドが異なる状態 (state) を用いて描画してしまい、ハイドレーションが失敗してしまいます。 + +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストア (data store) もしくは "状態コンテナ (state container)" に入っている必要があります。サーバーサイドでは描画する前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HTML に状態を埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれた状態を直接取得できます。 + +このような用途として、公式の状態管理ライブラリである [Vuex](https://github.com/vuejs/vuex/) を使っています。では `store.js` ファイルをつくって、そこに id に基づく item を取得するコードを書いてみましょう: + +```js +// store.js +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +// Promise を返すユニバーサルなアプリケーションを想定しています +// また、実装の詳細は割愛します +import { fetchItem } from './api' + +export function createStore () { + return new Vuex.Store({ + state: { + items: {} + }, + actions: { + fetchItem ({ commit }, id) { + // store.dispatch() 経由でデータがフェッチされたときにそれを知るために、Promise を返します + return fetchItem(id).then(item => { + commit('setItem', { id, item }) + }) + } + }, + mutations: { + setItem (state, { id, item }) { + Vue.set(state.items, id, item) + } + } + }) +} +``` + +そして `app.js` を更新します: + +```js +// app.js +import Vue from 'vue' +import App from './App.vue' +import { createRouter } from './router' +import { createStore } from './store' +import { sync } from 'vuex-router-sync' + +export function createApp () { + // ルーターとストアのインスタンスを作成します + const router = createRouter() + const store = createStore() + + // ルートの状態をストアの一部として利用できるよう同期します + sync(store, router) + + // アプリケーションのインスタンスを作成し、ルーターとストアの両方を挿入します + const app = new Vue({ + router, + store, + render: h => h(App) + }) + + // アプリケーション、ルーター、ストアを公開します + return { app, router, store } +} +``` + +## ロジックとコンポーネントとの結び付き + +ではデータをプリフェッチするアクションをディスパッチするコードはどこに置けばよいでしょうか? + +フェッチする必要があるデータはアクセスしたルート (route) によって決まります。またそのルートによってどのコンポーネントが描画されるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートで描画されるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に置くのが自然でしょう。 + +ルートコンポーネントではカスタム静的関数 `asyncData` が利用可能です。この関数はそのルートコンポーネントがインスタンス化される前に呼び出されるため `this` にアクセスできないことを覚えておいてください。ストアとルートの情報は引数として渡される必要があります: + +```html + + + + +``` + +## サーバーサイドのデータ取得 + +`entry-server.js` において `router.getMatchedComponents()` を使ってルートに一致したコンポーネントを取得できます。そしてコンポーネントが `asyncData` を利用可能にしていればそれを呼び出すことができます。そして描画のコンテキストに解決した状態を付属させる必要があります。 + +```js +// entry-server.js +import { createApp } from './app' + +export default context => { + return new Promise((resolve, reject) => { + const { app, router, store } = createApp() + + router.push(context.url) + + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + if (!matchedComponents.length) { + reject({ code: 404 }) + } + + // 一致したルートコンポーネントすべての asyncData() を呼び出します + Promise.all(matchedComponents.map(Component => { + if (Component.asyncData) { + return Component.asyncData({ + store, + route: router.currentRoute + }) + } + })).then(() => { + // すべてのプリフェッチのフックが解決されると、ストアには、 + // アプリケーションを描画するために必要とされる状態が入っています。 + // 状態を context に付随させ、`template` オプションがレンダラに利用されると、 + // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます + context.state = store.state + resolve(app) + }).catch(reject) + }, reject) + }) +} +``` + +`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL__` という形の状態として埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがその状態を取得します: + +```js +// entry-client.js + +const { app, router, store } = createApp() + +if (window.__INITIAL_STATE__) { + store.replaceState(window.__INITIAL_STATE__) +} +``` + +## クライアントサイドのデータ取得 + +クライアントサイドではデータ取得について 2つの異なるアプローチがあります: + +1. **ルートのナビゲーションの前にデータを解決する:** + +この方法では、アプリケーションは、遷移先のビューが必要とするデータが解決されるまで、現在のビューを保ちます。良い点は遷移先のビューがデータの準備が整い次第、フルの内容を直接描画できることです。しかしながら、データの取得に時間がかかるときは、ユーザーは現在のビューで「固まってしまった」と感じてしまうでしょう。そのため、この方法を用いるときにはローディングインジケーターを表示させることが推奨されます。 + +この方法は、クライアントサイドで一致するコンポーネントをチェックし、グローバルなルートのフック内で `asyncData` 関数を実行することにより実装できます。重要なことは、このフックは初期ルートが ready になった後に登録するということです。そうすれば、サーバーサイドで取得したデータをもう一度無駄に取得せずに済みます。 + +```js + // entry-client.js + + // ...関係のないコードは除外します + + router.onReady(() => { + // asyncData を扱うためにルーターのフックを追加します。これは初期ルートが解決された後に実行します + // そうすれば(訳注: サーバーサイドで取得したために)既に持っているデータを冗長に取得しなくて済みます + // すべての非同期なコンポーネントが解決されるように router.beforeResolve() を使います + router.beforeResolve((to, from, next) => { + const matched = router.getMatchedComponents(to) + const prevMatched = router.getMatchedComponents(from) + + // まだ描画されていないコンポーネントにのみ関心を払うため、 + // 2つの一致したリストに差分が表れるまで、コンポーネントを比較します + let diffed = false + const activated = matched.filter((c, i) => { + return diffed || (diffed = (prevMatched[i] !== c)) + }) + + if (!activated.length) { + return next() + } + + // もしローディングインジケーターがあるならば、 + // この箇所がローディングインジケーターを発火させるべき箇所です + + Promise.all(activated.map(c => { + if (c.asyncData) { + return c.asyncData({ store, route: to }) + } + })).then(() => { + + // ローディングインジケーターを停止させます + + next() + }).catch(next) + }) + + app.$mount('#app') + }) +``` + +1. **一致するビューが描画された後にデータを取得する:** + +この方法ではビューコンポーネントの `beforeMount` 関数内にクライアントサイドでデータを取得するロジックを置きます。こうすればルートのナビゲーションが発火したらすぐにビューを切り替えられます。そうすればアプリケーションはよりレスポンスが良いと感じられるでしょう。しかしながら、遷移先のビューは描画した時点では完全なデータを持っていません。したがって、この方法を使うコンポーネントの各々がローディング中か否かの状態を持つ必要があります。 + +この方法はクライアントサイド限定のグローバルな mixin で実装できます: + +```js + Vue.mixin({ + beforeMount () { + const { asyncData } = this.$options + if (asyncData) { + // データが準備できた後に、コンポーネント内で `this.dataPromise.then(...)` して + // 他のタスクを実行できるようにするため、Promise にフェッチ処理を割り当てます + this.dataPromise = asyncData({ + store: this.$store, + route: this.$route + }) + } + } + }) +``` + +これら 2つの方法のどちらを選ぶかは、究極的には異なる UX のどちらを選ぶかの判断であり、構築しようとしているアプリケーションの実際のシナリオに基づいて選択されるべきものです。しかし、どちらの方法を選択したかにかかわらず、ルートコンポーネントが再利用されたとき(つまりルートは同じだがパラメーターやクエリが変わったとき。例えば `user/1` から `user/2`) へ変わったとき)には `asyncData` 関数は呼び出されるようにすべきです。これはクライアントサイド限定のグローバルな mixin で処理できます: + +```js +Vue.mixin({ + beforeRouteUpdate (to, from, next) { + const { asyncData } = this.$options + if (asyncData) { + asyncData({ + store: this.$store, + route: to + }).then(next).catch(next) + } else { + next() + } + } +}) +``` + +## ストアコードの分割 + +大規模なアプリケーションでは、Vuex ストアは複数のモジュールに分割される可能性があります。もちろん、これらのモジュールを対応するルートコンポーネントチャンクにコード分割することもできます。次のストアモジュールがあるとします: + +```js +// store/modules/foo.js +export default { + namespaced: true, + // 重要: 状態は関数でなければならないため、 + // モジュールを複数回インスタン化できます + state: () => ({ + count: 0 + }), + actions: { + inc: ({ commit }) => commit('inc') + }, + mutations: { + inc: state => state.count++ + } +} +``` + +`store.registerModule` を使用して、ルートコンポーネントの `asyncData` フックにこのモジュールを遅延登録することができます: + +```html +// ルートコンポーネントの内部 + + +``` + +モジュールはルートコンポーネントの依存関係になっているので、webpack によってルートコンポーネントの非同期チャンクに移動されます。 + +--- + +ふぅ、コードが長いですね。これはどうしてかというと、ユニバーサルなデータ取得は、大抵の場合、サーバーサイドで描画するアプリケーションの最も複雑な問題であり、また、今後、スムーズに開発を進めていくための下準備をしているためです。一旦ひな形が準備できてしまえば、あとは、それぞれのコンポーネントを記述していく作業は、実際のところ実に楽しいものになるはずです。 diff --git a/docs/ja/guide/head.md b/docs/ja/guide/head.md new file mode 100644 index 00000000..5161b4d0 --- /dev/null +++ b/docs/ja/guide/head.md @@ -0,0 +1,90 @@ +# ヘッドの管理 + +アセットの挿入と同様に、ヘッド (Head) の管理も同じ考えに追従しています。つまり、コンポーネントのライフサイクルの描画 `context` に動的にデータを付随させ、そして `template` 内にデータを展開 (interpolate) できるという考えです。 + +> バージョン >=2.3.2 では、`this.$ssrContext` としてコンポーネントにおいて SSR コンテキストに直接アクセスできます。古いバージョンでは、`createApp()` によって手動で SSR コンテキストを渡して注入し、ルート (root) インスタンスの `$options` に公開する必要があります。子は、`this.$root.$options.ssrContext` を介してそれにアクセスすることができます。 + +タイトルを管理する単純な mixin を書くことができます: + +```js +// title-mixin.js + +function getTitle (vm) { + // コンポーネントはシンプルに `title` オプションを提供し、 + // これには文字列または関数を入れることができます + const { title } = vm.$options + if (title) { + return typeof title === 'function' + ? title.call(vm) + : title + } +} + +const serverTitleMixin = { + created () { + const title = getTitle(this) + if (title) { + this.$root.$options.ssrContext.title = title + } + } +} + +const clientTitleMixin = { + mounted () { + const title = getTitle(this) + if (title) { + document.title = title + } + } +} + +// VUE_ENV は webpack.DefinePlugin を使って注入できます +export default process.env.VUE_ENV === 'server' + ? serverTitleMixin + : clientTitleMixin +``` + +このようにすれば、ルート (route) コンポーネントはドキュメントのタイトルを制御するために context を利用することができます。 + +```js +// Item.vue +export default { + mixins: [titleMixin], + title () { + return this.item.title + }, + + asyncData ({ store, route }) { + return store.dispatch('fetchItem', route.params.id) + }, + + computed: { + item () { + return this.$store.state.items[this.$route.params.id] + } + } +} +``` + +そしてタイトルは `template` 内でバンドルレンダラに渡されます: + +```html + + + Codestin Search App + + + ... + + +``` + +**メモ:** + +- XSS 攻撃を防ぐために double-mustache(HTML エスケープした展開)を使うこと。 + +- 描画中にタイトルをセットするコンポーネントがない場合に備えて、`context` オブジェクトを作成する際にはデフォルトのタイトルをセットするようにすべきです。 + +--- + +同様のやり方で、この mixin を包括的にヘッドを管理するユーティリティに容易に拡張できます。 diff --git a/docs/ja/guide/hydration.md b/docs/ja/guide/hydration.md new file mode 100644 index 00000000..e67dfcbd --- /dev/null +++ b/docs/ja/guide/hydration.md @@ -0,0 +1,32 @@ +# クライアントサイドでのハイドレーション + +`entry-client.js` において、以下の記述で私たちは簡単にアプリケーションをマウントします: + +```js +// これは、ルート要素に id="app" をもつ App.vue テンプレートを想定します +app.$mount('#app') +``` + +サーバがマークアップを描画後に、この処理を実行し、すべての DOM を再生成することを私たちは当然したくありません。代わりに、静的なマークアップの"ハイドレート (hydrate)"とそれをインタラクティブに生成したいです。 + +サーバの描画出力を調べたら、アプリケーションのルート要素が以下のような特別な属性を持っていることに気づくでしょう: + +```js +
+``` + +この `data-server-rendered` という特別な属性は、クライアントサイドの Vue に、これがサーバ上で描画されたことを知らせ、この要素はハイドレーションモードでマウントされるはずです。`id="app"` に、単に `data-server-rendered` が追加されていないことに注意してください。ID または 他のセレクタを自分自身のルート要素に追加する必要があります。そうしないと、アプリケーションが適切にハイドレーションできなくなります。 + +開発モードでは、Vue はクラインアントサイドで生成された仮想 DOM が、サーバで描画された DOM の構成と一致しているか検証を行います。もしこれが一致していない場合、ハイドレーションを取りやめ、元の DOM を無視しスクラッチから描画を行います。**プロダクションモードでは、パフォーマンスの最大化のため、このアサーションは無効になります。** + +### ハイドレーション時の注意 + +サーバサイドの描画とクライアントサイドでのハイドレーションを行なった場合、ある特定の HTML の構造はブラウザによって変換されるかもしれないことがわかっています。例えば、あなたが Vue のテンプレート内に、以下のような記述をした場合です: + +```html + + +
hi
+``` + +ブラウザは、自動で `` を `` に挿入します。しかし、Vue によって生成された仮想 DOM は、`` を含みません。そのため、不一致が起こります。正しいマッチングを保証するために、あなたのテンプレート内では、必ず有効な HTML を記述してください。 diff --git a/docs/ja/guide/non-node.md b/docs/ja/guide/non-node.md new file mode 100644 index 00000000..2f7e6c1a --- /dev/null +++ b/docs/ja/guide/non-node.md @@ -0,0 +1,41 @@ +# 非 Node.js 環境における使用 + +`vue-server-renderer` の標準ビルドは Node.js 環境を想定していますが、これは、[PHP V8Js](https://github.com/phpv8/v8js) や [Oracle Nashorn](https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/) のような別のJavaScript 環境では使用できなくなります。2.5 のにおいて、環境にはほとんど影響されない、 `vue-server-renderer/basic.js` のビルドを出荷しました。これにより、上記の環境で使用できるようになります。 + +どちらの環境に対して、それは `global` と `process` オブジェクトをモックすることによって最初に環境を準備する必要があり、 `process.env.VUE_ENV` に `"server"` を設定し、そして `process.env.NODE_ENV` に `"development"` または {code6}"production"{/code6} を設定します。 + +Nashornでは、Java のネイティブタイマーを使用して、 `Promise{/ code0} または setTimeout` のポリフィルを提供する必要があります。 + +php-v8js での使用例: + +```php +executeString('var process = { env: { VUE_ENV: "server", NODE_ENV: "production" }}; this.global = { process: process };'); +$v8->executeString($vue_source); +$v8->executeString($renderer_source); +$v8->executeString($app_source); +?> +``` + +--- + +```js +// app.js +var vm = new Vue({ + template: `
{{ msg }}
`, + data: { + msg: 'hello' + } +}) + +// `vue-server-renderer/basic.js` によってエクスポーズ +renderVueComponentToString(vm, (err, res) => { + print(res) +}) +``` diff --git a/docs/ja/guide/routing.md b/docs/ja/guide/routing.md new file mode 100644 index 00000000..1a632773 --- /dev/null +++ b/docs/ja/guide/routing.md @@ -0,0 +1,153 @@ +# ルーティングとコード分割 + +## `vue-router` によるルーティング + +サーバーコードが任意の URL を受け入れる `*` ハンドラを使用していることに気付いたかもしれません。これにより訪れた URL を Vue アプリケーションに渡し、クライアントとサーバーの両方に同一のルーティング設定を再利用することが可能になります! + +この目的のために公式の `vue-router` を使用することが推奨されています。まずはルーターを作成するファイルを作成しましょう。 `createApp` に似ていますが、 リクエストごとに新たなルーターインスタンスも必要となるため、 `createRouter` 関数をエクスポートします: + +```js +// router.js +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +export function createRouter () { + return new Router({ + mode: 'history', + routes: [ + // ... + ] + }) +} +``` + +そして `app.js` を更新します: + +```js +// app.js +import Vue from 'vue' +import App from './App.vue' +import { createRouter } from './router' + +export function createApp () { + // ルーターインスタンスを作成します + const router = createRouter() + + const app new Vue({ + // ルーターをルートVueインスタンスに注入します + router, + render: h => h(App) + }) + + // アプリケーションとルーターの両方を返します + return { app, router } +} +``` + +`entry-server.js` にサーバー側のルーティングロジックを実装する必要があります: + +```js +// entry-server.js +import { createApp } from './app' + +export default context => { + // 非同期のルートフックまたはコンポーネントが存在する可能性があるため、 + // 描画する前にすべての準備が整うまでサーバーが待機できるように + // プロミスを返します + return new Promise((resolve, reject) => { + const { app, router } = createApp() + + // サーバーサイドのルーターの場所を設定します + router.push(context.url) + + // ルーターが非同期コンポーネントとフックを解決するまで待機します + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + // 一致するルートがない場合、404で拒否します + if (!matchedComponents.length) { + reject({ code: 404 }) + } + + // プロミスは描画できるようにアプリケーションインスタンスを解決するべきです + resolve(app) + }, reject) + }) +} +``` + +サーバーバンドルがすでにビルドされていると仮定すると(再度になりますが、今はビルド設定は無視します)、サーバーでの使用方法は次のようになります: + +```js +// server.js +const createApp = require('/path/to/built-server-bundle.js') + +server.get('*', (req, res) => { + const context = { url: req.url } + + createApp(context).then(app => { + renderer.renderToString(app, (err, html) => { + if (err) { + if (err.code === 404) { + res.status(404).end('Page not found') + } else { + res.status(500).end('Internal Server Error') + } + } else { + res.end(html) + } + }) + }) +}) +``` + +## コード分割 + +コード分割やアプリケーションの部分的な遅延ローディングは初期描画のためにブラウザがダウンロードする必要のあるアセットの量を減らすのに役立ち、巨大なバンドルを持つアプリケーションの TTI (操作可能になるまでの時間)を大幅に改善します。重要なことは初期画面では"必要なものだけを読み込む"ということです。 + +Vue は非同期コンポーネントを最重要コンセプトとして提供しており、 [webpack 2の動的インポートをコード分割点として使用することへのサポート](https://webpack.js.org/guides/code-splitting-async/) と組み合わせることも可能です。そのためにすべきことは以下です: + +```js +// これを... +import Foo from './Foo.vue' + +// このように変えます +const Foo = () => import('./Foo.vue') +``` + +純粋なクライアントサイドの Vue アプリケーションを構築する場合、これはどんなシナリオでも機能するでしょう。ただし、これをサーバーサイドの描画で使用する場合はいくつかの制限があります。まず、描画を開始する前にサーバー上のすべての非同期コンポーネントを先に解決する必要があります。そうしなければ、マークアップ内に空のプレースホルダが表示されます。クライアント側では、ハイドレーションを開始する前にこれを行う必要があります。そうしなければ、クライアントはコンテンツの不一致エラーに陥ります。 + +アプリケーション内の任意の場所で非同期コンポーネントを使用するのは少し難解です(これは将来的に改善される可能性があります)。 ただし、**ルート (route) レベルで行うとシームレスに動作します**(すなわち、ルート設定で非同期コンポーネントを使用する)。ルートを解決する際に、 `vue-router` は一致した非同期コンポーネントを自動的に解決するためです。 必要なことは、サーバーとクライアントの両方で `router.onReady` を使用することです。すでにサーバーのエントリーで行ったので、クライアントのエントリーを更新するだけです。 + +```js +// entry-client.js + +import { createApp } from './app' + +const { app, router } = createApp() + +router.onReady(() => { + app.$mount('#app') +}) +``` + +非同期ルートコンポーネントを使用したルート設定の例: + +```js +// router.js +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +export function createRouter () { + return new Router({ + mode: 'history', + routes: [ + { path: '/', component: () => import('./components/Home.vue') }, + { path: '/item/:id', component: () => import('./components/Item.vue') } + ] + }) +} +``` diff --git a/docs/ja/guide/streaming.md b/docs/ja/guide/streaming.md new file mode 100644 index 00000000..06aeb7f1 --- /dev/null +++ b/docs/ja/guide/streaming.md @@ -0,0 +1,33 @@ +# ストリーミング + +`vue-server-renderer` は、基本的なレンダラとバンドルレンダラの両方のためにストリームによる描画をサポートします。`renderToString` の代わりに`renderToStream`を使用するだけです: + +```js +const stream = renderer.renderToStream(context) +``` + +返り値は [Node.js stream](https://nodejs.org/api/stream.html) です: + +```js +let html = '' + +stream.on('data', data => { + html += data.toString() +}) + +stream.on('end', () => { + console.log(html) // 完全に描画 +}) + +stream.on('error', err => { + // エラーを処理 ... +}) +``` + +## ストリーミングに関する注意事項 + +ストリームによる描画モードでは、レンダラが仮想 DOM ツリーを横断するときに、できるだけ早くデータを出力します。つまり、より早く「最初のチャンク」を取得し、それをクライアントにすばやく出力し始めることを意味します。 + +しかし、最初のデータチャンクが発行したときに子コンポーネントがまだインスタンス化されていないと、ライフサイクルフックが呼び出されることはありません。つまり、子コンポーネントがライフサイクルフック内の描画コンテキストにデータを添付する必要がある場合、これらのデータはストリームの開始時に使用できません。アプリケーションは全体の HTML の表示の前に多くのコンテキスト情報(ヘッド情報やインラインに書かれたクリティカル CSS など)を表示する必要があるため、これらのコンテキストデータを使用する前にストリームの完了を待つ必要があります。 + +したがって、コンポーネントライフサイクルフックによって読み込まれたコンテキストデータに依存する場合は、ストリーミングモードを使用することは**推奨されません**。 diff --git a/docs/ja/guide/structure.md b/docs/ja/guide/structure.md new file mode 100644 index 00000000..0d203bd2 --- /dev/null +++ b/docs/ja/guide/structure.md @@ -0,0 +1,122 @@ +# ソースコードの構造 + +## ステートフルなシングルトンの回避 + +クライアントのみのコードを書くとき、私たちはコードが毎回新しいコンテキストで評価されるという事実に慣れています。しかし、 Node.js サーバーは長時間実行されるプロセスです。私たちのコードがプロセスに要求されるとき、それは一度評価されメモリにとどまります。つまりシングルトンのオブジェクトを作成したとき、それは全ての受信リクエスト間でシェアされると言うことです。 + +基本的な例としては、私たちは **リクエストごとに新しいルート Vue インスタンスを作成します**。それは各ユーザがそれぞれのブラウザでアプリケーションの新しいインスタンスを使用することに似ています。もし私たちが複数のリクエストをまたいでインスタンスを共有すると、それは容易にクロスリクエスト状態の汚染につながるでしょう。 + +そのため、直接アプリケーションのインスタンスを作成するのではなく、各リクエストで繰り返し実行される新しいアプリケーションのインスタンスを作成するファクトリ関数を公開する必要があります: + +```js +// app.js +const Vue = require('vue') + +module.exports = function createApp (context) { + return new Vue({ + data: { + url: context.url + }, + template: `
The visited URL is: {{ url }}
` + }) +} +``` + +そして私たちのサーバーコードはこうなります: + +```js +// server.js +const createApp = require('./app') + +server.get('*', (req, res) => { + const context = { url: req.url } + const app = createApp(context) + + renderer.renderToString(app, (err, html) => { + // エラーを処理 ... + res.end(html) + }) +}) +``` + +同じルールがルータ、ストア、イベントバスのインスタンスに適用されます。モジュールから直接エクスポートしアプリケーションにインポートするのでは無く、 `createApp` で新しいインスタンスを作成し、ルート (root) Vue インスタンスから注入する必要があります。 + +> `{ runInNewContext: true }` でバンドルレンダラを使用するとき、その制約を取り除くことが可能です。しかし各リクエストに対して新しい VM コンテキストを作成する必要があるため、いくらか重大なパフォーマンスコストがかかります。 + +## ビルドステップの紹介 + +これまでは、同じ Vue アプリケーションをクライアントへ配信する方法を論じてはいませんでした。これを行うには、webpack を使用して Vue アプリケーションをバンドルする必要があります。実際、webpack を使用して Vue アプリケーションをサーバーにバンドルしたいと思っているのはおそらく次の理由によるものです。 + +- 典型的な Vue アプリケーションは webpack と `vue-loader` によってビルドされ、 `file-loader` 経由でのファイルのインポートや`css-loader` 経由でCSSをインポートなどの多くの webpack 固有の機能は Node.jsで直接動作しません。 + +- Node.jsの最新バージョンはES2015の機能を完全にサポートしていますが、古いブラウザに対応するためにクライアントサイドのコードをトランスパイルする必要があります。これはビルドステップにも再び関係します。 + +従って基本的な考え方は webpack を使用してクライアントとサーバー両方をバンドルすることです。サーバーバンドルはサーバーによって SSR のために要求され、クライアントバンドルは静的なマークアップのためにブラウザに送信されます。 + +![architecture](https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png) + +セットアップの詳細については次のセクションで議論されます。今のところ、ビルドのセットアップが分かっていると仮定すると、webpack を有効にして Vue アプリケーションコードを書くことが可能になっています。 + +## webpackによるコード構造 + +webpack を使用してサーバーとクライアントのアプリケーションを処理しているので、ソースコードの大部分はユニバーサルに書かれており、すべての webpack の機能にアクセスできます。 同時に、ユニバーサルなコードを書くときに留意すべき[いくつかあります。](./universal.md) + +シンプルなプロジェクトは以下のようになります: + +```bash +src +├── components +│   ├── Foo.vue +│   ├── Bar.vue +│   └── Baz.vue +├── App.vue +├── app.js # 共通のエントリ +├── entry-client.js # ブラウザでのみ実行されます +└── entry-server.js # サーバでのみ実行されます +``` + +### `app.js` + +`app.js` はアプリケーションのユニバーサルエントリーです。クライアントアプリケーションでは、このファイルにルート Vue インスタンスを作成し、DOM に直接マウントします。しかし、SSRの場合は責務はクライアント専用のエントリファイルに映されます。`app.js` はシンプルに `createApp` 関数をエクスポートします: + +```js +import Vue from 'vue' +import App from './App.vue' + +// 新しいアプリケーション、ルータ、ストアを作成するためのファクトリ関数をエクスポートします +// インスタンス +export function createApp () { + const app = new Vue({ + // ルートインスタンスは単に App コンポーネントを描画します + render: h => h(App) + }) + return { app } +} +``` + +### `entry-client.js`: + +クライアントエントリは単にアプリケーションを作成しそれをDOMにマウントします: + +```js +import { createApp } from './app' + +// クライアント固有の初期化ロジック + +const { app } = createApp() +// これは App.vue テンプレートのルート要素が id="app" だからです。 +app.$mount('#app') +``` + +### `entry-server.js`: + +サーバーエントリは描画ごとに繰り返し呼び出すことができる関数をデフォルトでエクスポートします。現時点では、アプリケーションインスタンスを作成して返す以外のことはほとんど行いませんが、後でサーバーサイドのルートマッチングとデータプリフェッチロジックを実行します。 + +```js +import { createApp } from './app' + +export default context => { + const { app } = createApp() + return app +} +``` diff --git a/docs/ja/guide/universal.md b/docs/ja/guide/universal.md new file mode 100644 index 00000000..bf569a5f --- /dev/null +++ b/docs/ja/guide/universal.md @@ -0,0 +1,32 @@ +# ユニバーサルなコードを書く + +サーバサイドの描画について、さらに見ていく前に、"ユニバーサル"なコード(サーバーとクライアントの両方で動作するコード)を記述するときの制約について考えてみましょう。ユースケースとプラットフォームの API の相違により、異なる環境で実行したコードの動作は全く同じものにはなりません。ここでは、サーバサイドの描画を行う上で、知っておく必要がある重要な項目について説明します。 + +## サーバー上でのデータリアクティビテイ + +クライアントだけで実行するアプリでは、全てのユーザーがブラウザでアプリケーションの新しいインスタンスを使用します。サーバサイドの描画でも、同じ振る舞いが必要とされます: すなわち、複数のリクエストに跨った状態の汚染がないよう各リクエストは新しく独立したアプリケーションのインスタンスが必要になります。 + +実際の描画プロセスは決定的であることが求められるので、サーバー上でデータを"プリフェッチ"することもあります。これは描画を開始する時、アプリケーションの状態は既に解決済みであることを意味します。つまり、サーバー上では、データがリアクティブである必要はないので、デフォルトで無効になっています。データをリアクティブにしないことで、データをリアクティブなオブジェクトに変換する際のパフォーマンスコストを無視できます。 + +## コンポーネントのライフサイクルフック + +動的な更新がないので、ライフサイクルフックのうち、`beforeCreate` と `created` のみが SSR 中に呼び出されます。つまり、 `beforeMount` や `mounted` などの他のコンポーネントサイクルフックは、クライアントでのみ実行されます。 + +注意すべきもう一つは、`beforeCreate` と `created` において、例えば `setInterval` でタイマーを設定するような、グローバルな副作用を引き起こすコード避けるべきです。クライアントサイドのみのコードでは、タイマーを設定してから `beforeDestroy` または `destroyed` したりすることができます。しかしながら、SSR 中に破棄フックは呼び出されないため、タイマーは永遠に残ります。 これを回避するために、代わりに副作用コードを `beforeMount` または `mounted` に移動してください。 + +## プラットフォーム固有の API にアクセスする + +ユニバーサルコードでは、プラットフォーム固有の API へのアクセスは想定されていないので、`window` や `document` といったブラウザ環境のグローバル変数を直接使用すると、Node.js ではエラーが発生します。 + +サーバーとクライアントでコードを共有するものの、タスクが使用する API がプラットフォームによって異なる場合は、プラットフォーム固有の実装を ユニバーサル な API の内部でラップするか、それを行うライブラリを使用することをお勧めします。例えば、[axios](https://github.com/mzabriskie/axios) は、サーバーとクライアントの両方に同じ API を提供する HTTP クライアントです。 + +ブラウザ API を利用する際の一般的なアプローチは、クライアントだけで実行されるライフサイクルの中で遅延的にアクセスすることです。 + +サードパーティライブラリがユニバーサルに使用することを考慮していない場合、それをサーバサイドによって描画されるアプリケーションに統合することは難しいので注意してください。グローバル変数のいくつかをモックすることで動かすことができるようになる *かも*しれませんが、それはハックであり、他のライブラリの環境検出のコードを妨げる恐れがあります。 + +## カスタムディレクティブ + +ほとんどの カスタムディレクティブ は直接 DOM を操作するため、SSR 中にエラーが発生します。これを回避するには、2つの方法があります: + +1. 抽象化の仕組みとしてコンポーネントを使用し、カスタムディレクティブの代わりに仮想 DOM レベル(例えば、render 関数を使用すること)で実装することをお勧めします。 +2. コンポーネントに簡単に置き換えができないカスタムディレクティブの場合、サーバーレンダラを生成する際の [`directives`](../api/#directives) オプションを使用して、そのオプションの "サーバーサイドのバージョン" を用意することで回避できます。 From ec7f9c331ba5db57ccc27e9f958c504a272f36dd Mon Sep 17 00:00:00 2001 From: Alexander Sokolov Date: Mon, 28 May 2018 17:35:21 +0300 Subject: [PATCH 08/70] Russian translation moved to VuePress (#184) --- docs/.vuepress/config.js | 36 ++++ docs/.vuepress/public/_redirects | 5 + docs/ru/README.md | 53 ++++++ docs/ru/api/README.md | 270 ++++++++++++++++++++++++++ docs/ru/guide/README.md | 157 +++++++++++++++ docs/ru/guide/build-config.md | 215 +++++++++++++++++++++ docs/ru/guide/bundle-renderer.md | 52 +++++ docs/ru/guide/caching.md | 87 +++++++++ docs/ru/guide/css.md | 114 +++++++++++ docs/ru/guide/data.md | 316 +++++++++++++++++++++++++++++++ docs/ru/guide/head.md | 90 +++++++++ docs/ru/guide/hydration.md | 34 ++++ docs/ru/guide/non-node.md | 41 ++++ docs/ru/guide/routing.md | 153 +++++++++++++++ docs/ru/guide/streaming.md | 33 ++++ docs/ru/guide/structure.md | 123 ++++++++++++ docs/ru/guide/universal.md | 33 ++++ 17 files changed, 1812 insertions(+) create mode 100644 docs/ru/README.md create mode 100644 docs/ru/api/README.md create mode 100644 docs/ru/guide/README.md create mode 100644 docs/ru/guide/build-config.md create mode 100644 docs/ru/guide/bundle-renderer.md create mode 100644 docs/ru/guide/caching.md create mode 100644 docs/ru/guide/css.md create mode 100644 docs/ru/guide/data.md create mode 100644 docs/ru/guide/head.md create mode 100644 docs/ru/guide/hydration.md create mode 100644 docs/ru/guide/non-node.md create mode 100644 docs/ru/guide/routing.md create mode 100644 docs/ru/guide/streaming.md create mode 100644 docs/ru/guide/structure.md create mode 100644 docs/ru/guide/universal.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 01e9834c..42e5e8eb 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -14,6 +14,11 @@ module.exports = { lang: 'ja', title: 'Vue SSR ガイド', description: 'Vue.js サーバーサイドレンダリングガイド' + }, + '/ru/': { + lang: 'ru', + title: 'Руководство Vue SSR', + description: 'Руководство по серверному рендерингу Vue.js' } }, serviceWorker: true, @@ -113,6 +118,37 @@ module.exports = { '/ja/guide/streaming', '/ja/guide/non-node' ] + }, + '/ru/': { + label: 'Русский', + selectText: 'Languages', + editLinkText: 'Изменить эту страницу на GitHub', + nav: [ + { + text: 'Руководство', + link: '/ru/guide/' + }, + { + text: 'Справочник API', + link: '/ru/api/' + } + ], + sidebar: [ + ['/ru/', 'Введение'], + '/ru/guide/', + '/ru/guide/universal', + '/ru/guide/structure', + '/ru/guide/routing', + '/ru/guide/data', + '/ru/guide/hydration', + '/ru/guide/bundle-renderer', + '/ru/guide/build-config', + '/ru/guide/css', + '/ru/guide/head', + '/ru/guide/caching', + '/ru/guide/streaming', + '/ru/guide/non-node' + ] } } } diff --git a/docs/.vuepress/public/_redirects b/docs/.vuepress/public/_redirects index dabdceb1..5e2ecf29 100644 --- a/docs/.vuepress/public/_redirects +++ b/docs/.vuepress/public/_redirects @@ -7,3 +7,8 @@ /zh/api.html /zh/api/ /zh/basic.html /zh/guide/ /zh/* /zh/guide/:splat + +/ru/ /ru/ +/ru/api.html /ru/api/ +/ru/basic.html /ru/guide/ +/ru/* /ru/guide/:splat \ No newline at end of file diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 00000000..62da90c0 --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,53 @@ +# Руководство по серверному рендерингу Vue.js + +::: tip Примечание +Для этого руководства требуются следующие версии Vue и библиотек: + +- vue & vue-server-renderer 2.3.0+ +- vue-router 2.5.0+ +- vue-loader 12.0.0+ & vue-style-loader 3.0.0+ + +Если вы ранее использовали Vue 2.2 с серверным рендерингом, вы заметите, что рекомендуемая структура кода теперь [немного отличается](./guide/structure.md) (с новой опцией [runInNewContext](./api/#runinnewcontext), установленной в `false`). Ваше существующее приложение по-прежнему будет работать, но лучше внесите изменения с учётом новых рекомендаций. +::: + +## Что такое серверный рендеринг (SSR)? + +Vue.js — это фреймворк для создания приложений, выполняемых на клиенте (client-side). По умолчанию компоненты Vue создают и манипулируют DOM в браузере. Однако, также возможно рендерить те же компоненты в HTML-строки на сервере, отправлять их в браузер, и наконец «гидрировать» статическую разметку в полностью интерактивное приложение на клиенте. + +Приложение Vue.js отрендеренное на сервере также можно считать «изоморфным» или «универсальным», в том смысле, что большая часть кода приложения **является общей для сервера и клиента**. + +## Нужен ли вам SSR? + +По сравнению с традиционным SPA (Single-Page Application), преимуществами серверного рендеринга будут: + +- Лучшее SEO, поскольку поисковые роботы будут видеть полностью отрендеренную страницу. + + Обратите внимание, что на данный момент Google and Bing могут без проблем индексировать синхронные приложения JavaScript. Ключевое слово здесь — синхронные. Если ваше приложение запускается с индикатором загрузки, а потом догружает контент через Ajax, то поисковый робот просто не будет дожидаться окончания загрузки. Это значит, что если у вас есть асинхронный контент на страницах где SEO важен, то может потребоваться серверный рендеринг. + +- Лучшие показатели времени до отображения контента (time-to-content), особенно при плохом интернете или на медленных устройствах. Для разметки, отрендеренной на сервере, не требуется дожидаться пока весь JavaScript будет загружен и выполнен, поэтому ваш пользователь увидит полностью отрендеренную страницу раньше. Как правило, это приводит к лучшему пользовательскому опыту и может быть критичным для приложений, где время до отображения контента напрямую связано с коэффициентом конверсии. + +Следует учитывать и некоторые компромиссы при использовании серверного рендеринга: + +- Ограничения при разработке. Код только для браузера может быть использован лишь в определённых хуках жизненного цикла; некоторые внешние библиотеки могут нуждаться в особой обработке, чтобы иметь возможность запускаться в приложении с серверным рендерингом. + +- Более сложные требования по настройке и развёртыванию сборки. В отличие от полностью статичного SPA, который может быть развёрнут на любом статичном файловом сервере, приложение с серверным рендерингом требует окружения, где есть возможность запустить сервер Node.js. + +- Повышенная нагрузка на стороне сервера. Рендеринг полноценного приложения в Node.js очевидно более требователен к ресурсам процессора, чем простая раздача статичных файлов, поэтому если вы ожидаете большой трафик, будьте готовы к соответствующей нагрузке на сервер и используйте стратегии кэширования. + +Прежде чем использовать серверный рендеринг для вашего приложения, задайте себе вопрос, действительно ли он вам нужен. Ответ зависит от того, насколько важно время до контента для вашего приложения. Например, если вы разрабатываете панель мониторинга для внутренних нужд, где дополнительные несколько сотен миллисекунд начальной загрузки не так важны, то серверный рендеринг будет излишеством. Однако, в тех случаях, когда время до контента критично, серверный рендеринг может позволит достичь наилучшей производительности начальной загрузки. + +## SSR vs Пререндеринг + +Если вы интересуетесь серверным рендерингом только для того, чтобы улучшить SEO на нескольких маркетинговых страницах (например, `/`, `/about`, `/contact`, и т.д.), вам скорее всего будет достаточно __пререндеринга__. Вместо того, чтобы заставлять веб-сервер компилировать HTML на лету, пререндеринг просто сгенерирует статичные HTML-файлы для указанных маршрутов на этапе сборки. Преимуществом пререндеринга будет простота реализации, кроме того этот подход позволит вам оставить фронтенд полностью статичным. + +Если вы используете Webpack, то для добавления пререндеринга достаточно установить плагин [prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin). Он был тщательно протестирован с приложениями Vue, а его [создатель](https://github.com/chrisvfritz) — член основной команды разработки Vue. + +## Об этом руководстве + +Это руководство ориентировано на SPA приложения с рендерингом на сервере, используя Node.js в качестве сервера. Использование серверного рендеринга Vue совместно с другими технологиями и настройками бэкэнда являются отдельной темой и кратко обсуждается в [отдельном разделе](./guide/non-node.md). + +Это руководство будет очень детальным и предполагает, что вы уже знакомы с самим Vue.js, имеете знания и опыт работы с Node.js и Webpack. Если вы предпочитаете более высокоуровневые решения, обеспечивающие работу из коробки — вам следует попробовать [Nuxt.js](https://nuxtjs.org/). Он построен на том же стеке Vue, но позволяет абстрагироваться от написания шаблонного кода, а также предоставляет некоторые дополнительные возможности, такие как генерация статичного сайта. Однако он может не подойти, если вам необходим полный контроль над структурой приложения. В любом случае, вам будет полезно прочитать это руководство, чтобы лучше понимать, как все составляющие работают вместе. + +По мере прочтения руководства, будет полезным обращаться к официальному [демо HackerNews](https://github.com/vuejs/vue-hackernews-2.0/), в котором используется большинство техник, изложенных в этом руководстве. + +Наконец, обратите внимание, что решения в этом руководстве не являются окончательными — мы решили, что они хорошо работают для нас, но это не означает, что они не могут быть улучшены. Они могут быть пересмотрены в будущем — поэтому не стесняйтесь вносить свой вклад, отправляя пулл-реквесты! diff --git a/docs/ru/api/README.md b/docs/ru/api/README.md new file mode 100644 index 00000000..2a0aaaa8 --- /dev/null +++ b/docs/ru/api/README.md @@ -0,0 +1,270 @@ +--- +sidebar: auto +--- + +# Справочник API + +## createRenderer + +Создаёт экземпляр [`Renderer`](#class-renderer) с (опциональными) [настройками](#renderer-options). + +``` js +const { createRenderer } = require('vue-server-renderer') +const renderer = createRenderer({ /* настройки */ }) +``` + +## createBundleRenderer + +Создаёт экземпляр [`BundleRenderer`](#class-bundlerenderer) с сборкой сервера и (опциональными) [настройками](#renderer-options). + +``` js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer(serverBundle, { /* настройки */ }) +``` + +Аргумент `serverBundle` может быть одним из следующих: + +- Абсолютный путь к созданному файлу сборки (`.js` или `.json`). Должен начинаться с `/`, чтобы трактоваться как путь к файлу. + +- Объект сборки, сгенерированный Webpack + `vue-server-renderer/server-plugin`. + +- Строка с кодом JavaScript (не рекомендуется). + +Подробнее в разделах [Представляем Bundle Renderer](../guide/bundle-renderer.md) и [Конфигурация сборки](../guide/build-config.md). + +## Класс: Renderer + +### renderer.renderToString + +Сигнатура: + +``` js +renderer.renderToString(vm, context?, callback?): ?Promise +``` + +Рендерит экземпляр Vue в строку. Объект контекста опционален. Коллбэк является обычным для Node.js коллбэком, где первый аргумент является ошибкой, а второй аргумент — отрендеренной строкой. + +С версии 2.5.0+ коллбэк является опциональным. Когда коллбэк не указан, метод возвращает Promise, который разрешается отрендеренным HTML. + +### renderer.renderToStream + +Сигнатура: + +``` js +renderer.renderToStream(vm[, context]): stream.Readable +``` + +Рендерит экземпляр Vue в [Node.js readable stream](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams). Объект контекста опционален. Подробнее в разделе [Стриминг](../guide/streaming.md). + +## Класс: BundleRenderer + +### bundleRenderer.renderToString + +Сигнатура: + +``` js +bundleRenderer.renderToString([context, callback]): ?Promise +``` + +Рендерит сборку в строку. Объект контекста опционален. Коллбэк является обычным для Node.js коллбэком, где первый аргумент является ошибкой, а второй аргумент — отрендеренной строкой. + +С версии 2.5.0+ коллбэк является опциональным. Когда коллбэк не указан, метод возвращает Promise, который разрешается отрендеренным HTML. + +### bundleRenderer.renderToStream + +Сигнатура: + +``` js +bundleRenderer.renderToStream([context]): stream.Readable +``` + +Рендерит сборку в [Node.js readable stream](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams). Объект контекста опционален. Подробнее в разделе [Стриминг](../guide/streaming.md). + +## Опции рендерера + +### template + +Предоставляет шаблон для всей HTML-страницы. Шаблон должен содержать комментарий ``, который определяет место подстановки отрендеренного контента приложения. + +Шаблон также поддерживает базовые интерполяции с использованием контекста рендера: + +- Используйте двойные фигурные скобки для интерполяции экранированного HTML; +- Используйте тройные фигурные скобки для интерполяции сырого HTML. + +Шаблон автоматически внедряет соответствующий контент, когда определённые свойства найдены в контексте рендера: + +- `context.head`: (string) любая разметка для head, которая должна быть вставлена в заголовочный тег страницы. + +- `context.styles`: (string) любой встроенный CSS, который должен быть вставлен в заголовочный тег страницы. Обратите внимание, что это свойство будет автоматически заполнено при использовании `vue-loader` + `vue-style-loader` для CSS компонента. + +- `context.state`: (Object) начальное состояние хранилища Vuex, которое должно быть внедрено в страницу как `window.__INITIAL_STATE__`. Внедряемый JSON автоматически обрабатывается с помощью [serialize-javascript](https://github.com/yahoo/serialize-javascript) для предотвращения XSS-уязвимостей. + + С версии 2.5.0+, встраиваемый скрипт также автоматически удаляется в режиме production. + +Кроме того, когда предоставлен `clientManifest`, шаблон автоматически внедряет следующее: + +- JavaScript и CSS ресурсы для клиентской части, необходимые для рендеринга (с асинхронными фрагментами добавляемыми автоматически); +- Оптимальные ресурсы `` для отображаемой страницы. + +Вы можете отключить все автоматические внедрения передав `inject: false` в рендерер. + +См. также: + +- [Использование шаблона страниц](../guide/#using-a-page-template) +- [Внедрение ресурсов вручную](../guide/build-config.md#manual-asset-injection) + +### clientManifest + +Предоставляет объект манифеста клиентской сборки, сгенерированный `vue-server-renderer/client-plugin`. Клиентский манифест предоставляет для рендерера сборки необходимую информацию для автоматического внедрения ресурсов в шаблон HTML. Подробнее в разделе [Генерация `clientManifest`](../guide/build-config.md#generating-clientmanifest). + +### inject + +Контролирует, выполнять ли автоматические внедрения при использовании `template`. По умолчанию `true`. + +См. также: [Внедрение ресурсов вручную](../guide/build-config.md#manual-asset-injection). + +### shouldPreload + +Функция, определяющая какие файлы должны иметь `` в генерируемых ресурсах. + +По умолчанию, только JavaScript и CSS файлы будут предзагружаться, так как они абсолютно необходимы для загрузки приложения. + +Для других типов ресурсов, таких как изображения или шрифты, предзагрузка может привести к ненужному увеличению объёмов передаваемой информации и даже к ухудшению производительности, поэтому список файлов, которые нужно предзагружать, зависит от ситуации. Вы можете точно контролировать, что требует предзагрузки, используя опцию `shouldPreload`: + +``` js +const renderer = createBundleRenderer(bundle, { + template, + clientManifest, + shouldPreload: (file, type) => { + // тип определяется на основе расширения файла. + // https://fetch.spec.whatwg.org/#concept-request-destination + if (type === 'script' || type === 'style') { + return true + } + if (type === 'font') { + // предзагружать только woff2 шрифты + return /\.woff2$/.test(file) + } + if (type === 'image') { + // предзагружать только важные изображения + return file === 'hero.jpg' + } + } +}) +``` + +### shouldPrefetch + +- Добавлено в версии 2.5.0+ + +Функция для управления файлами, которые должны содержаться в генерируемых ``. + +По умолчанию все ресурсы в асинхронных частях будут предварительно загружены, так как это директива с низким приоритетом; однако вы можете настроить что требуется предзагружать для лучшего контроля над использованием канала загрузки. Этот параметр ожидает такую же сигнатуру функции, как `shouldPreload`. + +### runInNewContext + +- Используется только в `createBundleRenderer` +- Возможные значения: `boolean | 'once'` (`'once'` поддерживается только с версии 2.3.1+) + +По умолчанию, рендерер сборки будет создавать новый контекст V8 для каждого рендеринга и повторно исполнять всю сборку. Это имеет некоторые преимущества — например, код приложения изолирован от процесса сервера и не нужно беспокоиться [о проблеме «синглтона с состоянием»](../guide/structure.md#avoid-stateful-singletons), которая упоминалась ранее в руководстве. Однако этот режим требует значительных затрат производительности, поскольку повторное выполнение сборки обходится дорого, особенно когда приложение становится большим. + +По умолчанию эта опция имеет значение `true` для обеспечения обратной совместимости, но рекомендуется использовать `runInNewContext: false` или `runInNewContext: 'once'` всегда, когда это возможно. + +> В версии 2.3.0 у этой опции есть ошибка, когда при `runInNewContext: false` сборка всё ещё исполнялась в отдельном глобальном контексте. Информация далее предполагает использование версии 2.3.1+. + +С опцией `runInNewContext: false`, код сборки будет выполняться в том же контексте `global`, что и серверный процесс, поэтому нужно быть осторожным с кодом, который изменяет `global` в вашем приложении. + +С опцией `runInNewContext: 'once'` (добавлено в версии 2.3.1+), сборка выполняется в отдельном контексте `global`, но только один раз при запуске. Это обеспечивает лучшую изоляцию кода приложения поскольку предотвращает случайно загрязнение объекта `global` серверного процесса. Предостережения заключаются в следующем: + +1. Зависимости, которые изменяют `global` (например, полифиллы) не должны быть объявлены внешними зависимостями в этом режиме; +2. Значения, возвращаемые при выполнении сборки будут использовать разные глобальные конструкторы, например, ошибка внутри сборки не будет экземпляром `Error` в серверном процессе. + +См. также: [Структура исходного кода](../guide/structure.md) + +### basedir + +- Используется только в `createBundleRenderer` + +Указание пути базового каталога для серверной сборки для разрешения зависимостей из `node_modules` в нём. Это необходимо только в том случае, если сгенерированный файл сборки располагается в другом месте, в отличии от используемых NPM-зависимостей, или когда ваш `vue-server-renderer` подключен NPM-ссылкой в вашем текущем проекте. + +### cache + +Реализация [кэширования на уровне компонентов](../guide/caching.md#component-level-caching). Объект кэша должен реализовать следующий интерфейс (соответствуя нотациям Flow): + +``` js +type RenderCache = { + get: (key: string, cb?: Function) => string | void; + set: (key: string, val: string) => void; + has?: (key: string, cb?: Function) => boolean | void; +}; +``` + +Для обычного использования достаточно передать [lru-cache](https://github.com/isaacs/node-lru-cache): + +``` js +const LRU = require('lru-cache') + +const renderer = createRenderer({ + cache: LRU({ + max: 10000 + }) +}) +``` + +Обратите внимание, что объект кэша по крайне мере должен реализовывать `get` и `set`. Кроме того, `get` и `has` опционально могут быть асинхронными, если они принимают второй аргумент как коллбэк. Это позволяет кэшу использовать асинхронные API, например для Redis: + +``` js +const renderer = createRenderer({ + cache: { + get: (key, cb) => { + redisClient.get(key, (err, res) => { + // обработка ошибок, если таковые будут + cb(res) + }) + }, + set: (key, val) => { + redisClient.set(key, val) + } + } +}) +``` + +### directives + +Позволяет предоставить серверную реализацию для ваших пользовательских директив: + +``` js +const renderer = createRenderer({ + directives: { + example (vnode, directiveMeta) { + // преобразуем vnode на основе метаданных привязанных к директиве + } + } +}) +``` + +Например, можете посмотреть [серверную реализацию для директивы `v-show`](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js). + +## Плагины webpack + +Webpack плагины предоставляются как отдельные файлы, которые должны быть подключены напрямую: + +``` js +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') +``` + +По умолчанию генерируются файлы: + +- `vue-ssr-server-bundle.json` для серверной сборки; +- `vue-ssr-client-manifest.json` для клиентской сборки. + +Имена файлов могут быть изменены при создании экземпляров плагина: + +``` js +const plugin = new VueSSRServerPlugin({ + filename: 'my-server-bundle.json' +}) +``` + +Подробнее в разделе [Конфигурация сборки](../guide/build-config.md). diff --git a/docs/ru/guide/README.md b/docs/ru/guide/README.md new file mode 100644 index 00000000..fe2e4473 --- /dev/null +++ b/docs/ru/guide/README.md @@ -0,0 +1,157 @@ +# Начало работы + +## Установка + +``` bash +npm install vue vue-server-renderer --save +``` + +В руководстве мы будем использовать NPM, но вы свободно можете использовать и [Yarn](https://yarnpkg.com/en/). + +#### Примечания + +- Рекомендуется использовать Node.js версии 6+. +- `vue-server-renderer` и `vue` должны иметь одинаковые версии. +- `vue-server-renderer` зависит от некоторых нативных модулей Node.js и поэтому может использоваться только в Node.js. Возможно в будущем мы предоставим более простую сборку, которая сможет быть запущена в других средах исполнения JavaScript. + +## Рендеринг экземпляра Vue + +``` js +// Шаг 1: Создаём экземпляр Vue +const Vue = require('vue') +const app = new Vue({ + template: `
Hello World
` +}) + +// Шаг 2: Создаём рендерер +const renderer = require('vue-server-renderer').createRenderer() + +// Шаг 3: Рендерим экземпляр Vue в HTML +renderer.renderToString(app, (err, html) => { + if (err) throw err + console.log(html) + // =>
Hello World
+}) + +// с версии 2.5.0+, возвращает Promise если коллбэк не указан: +renderer.renderToString(app).then(html => { + console.log(html) +}).catch(err => { + console.error(err) +}) +``` + +## Интеграция с сервером + +Это достаточно просто когда мы используем сервер на Node.js, например [Express](https://expressjs.com/): + +``` bash +npm install express --save +``` +--- +``` js +const Vue = require('vue') +const server = require('express')() +const renderer = require('vue-server-renderer').createRenderer() + +server.get('*', (req, res) => { + const app = new Vue({ + data: { + url: req.url + }, + template: `
Вы открыли URL: {{ url }}
` + }) + + renderer.renderToString(app, (err, html) => { + if (err) { + res.status(500).end('Внутренняя ошибка сервера') + return + } + res.end(` + + + Codestin Search App + ${html} + + `) + }) +}) + +server.listen(8080) +``` + +## Использование шаблона страниц + +Когда вы рендерите приложение Vue, рендерер генерирует только разметку приложения. В примере выше нам потребовалось обернуть вывод дополнительным кодом для создания обычной HTML-страницы. + +Вы можете упростить это, предоставив шаблон страницы при создании рендерера. Чаще всего нам требуется расположить шаблон в отдельном файле, например `index.template.html`: + +``` html + + + Codestin Search App + + + + +``` + +Обратите внимание на комментарий `` — сюда будет подставлена разметка вашего приложения. + +Теперь мы можем прочитать этот файл и передать его в рендерер Vue: + +``` js +const renderer = createRenderer({ + template: require('fs').readFileSync('./index.template.html', 'utf-8') +}) + +renderer.renderToString(app, (err, html) => { + console.log(html) // выведется код всей страницы, с подставленным кодом приложения. +}) +``` + +### Интерполяции в шаблоне + +Шаблон поддерживает простые интерполяции. Например: + +``` html + + + + Codestin Search App + + + {{{ meta }}} + + + + + +``` + +Мы можем предоставить необходимые данные для интерполяции, передав объект контекста для рендера вторым аргументом в `renderToString`: + +``` js +const context = { + title: 'привет', + meta: ` + + + ` +} + +renderer.renderToString(app, context, (err, html) => { + // заголовок страницы будет "привет" + // meta-теги также будут подставлены в код страницы +}) +``` + +Объект `context` может также использоваться совместно с экземпляром Vue приложения, что разрешает компонентам динамически регистрировать данные для интерполяции в шаблоне. + +Кроме того, шаблон поддерживает некоторые продвинутые функции: + +- Автоматическую подстановку критически важного CSS при использовании `*.vue` компонентов; +- Автоматическую подстановку ссылок и подсказок для ресурсов (preload / prefetch) при использовании `clientManifest`; +- Автоматическую подстановку и предотвращение XSS при встраивании Vuex-состояния для гидратации на стороне клиента. + +Мы обсудим это дальше, когда будем разбирать все связанные концепции. diff --git a/docs/ru/guide/build-config.md b/docs/ru/guide/build-config.md new file mode 100644 index 00000000..63ed3148 --- /dev/null +++ b/docs/ru/guide/build-config.md @@ -0,0 +1,215 @@ +# Конфигурация сборки + +Мы предполагаем, что вы уже знаете как настраивать Webpack для клиентской части проектов. Конфигурация для проекта SSR будет во многом схожей, но мы предлагаем разбивать конфигурацию на три файла: *base*, *client* и *server*. Базовая конфигурация (base) содержит конфигурацию, совместно используемую для обоих окружений, такие как пути вывода, псевдонимы и загрузчики. Конфигурация сервера (server) и конфигурация клиента (client) просто расширяют базовую конфигурацию, используя [webpack-merge](https://github.com/survivejs/webpack-merge). + +## Конфигурация серверной части + +Конфигурация серверной части предназначена для создания серверной сборки, которая будет передана в `createBundleRenderer`. Это должно выглядеть так: + +``` js +const merge = require('webpack-merge') +const nodeExternals = require('webpack-node-externals') +const baseConfig = require('./webpack.base.config.js') +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') + +module.exports = merge(baseConfig, { + // Укажите точку входа серверной части вашего приложения + entry: '/path/to/entry-server.js', + + // Это позволяет Webpack обрабатывать динамические импорты в Node-стиле, + // а также сообщает `vue-loader` генерировать серверно-ориентированный код + // при компиляции компонентов Vue. + target: 'node', + + // Для поддержки source map в bundle renderer + devtool: 'source-map', + + // Это сообщает что в серверной сборке следует использовать экспорты в стиле Node + output: { + libraryTarget: 'commonjs2' + }, + + // https://webpack.js.org/configuration/externals/#function + // https://github.com/liady/webpack-node-externals + // Внешние зависимости приложения. Это значительно ускоряет процесс + // сборки серверной части и уменьшает размер итогового файла сборки. + externals: nodeExternals({ + // не выделяйте зависимости, которые должны обрабатываться Webpack. + // здесь вы можете добавить больше типов файлов, например сырые *.vue файлы + // нужно также указывать белый список зависимостей изменяющих `global` (например, полифиллы) + whitelist: /\.css$/ + }), + + // Этот плагин преобразует весь результат серверной сборки + // в один JSON-файл. Имя по умолчанию будет + // `vue-ssr-server-bundle.json` + plugins: [ + new VueSSRServerPlugin() + ] +}) +``` + +После создания `vue-ssr-server-bundle.json` просто передайте путь к файлу в `createBundleRenderer`: + +``` js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { + // ...другие настройки рендерера +}) +``` + +В качестве альтернативы, вы также можете передать сборку как объект в `createBundleRenderer`. Это полезно для горячей перезагрузки во время разработки — см. демо HackerNews для [примера настройки](https://github.com/vuejs/vue-hackernews-2.0/blob/master/build/setup-dev-server.js). + +### Ограничения externals + +Обратите внимание, что в параметре `externals` мы указываем белый список CSS файлов. Это связано с тем, что CSS, импортированный из зависимостей всё равно должен быть обработан Webpack. Если вы импортируете любые другие типы файлов, которые также полагаются на Webpack (например, `*.vue`, `*.sass`), вы должны их также добавить в белый список. + +Если вы используете `runInNewContext: 'once'` или `runInNewContext: true`, вам также требуется добавить в белый список являются полифиллы, которые изменяют `global`, например `babel-polyfill`. Это связано с тем, что при использовании режима нового контекста, **код внутри серверной сборки имеет свой собственный объект `global`**. Поскольку это не будет нужно на сервере при использовании Node 7.6+, на самом деле проще просто импортировать его в клиентской точке входа. + +## Конфигурация клиентской части + +Конфигурация клиентской части может оставаться практически такой же, как и базовой. Очевидно, вам нужно указать `entry` на файл входной точки клиентской части. Кроме того, если вы используете `CommonsChunkPlugin`, убедитесь, что используете его только в конфигурации клиентской части, потому что для серверной сборки требуется одна точка входа. + +### Generating `clientManifest` + +> требуется версия 2.3.0+ + +Помимо серверной сборки, мы также можем сгенерировать манифест сборки. С помощью манифеста клиентской части и серверной сборки, у рендерера появится информация о серверной *и* клиентской сборке, поэтому он может автоматически внедрять [директивы preload/prefetch](https://css-tricks.com/prefetching-preloading-prebrowsing/) в ссылки на CSS / теги script в отображаемом HTML. + +Выгода тут двойная: + +1. Он может заменить `html-webpack-plugin` для внедрения правильных URL-адресов ресурсов, когда в генерируемых именах файлов есть хэши. + +2. При рендеринге сборки, которая использует возможности разделения кода Webpack, мы можем гарантировать, что оптимальные части были предзагружены и предзаполнены, а также интеллектуально внедрять теги ` + + + + +` +``` + +### Внедрение ресурсов вручную + +По умолчанию, внедрение ресурсов выполняется автоматически при использовании опции `template` для рендера. Но иногда вам может понадобиться больше контроля над тем, как ресурсы должны внедряться в шаблон, или, возможно, вы не используете шаблон вообще. В таком случае вы можете передать опцию `inject: false` при создании рендерера и производить внедрение ресурсов вручную. + +В коллбэке `renderToString` объект `context`, который вы передали, предоставляет следующие методы: + +- `context.renderStyles()` + + Возвращает встроенные теги `