diff --git a/.gitignore b/.gitignore index b5ec4b0a..65cdb1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules .DS_Store -_book -test +.idea +docs/.vuepress/dist diff --git a/CNAME b/CNAME deleted file mode 100644 index 87b98a28..00000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -ssr.vuejs.org diff --git a/LANGS.md b/LANGS.md deleted file mode 100644 index 5f855067..00000000 --- a/LANGS.md +++ /dev/null @@ -1,6 +0,0 @@ -* [English](en/) -* [Français](fr/) -* [中文](zh/) -* [Русский](ru/) -* [한국어 (Korean)](ko/) -* [日本語](ja/) diff --git a/README.md b/README.md index 8dcba747..5daa6440 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,14 @@ -# vue-ssr-docs +# vue2-ssr-docs -Comprehensive guide for SSR (Server-Side Rendering) in Vue.js. Live at [ssr.vuejs.org](https://ssr.vuejs.org). +Comprehensive guide for SSR (Server-Side Rendering) in Vue 2. Live at [v2.ssr.vuejs.org](https://v2.ssr.vuejs.org). -## Development +The latest SSR guide for Vue 3, see [Vue 3 docs](https://vuejs.org/guide/scaling-up/ssr.html). -``` bash -# install gitbook CLI -npm install gitbook-cli -g +## Development -# install local plugins -npm install +This site is powered by [VuePress](https://vuepress.vuejs.org/). -# serve -npm run dev +```bash +yarn +yarn dev ``` - -## Contribute - -[gl]: https://gitlocalize.com -[gl-help]: https://docs.gitlocalize.com/ -[gl-issue-tracker]: https://github.com/gitlocalize/feedback -[gl-repo]: https://gitlocalize.com/repo/101 - -### Translation - -We are useing with a translation tool called [GitLocalize][gl] and follow the steps to get started with your contribution: - -1. Go to [GitLocalize's vuejs/vue-ssr-docs repository][gl-repo]. -1. Sign up using your GitHub account :octocat:. -1. Find the document you are going to translate. -1. Happy translating :sparkles: . -1. When you are done, send the translation for reviews. -1. Reviewed translation will be sent as a Pull Request to GitHub by language admin in the community. - -To learn more about how GitLocalize works, visit their [help page][gl-help]. If you find any issues or feature requests, please file them in [GitLocalize's issue tracker][gl-issue-tracker]. - diff --git a/book.json b/book.json deleted file mode 100644 index 82401655..00000000 --- a/book.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "gitbook": ">3.0.0", - "plugins": ["edit-link", "theme-vuejs", "-fontsettings", "github"], - "pluginsConfig": { - "edit-link": { - "base": "https://github.com/vuejs/vue-ssr-docs/edit/master/", - "label": "Edit This Page" - }, - "github": { - "url": "https://github.com/vuejs/vue-ssr-docs/" - } - }, - "links": { - "sharing": { - "facebook": false, - "twitter": false - } - } -} diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index 2f726c92..00000000 --- a/deploy.sh +++ /dev/null @@ -1,10 +0,0 @@ -rm -rf _book -gitbook install -gitbook build -mkdir _book -cp CNAME _book/CNAME -cd _book -git init -git add -A -git commit -m 'update book' -git push -f git@github.com:vuejs/vue-ssr-docs.git master:gh-pages diff --git a/fr/README.md b/docs-gitbook/fr/README.md similarity index 100% rename from fr/README.md rename to docs-gitbook/fr/README.md diff --git a/fr/api.md b/docs-gitbook/fr/api.md similarity index 100% rename from fr/api.md rename to docs-gitbook/fr/api.md diff --git a/fr/basic.md b/docs-gitbook/fr/basic.md similarity index 100% rename from fr/basic.md rename to docs-gitbook/fr/basic.md diff --git a/fr/build-config.md b/docs-gitbook/fr/build-config.md similarity index 99% rename from fr/build-config.md rename to docs-gitbook/fr/build-config.md index 857ada91..6a9dd640 100644 --- a/fr/build-config.md +++ b/docs-gitbook/fr/build-config.md @@ -143,7 +143,7 @@ Avec cette mise en place, votre rendu HTML côté serveur pour un build avec sci -` + ``` ### Injection manuelle des fichiers diff --git a/fr/bundle-renderer.md b/docs-gitbook/fr/bundle-renderer.md similarity index 100% rename from fr/bundle-renderer.md rename to docs-gitbook/fr/bundle-renderer.md diff --git a/fr/caching.md b/docs-gitbook/fr/caching.md similarity index 100% rename from fr/caching.md rename to docs-gitbook/fr/caching.md diff --git a/fr/css.md b/docs-gitbook/fr/css.md similarity index 100% rename from fr/css.md rename to docs-gitbook/fr/css.md diff --git a/fr/data.md b/docs-gitbook/fr/data.md similarity index 100% rename from fr/data.md rename to docs-gitbook/fr/data.md diff --git a/fr/head.md b/docs-gitbook/fr/head.md similarity index 100% rename from fr/head.md rename to docs-gitbook/fr/head.md diff --git a/fr/hydration.md b/docs-gitbook/fr/hydration.md similarity index 100% rename from fr/hydration.md rename to docs-gitbook/fr/hydration.md diff --git a/fr/non-node.md b/docs-gitbook/fr/non-node.md similarity index 100% rename from fr/non-node.md rename to docs-gitbook/fr/non-node.md diff --git a/fr/routing.md b/docs-gitbook/fr/routing.md similarity index 100% rename from fr/routing.md rename to docs-gitbook/fr/routing.md diff --git a/fr/streaming.md b/docs-gitbook/fr/streaming.md similarity index 100% rename from fr/streaming.md rename to docs-gitbook/fr/streaming.md diff --git a/fr/structure.md b/docs-gitbook/fr/structure.md similarity index 100% rename from fr/structure.md rename to docs-gitbook/fr/structure.md diff --git a/fr/universal.md b/docs-gitbook/fr/universal.md similarity index 100% rename from fr/universal.md rename to docs-gitbook/fr/universal.md diff --git a/ja/README.md b/docs-gitbook/ja/README.md similarity index 100% rename from ja/README.md rename to docs-gitbook/ja/README.md diff --git a/ja/api.md b/docs-gitbook/ja/api.md similarity index 100% rename from ja/api.md rename to docs-gitbook/ja/api.md diff --git a/ja/basic.md b/docs-gitbook/ja/basic.md similarity index 100% rename from ja/basic.md rename to docs-gitbook/ja/basic.md diff --git a/ja/build-config.md b/docs-gitbook/ja/build-config.md similarity index 100% rename from ja/build-config.md rename to docs-gitbook/ja/build-config.md diff --git a/ja/bundle-renderer.md b/docs-gitbook/ja/bundle-renderer.md similarity index 100% rename from ja/bundle-renderer.md rename to docs-gitbook/ja/bundle-renderer.md diff --git a/ja/caching.md b/docs-gitbook/ja/caching.md similarity index 100% rename from ja/caching.md rename to docs-gitbook/ja/caching.md diff --git a/ja/css.md b/docs-gitbook/ja/css.md similarity index 100% rename from ja/css.md rename to docs-gitbook/ja/css.md diff --git a/ja/data.md b/docs-gitbook/ja/data.md similarity index 98% rename from ja/data.md rename to docs-gitbook/ja/data.md index e3024cea..0dde4c83 100644 --- a/ja/data.md +++ b/docs-gitbook/ja/data.md @@ -87,7 +87,7 @@ export default { }, computed: { // ストアの状態から item を表示します - items () { + item () { return this.$store.state.items[this.$route.params.id] } } @@ -132,7 +132,7 @@ export default context => { } ``` -`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL__` という形の状態として埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがその状態を取得します: +`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL_STATE__` という形の状態として埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがその状態を取得します: ```js // entry-client.js diff --git a/ja/head.md b/docs-gitbook/ja/head.md similarity index 100% rename from ja/head.md rename to docs-gitbook/ja/head.md diff --git a/ja/hydration.md b/docs-gitbook/ja/hydration.md similarity index 100% rename from ja/hydration.md rename to docs-gitbook/ja/hydration.md diff --git a/ja/non-node.md b/docs-gitbook/ja/non-node.md similarity index 100% rename from ja/non-node.md rename to docs-gitbook/ja/non-node.md diff --git a/ja/routing.md b/docs-gitbook/ja/routing.md similarity index 100% rename from ja/routing.md rename to docs-gitbook/ja/routing.md diff --git a/ja/streaming.md b/docs-gitbook/ja/streaming.md similarity index 100% rename from ja/streaming.md rename to docs-gitbook/ja/streaming.md diff --git a/ja/structure.md b/docs-gitbook/ja/structure.md similarity index 100% rename from ja/structure.md rename to docs-gitbook/ja/structure.md diff --git a/ja/universal.md b/docs-gitbook/ja/universal.md similarity index 100% rename from ja/universal.md rename to docs-gitbook/ja/universal.md diff --git a/ko/README.md b/docs-gitbook/ko/README.md similarity index 100% rename from ko/README.md rename to docs-gitbook/ko/README.md diff --git a/ko/api.md b/docs-gitbook/ko/api.md similarity index 100% rename from ko/api.md rename to docs-gitbook/ko/api.md diff --git a/ko/basic.md b/docs-gitbook/ko/basic.md similarity index 100% rename from ko/basic.md rename to docs-gitbook/ko/basic.md diff --git a/ko/build-config.md b/docs-gitbook/ko/build-config.md similarity index 99% rename from ko/build-config.md rename to docs-gitbook/ko/build-config.md index ca167781..17702245 100644 --- a/ko/build-config.md +++ b/docs-gitbook/ko/build-config.md @@ -115,8 +115,8 @@ const renderer = createBundleRenderer(serverBundle, { 이 설정을 사용하면 코드 분할을 사용하는 빌드의 서버렌더링된 HTML이 다음과 같이 표시됩니다.(전체가 자동으로 주입됩니다) ```html - - + + @@ -132,8 +132,8 @@ const renderer = createBundleRenderer(serverBundle, { - -` + + ``` ### 수동 에셋 주입 diff --git a/ko/bundle-renderer.md b/docs-gitbook/ko/bundle-renderer.md similarity index 100% rename from ko/bundle-renderer.md rename to docs-gitbook/ko/bundle-renderer.md diff --git a/ko/caching.md b/docs-gitbook/ko/caching.md similarity index 100% rename from ko/caching.md rename to docs-gitbook/ko/caching.md diff --git a/ko/head.md b/docs-gitbook/ko/head.md similarity index 100% rename from ko/head.md rename to docs-gitbook/ko/head.md diff --git a/ko/hydration.md b/docs-gitbook/ko/hydration.md similarity index 100% rename from ko/hydration.md rename to docs-gitbook/ko/hydration.md diff --git a/ko/routing.md b/docs-gitbook/ko/routing.md similarity index 100% rename from ko/routing.md rename to docs-gitbook/ko/routing.md diff --git a/ko/streaming.md b/docs-gitbook/ko/streaming.md similarity index 100% rename from ko/streaming.md rename to docs-gitbook/ko/streaming.md diff --git a/ko/structure.md b/docs-gitbook/ko/structure.md similarity index 100% rename from ko/structure.md rename to docs-gitbook/ko/structure.md diff --git a/ko/universal.md b/docs-gitbook/ko/universal.md similarity index 100% rename from ko/universal.md rename to docs-gitbook/ko/universal.md diff --git a/ru/README.md b/docs-gitbook/ru/README.md similarity index 100% rename from ru/README.md rename to docs-gitbook/ru/README.md diff --git a/ru/api.md b/docs-gitbook/ru/api.md similarity index 100% rename from ru/api.md rename to docs-gitbook/ru/api.md diff --git a/ru/basic.md b/docs-gitbook/ru/basic.md similarity index 100% rename from ru/basic.md rename to docs-gitbook/ru/basic.md diff --git a/ru/build-config.md b/docs-gitbook/ru/build-config.md similarity index 99% rename from ru/build-config.md rename to docs-gitbook/ru/build-config.md index 4f8bbcec..0f8c3daf 100644 --- a/ru/build-config.md +++ b/docs-gitbook/ru/build-config.md @@ -142,7 +142,7 @@ const renderer = createBundleRenderer(serverBundle, { -` + ``` ### Внедрение ресурсов вручную diff --git a/ru/bundle-renderer.md b/docs-gitbook/ru/bundle-renderer.md similarity index 100% rename from ru/bundle-renderer.md rename to docs-gitbook/ru/bundle-renderer.md diff --git a/ru/caching.md b/docs-gitbook/ru/caching.md similarity index 100% rename from ru/caching.md rename to docs-gitbook/ru/caching.md diff --git a/ru/css.md b/docs-gitbook/ru/css.md similarity index 100% rename from ru/css.md rename to docs-gitbook/ru/css.md diff --git a/ru/data.md b/docs-gitbook/ru/data.md similarity index 100% rename from ru/data.md rename to docs-gitbook/ru/data.md diff --git a/ru/head.md b/docs-gitbook/ru/head.md similarity index 100% rename from ru/head.md rename to docs-gitbook/ru/head.md diff --git a/ru/hydration.md b/docs-gitbook/ru/hydration.md similarity index 100% rename from ru/hydration.md rename to docs-gitbook/ru/hydration.md diff --git a/ru/non-node.md b/docs-gitbook/ru/non-node.md similarity index 100% rename from ru/non-node.md rename to docs-gitbook/ru/non-node.md diff --git a/ru/routing.md b/docs-gitbook/ru/routing.md similarity index 100% rename from ru/routing.md rename to docs-gitbook/ru/routing.md diff --git a/ru/streaming.md b/docs-gitbook/ru/streaming.md similarity index 100% rename from ru/streaming.md rename to docs-gitbook/ru/streaming.md diff --git a/ru/structure.md b/docs-gitbook/ru/structure.md similarity index 100% rename from ru/structure.md rename to docs-gitbook/ru/structure.md diff --git a/ru/universal.md b/docs-gitbook/ru/universal.md similarity index 100% rename from ru/universal.md rename to docs-gitbook/ru/universal.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 00000000..cd27fe8e --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,155 @@ +module.exports = { + locales: { + '/': { + lang: 'en-US', + title: 'Vue SSR Guide', + description: 'Vue.js Server-Side Rendering Guide' + }, + '/zh/': { + lang: 'zh-CN', + title: 'Vue SSR 指南', + description: 'Vue.js 服务端渲染指南' + }, + '/ja/': { + lang: 'ja', + title: 'Vue SSR ガイド', + description: 'Vue.js サーバーサイドレンダリングガイド' + }, + '/ru/': { + lang: 'ru', + title: 'Руководство Vue SSR', + description: 'Руководство по серверному рендерингу Vue.js' + } + }, + // serviceWorker: true, + // theme: 'vue', + themeConfig: { + repo: 'vuejs/vue-ssr-docs', + docsDir: 'docs', + locales: { + '/': { + label: 'English', + selectText: 'Languages', + editLinkText: 'Edit this page on GitHub', + nav: [ + { + text: 'Guide', + link: '/guide/' + }, + { + text: 'API Reference', + link: '/api/' + } + ], + sidebar: [ + ['/', 'Introduction'], + '/guide/', + '/guide/universal', + '/guide/structure', + '/guide/routing', + '/guide/data', + '/guide/hydration', + '/guide/bundle-renderer', + '/guide/build-config', + '/guide/css', + '/guide/head', + '/guide/caching', + '/guide/streaming', + '/guide/non-node' + ] + }, + '/zh/': { + label: '简体中文', + selectText: '选择语言', + editLinkText: '在 GitHub 上编辑此页', + nav: [ + { + text: '指南', + link: '/zh/guide/' + }, + { + text: 'API 参考', + link: '/zh/api/' + } + ], + sidebar: [ + ['/zh/', '介绍'], + '/zh/guide/', + '/zh/guide/universal', + '/zh/guide/structure', + '/zh/guide/routing', + '/zh/guide/data', + '/zh/guide/hydration', + '/zh/guide/bundle-renderer', + '/zh/guide/build-config', + '/zh/guide/css', + '/zh/guide/head', + '/zh/guide/caching', + '/zh/guide/streaming', + '/zh/guide/non-node' + ] + }, + '/ja/': { + label: '日本語', + selectText: '言語', + editLinkText: 'GitHub 上でこのページを編集する', + nav: [{ + text: 'ガイド', + link: '/ja/guide/' + }, + { + text: 'API リファレンス', + link: '/ja/api/' + } + ], + sidebar: [ + ['/ja/', 'はじめに'], + '/ja/guide/', + '/ja/guide/universal', + '/ja/guide/structure', + '/ja/guide/routing', + '/ja/guide/data', + '/ja/guide/hydration', + '/ja/guide/bundle-renderer', + '/ja/guide/build-config', + '/ja/guide/css', + '/ja/guide/head', + '/ja/guide/caching', + '/ja/guide/streaming', + '/ja/guide/non-node' + ] + }, + '/ru/': { + label: 'Русский', + selectText: 'Переводы', + 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 new file mode 100644 index 00000000..b08c292d --- /dev/null +++ b/docs/.vuepress/public/_redirects @@ -0,0 +1,19 @@ +/en/ / +/en/api.html /api/ +/en/basic.html /guide/ +/en/* /guide/:splat + +/zh/ /zh/ +/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 + +/ja/ /ja/ +/ja/api.html /ja/api/ +/ja/basic.html /ja/guide/ +/ja/* /ja/guide/:splat diff --git a/en/README.md b/docs/README.md similarity index 86% rename from en/README.md rename to docs/README.md index e3f1a1ca..f6d8406c 100644 --- a/en/README.md +++ b/docs/README.md @@ -1,11 +1,14 @@ # Vue.js Server-Side Rendering Guide -> **Note:** this guide requires the following minimum versions of Vue and supporting libraries: -> - vue & vue-server-renderer 2.3.0+ -> - vue-router 2.5.0+ -> - vue-loader 12.0.0+ & vue-style-loader 3.0.0+ +::: tip NOTE +This guide requires the following minimum versions of Vue and supporting libraries: -> If you have previously used Vue 2.2 with SSR, you will notice that the recommended code structure is now [a bit different](./structure.md) (with the new [runInNewContext](./api.md#runinnewcontext) option set to `false`). Your existing app should continue to work, but it's recommended to migrate to the new recommendations. +- vue & vue-server-renderer 2.3.0+ +- vue-router 2.5.0+ +- vue-loader 12.0.0+ & vue-style-loader 3.0.0+ + +If you have previously used Vue 2.2 with SSR, you will notice that the recommended code structure is now [a bit different](./guide/structure.md) (with the new [runInNewContext](./api/README.md#runinnewcontext) option set to `false`). Your existing app should continue to work, but it's recommended to migrate to the new recommendations. +::: ## What is Server-Side Rendering (SSR)? @@ -37,11 +40,11 @@ Before using SSR for your app, the first question you should ask is whether you If you're only investigating SSR to improve the SEO of a handful of marketing pages (e.g. `/`, `/about`, `/contact`, etc), then you probably want __prerendering__ instead. Rather than using a web server to compile HTML on-the-fly, prerendering simply generates static HTML files for specific routes at build time. The advantage is setting up prerendering is much simpler and allows you to keep your frontend as a fully static site. -If you're using webpack, you can easily add prerendering with the [prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin). It's been extensively tested with Vue apps - and in fact, [the creator](https://github.com/chrisvfritz) is a member of the Vue core team. +If you're using webpack, you can easily add prerendering with the [prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin). It's been extensively tested with Vue apps. ## About This Guide -This guide is focused on server-rendered Single-Page Applications using Node.js as the server. Mixing Vue SSR with other backend setups is a topic of its own and briefly discussed in a [dedicated section](./non-node.md). +This guide is focused on server-rendered Single-Page Applications using Node.js as the server. Mixing Vue SSR with other backend setups is a topic of its own and briefly discussed in a [dedicated section](./guide/non-node.md). This guide will be very in-depth and assumes you are already familiar with Vue.js itself, and have decent working knowledge of Node.js and webpack. If you prefer a higher-level solution that provides a smooth out-of-the-box experience, you should probably give [Nuxt.js](https://nuxtjs.org/) a try. It's built upon the same Vue stack but abstracts away a lot of the boilerplate, and provides some extra features such as static site generation. However, it may not suit your use case if you need more direct control of your app's structure. Regardless, it would still be beneficial to read through this guide to better understand how things work together. diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000..65acb4e7 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,332 @@ +--- +sidebar: auto +--- + +# API Reference + +## createRenderer + +Create a [`Renderer`](#class-renderer) instance with (optional) [options](#renderer-options). + +``` js +const { createRenderer } = require('vue-server-renderer') +const renderer = createRenderer({ /* options */ }) +``` + +## createBundleRenderer + +Create a [`BundleRenderer`](#class-bundlerenderer) instance with a server bundle and (optional) [options](#renderer-options). + +``` js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer(serverBundle, { /* options */ }) +``` + +The `serverBundle` argument can be one of the following: + +- An absolute path to generated bundle file (`.js` or `.json`). Must start with `/` to be treated as a file path. + +- A bundle object generated by webpack + `vue-server-renderer/server-plugin`. + +- A string of JavaScript code (not recommended). + +See [Introducing the Server Bundle](../guide/bundle-renderer.md) and [Build Configuration](../guide/build-config.md) for more details. + +## Class: Renderer + +### renderer.renderToString + +Signature: + +``` js +renderer.renderToString(vm, context?, callback?): ?Promise +``` + +Render a Vue instance to string. The context object is optional. The callback is a typical Node.js style callback where the first argument is the error and the second argument is the rendered string. + +In 2.5.0+, the callback is also optional. When no callback is passed, the method returns a Promise which resolves to the rendered HTML. + +### renderer.renderToStream + +Signature: + +``` js +renderer.renderToStream(vm[, context]): stream.Readable +``` + +Render a Vue instance to a [Node.js readable stream](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams). The context object is optional. See [Streaming](../guide/streaming.md) for more details. + +## Class: BundleRenderer + +### bundleRenderer.renderToString + +Signature: + +``` js +bundleRenderer.renderToString([context, callback]): ?Promise +``` + +Render the bundle to a string. The context object is optional. The callback is a typical Node.js style callback where the first argument is the error and the second argument is the rendered string. + +In 2.5.0+, the callback is also optional. When no callback is passed, the method returns a Promise which resolves to the rendered HTML. + +### bundleRenderer.renderToStream + +Signature: + +``` js +bundleRenderer.renderToStream([context]): stream.Readable +``` + +Render the bundle to a [Node.js readable stream](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams). The context object is optional. See [Streaming](../guide/streaming.md) for more details. + +## Renderer Options + +### template + +- **Type:** + - `string` + - `string | (() => string | Promise)` (since 2.6) + +**If using a string template:** + +Provide a template for the entire page's HTML. The template should contain a comment `` which serves as the placeholder for rendered app content. + +The template also supports basic interpolation using the render context: + +- Use double-mustache for HTML-escaped interpolation; +- Use triple-mustache for non-HTML-escaped interpolation. + +The template automatically injects appropriate content when certain data is found on the render context: + +- `context.head`: (string) any head markup that should be injected into the head of the page. + +- `context.styles`: (string) any inline CSS that should be injected into the head of the page. Note this property will be automatically populated if using `vue-loader` + `vue-style-loader` for component CSS. + +- `context.state`: (Object) initial Vuex store state that should be inlined in the page as `window.__INITIAL_STATE__`. The inlined JSON is automatically sanitized with [serialize-javascript](https://github.com/yahoo/serialize-javascript) to prevent XSS. + + In 2.5.0+, the embed script also self-removes in production mode. + + In 2.6.0+, if `context.nonce` is present, it will be added as the `nonce` attribute to the embedded script. This allows the inline script to conform to CSP that requires nonce. + +In addition, when `clientManifest` is also provided, the template automatically injects the following: + +- Client-side JavaScript and CSS assets needed by the render (with async chunks automatically inferred); +- Optimal `` resource hints for the rendered page. + +You can disable all automatic injections by also passing `inject: false` to the renderer. + +**If using a function template:** + +::: warning +Function template is only supported in 2.6+ and when using `renderer.renderToString`. It is NOT supported in `renderer.renderToStream`. +::: + +The `template` option can also be function that returns the rendered HTML or a Promise that resolves to the rendered HTML. This allows you to leverage native JavaScript template strings and potential async operations in the template rendering process. + +The function receives two arguments: + +1. The rendering result of the app component as a string; +2. The rendering context object. + +Example: + +``` js +const renderer = createRenderer({ + template: (result, context) => { + return ` + ${context.head} + ${result} + ` + } +}) +``` + +Note that when using a custom template function, nothing will be automatically injected - you will be in full control of what the eventual HTML includes, but also will be responsible for including everything yourself (e.g. asset links if you are using the bundle renderer). + +See also: + +- [Using a Page Template](../guide/#using-a-page-template) +- [Manual Asset Injection](../guide/build-config.md#manual-asset-injection) + +### clientManifest + +Provide a client build manifest object generated by `vue-server-renderer/client-plugin`. The client manifest provides the bundle renderer with the proper information for automatic asset injection into the HTML template. For more details, see [Generating clientManifest](../guide/build-config.md#generating-clientmanifest). + +### inject + +Controls whether to perform automatic injections when using `template`. Defaults to `true`. + +See also: [Manual Asset Injection](../guide/build-config.md#manual-asset-injection). + +### shouldPreload + +A function to control what files should have `` resource hints generated. + +By default, only JavaScript and CSS files will be preloaded, as they are absolutely needed for your application to boot. + +For other types of assets such as images or fonts, preloading too much may waste bandwidth and even hurt performance, so what to preload will be scenario-dependent. You can control precisely what to preload using the `shouldPreload` option: + +``` js +const renderer = createBundleRenderer(bundle, { + template, + clientManifest, + shouldPreload: (file, type) => { + // type is inferred based on the file extension. + // https://fetch.spec.whatwg.org/#concept-request-destination + if (type === 'script' || type === 'style') { + return true + } + if (type === 'font') { + // only preload woff2 fonts + return /\.woff2$/.test(file) + } + if (type === 'image') { + // only preload important images + return file === 'hero.jpg' + } + } +}) +``` + +### shouldPrefetch + +- 2.5.0+ + +A function to control what files should have `` resource hints generated. + +By default, all assets in async chunks will be prefetched since this is a low-priority directive; however you can customize what to prefetch in order to better control bandwidth usage. This option expects the same function signature as `shouldPreload`. + +### runInNewContext + +- only used in `createBundleRenderer` +- Expects: `boolean | 'once'` (`'once'` only supported in 2.3.1+) + +By default, for each render the bundle renderer will create a fresh V8 context and re-execute the entire bundle. This has some benefits - for example, the app code is isolated from the server process and we don't need to worry about the [stateful singleton problem](../guide/structure.md#avoid-stateful-singletons) mentioned in the docs. However, this mode comes at some considerable performance cost because re-executing the bundle is expensive especially when the app gets bigger. + +This option defaults to `true` for backwards compatibility, but it is recommended to use `runInNewContext: false` or `runInNewContext: 'once'` whenever you can. + +> In 2.3.0 this option has a bug where `runInNewContext: false` still executes the bundle using a separate global context. The following information assumes version 2.3.1+. + +With `runInNewContext: false`, the bundle code will run in the same `global` context with the server process, so be careful about code that modifies `global` in your application code. + +With `runInNewContext: 'once'` (2.3.1+), the bundle is evaluated in a separate `global` context, however only once at startup. This provides better app code isolation since it prevents the bundle from accidentally polluting the server process' `global` object. The caveats are that: + +1. Dependencies that modifies `global` (e.g. polyfills) cannot be externalized in this mode; +2. Values returned from the bundle execution will be using different global constructors, e.g. an error caught inside the bundle will not be an instance of `Error` in the server process. + +See also: [Source Code Structure](../guide/structure.md) + +### basedir + +- only used in `createBundleRenderer` + +Explicitly declare the base directory for the server bundle to resolve `node_modules` dependencies from. This is only needed if your generated bundle file is placed in a different location from where the externalized NPM dependencies are installed, or your `vue-server-renderer` is NPM-linked into your current project. + +### cache + +Provide a [component cache](../guide/caching.md#component-level-caching) implementation. The cache object must implement the following interface (using Flow notations): + +``` js +type RenderCache = { + get: (key: string, cb?: Function) => string | void; + set: (key: string, val: string) => void; + has?: (key: string, cb?: Function) => boolean | void; +}; +``` + +A typical usage is passing in an [lru-cache](https://github.com/isaacs/node-lru-cache): + +``` js +const LRU = require('lru-cache') + +const renderer = createRenderer({ + cache: LRU({ + max: 10000 + }) +}) +``` + +Note that the cache object should at least implement `get` and `set`. In addition, `get` and `has` can be optionally async if they accept a second argument as callback. This allows the cache to make use of async APIs, e.g. a Redis client: + +``` js +const renderer = createRenderer({ + cache: { + get: (key, cb) => { + redisClient.get(key, (err, res) => { + // handle error if any + cb(res) + }) + }, + set: (key, val) => { + redisClient.set(key, val) + } + } +}) +``` + +### directives + +Allows you to provide server-side implementations for your custom directives: + +``` js +const renderer = createRenderer({ + directives: { + example (vnode, directiveMeta) { + // transform vnode based on directive binding metadata + } + } +}) +``` + +As an example, check out [`v-show`'s server-side implementation](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js). + +### serializer + +> New in 2.6 + +Provide a custom serializer function for `context.state`. Since the serialized state will be part of your final HTML, it is important to use a function that properly escapes HTML characters for security reasons. The default serializer is [serialize-javascript](https://github.com/yahoo/serialize-javascript) with `{ isJSON: true }`. + +## Server-only Component Options + +### serverCacheKey + +- **Type:** `(props) => any` + + Return the cache key for the component based on incoming props. Does NOT have access to `this`. + + Since 2.6, you can explicitly bail out of caching by returning `false`. + + See more details in [Component-level Caching](../guide/caching.html#component-level-caching). + +### serverPrefetch + +- **Type:** `() => Promise` + + Fetch async data during server side rendering. It should store fetched data into a global store and return a Promise. The server renderer will wait on this hook until the Promise is resolved. This hook has access to the component instance via `this`. + + See more details in [Data Fetching](../guide/data.html). + +## webpack Plugins + +The webpack plugins are provided as standalone files and should be required directly: + +``` js +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') +``` + +The default files generated are: + +- `vue-ssr-server-bundle.json` for the server plugin; +- `vue-ssr-client-manifest.json` for the client plugin. + +The filenames can be customized when creating the plugin instances: + +``` js +const plugin = new VueSSRServerPlugin({ + filename: 'my-server-bundle.json' +}) +``` + +See [Build Configuration](../guide/build-config.md) for more information. diff --git a/en/basic.md b/docs/guide/README.md similarity index 80% rename from en/basic.md rename to docs/guide/README.md index afc7a7a7..b56951b3 100644 --- a/en/basic.md +++ b/docs/guide/README.md @@ -1,4 +1,4 @@ -# Basic Usage +# Getting Started ## Installation @@ -10,7 +10,7 @@ We will be using NPM throughout the guide, but feel free to use [Yarn](https://y #### Notes -- It's recommended to use Node.js version 6+. +- It's recommended to use Node.js version 10+. - `vue-server-renderer` and `vue` must have matching versions. - `vue-server-renderer` relies on some Node.js native modules and therefore can only be used in Node.js. We may provide a simpler build that can be run in other JavaScript runtimes in the future. @@ -101,7 +101,7 @@ Notice the `` comment -- this is where your app's markup wi We can then read and pass the file to the Vue renderer: ``` js -const renderer = createRenderer({ +const renderer = require('vue-server-renderer').createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) @@ -155,3 +155,47 @@ In addition, the template supports some advanced features such as: - Auto injection and XSS prevention when embedding Vuex state for client-side hydration. We will discuss these when we introduce the associated concepts later in the guide. + +## full demo codes + +```js + +const Vue = require('vue'); +const server = require('express')(); + +const template = require('fs').readFileSync('./index.template.html', 'utf-8'); + +const renderer = require('vue-server-renderer').createRenderer({ + template, +}); + +const context = { + title: 'vue ssr', + meta: ` + + + `, +}; + +server.get('*', (req, res) => { + const app = new Vue({ + data: { + url: req.url + }, + template: `
The visited URL is: {{ url }}
`, + }); + + renderer + .renderToString(app, context, (err, html) => { + console.log(html); + if (err) { + res.status(500).end('Internal Server Error') + return; + } + res.end(html); + }); +}) + +server.listen(8080); + +``` diff --git a/en/build-config.md b/docs/guide/build-config.md similarity index 99% rename from en/build-config.md rename to docs/guide/build-config.md index 646f4760..d9821135 100644 --- a/en/build-config.md +++ b/docs/guide/build-config.md @@ -76,7 +76,7 @@ The client config can remain largely the same with the base config. Obviously yo In addition to the server bundle, we can also generate a client build manifest. With the client manifest and the server bundle, the renderer now has information of both the server *and* client builds, so it can automatically infer and inject [preload / prefetch directives](https://css-tricks.com/prefetching-preloading-prebrowsing/) and css links / script tags into the rendered HTML. -The benefits is two-fold: +The benefits are two-fold: 1. It can replace `html-webpack-plugin` for injecting the correct asset URLs when there are hashes in your generated filenames. @@ -143,7 +143,7 @@ With this setup, your server-rendered HTML for a build with code-splitting will -` + ``` ### Manual Asset Injection @@ -188,7 +188,7 @@ In the `renderToString` callback, the `context` object you passed in will expose - Preload the JavaScript and CSS files needed by the page - Prefetch async JavaScript chunks that might be needed later - Preloaded files can be further customized with the [`shouldPreload`](./api.md#shouldpreload) option. + Preloaded files can be further customized with the [`shouldPreload`](../api/#shouldpreload) option. - `context.getPreloadFiles()` diff --git a/en/bundle-renderer.md b/docs/guide/bundle-renderer.md similarity index 89% rename from en/bundle-renderer.md rename to docs/guide/bundle-renderer.md index 8107be24..eff28018 100644 --- a/en/bundle-renderer.md +++ b/docs/guide/bundle-renderer.md @@ -20,7 +20,7 @@ This is straightforward, however whenever you edit your app source code, you wou - Critical CSS injection (when using `*.vue` files): automatically inlines the CSS needed by components used during the render. See the [CSS](./css.md) section for more details. -- Asset injection with [clientManifest](./api.md#clientmanifest): automatically infers the optimal preload and prefetch directives, and the code-split chunks needed for the initial render. +- Asset injection with [clientManifest](../api/#clientmanifest): automatically infers the optimal preload and prefetch directives, and the code-split chunks needed for the initial render. --- @@ -49,4 +49,4 @@ server.get('*', (req, res) => { When `renderToString` is called on a bundle renderer, it will automatically execute the function exported by the bundle to create an app instance (passing `context` as the argument) , and then render it. -Note it's recommended to set the `runInNewContext` option to `false` or `'once'`. See its [API reference](./api.md#runinnewcontext) for more details. +Note it's recommended to set the `runInNewContext` option to `false` or `'once'`. See its [API reference](../api/#runinnewcontext) for more details. diff --git a/en/caching.md b/docs/guide/caching.md similarity index 92% rename from en/caching.md rename to docs/guide/caching.md index 87f7f760..5ff34b72 100644 --- a/en/caching.md +++ b/docs/guide/caching.md @@ -41,7 +41,7 @@ Since the content is cached for only one second, users will not see outdated con ## Component-level Caching -`vue-server-renderer` has built-in support for component-level caching. To enable it you need to provide a [cache implementation](./api.md#cache) when creating the renderer. Typical usage is passing in an [lru-cache](https://github.com/isaacs/node-lru-cache): +`vue-server-renderer` has built-in support for component-level caching. To enable it you need to provide a [cache implementation](../api/#cache) when creating the renderer. Typical usage is passing in an [lru-cache](https://github.com/isaacs/node-lru-cache): ``` js const LRU = require('lru-cache') @@ -73,6 +73,10 @@ The key returned from `serverCacheKey` should contain sufficient information to Returning a constant will cause the component to always be cached, which is good for purely static components. +::: tip Bailing out from Caching +Since 2.6.0, explicitly returning `false` in `serverCacheKey` will cause the component to bail out of caching and be rendered afresh. +::: + ### When to use component caching If the renderer hits a cache for a component during render, it will directly reuse the cached result for the entire sub tree. This means you should **NOT** cache a component when: diff --git a/en/css.md b/docs/guide/css.md similarity index 91% rename from en/css.md rename to docs/guide/css.md index 9b4cf21b..b43d785f 100644 --- a/en/css.md +++ b/docs/guide/css.md @@ -16,7 +16,7 @@ More importantly, `vue-style-loader`, the loader used internally by `vue-loader` - Common CSS Extraction. - This setup support using [`extract-text-webpack-plugin`](https://github.com/webpack-contrib/extract-text-webpack-plugin) to extract the CSS in the main chunk into a separate CSS file (auto injected with `template`), which allows the file to be individually cached. This is recommended when there is a lot of shared CSS. + This setup supports using [`extract-text-webpack-plugin`](https://github.com/webpack-contrib/extract-text-webpack-plugin) to extract the CSS in the main chunk into a separate CSS file (auto injected with `template`), which allows the file to be individually cached. This is recommended when there is a lot of shared CSS. CSS inside async components will remain inlined as JavaScript strings and handled by `vue-style-loader`. diff --git a/docs/guide/data.md b/docs/guide/data.md new file mode 100644 index 00000000..75cb87f5 --- /dev/null +++ b/docs/guide/data.md @@ -0,0 +1,281 @@ +# Data Pre-Fetching and State + +## Data Store + +During SSR, we are essentially rendering a "snapshot" of our app. The asynchronous data from our components needs to be available before we mount the client side app - otherwise the client app would render using different state and the hydration would fail. + +To address this, the fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, we can pre-fetch and fill data into the store while rendering. In addition, we will serialize and inline the state in the HTML after the app has finished rendering. The client-side store can directly pick up the inlined state before we mount the app. + +We will be using the official state management library [Vuex](https://github.com/vuejs/vuex/) for this purpose. Let's create a `store.js` file, with some mocked logic for fetching an item based on an id: + +``` js +// store.js +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +// Assume we have a universal API that returns Promises +// and ignore the implementation details +import { fetchItem } from './api' + +export function createStore () { + return new Vuex.Store({ + // IMPORTANT: state must be a function so the module can be + // instantiated multiple times + state: () => ({ + items: {} + }), + + actions: { + fetchItem ({ commit }, id) { + // return the Promise via `store.dispatch()` so that we know + // when the data has been fetched + return fetchItem(id).then(item => { + commit('setItem', { id, item }) + }) + } + }, + + mutations: { + setItem (state, { id, item }) { + Vue.set(state.items, id, item) + } + } + }) +} +``` + +::: warning +Most of the time, you should wrap `state` in a function, so that it will not leak into the next server-side runs. +[More info](./structure.md#avoid-stateful-singletons) +::: + +And update `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 () { + // create router and store instances + const router = createRouter() + const store = createStore() + + // sync so that route state is available as part of the store + sync(store, router) + + // create the app instance, injecting both the router and the store + const app = new Vue({ + router, + store, + render: h => h(App) + }) + + // expose the app, the router and the store. + return { app, router, store } +} +``` + +## Logic Collocation with Components + +So, where do we place the code that dispatches the data-fetching actions? + +The data we need to fetch is determined by the route visited - which also determines what components are rendered. In fact, the data needed for a given route is also the data needed by the components rendered at that route. So it would be natural to place the data fetching logic inside route components. + +We will use the `serverPrefetch` option (new in 2.6.0+) in our components. This option is recognized by the server renderer and will pause the rendering until the promise it returns is resolved. This allows us to "wait" on async data during the rendering process. + +::: tip +You can use `serverPrefetch` in any component, not just the route-level components. +::: + +Here is an example `Item.vue` component that is rendered at the `'/item/:id'` route. Since the component instance is already created at this point, it has access to `this`: + +``` html + + + + +``` + +::: warning +You should check if the component was server-side rendered in the `mounted` hook to avoid executing the logic twice. +::: + +::: tip +You may find the same `fetchItem()` logic repeated multiple times (in `serverPrefetch`, `mounted` and `watch` callbacks) in each component - it is recommended to create your own abstraction (e.g. a mixin or a plugin) to simplify such code. +::: + +## Final State Injection + +Now we know that the rendering process will wait for data fetching in our components, how do we know when it is "done"? In order to do that, we need to attach a `rendered` callback to the render context (also new in 2.6), which the server renderer will call when the entire rendering process is finished. At this moment, the store should have been filled with the final state. We can then inject it on to the context in that callback: + +``` 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(() => { + // This `rendered` hook is called when the app has finished rendering + context.rendered = () => { + // After the app is rendered, our store is now + // filled with the state from our components. + // When we attach the state to the context, and the `template` option + // is used for the renderer, the state will automatically be + // serialized and injected into the HTML as `window.__INITIAL_STATE__`. + context.state = store.state + } + + resolve(app) + }, reject) + }) +} +``` + +When using `template`, `context.state` will automatically be embedded in the final HTML as `window.__INITIAL_STATE__` state. On the client, the store should pick up the state before mounting the application: + +``` js +// entry-client.js + +import { createApp } from './app' + +const { app, store } = createApp() + +if (window.__INITIAL_STATE__) { + // We initialize the store state with the data injected from the server + store.replaceState(window.__INITIAL_STATE__) +} + +app.$mount('#app') +``` + +## Store Code Splitting + +In a large application, our Vuex store will likely be split into multiple modules. Of course, it is also possible to code-split these modules into corresponding route component chunks. Suppose we have the following store module: + +``` js +// store/modules/foo.js +export default { + namespaced: true, + + // IMPORTANT: state must be a function so the module can be + // instantiated multiple times + state: () => ({ + count: 0 + }), + + actions: { + inc: ({ commit }) => commit('inc') + }, + + mutations: { + inc: state => state.count++ + } +} +``` + +We can use `store.registerModule` to lazy-register this module in a route component's `serverPrefetch` hook: + +``` html +// inside a route component + + + +``` + +Because the module is now a dependency of the route component, it will be moved into the route component's async chunk by webpack. + +::: warning +Don't forget to use the `preserveState: true` option for `registerModule` so we keep the state injected by the server. +::: diff --git a/en/head.md b/docs/guide/head.md similarity index 95% rename from en/head.md rename to docs/guide/head.md index 5f3bcaca..d5b47760 100644 --- a/en/head.md +++ b/docs/guide/head.md @@ -50,12 +50,13 @@ Now, a route component can make use of this to control the document title: // Item.vue export default { mixins: [titleMixin], + title () { return this.item.title }, - asyncData ({ store, route }) { - return store.dispatch('fetchItem', route.params.id) + serverPrefetch () { + return this.$store.dispatch('fetchItem', this.$route.params.id) }, computed: { diff --git a/en/hydration.md b/docs/guide/hydration.md similarity index 89% rename from en/hydration.md rename to docs/guide/hydration.md index 65e93f0e..38e8d9e2 100644 --- a/en/hydration.md +++ b/docs/guide/hydration.md @@ -13,12 +13,19 @@ Since the server has already rendered the markup, we obviously do not want to th If you inspect the server-rendered output, you will notice that the app's root element has had a special attribute added: -``` js +``` html
``` The `data-server-rendered` special attribute lets the client-side Vue know that the markup is rendered by the server and it should mount in hydration mode. Note that it didn't add `id="app"`, just the `data-server-rendered` attribute: you need to add the ID or some other selector to the app's root element yourself or the app won't hydrate properly. +Note that on elements without the `data-server-rendered` attribute, hydration can also be forced by passing `true` to the `hydrating` argument of `$mount`: + +``` js +// Force hydration of the app +app.$mount('#app', true) +``` + In development mode, Vue will assert the client-side generated virtual DOM tree matches the DOM structure rendered from the server. If there is a mismatch, it will bail hydration, discard existing DOM and render from scratch. **In production mode, this assertion is disabled for maximum performance.** ### Hydration Caveats diff --git a/en/non-node.md b/docs/guide/non-node.md similarity index 100% rename from en/non-node.md rename to docs/guide/non-node.md diff --git a/en/routing.md b/docs/guide/routing.md similarity index 100% rename from en/routing.md rename to docs/guide/routing.md diff --git a/en/streaming.md b/docs/guide/streaming.md similarity index 100% rename from en/streaming.md rename to docs/guide/streaming.md diff --git a/en/structure.md b/docs/guide/structure.md similarity index 100% rename from en/structure.md rename to docs/guide/structure.md diff --git a/en/universal.md b/docs/guide/universal.md similarity index 97% rename from en/universal.md rename to docs/guide/universal.md index 95ddf645..5100a2e9 100644 --- a/en/universal.md +++ b/docs/guide/universal.md @@ -30,4 +30,4 @@ Most custom directives directly manipulate the DOM, and therefore will cause err 1. Prefer using components as the abstraction mechanism and work at the Virtual-DOM level (e.g. using render functions) instead. -2. If you have a custom directive that cannot be easily replaced by components, you can provide a "server-side version" of it using the [`directives`](./api.md#directives) option when creating the server renderer. +2. If you have a custom directive that cannot be easily replaced by components, you can provide a "server-side version" of it using the [`directives`](../api/#directives) option when creating the server renderer. diff --git a/docs/ja/README.md b/docs/ja/README.md new file mode 100644 index 00000000..2a0c0ccf --- /dev/null +++ b/docs/ja/README.md @@ -0,0 +1,50 @@ +# Vue.js サーバサイドレンダリングガイド + +> **注意:** このガイドは Vue.js またはサポートしているライブラリの以下の最小バージョンを必須としています: +> - vue & vue-server-renderer 2.3.0 以上 +> - vue-router 2.5.0 以上 +> - vue-loader 12.0.0 以上 & vue-style-loader 3.0.0 以上 + +> もし以前に、 SSR で Vue 2.2 を使用していた場合、 推奨されるコードの構造が[少しだけ違うこと](./guide/structure.md)に気がつくでしょう (新しいオプションの [runInNewContext](./api/README.md#runinnewcontext) を `false` にしている場合)。あなたの既存のアプリケーションは依然として動作はするでしょうが、新しく推奨される方に移行されることをオススメします。 + +## サーバサイドレンダリング (SSR) とは何か? + +Vue.js はクライアントサイドアプリケーションを構築するためのフレームワークです。通常では、Vue コンポーネントはブラウザで DOM を生成し操作がされます。しかし、同じ Vue コンポーネントをサーバ上の HTML 文字列に描画し、ブラウザに直接送信し、最終的に静的なマークアップとしてクライアント上の完全なインタラクティブアプリケーションに "ハイドレート (hydrate)" することもできます。 + +サーバで描画された Vue.js のアプリケーションは、アプリケーションのコードの大部分が、サーバとクライアントの**両方**で実行されるという意味で、"アイソモルフィック (isomorphic)" や "ユニバーサル (universal)" と見なすことができます。 + +## どうして SSR なのか? + +従来の SPA (シングルページアプリケーション) と比べて、SSR の利点は主に次の点にあります: + +- 検索エンジンのクローラが完全に描画されたページを直接解析するため、SEO が向上します。 + + 現在のところ、Google と Bing は同期的 JavaScript アプリケーションのインデックスを作成できます。同期がキーワードです。あなたのアプリケーションが読み込み中にスピナが表示され、Ajax 経由でコンテンツを取得する場合、クローラーはあなたが完了するまで待たないでしょう。つまり、SEO が重要なページで非同期にコンテンツを取得する場合は、SSR が必要な場合があります。 + +- 特にインターネットの遅さや遅いデバイスでは、コンテンツの再生時間が短縮されます。サーバで描画されたマークアップは、すべての JavaScript がダウンロードされて表示されるまで待つ必要がないので、ユーザは完全に描画されたページをすぐに見ることができます。これにより、一般的にユーザーエクスペリエンスが向上し、コンテンツの所要時間が直接コンバージョン率に関連付けられているアプリケーションにとっては重要になります。 + +SSR を使用する際に考慮すべきトレードオフも何点かあります: + +- 開発上の制約。 ブラウザ固有のコードは、特定のライフサイクルフック内でのみ使用できます。一部の外部ライブラリは、サーバで描画されたアプリケーションで実行できるように特別な処理が必要な場合があります。 + +- より複雑なセットアップと開発の要件を構築します。静的ファイルサーバに展開できる完全静的 SPA とは異なり、サーバで描画されたアプリケーションでは Node.js サーバを実行できる環境が必要になります。 + +- サーバ側の負荷が増えます。 Node.js の完全なアプリケーションを描画することは、静的ファイルを提供するだけでなく、CPU を多用することになるので、トラフィックが多いことが見込まれる場合は、対応するサーバの負荷に備え、キャッシュの対策を賢明に行なってください。 + +あなたのアプリケーションに SSR を使用する前に、まず初めに、実際に SSR が必要かどうかを考える必要があります。これは主に、アプリケーションのコンテンツに対する時間の重要性によります。 例えば、最初の負荷の数百ミリ秒がそれほど重要ではない内部的なダッシュボードを構築する場合、SSR は過度なものとなるでしょう。しかし、コンテンツの所要時間が非常に重要な場合は、SSR を使用してできるだけ早く初期ロードパフォーマンスを保つことができます。 + +## SSR vs プリレンダリング (事前描画) + +もしあなたが、幾つかのマーケティングのページの SEO を向上させるためだけに SSR を調べているとしたら (たとえば `/`, `/about`, `/contact` など)、代わりに **プリレンダリング (事前描画)** を使用することをオススメします。 HTML を急いでコンパイルするために Web サーバーを使用するのではなく、プリレンダリングは、ビルド時に特定のルートに対して静的な HTML ファイルを生成します。利点はプリレンダリングを設定する方が遥かに簡単で、フロントエンドを完全に静的なサイトとして保つことができることです。 + +もしあなたが webpack を使用している場合、[prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin) を使用することで簡単にプリレンダリングを実装することができます。 これは Vue アプリケーションで広くテストが行われています。実際には、[作成者は](https://github.com/chrisvfritz) Vue のコアチームメンバです。 + +## このガイドについて + +このガイドはサーバとして Node.js を使用してサーバで描画されたシングルページアプリケーションに焦点が当てられています。他のバックエンドの設定と Vue の SSR と混在させることは、それ自体主題であり、[専用セクション](./guide/non-node.md)で手短に議論されています。 + +このガイドは、Vue.js 自体に精通しており、且つ Node.js と webpack に関する実用的な知識を持っていることを前提としています。もしあなたが、すぐに使用できる高度なソリューションを求めている場合は、[Nuxt.js](http://nuxtjs.org/) を試してみてください。これは同じ Vue スタック上に構築されていますが、多くの定型文が抽象化されており、静的なサイト生成などの追加機能を提供します。しかし、アプリケーションの構造をより直接的に制御する必要がある場合は、ユースケースに合わない場合があります。いずれにしても、仕組みをより理解するために、このガイドを読むことはまだ有益です。 + +また、あなたが読まれているように、このガイドで説明されている技術のほとんどが [HackerNews Demo](https://github.com/vuejs/vue-hackernews-2.0/) にて使用されており、そちらを参照するととても役立つことでしょう。 + +最後に、このガイドの解決策は決定的なものではないことを覚えておいてください。私たちはこれらがうまく働くことを見つけていますが、これ以上改善がされないということではありません。将来改訂されるかもしれません。プルリクエストを送ることによって貢献することも、もちろん自由です! diff --git a/docs/ja/api/README.md b/docs/ja/api/README.md new file mode 100644 index 00000000..3cb24d36 --- /dev/null +++ b/docs/ja/api/README.md @@ -0,0 +1,329 @@ +--- +sidebar: auto +--- + +# API リファレンス + +## createRenderer + +任意の引数 [options](#renderer-options) を用いて [`Renderer`](#class-renderer) インスタンスを生成します。 + +```js +const { createRenderer } = require('vue-server-renderer') +const renderer = createRenderer({ /* options */ }) +``` +## createBundleRenderer + +サーババンドルと任意の引数 [options](#renderer-options) を用いて [`BundleRenderer`](#class-bundlerenderer) インスタンスを生成します。 + +```js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer(serverBundle, { /* options */ }) +``` + +引数 `serverBundle` には次のいずれか1つを指定できます: + +- 生成されたバンドルファイル (`.js` または `.json`)への絶対パス。ファイルパスとして扱われるために `/` で開始されなければなりません。 + +- webpack と `vue-server-renderer/server-plugin` によって生成されたバンドルオブジェクト。 + +- JavaScript コードの文字列 (非推奨)。 + +より詳しい情報は、 [サーババンドルの紹介](../guide/bundle-renderer.md) と [ビルド設定](../guide/build-config.md) の項目を参照してください。 + +## `クラス: Renderer` + +### renderer.renderToString + +シグネチャ: + +```js +renderer.renderToString(vm[, context, callback]): ?Promise +``` + +Vue インスタンスを文字列として描画します。context オブジェクトの指定は、任意です。callback は、第1引数にエラー内容、 第2引数に描画された文字列を受け取る、典型的な Node.js のコーディングスタイルである関数を指定します。 + +2.5.0 以降においては、コールバックはオプションです。コールバックなしで渡されるとき、HTML に描画されるのを解決するプロミスを返します。 + +### renderer.renderToStream + +シグネチャ: +```js +renderer.renderToStream(vm[, context]): stream.Readable +``` + +Vue インスタンスを [Node.js の読み取り可能なストリーム](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 +``` + +サーババンドルを文字列として描画します。context オブジェクトの指定は、任意です。callback は、第1引数にエラー内容、 第2引数に描画された文字列を受け取る、典型的な Node.js のコーディングスタイルである関数を指定します。 + +2.5.0 以降においては、コールバックは任意です。コールバックなしで渡されたとき、そのメソッドは描画された HTML に解決するプロミスを返します。 + +### bundleRenderer.renderToStream + +シグネチャ: + +```js +bundleRenderer.renderToStream([context]): stream.Readable +``` + +バンドルを [Node.js の読み取り可能なストリーム](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html#stream_readable_streams)に描画します。コンテキストオブジェクトはオプションです。より詳細は [ストリーミング](../guide/streaming.md)を参照してください。 + +## レンダラオプション + +### template + +- **型:** + - `string` + - `string | (() => string | Promise)` (2.6 から) + +**文字列テンプレートを使用している場合:** + +ページ全体の HTML を表すテンプレートを設定します。描画されたアプリケーションの内容を指し示すプレースホルダの代わりになるコメント文 `` をテンプレートには含むべきです。 + +テンプレートは、次の構文を使用した簡単な補間もサポートします。 + +- エスケープされたHTMLを補間する Mustache 構文(二重中括弧)の使用 +- エスケープしない生のHTMLを補間する Mustache 構文(三重中括弧)の使用 + +次の構文を見つけた時、テンプレートは自動で適切な内容を挿入します。 + +- `context.head`: (string) ページ内の head に挿入されるべき任意のマークアップを文字列で指定します。 + +- `context.styles`: (string) ページ内の head に挿入されるべき任意のインライン CSS を文字列で指定します。もし CSS コンポーネントのために `vue-loader` + `vue-style-loader` を使用する場合、このプロパティは自動で追加されることに注意してください。 + +- `context.state`: (Object) `window.__INITIAL_STATE__` としてページ内にインライン展開されるべき Vuex のストアの初期状態を指定します。このインライン JSON は自動でクロスサイトスプリクティングを防ぐ [シリアライズされた javascript](https://github.com/yahoo/serialize-javascript) へサニタイズされます。 + + 2.5.0 以降においては、埋め込みスクリプトはプロダクションモードで自動的に削除されます。 + + 2.6.0 以降では、 `context.nonce` が存在すれば、それは、埋め込みスクリプトに `nonce` 属性として追加されます。これにより、インラインスクリプトを nonce を必要とする CSP に準拠することができます。 + +加えて、`clientManifest` も渡された場合、テンプレートは自動で以下を挿入します。 + +- (自動で受信される非同期のデータを含んだ)描画対象が必要とするクライアントサイドの JavaScript と CSS アセット +- 描画済みのページに対する最適な `` リソースヒント + +レンダラに `inject: false` も渡すことで、すべての自動挿入を無効にすることができます。 + +**関数テンプレートを使用している場合:** + +::: warning +関数テンプレートは `renderer.renderToString` を使用するとき、2.6 以降でのみサポートされます。`renderer.renderToStream` はまだサポートされていません。 +::: + +`template` オプションは、描画された HTML 、もしくは描画された HTML を解決する Promise を返す関数を指定できます。これにより、テンプレート文字列、そしてテンプレート描画プロセスにあり得る非同期な操作を利用できます。 + +関数は 2 つの引数を受け取ります: + +1. アプリケーションコンポーネントの描画結果の文字列 +2. 描画コンテキストオブジェクト + +例: + +```js +const renderer = createRenderer({ + template: (result, context) => { + return ` + ${context.head} + ${result} + ` + } +}) +``` + +カスタムテンプレート関数を使用するとき、自動的に注入されるものが何もないことに注意してください。最終的な HTML に含まれるものを完全に制御できますが、全てあなた自身で含める必要があります (例えば、バンドルレンダラを使用する場合のアセットのリンク)。 + +参照: + +- [ページテンプレートの使用](../guide/#using-a-page-template) +- [手動によるアセットインジェクション](../guide/build-config.md#manual-asset-injection) + +### clientManifest + +`vue-server-renderer/server-plugin` によって生成されたクライアントビルドマニフェストオブジェクトを提供します。クライアントマニフェストは、HTML テンプレートへの自動アセット挿入に適した情報とともに、バンドルレンダラを提供します。より詳しい情報は [クライアントマニフェストの生成](../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) => { + // 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 以降でのみサポートされる) + +デフォルトでは、BundleRenderer の描画ごとに未使用の 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 依存関係とは異なる場所に置かれた生成済みバンドルファイル、または、あなたの現在のプロジェクト内へ npm link された `vue-server-renderer` のみが必要です。 + +### cache + +[コンポーネントキャッシュ](../guide/caching.md#component-level-caching) の実装を提供します。 キャッシュオブジェクトは以下のインタフェースで実装しなければいけません(以下のような記法を用いる): + +```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` は、もし第 2 引数に callback が指定された場合、必要に応じてこれを非同期処理にできます。これは、非同期 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) です。 + +### serializer + +> 2.6 で新規追加 + +`context.state` に対してカスタムシリアライザ関数を提供します。シリアライズされた状態は最終的な HTML の一部になるため、セキュリティ上の理由から、HTML 文字を適切にエスケープする関数を使用することは重要です。デフォルトシリアライザは、`{ isJSON: true }` がセットされた [serialize-javascript](https://github.com/yahoo/serialize-javascript) です。 + +## サーバのみのコンポーネントオプション + +### serverCacheKey + +- **型:** `(props) => any` + + 受信プロパティ (incoming props) に基づいたコンポーネントのキャッシュキーを返します。 `this` にアクセスできません。 + 2.6 以降、`false` を返すことによってキャッシュを明示的に回避することができます。 + + 詳細は[コンポーネントレベルでのキャッシュ](../guide/caching.html#component-level-caching)を参照してください。 + +### serverPrefetch + +- **型:** `() => Promise` + + サーバサイドレンダリング中に非同期データをフェッチします。この関数はフェッチしたデータをグローバルストアに保存し、Promise を返します。サーバレンダラはこのフック上で Promise が解決されるまで待ちます。このフックは `this` 経由でコンポーネントインスタンスにアクセスします。 + + 詳細は[データのプリフェッチと状態](../guide/data.html)を参照してください。 + +## 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/ja/guide/README.md b/docs/ja/guide/README.md new file mode 100644 index 00000000..9e659af8 --- /dev/null +++ b/docs/ja/guide/README.md @@ -0,0 +1,153 @@ +# 基本的な使い方 + +## インストール + +```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: `
The visited URL is: {{ url }}
` + }) + + renderer.renderToString(app, (err, html) => { + if (err) { + res.status(500).end('Internal Server Error') + 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) // アプリのコンテンツを含む完全なページになります +}) +``` + +### テンプレートの展開 + +テンプレートはシンプルな展開 (interpolation) にも対応しています。 次のようなテンプレートであれば: + +```html + + + Codestin Search App + {{{ meta }}} + + + + + +``` + +`renderToString` の第2引数として "描画コンテキストオブジェクト"(render context object) を渡すことで展開データを提供することができます: + +```js +const context = { + title: 'hello', + meta: ` + + + ` +} + +renderer.renderToString(app, context, (err, html) => { + // ページタイトルは "hello" になり、 + // メタタグが注入されます +}) +``` + +`context` オブジェクトも Vue アプリインスタンスと共有することができ、コンポーネントがテンプレート展開のためにデータを動的に追加することができます。 + +さらに、テンプレートは次のような高度な機能をサポートしています: + +- `*.vue` コンポーネントを使用する際の、重要な CSS の自動注入 +- `clientManifest` を使用する際の、アセットリンクとリソースヒントの自動注入 +- クライアントサイドハイドレーションのために Vuex の状態を埋め込む際に XSS 防止の自動注入 + +関連する概念については、後でこのガイドで紹介します。 diff --git a/docs/ja/guide/build-config.md b/docs/ja/guide/build-config.md new file mode 100644 index 00000000..4c6449f9 --- /dev/null +++ b/docs/ja/guide/build-config.md @@ -0,0 +1,214 @@ +# ビルド設定 + +クライアントサイドで完結するプロジェクトの webpack 設定は既に知っての通りでしょう。SSR プロジェクトにおいても大枠は似たようなものですが、設定ファイルを 3 つのファイル(*base*、*client*、*server*)に分けることを提案しています。base 設定は出力パス、エイリアス、ローダーのような、client と server 両方の環境に共有される設定を含み、server 設定と client 設定は単純に、[webpack-merge](https://github.com/survivejs/webpack-merge) を使って、base 設定を拡張することができるものです。 + +## server 設定 + +server 設定は `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 コンポーネントをコンパイルするときにサーバー指向のコードを出力するよう + // `vue-loader`に指示する + target: 'node', + + // バンドルレンダラーのソースマップのサポート + 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 ファイルのようなファイルタイプを追加できる + // `グローバル` (例 ポリフィル) を変更する deps もホワイトリストに登録する必要がある + whitelist: /\.css$/ + }), + + // これはサーバービルドの出力全体を + // 1つの 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` に渡すことも可能です。これは開発中のホットリロードに対して便利で、[参考のセットアップとして](https://github.com/vuejs/vue-hackernews-2.0/blob/master/build/setup-dev-server.js) HackerNews の設定を見てみてください。 + +### externals の注意 + +CSS ファイルを `externals` オプションにホワイトリスト登録していることに注目してください。その理由は、依存関係からインポートされる CSS は webpack によって処理されないといけないからです。 もし同じように webpack に依存する他のタイプのファイルをインポートしているなら、 (例: `*.vue`, `*.sass`)、 それらも同じようにホワイトリストに加えなければいけません。 + +`runInNewContext: 'once'` または `runInNewContext: true` を使用する場合、例えば `babel-polyfill` のような `global` のように変更するポリフィルがホワイトリスト登録するために必要です。これは、新しいコンテキストモードを使用するときに、サーババンドルの内部コードは独自の `global` オブジェクトを持っているからです。Node 7.6 以降を使っていればサーバに `babel-polyfill` はあまり必要ないので、単純にクライアントエントリーにインポートする方が簡単です。 + +## client 設定 + +client 設定は base 設定とほぼ同じままです。言うまでもなく、クライアント側のエントリファイルに `entry` を示す必要があります。またそれとは別に、`CommonsChunkPlugin` 使っていたら、それが client 設定だけで使われていることを確認しておかないといけません。なぜなら、サーババンドルは単一のエントリーチャンクを要求するからです。 + +### `clientManifest` を生成する + +> version 2.3.0 以降必須 + +サーババンドルに加えて、クライアントビルドマニフェストを作成することもできます。レンダラーは、クライアントマニフェストとサーババンドルでサーバ側*と*クライアント側の両方のビルド情報を持つことになり、 描画された HTML に [preload / prefetch directives](https://css-tricks.com/prefetching-preloading-prebrowsing/) や CSS の link や script タグを自動的に挿入することができます。 + +これには2重の恩恵があります: + +1. 生成されたファイル名にハッシュがある時に、正しい URL を注入する `html-webpack-plugin` の代替になります。 + +2. webpack のオンデマンドコード分割機能(code spliting)を利用するバンドルを描画する時に、最適なチャンクが preloaded / prefetched されるのを保証でき、かつ、クライアントに対するウォーターフォールリクエストを避けるために、必要な非同期チャンクに `` タグを挿入することができます。そのようにして 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..7ffc240a --- /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..78728ece --- /dev/null +++ b/docs/ja/guide/caching.md @@ -0,0 +1,91 @@ +# キャッシュ + +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` の実装を修正する必要があります。 + +定数を返すと、コンポーネントは常にキャッシュされ、単なる静的なコンポーネントには効果的です。 + +::: tip キャッシングの回避 +2.6.0 以降、 `serverCacheKey` で明示的に `false` を返すことでコンポーネントはキャッシングを回避して新たに描画されるようになります。 +::: + +### いつコンポーネントキャッシュを使うか + +描画中にレンダラがコンポーネントのキャッシュにヒットした場合、キャッシュされた結果をサブツリー全体で直接再利用します。 つまり、次の場合にコンポーネントをキャッシュ **しない** でください。 + +- グローバルな状態に依存する子コンポーネントがあります。 +- 描画 `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..eb7f5b18 --- /dev/null +++ b/docs/ja/guide/data.md @@ -0,0 +1,278 @@ +# データのプリフェッチと状態 + +## データストア + +SSR をしているとき、基本的にはアプリケーションの"スナップショット"を描画しています。クライアントサイドのアプリケーションがマウントする前に、コンポーネントから非同期データが、利用可能である必要があります。つまり、それ以外の場合、クライアントアプリケーションは異なる状態を使用して描画するため、ハイドレーションは失敗します。 + +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり専用のデータストア (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) + } + } + }) +} +``` + +::: warning +ほとんどの場合、次のサーバサイドの実行においてリークしないよう、 `state` を関数でラップする必要があります。[詳細情報はこちら](./structure.md#avoid-stateful-singletons) +::: + +そして `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) によって決まります。またそのルートによってどのコンポーネントが描画されるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートで描画されるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に置くのが自然でしょう。 + +コンポーネントでは、 `serverPrefetch` オプション (2.6.0 以降で新規追加)を使用します。このオプションは、サーバレンダラによって認識され、そして それを返す Promise が解決されるまで描画を一時停止します。これにより、描画処理中に非同期データを"待つ"ことができます。 + +::: tip +ルートレベルのコンポーネントだけでなく、任意のコンポーネントで `serverPrefetch` を使用できます。 +::: + +これは、`'/item/:id'` ルートで描画される `Item.vue` コンポーネントの例です。コンポーネントインスタンスはこの時点では既に作成されているので、 `this` にアクセスできます: + +```html + + + + +``` + +::: warning +ロジックが 2 回実行されないようにするために、コンポーネントは `mounted` フックでサーバサイドで描画されているかどうかチェックする必要があります。 +::: + +::: tip +各コンポーネントで同じ `fetchItem()` ロジックが複数回 (`serverPrefetch`、`mounted`、そして `watch` コールバック)繰り返されているのを見つけるかもしれません。そのようなコードをシンプルにするために、あなた自身で抽象化(例えばミックスインまたはプラグイン)することを推奨します。 +::: + +## 最終状態注入 + +これで、描画プロセスがコンポーネント内のデータフェッチを待つことがわかりましたが、それが"完了"したというのをどうやって分かるのでしょうか?それをするために、描画コンテキストに `rendered` コールバックをアタッチする必要があります(これも 2.6 での新機能)。これは描画プロセス全体が終了したときにサーバーレンダラによって呼ばれます。現時点で、ストアは最終的な状態で満たされているはずです。そのコールバック内でコンテキストに状態を注入できます: + +```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(() => { + // この `rendered` フックは、アプリケーションの描画が終えたときに呼び出されます + context.rendered = () => { + // アプリケーションが描画された後、ストアには、 + // コンポーネントからの状態で満たされています + // 状態を context に付随させ、`template` オプションがレンダラに利用されると、 + // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます + context.state = store.state + } + + resolve(app) + }, reject) + }) +} +``` + +`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL_STATE__` という形の状態として埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがその状態を取得します: + +```js +// entry-client.js + +const { app, store } = createApp() + +if (window.__INITIAL_STATE__) { + // サーバから注入されたデータでストアの状態を初期化します + store.replaceState(window.__INITIAL_STATE__) +} + +app.$mount('#app') +``` + +## ストアコードの分割 + +大規模なアプリケーションでは、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 によってルートコンポーネントの非同期チャンクに移動されます。 + +::: warning +サーバによって注入された状態を維持するため、`registerModule` に `preserveState: true` オプションを使用することを忘れないでください。 +::: 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..afbbe7b6 --- /dev/null +++ b/docs/ja/guide/hydration.md @@ -0,0 +1,39 @@ +# クライアントサイドでのハイドレーション + +`entry-client.js` において、以下の記述で私たちは簡単にアプリケーションをマウントします: + +```js +// これは、ルート要素に id="app" をもつ App.vue テンプレートを想定します +app.$mount('#app') +``` + +サーバがマークアップを描画後に、この処理を実行し、すべての DOM を再生成することを私たちは当然したくありません。代わりに、"ハイドレート (hydrate)"することで、静的なマークアップを単にインタラクティブにしたいです。 + +サーバの描画出力を調べたら、アプリケーションのルート要素が以下のような特別な属性を持っていることに気づくでしょう: + +```js +
+``` + +この `data-server-rendered` という特別な属性は、クライアントサイドの Vue に、これがサーバ上で描画されたことと、この要素はハイドレーションモードでマウントしなくてはいけないことを知らせます。この属性は`id="app"`をタグに追加しないことに注意してください。それは単なる`data-server-rendered`属性です。つまり、ID または 他のセレクタをあなた自身でルート要素に追加する必要があります。そうしないと、アプリケーションが適切にハイドレートできなくなります。 + + +`data-server-rendered`属性を要素に指定しない場合でも、`$mount`の`hydrating`実引数に`true`を指定することでハイドレートさせることができます。 +```js +// Force hydration of the app +app.$mount('#app', true) +``` + +開発モードでは、Vue はクラインアントサイドで生成された仮想 DOM が、サーバで描画された DOM の構成と一致しているかアサートを行います。もしこれが一致しない場合、 Vue はハイドレーションを取りやめ、サーバーが生成した既存の 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..e73ef697 --- /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..f5551efc --- /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) オプションを使用して、そのオプションの "サーバーサイドのバージョン" を用意することで回避できます。 diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 00000000..f399f06b --- /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 и 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. + +## Об этом руководстве + +Это руководство ориентировано на 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..32336a73 --- /dev/null +++ b/docs/ru/api/README.md @@ -0,0 +1,332 @@ +--- +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-v10.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-v10.x/docs/api/stream.html#stream_readable_streams). Объект контекста опционален. Подробнее в разделе [Стриминг](../guide/streaming.md). + +## Опции рендерера + +### template + +- **Тип:** + - `string` + - `string | (() => string | Promise)` (с версии 2.6) + +**При использовании строкового шаблона:** + +Предоставляет шаблон для всей 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. + + С версии 2.6.0+, если присутствует `context.nonce`, он будет добавлен как атрибут `nonce` во встраиваемый скрипт. Это позволит встраиваемому скрипту соответствовать CSP, который требует nonce. + +Кроме того, когда предоставлен `clientManifest`, шаблон автоматически внедряет следующее: + +- JavaScript и CSS ресурсы для клиентской части, необходимые для рендеринга (с асинхронными фрагментами добавляемыми автоматически); +- Оптимальные ресурсы `` для отображаемой страницы. + +Вы можете отключить все автоматические внедрения передав `inject: false` в рендерер. + +**При использовании функции шаблона:** + +::: warning ВНИМАНИЕ +Шаблоны в виде функции поддерживаются только с версии 2.6+ и при использовании `renderer.renderToString`. Это НЕ ПОДДЕРЖИВАЕТСЯ в `renderer.renderToStream`. +::: + +Опция `template` также может быть функцией, которая возвращает отрендеренный HTML или Promise, который разрешится отрендеренным HTML. Это позволит использовать собственные строковые шаблоны JavaScript и потенциальные асинхронные операции в процессе рендеринга шаблона. + +Функция принимает два аргумента: + +1. Результат рендеринга компонента приложения в виде строки; +2. Объект контекста рендеринга. + +Пример: + +``` js +const renderer = createRenderer({ + template: (result, context) => { + return ` + ${context.head} + ${result} + ` + } +}) +``` + +Обратите внимание, что при использовании собственной функции для шаблона ничего автоматически не будет добавляться — вы полностью контролируете то, что будет включаться в HTML, но также нести ответственность за включение всего необходимого (например, ссылки на ресурсы, если вы используете bundle renderer). + +См. также: + +- [Использование шаблона страниц](../guide/#испоnьзование-шабnона-страниц) +- [Внедрение ресурсов вручную](../guide/build-config.md#внедрение-ресурсов-вручную) + +### clientManifest + +Предоставляет объект манифеста клиентской сборки, сгенерированный `vue-server-renderer/client-plugin`. Клиентский манифест предоставляет для рендерера сборки необходимую информацию для автоматического внедрения ресурсов в шаблон HTML. Подробнее в разделе [Генерация `clientManifest`](../guide/build-config.md#генерация-clientmanifest). + +### inject + +Контролирует, выполнять ли автоматические внедрения при использовании `template`. По умолчанию `true`. + +См. также: [Внедрение ресурсов вручную](../guide/build-config.md#внедрение-ресурсов-вручную). + +### 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#избегайте-сингnтонов-с-состоянием), которая упоминалась ранее в руководстве. Однако этот режим требует значительных затрат производительности, поскольку повторное выполнение сборки обходится дорого, особенно когда приложение становится большим. + +По умолчанию эта опция имеет значение `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#кэширование-на-уровне-компонентов). Объект кэша должен реализовать следующий интерфейс (соответствуя нотациям 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). + +### serializer + +> Добавлено в версии 2.6 + +Пользовательская функция сериализатора для `context.state`. Поскольку сериализованное состояние будет частью вашего итогового HTML, важно использовать функцию, которая должным образом экранирует символы HTML по соображениям безопасности. Сериализатор по умолчанию — [serialize-javascript](https://github.com/yahoo/serialize-javascript) с опцией `{ isJSON: true }`. + +## Опции компонента только для сервера + +### serverCacheKey + +- **Тип:** `(props) => any` + + Возвращает ключ кэша для компонента на основе входных параметров. НЕ ИМЕЕТ доступа к `this`. + + Начиная с версии 2.6, вы можете явно отказываться от кэширования, возвращая значение `false`. + + Подробнее в [Кэшировании на уровне компонентов](../guide/caching.html#кэширование-на-уровне-компонентов). + +### serverPrefetch + +- **Тип:** `() => Promise` + + Загрузка асинхронных данных во время рендеринга на стороне сервера. Он должен сохранить полученные данные в глобальном хранилище или вернуть Promise. Рендерер сервера будет дожидаться разрешения Promise в этом хуке. Этот хук имеет доступ к экземпляру компонента через `this`. + + Подробнее в [Предзагрузке данных и состояния](../guide/data.html). + +## Плагины 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..5e155b0d --- /dev/null +++ b/docs/ru/guide/README.md @@ -0,0 +1,201 @@ +# Начало работы + +## Установка + +``` bash +npm install vue vue-server-renderer --save +``` + +В руководстве мы будем использовать NPM, но вы свободно можете использовать и [Yarn](https://yarnpkg.com/en/). + +#### Примечания + +- Рекомендуется использовать Node.js версии 10+. +- `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 = require('vue-server-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-состояния для гидратации на стороне клиента. + +Мы обсудим это дальше, когда будем разбирать все связанные концепции. + +## Полный листинг кода для демо + +```js +const Vue = require('vue'); +const server = require('express')(); + +const template = require('fs').readFileSync('./index.template.html', 'utf-8'); + +const renderer = require('vue-server-renderer').createRenderer({ + template, +}); + +const context = { + title: 'vue ssr', + meta: ` + + + `, +}; + +server.get('*', (req, res) => { + const app = new Vue({ + data: { + url: req.url + }, + template: `
Вы открыли URL: {{ url }}
`, + }); + +renderer + .renderToString(app, context, (err, html) => { + console.log(html); + if (err) { + res.status(500).end('Internal Server Error') + return; + } + res.end(html); + }); +}) + +server.listen(8080); +``` diff --git a/docs/ru/guide/build-config.md b/docs/ru/guide/build-config.md new file mode 100644 index 00000000..79930a38 --- /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`, убедитесь, что используете его только в конфигурации клиентской части, потому что для серверной сборки требуется одна точка входа. + +### Генерация `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()` + + Возвращает встроенные теги `