diff --git a/ja/README.md b/ja/README.md new file mode 100644 index 00000000..212a39ae --- /dev/null +++ b/ja/README.md @@ -0,0 +1,46 @@ +# 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 を使用していた場合、 推奨されるコードの構造が[少しだけ違うこと](./structure.md)に気がつくでしょう (新しいオプションの [runInNewContext](./api.md#runinnewcontext) を `false` にしている場合)。あなたの既存のアプリケーションは依然として動作はするでしょうが、新しく推奨される方に移行されることをオススメします。 + +## サーバサイドレンダリング (SSR) とは何か? + +Vue.js はクライアントサイドアプリケーションを構築するためのフレームワークです。通常では、Vue コンポーネントはブラウザで DOM を生成し操作がされます。しかし、同じ Vue コンポーネントをサーバー上の HTML 文字列にレンダリングし、ブラウザに直接送信し、最終的に静的なマークアップとしてクライアント上の完全なインタラクティブアプリケーションに "ハイドレート" することもできます。 + +サーバでレンダリングされた 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 と他のバックエンドの設定を混在させることは、独自のトピックであり、このガイドでは説明していません。 + +このガイドは、Vue.js 自体に精通しており、且つ Node.js と webpack に関する実用的な知識を持っていることを前提としています。もしあなたが、すぐに使用できる高度なソリューションを求めている場合は、[Nuxt.js](http://nuxtjs.org/) を試してみてください。これは同じ Vue スタック上に構築されていますが、多くの定型文が抽象化されており、静的なサイト生成などの追加機能を提供します。しかし、アプリケーションの構造をより直接的に制御する必要がある場合は、ユースケースに合わない場合があります。いずれにしても、仕組みをより理解するために、このガイドを読むことはまだ有益です。 + +また、あなたが読まれているように、このガイドで説明されている技術のほとんどが [HackerNews Demo](https://github.com/vuejs/vue-hackernews-2.0/) にて使用されており、そちらを参照するととても役立つことでしょう。 + +最後に、このガイドの解決策は決定的なものではないことを覚えておいてください。 - 私たちはこれらがうまく働くことを見つけていますが、これ以上改善がされないということではありません。将来改訂されるかもしれません - プルリクエストを送ることによって貢献することも、もちろん自由です! diff --git a/ja/SUMMARY.md b/ja/SUMMARY.md new file mode 100644 index 00000000..907d91d6 --- /dev/null +++ b/ja/SUMMARY.md @@ -0,0 +1,27 @@ +- [基本的な使い方](basic.md) +- [ユニバーサルなコードを書く](universal.md) +- [ソースコードの構造](structure.md) +- [ルーティングとコード分割](routing.md) +- [データのプリフェッチとステート](data.md) +- [クライアントサイドでのハイドレーション](hydration.md) +- [バンドルレンダラの紹介](bundle-renderer.md) +- [ビルド設定](build-config.md) +- [CSS の管理](css.md) +- [ヘッドの管理](head.md) +- [キャッシュ](caching.md) +- [ストリーミング](streaming.md) +- [API リファレンス](api.md) + - [createRenderer](api.md#createrendereroptions) + - [createBundleRenderer](api.md#createbundlerendererbundle-options) + - [クラス: Renderer](api.md#class-renderer) + - [クラス: BundleRenderer](api.md#class-bundlerenderer) + - [Renderer 生成時のオプション](api.md#renderer-options) + - [template](api.md#template) + - [clientManifest](api.md#clientmanifest) + - [inject](api.md#inject) + - [shouldPreload](api.md#shouldpreload) + - [runInNewContext](api.md#runinnewcontext) + - [basedir](api.md#basedir) + - [cache](api.md#cache) + - [directives](api.md#directives) + - [Webpack プラグイン](api.md#webpack-plugins) diff --git a/ja/api.md b/ja/api.md new file mode 100644 index 00000000..df578179 --- /dev/null +++ b/ja/api.md @@ -0,0 +1,224 @@ +# API リファレンス + +## `createRenderer([options])` + +任意の引数 [options](#renderer-options) を用いて [`Renderer`](#class-renderer) インスタンスを生成します。 + +```js +const { createRenderer } = require('vue-server-renderer') +const renderer = createRenderer({ ... }) +``` + +## `createBundleRenderer(bundle[, options])` + +サーババンドルと任意の引数 [options](#renderer-options) を用いて [`BundleRenderer`](#class-bundlerenderer) インスタンスを生成します。 + +```js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer(serverBundle, { ... }) +``` + +引数 `serverBundle` には次のいずれか1つを指定できます。 + +- 生成されたバンドルファイル (`.js` または `.json`) への絶対パス。 ファイルパスは、 `/` で始めなければいけません。 +- webpack と `vue-server-renderer/server-plugin` によって生成されたバンドルオブジェクト。 +- JavaScript コードの文字列 (非推奨)。 + +より詳しい情報は、 [サーババンドルの紹介](./bundle-renderer.md) と [ビルド設定](./build-config.md) の項目を参照してください。 + +## `クラス: Renderer` + +- #### `renderer.renderToString(vm[, context], callback)` + +Vue インスタンスを文字列として描画します。context オブジェクトの指定は、任意です。callback は、第1引数にエラー内容、 第2引数に描画された文字列を受け取る、典型的な Node.js のコーディングスタイルである関数を指定します。 + +- #### `renderer.renderToStream(vm[, context])` + +Vue インスタンスを Node.js のストリームへ描画します。context オブジェクトの指定は任意です。より詳しい情報は、[ストリーミング](./streaming.md) の項目を参照してください。 + +## `クラス: BundleRenderer` + +- #### `bundleRenderer.renderToString([context, ]callback)` + +サーババンドルを文字列として描画します。context オブジェクトの指定は、任意です。callback は、第1引数にエラー内容、 第2引数に描画された文字列を受け取る、典型的な Node.js のコーディングスタイルである関数を指定します。 + +- #### `bundleRenderer.renderToStream([context])` + +サーババンドルを Node.js のストリームへ描画します。context オブジェクトの指定は任意です。より詳しい情報は、[ストリーミング](./streaming.md) の項目を参照してください。 + +## Renderer 生成時のオプション + +- #### `template` + +ページ全体の 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) へサニタイズされます。 + +加えて、`clientManifest` も渡された場合、テンプレートは自動で以下を挿入します。 + +- (自動で受信される非同期のデータを含んだ)描画対象が必要とするクライアントサイドの JavaScript と CSS アセット +- 描画済みのページに対する最適な `` Resource Hints + +Renderer に `inject: false` も渡すことで、すべての自動挿入を無効にすることができます。 + +参照: + +- [ページテンプレートの使用](./basic.md#using-a-page-template) +- [手動によるアセットインジェクション](./build-config.md#manual-asset-injection) + - #### `clientManifest` +- 2.3.0以上 +- `createBundleRenderer` メソッド内でのみ使用可能 + +`vue-server-renderer/server-plugin` によって生成されたクライアントビルドマニフェストオブジェクトを提供します。clientManifest は、HTML テンプレートへの自動アセット挿入に適した情報とともに、BundleRenderer を提供します。より詳しい情報は [clientManifest の生成](./build-config.md#generating-clientmanifest) の項目を参照してください。 + +- +#### `inject` + + - 2.3.0以上 + + `template` 使用時に、自動挿入を行うかどうかを制御します。デフォルトは `true` です。 + +参考:[手動によるアセットインジェクション](./build-config.md#manual-asset-injection) + +- +#### `shouldPreload` + + - 2.3.0以上 + +どのファイルが `` 生成済みの Resource Hints を持つべきか制御するための関数を指定します。 + +デフォルトでは、JavaScript と CSS ファイルのみがプリロードされます。これらはアプリケーション起動時に必須なためです。 + +画像やフォントのようなその他のアセット種別を指定した際、 多すぎるプリロードは処理能力を無駄にし、またパフォーマンスさえも損なうかもしれません。そのため、 プリロードすべきものはアプリケーションの実装依存になるでしょう。 次のように `shouldPreload` オプションを使用することで、プリロードすべきものを正確に制御できます。 + +```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' + } + } + }) +``` + +- +#### `runInNewContext` + + - 2.3.0以上 + - `createBundleRenderer` メソッド内でのみ使用可能 + +デフォルトでは、BundleRenderer の描画ごとに未使用の V8 コンテキストを生成し、バンドル全体を再実行するでしょう。これにはいくつかのメリットがあります。例えば、私たちが以前から言及してきた「ステートフルでシングルトン」なデータを管理することの問題点について心配する必要がありません。しかしながら、このモードはいくつかの無視できないパフォーマンスの問題が起こります。 なぜなら、アプリケーションが大きくなるとき、バンドルの再実行は著しくコストがかかるためです。 + +このオプションは、下位互換のためデフォルトは `true` です。しかし、可能ならば常に `runInNewContext: false` を使用することが推奨されます。 + +参考:[ソースコードの構造](./structure.md) + +- +#### `basedir` + + - 2.2.0以上 + - `createBundleRenderer` メソッド内でのみ使用可能 + +`node_modules` の依存関係を解決するために、サーババンドルのためのルートディレクトリを明示的に宣言します。 ここでは、インストール済み外部 npm 依存関係とは異なる場所に置かれた生成済みバンドルファイル、または、あなたの現在のプロジェクト内へ npm link された `vue-server-renderer` のみが必要です。 + +- #### `cache` + +[コンポーネントキャッシュ](./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` は、もし第二引数に callback が指定された場合、必要に応じてこれを非同期処理にできます。これは、非同期 API を使用したキャッシュの利用を可能にします。 例)以下のような redis クライアント使用する場合 + +```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` + +以下のように、カスタムディレクティブをサーバサイドの実装で使用可能にします。 + +```js + const renderer = createRenderer({ + directives: { + example (vnode, directiveMeta) { + // transform vnode based on directive binding metadata + } + } + }) +``` + +一例として、[`v-show` のサーバサイド実装はこちら](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js) + +## Webpack プラグイン + +webpack プラグインは、スタンドアロンのファイルとして提供され、次の値を必要とします。 + +```js +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') +``` + +デフォルトで生成されるファイルは以下のものです。 + +- サーバサイドプラグインのための `vue-ssr-server-bundle.json` +- クライアントサイドプラグインのための `vue-ssr-client-manifest.json` + +プラグインのインスタンス生成時、これらのファイル名は以下のようにカスタマイズ可能です。 + +```js +const plugin = new VueSSRServerPlugin({ + filename: 'my-server-bundle.json' +}) +``` + +より詳しい情報は、 [ビルド設定](./build-config.md) の項目を参照してください。 diff --git a/ja/basic.md b/ja/basic.md new file mode 100644 index 00000000..9c9bac87 --- /dev/null +++ b/ja/basic.md @@ -0,0 +1,142 @@ +# 基本的な使い方 + +## インストール + +```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
+}) +``` + +## サーバと連携する + +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(` + + + Hello + ${html} + + `) + }) +}) +server.listen(8080) +``` + +## ページテンプレートを使用する + +Vueアプリを描画する際、レンダラはアプリのマークアップのみを生成します。この例では、出力を余計なHTMLページシェルでラップする必要がありました。 + +これをシンプル化するために、レンダラの作成時にページテンプレートを直接提供することができます。ほとんどの場合、ページテンプレートを単独のファイルに記述します。 例 `index.template.html`: + +```html + + + Hello + + + + +``` + +`` コメントに注目してみてください。 -- これはあなたのアプリケーションのマークアップが注入される場所です。 + +ファイルを読み込みVueレンダラに渡すことができます。 + +```js +const renderer = createRenderer({ + template: require('fs').readFileSync('./index.template.html', 'utf-8') +}) +renderer.renderToString(app, (err, html) => { + console.log(html) // will be the full page with app content injected. +}) +``` + +### テンプレート展開 + +テンプレートはシンプルな展開にも対応しています。 次のようなテンプレートであれば: + +```html + + + {{ title }} + {{{ 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/ja/build-config.md b/ja/build-config.md new file mode 100644 index 00000000..e5445436 --- /dev/null +++ b/ja/build-config.md @@ -0,0 +1,202 @@ +# ビルド設定 + +クライアントサイドで完結するプロジェクトの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, { + // Point entry to your app's server entry file + entry: '/path/to/entry-server.js', + // This allows webpack to handle dynamic imports in a Node-appropriate + // fashion, and also tells `vue-loader` to emit server-oriented code when + // compiling Vue components. + target: 'node', + // For bundle renderer source map support + devtool: 'source-map', + // This tells the server bundle to use Node-style exports + output: { + libraryTarget: 'commonjs2' + }, + // https://webpack.js.org/configuration/externals/#function + // https://github.com/liady/webpack-node-externals + // Externalize app dependencies. This makes the server build much faster + // and generates a smaller bundle file. + externals: nodeExternals({ + // do not externalize dependencies that need to be processed by webpack. + // you can add more file types here e.g. raw *.vue files + // you should also whitelist deps that modifies `global` (e.g. polyfills) + whitelist: /\.css$/ + }), + // This is the plugin that turns the entire output of the server build + // into a single JSON file. The default file name will be + // `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', { + // ...other renderer options +}) +``` + +別の方法として、 バンドルをオブジェクトとして`createBundleRenderer`に渡すことも可能で、これは開発中のホットリロードに対して便利です。 参考として [HackerNewsの設定](https://github.com/vuejs/vue-hackernews-2.0/blob/master/build/setup-dev-server.js) を見てみてください。 + +### externalsの注意 + + CSSファイルを`externals`オプションにホワイトリスト登録していることに注目してください。その理由は、依存関係からインポートされるCSS はwebpackによって処理されないといけないからです。 もし同じようにwebpackに依存する他のタイプのファイルをインポートしているなら、 (例: `*.vue`, `*.sass`)、 それらも同じようにホワイトリストに加えなければいけません。 + +ホワイトリスト登録する他のタイプのモジュールは、例えば `babel-polyfill`のような`global`を修正するポリフィルです。なぜなら、サーババンドルの中のコードは独自の ** `global` **オブジェクトを持っているからです。Node7.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: [ + // Important: this splits the webpack runtime into a leading chunk + // so that async chunks can be injected right after it. + // this also enables better caching for your app/vendor code. + new webpack.optimize.CommonsChunkPlugin({ + name: "manifest", + minChunks: Infinity + }), + // This plugins generates `vue-ssr-client-manifest.json` in the + // output directory. + 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 Management](./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.md#shouldpreload) オプションによってさらにカスタマイズが可能です。 + +- `context.getPreloadFiles()` + - 必須 `clientManifest` + +このメソッドは stringを返さない代わりに、プリロードされるべきアセットを表すファイルオブジェクトの配列を返します。これは HTTP/2サーバプッシュをプログラムで行うときに使えるでしょう。 + +`createBundleRenderer`に渡された`template`は`context`を使って挿入されるので、これらのメソッドをテンプレート内で(`inject: false`で)使用することができます: + +```html + + + + {{{ renderResourceHints() }}} + {{{ renderStyles() }}} + + + + {{{ renderState() }}} + {{{ renderScripts() }}} + + +``` + +もし `template` を全く使っていないのなら、自分自身でstringを結合することができます。 diff --git a/ja/bundle-renderer.md b/ja/bundle-renderer.md new file mode 100644 index 00000000..54050e42 --- /dev/null +++ b/ja/bundle-renderer.md @@ -0,0 +1,55 @@ +# バンドルレンダラの紹介 + +## 根本的なサーバサイドレンダリングの問題 + +今までは、バンドルされたサーバサイドのコードが `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.md#clientmanifest) によるアセットの注入: 自動的に最適なディレクティブが推測され、プリロードとプリフェッチを実行します。また、初期レンダリング時にはコード分割チャンクを必要とします。 + +--- + +次のセクションで、バンドルレンダラと webpack のビルドの設定方法ついて説明します。今はすでに必要なものが準備できていると仮定しましょう。こちらはバンドルレンダラを生成し、使用する方法です: + +```js +const { createBundleRenderer } = require('vue-server-renderer') +const renderer = createBundleRenderer(serverBundle, { + runInNewContext: false, // recommended + template, // (optional) page template + clientManifest // (optional) client build manifest +}) +// inside a server handler... +server.get('*', (req, res) => { + const context = { url: req.url } + // No need to pass an app here because it is auto-created by the + // executing the bundle. Now our server is decoupled from our Vue app! + renderer.renderToString(context, (err, html) => { + // handle error... + res.end(html) + }) +}) +``` + +バンドルレンダラによって`rendertoString` が呼び出されると、バンドルによってエクスポートされた関数が自動的に実行され、(引数としてコンテキストを渡して)アプリケーションのインスタンスを生成し、レンダリングを実行します。 + +--- + +### `runInNewContext` オプション + +通常では、それぞれのレンダリング毎にバンドルレンダラは新しい V8 コンテキストを生成し、バンドル全体を再実行します。これにはいくつかの利点があります。例えば、私たちが以前に言及した "ステートフルシングルトン" 問題について心配する必要がなくなります。しかし、このモードにはかなりのパフォーマンスコストがかかります。なぜなら、アプリケーションを大きくなるにつれ、バンドル全体を再実行することは特にコストとなるからです。 + +`vue-server-renderer >= 2.3.0` では、このオプションは下位互換性のために依然として初期値が `true` にされています。しかし、できる限り `runInNewContext: false` にしておくことをオススメします。 + +`runInNewContext: false`の場合は、バンドルは引き続き**別なグローバルコンテキスト**として1度だけ評価されます。これにより、バンドルが誤ってサーバプロセスのグローバルオブジェクトを汚染してしまうことを防ぎます。通常との動作の違いは、それぞれのレンダリングの実行時に**新しい**コンテキストを生成しないことです。 diff --git a/ja/caching.md b/ja/caching.md new file mode 100644 index 00000000..91f9e6e2 --- /dev/null +++ b/ja/caching.md @@ -0,0 +1,83 @@ +# キャッシュ + +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.md#cache)を有効にする必要があります。代表的な使用例は [lru-cache](https://github.com/isaacs/node-lru-cache) を渡すことです。 + +```js +const LRU = require('lru-cache') +const renderer = createRenderer({ + cache: LRU({ + max: 10000, + maxAge: ... + }) +}) +``` + +次に `serverCacheKey` 関数を実装してコンポーネントをキャッシュすることが出来ます。 + +```js +export default { + name: 'item', // required + props: ['item'], + serverCacheKey: props => props.item.id, + render (h) { + return h('div', this.item.id) + } +} +``` + +キャッシュ可能なコンポーネントは **一意の "name" オプションも定義しなければいけない**ことに注意してください。一意の名前を持つと、キャッシュキーがコンポーネントごとに異なるため、同一キーを返す2つのコンポーネントについて心配する必要はありません。 + +`serverCacheKey` から返されるキーはレンダリング結果を表すのに十分な情報を含んでいる必要があります。レンダリング結果が単に `props.item.id` によって決定される場合、上記は良い実装でしょう。しかしながら、同じIDを持つアイテムが時間の経過とともに変わる場合やレンダリング結果が他のプロパティに依存する場合、 他の変数を考慮して `getCacheKey` の実装を修正する必要があります。 + +定数を返すと、コンポーネントは常にキャッシュされ、単なる静的なコンポーネントには効果的です。 + +### いつコンポーネントキャッシュを使うか + +レンダリング中にレンダラーがコンポーネントのキャッシュにヒットした場合、キャッシュされた結果をサブツリー全体で直接再利用します。 つまり、次の場合にコンポーネントをキャッシュ**しない**でください。 + +- グローバルな状態に依存する子コンポーネントがあります。 +- レンダリング `context` に副作用をもたらす子コンポーネントがあります。 + +したがって、コンポーネントのキャッシングは、パフォーマンスのボトルネックに取り組むために慎重に適用する必要があります。 ほとんどの場合、単一インスタンスのコンポーネントをキャッシュする必要はなく、すべきではありません。キャッシングに適した最も一般的なコンポーネントのタイプは、大きな `v-for` リストで繰り返されるコンポーネントです。 これらのコンポーネントは通常、データベースコレクション内のオブジェクトを元にするため、一意のIDと最後に更新されたタイムスタンプを合わせて使用してキャッシュキーを生成するという単純なキャッシュ戦略を使用できます。 + +```js +serverCacheKey: props => props.item.id + '::' + props.item.last_updated +``` diff --git a/ja/css.md b/ja/css.md new file mode 100644 index 00000000..d51b7eb1 --- /dev/null +++ b/ja/css.md @@ -0,0 +1,110 @@ +# CSS の管理 + +CSS を管理するためのおすすめの方法は、シンプルに単一ファイルコンポーネントである `*.vue` の中で `` を使うことができます。 + +もし `import 'foo.css'` のように JavaScriptからCSSをインポートしたいならば、適切な loader の設定が必要です: + +```js +module.exports = { + // ... + module: { + rules: [ + { + test: /\.css$/, + // important: use vue-style-loader instead of 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: [ + // it is common to extract deps into a vendor chunk for better caching. + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module) { + // a module is extracted into the vendor chunk when... + return ( + // if it's inside node_modules + /node_modules/.test(module.context) && + // do not externalize if the request is a CSS file + !/\.css$/.test(module.request) + ) + } + }), + // extract webpack runtime & manifest + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest' + }), + // ... + ] + } +``` diff --git a/ja/data.md b/ja/data.md new file mode 100644 index 00000000..69c59dae --- /dev/null +++ b/ja/data.md @@ -0,0 +1,231 @@ +# データのプリフェッチとステート + +## データストア + +SSR をしているとき、基本的にはアプリケーションの「スナップショット」をレンダリングしています、したがって、アプリケーションがいくつかの非同期データに依存している場合においては、**それらのデータを、レンダリング処理を開始する前にプリフェッチして解決する必要があります**。 + +もうひとつの重要なことは、クライアントサイドでアプリケーションがマウントされる前に、クライアントサイドで同じデータを利用可能である必要があるということです。そうしないと、クライアントサイドが異なるステートを用いてレンダリングしてしまい、ハイドレーションが失敗してしまいます。 + +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストアもしくは "ステート・コンテナ" に入っている必要があります。サーバーサイドではレンダリングする前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HTML にステートを埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれたステートを直接取得できます。 + +このような用途として、公式のステート管理ライブラリである [Vuex](https://github.com/vuejs/vuex/) を使っています。では `store.js` ファイルをつくって、そこに id に基づく item を取得するコードを書いてみましょう: + +```js +// store.js +import Vue from 'vue' +import Vuex from 'vuex' +Vue.use(Vuex) +// Promise を返すユニバーサルなアプリケーションを想定しています +// また、実装の詳細は割愛します +import { fetchItem } from './api' +export function createStore () { + return new Vuex.Store({ + state: { + items: {} + }, + actions: { + fetchItem ({ commit }, id) { + // store.dispatch() 経由でデータがフェッチされたときにそれを知るために、Promise を返します + return fetchItem(id).then(item => { + commit('setItem', { id, item }) + }) + } + }, + mutations: { + setItem (state, { id, item }) { + Vue.set(state.items, id, item) + } + } + }) +} +``` + +そして `app.js` を更新します: + +```js +// app.js +import Vue from 'vue' +import App from './App.vue' +import { createRouter } from './router' +import { createStore } from './store' +import { sync } from 'vuex-router-sync' +export function createApp () { + // ルーターとストアのインスタンスを作成します + const router = createRouter() + const store = createStore() + // ルートのステートをストアの一部として利用できるよう同期します + sync(store, router) + // アプリケーションのインスタンスを作成し、ルーターとストアの両方を挿入します + const app = new Vue({ + router, + store, + render: h => h(App) + }) + // アプリケーション、ルーター、ストアを公開します + return { app, router, store } +} +``` + +## ロジックとコンポーネントとの結び付き + +ではデータをプリフェッチするアクションをディスパッチするコードはどこに置けばよいでしょうか? + +フェッチする必要があるデータはアクセスしたルートによって決まります。またそのルートによってどのコンポーネントがレンダリングされるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートでレンダリングされるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に置くのが自然でしょう。 + +ルートコンポーネントではカスタム静的関数 `asyncData` が利用可能です。この関数はそのルートコンポーネントがインスタンス化される前に呼び出されるため `this` にアクセスできないことを覚えておいてください。ストアとルートの情報は引数として渡される必要があります: + +```html + + + +``` + +## サーバーサイドのデータ取得 + +`entry-server.js` において `router.getMatchedComponents()` を使ってルートにマッチしたコンポーネントを取得できます。そしてコンポーネントが `asyncData` を利用可能にしていればそれを呼び出すことができます。そしてレンダリングのコンテキストに解決したステートを付属させる必要があります。 + +```js +// entry-server.js +import { createApp } from './app' +export default context => { + return new Promise((resolve, reject) => { + const { app, router, store } = createApp() + router.push(context.url) + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + if (!matchedComponents.length) { + reject({ code: 404 }) + } + // マッチしたルートコンポーネントすべての asyncData() を呼び出します + Promise.all(matchedComponents.map(Component => { + if (Component.asyncData) { + return Component.asyncData({ + store, + route: router.currentRoute + }) + } + })).then(() => { + // すべてのプリフェッチのフックが解決されると、ストアには、 + // アプリケーションをレンダリングするために必要とされるステートが入っています。 + // ステートを context に付随させ、`template` オプションがレンダラーに利用されると、 + // ステートは自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます + context.state = store.state + resolve(app) + }).catch(reject) + }, reject) + }) +} +``` + +`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL__` というかたちのステートとして埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがそのステートを取得します: + +```js +// entry-client.js +const { app, router, store } = createApp() +if (window.__INITIAL_STATE__) { + store.replaceState(window.__INITIAL_STATE__) +} +``` + +## クライアントサイドのデータ取得 + +クライアントサイドではデータ取得について 2つの異なるアプローチがあります: + +1. **ルートのナビゲーションの前にデータを解決する:** + +この方法では、アプリケーションは、遷移先のビューが必要とするデータが解決されるまで、現在のビューを保ちます。良い点は遷移先のビューがデータの準備が整い次第、フルの内容をダイレクトにレンダリングできることです。しかしながら、データの取得に時間がかかるときは、ユーザーは現在のビューで「固まってしまった」と感じてしまうでしょう。そのため、この方法を用いるときにはローディング・インジケーターを表示させることが推奨されます。 + +この方法は、クライアントサイドでマッチするコンポーネントをチェックし、グローバルなルートのフック内で `asyncData` 関数を実行することにより実装できます。重要なことは、このフックは初期ルートが ready になった後に登録するということです。そうすれば、サーバーサイドで取得したデータをもう一度無駄に取得せずに済みます。 + +```js + // entry-client.js + // ...関係のないコードは除外します + router.onReady(() => { + // asyncData を扱うためにルーターのフックを追加します。これは初期ルートが解決された後に実行します + // そうすれば(訳注: サーバーサイドで取得したために)既に持っているデータを冗長に取得しなくて済みます + // すべての非同期なコンポーネントが解決されるように router.beforeResolve() を使います + router.beforeResolve((to, from, next) => { + const matched = router.getMatchedComponents(to) + const prevMatched = router.getMatchedComponents(from) + // まだレンダリングされていないコンポーネントにのみ関心を払うため、 + // 2つのマッチしたリストに差分が表れるまで、コンポーネントを比較します + let diffed = false + const activated = matched.filter((c, i) => { + return diffed || (diffed = (prevMatched[i] !== c)) + }) + if (!activated.length) { + return next() + } + // もしローディングインジケーターがあるならば、 + // この箇所がローディングインジケーターを発火させるべき箇所です + Promise.all(activated.map(c => { + if (c.asyncData) { + return c.asyncData({ store, route: to }) + } + })).then(() => { + // ローディングインジケーターを停止させます + next() + }).catch(next) + }) + app.$mount('#app') + }) +``` + +1. **マッチするビューがレンダリングされた後にデータを取得する:** + +この方法ではビューコンポーネントの `beforeMount` 関数内にクライアントサイドでデータを取得するロジックを置きます。こうすればルートのナビゲーションが発火したらすぐにビューを切り替えられます。そうすればアプリケーションはよりレスポンスが良いと感じられるでしょう。しかしながら、遷移先のビューはレンダリングした時点ではフルのデータを持っていません。したがって、この方法を使うコンポーネントの各々がローディング中か否かの状態を持つ必要があります。 + +この方法はクライアントサイド限定のグローバルな mixin で実装できます: + +```js + Vue.mixin({ + beforeMount () { + const { asyncData } = this.$options + if (asyncData) { + // データが準備できた後に、コンポーネント内で `this.dataPromise.then(...)` して + // 他のタスクを実行できるようにするため、Promise にフェッチ処理を割り当てます + this.dataPromise = asyncData({ + store: this.$store, + route: this.$route + }) + } + } + }) +``` + +これら 2つの方法のどちらを選ぶかは、究極的には異なる UX のどちらを選ぶかの判断であり、構築しようとしているアプリケーションの実際のシナリオに基づいて選択されるべきものです。しかし、どちらの方法を選択したかにかかわらず、ルートコンポーネントが再利用されたとき(つまりルートは同じだがパラメーターやクエリが変わったとき。例えば `user/1` から `user/2`) へ変わったとき)には `asyncData` 関数は呼び出されるようにすべきです。これはクライアントサイド限定のグローバルな mixin でハンドリングできます: + +```js +Vue.mixin({ + beforeRouteUpdate (to, from, next) { + const { asyncData } = this.$options + if (asyncData) { + asyncData({ + store: this.$store, + route: to + }).then(next).catch(next) + } else { + next() + } + } +}) +``` + +--- + +ふぅ、コードが長いですね。これはどうしてかというと、ユニバーサルなデータ取得は、大抵の場合、サーバーサイドでレンダリングするアプリケーションの最も複雑な問題であり、また、今後、スムーズに開発を進めていくための下準備をしているためです。一旦ひな形が準備できてしまえば、あとは、それぞれのコンポーネントを記述していく作業は、実際のところ実に楽しいものになるはずです。 diff --git a/ja/head.md b/ja/head.md new file mode 100644 index 00000000..3bd25eea --- /dev/null +++ b/ja/head.md @@ -0,0 +1,100 @@ +# ヘッドの管理 + +アセットの挿入と同様に、ヘッドの管理も同じ考えに追従しています。つまり、コンポーネントのライフサイクルのレンダリング `context` に動的にデータを付随させ、そして `template` 内にデータを挿入できるという考えです。 + +そうするためには、ネストしたコンポーネントの内側で SSR コンテキストへアクセスできる必要があります。単純に `context` を `createApp()` へ渡し、これをルートインスタンスの `$options` で公開することができます。 + +```js +// app.js +export function createApp (ssrContext) { + // ... + const app = new Vue({ + router, + store, + // this.$root.$options.ssrContext というように、すべての子コンポーネントは this にアクセスできます + ssrContext, + render: h => h(App) + }) + // ... +} +``` + +これと同様のことが `provide/inject` 経由でも可能ですが、そうすると context が `$root` 上に存在することになるため、インジェクションを解決するコストを避けたほうが良いでしょう。 + +インジェクトされた context を用いて、タイトルを管理する単純な 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 +``` + +このようにすれば、ルートコンポーネントはドキュメントのタイトルをコントロールするために 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 + + + {{ title }} + + + ... + + +``` + +**メモ:** + +- XSS 攻撃を防ぐために double-mustache(HTML エスケープした挿入)を使うこと。 +- レンダリング中にタイトルをセットするコンポーネントがない場合に備えて、`context` オブジェクトを作成する際にはデフォルトのタイトルをセットするようにすべきです。 + +--- + +同様のやり方で、この mixin を包括的にヘッドを管理するユーティリティに容易に拡張できます。 diff --git a/ja/hydration.md b/ja/hydration.md new file mode 100644 index 00000000..4fe4e0e3 --- /dev/null +++ b/ja/hydration.md @@ -0,0 +1,32 @@ +# クライアントサイドでのハイドレーション + +`entry-client.js` において、以下の記述で私たちは簡単にアプリケーションをマウントします。 + +```js +// これは、ルート要素に id="app" をもつ App.vue テンプレートを想定します。 +app.$mount('#app') +``` + +サーバがマークアップを描画後に、この処理を実行し、すべての DOM を再生成することを私たちは当然したくありません。代わりに、静的なマークアップの"ハイドレート"とそれをインタラクティブに生成したいです。 + +もしあなたがサーバレンダリングの出力を調べたら、アプリケーションのルート要素が以下のような特別な属性を持っていることに気づくでしょう。 + +```js +
+``` + +この `data-server-rendered` という特別な属性は、クライアントサイドの Vue に、これがサーバ上で描画されたことを知らせ、この要素はハイドレーションモードでマウントされるはずです。 + +開発モードでは、Vue はクラインアントサイドで生成された仮想 DOM が、サーバで描画された DOM の構成とマッチしているか検証を行います。もしこれがマッチしていない場合、ハイドレーションを取りやめ、元の DOM を無視しスクラッチから描画を行います。**プロダクションモードでは、パフォーマンスの最大化のため、このアサーションは無効になります。** + +### ハイドレーション時の注意 + +サーバサイドレンダリングとクライアントサイドでのハイドレーションを行なった場合、ある特定の HTML の構造はブラウザによって変換されるかもしれないことがわかっています。例えば、あなたが Vueのテンプレート内に、以下のような記述をした場合です。 + +```html + + +
hi
+``` + +ブラウザは、自動で `` を `` に挿入します。しかし、Vue によって生成された仮想 DOM は、`` を含みません。そのため、ミスマッチが起こります。正しいマッチングを保証するために、あなたのテンプレート内では、必ず有効な HTML を記述してください。 diff --git a/ja/routing.md b/ja/routing.md new file mode 100644 index 00000000..41c71415 --- /dev/null +++ b/ja/routing.md @@ -0,0 +1,136 @@ +# ルーティングとコード分割 + +## `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 アプリケーションを構築する場合、これはどんなシナリオでも機能するでしょう。ただし、これをサーバーサイドレンダリングで使用する場合はいくつかの制限があります。まず、レンダリングを開始する前にサーバー上のすべての非同期コンポーネントを先に解決する必要があります。そうしなければ、マークアップ内に空のプレースホルダが表示されます。クライアント側では、ハイドレーションを開始する前にこれを行う必要があります。そうしなければ、クライアントはコンテンツの不一致エラーに陥ります。 + +アプリケーション内の任意の場所で非同期コンポーネントを使用するのは少し難解です(これは将来的に改善される可能性があります)。 ただし、**ルートレベルで行うとシームレスに動作します**(すなわち、ルート設定で非同期コンポーネントを使用する)。ルートを解決する際に、 `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/ja/streaming.md b/ja/streaming.md new file mode 100644 index 00000000..91cd9354 --- /dev/null +++ b/ja/streaming.md @@ -0,0 +1,30 @@ +# ストリーミング + +`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) // render complete +}) +stream.on('error', err => { + // handle error... +}) +``` + +## ストリーミングに関する注意事項 + +ストリームレンダリングモードでは、レンダラーが仮想 DOM ツリーを横断するときに、できるだけ早くデータを出力します。つまり、より早く「最初のチャンク」を取得し、それをクライアントにすばやく出力し始めることを意味します。 + +しかし、最初のデータチャンクが発行したときに子コンポーネントがまだインスタンス化されていないと、ライフサイクルフックが呼び出されることはありません。つまり、子コンポーネントがライフサイクルフック内のレンダリングコンテキストにデータを添付する必要がある場合、これらのデータはストリームの開始時に使用できません。アプリケーションは全体の HTML の表示の前に多くのコンテキスト情報(ヘッド情報やインラインに書かれたクリティカル CSS など)を表示する必要があるため、これらのコンテキストデータを使用する前にストリームの完了を待つ必要があります。 + +したがって、コンポーネントライフサイクルフックによって読み込まれたコンテキストデータに依存する場合は、ストリーミングモードを使用することは**推奨されません**。 diff --git a/ja/structure.md b/ja/structure.md new file mode 100644 index 00000000..a156b862 --- /dev/null +++ b/ja/structure.md @@ -0,0 +1,114 @@ +# ソースコードの構造 + +## ステートフルなシングルトンは避ける + +クライアントのみのコードを書くとき、私たちはコードが毎回新しいコンテキストで評価されるという事実に慣れています。しかし、 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` で新しいインスタンスを作成し、ルート 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/ja/universal.md b/ja/universal.md new file mode 100644 index 00000000..f2bd9713 --- /dev/null +++ b/ja/universal.md @@ -0,0 +1,30 @@ +# ユニバーサルなコードを書く + +サーバサイドレンダリングについて、さらに見ていく前に、"ユニバーサル"なコード(サーバーとクライアントの両方で動作するコード)を記述するときの制約について考えてみましょう。ユースケースとプラットフォームの API の相違により、異なる環境で実行したコードの動作は全く同じものにはなりません。ここでは、サーバーサイドレンダリングを行う上で、知っておく必要がある重要な項目について説明します。 + +## サーバー上でのデータリアクティビテイ + +クライアントだけで実行するアプリでは、全てのユーザーがブラウザでアプリケーションの新しいインスタンスを使用します。サーバーサイドレンダリングでも、同じ振る舞いが必要とされます: すなわち、複数のリクエストに跨った状態の汚染がないよう各リクエストは新しく独立したアプリケーションのインスタンスが必要になります。 + +実際のレンダリングプロセスは決定的であることが求められるので、サーバー上でデータを"プリフェッチ"することもあります。これはレンダリングを開始する時、アプリケーションの状態は既に解決済みであることを意味します。つまり、サーバー上では、データがリアクティブである必要はないので、デフォルトで無効になっています。データをリアクティブにしないことで、データをリアクティブなオブジェクトに変換する際のパフォーマンスコストを無視できます。 + +## コンポーネントライフサイクルフック + +動的な更新がないので、ライフサイクルフックのうち、`beforeCreate` と `created` のみが 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.md#directives) オプションを使用して、そのオプションの "サーバーサイドのバージョン" を用意することで回避できます。