diff --git a/docs/ja/api/README.md b/docs/ja/api/README.md index b6b8d968..3cb24d36 100644 --- a/docs/ja/api/README.md +++ b/docs/ja/api/README.md @@ -82,6 +82,12 @@ bundleRenderer.renderToStream([context]): stream.Readable ### template +- **型:** + - `string` + - `string | (() => string | Promise)` (2.6 から) + +**文字列テンプレートを使用している場合:** + ページ全体の HTML を表すテンプレートを設定します。描画されたアプリケーションの内容を指し示すプレースホルダの代わりになるコメント文 `` をテンプレートには含むべきです。 テンプレートは、次の構文を使用した簡単な補間もサポートします。 @@ -99,6 +105,8 @@ bundleRenderer.renderToStream([context]): stream.Readable 2.5.0 以降においては、埋め込みスクリプトはプロダクションモードで自動的に削除されます。 + 2.6.0 以降では、 `context.nonce` が存在すれば、それは、埋め込みスクリプトに `nonce` 属性として追加されます。これにより、インラインスクリプトを nonce を必要とする CSP に準拠することができます。 + 加えて、`clientManifest` も渡された場合、テンプレートは自動で以下を挿入します。 - (自動で受信される非同期のデータを含んだ)描画対象が必要とするクライアントサイドの JavaScript と CSS アセット @@ -106,6 +114,34 @@ bundleRenderer.renderToStream([context]): stream.Readable レンダラに `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) @@ -243,6 +279,31 @@ const renderer = createRenderer({ 例として、[`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 プラグインは、スタンドアロンのファイルとして提供され、次の値を必要とします: diff --git a/docs/ja/guide/caching.md b/docs/ja/guide/caching.md index 7877e12e..78728ece 100644 --- a/docs/ja/guide/caching.md +++ b/docs/ja/guide/caching.md @@ -73,6 +73,10 @@ export default { 定数を返すと、コンポーネントは常にキャッシュされ、単なる静的なコンポーネントには効果的です。 +::: tip キャッシングの回避 +2.6.0 以降、 `serverCacheKey` で明示的に `false` を返すことでコンポーネントはキャッシングを回避して新たに描画されるようになります。 +::: + ### いつコンポーネントキャッシュを使うか 描画中にレンダラがコンポーネントのキャッシュにヒットした場合、キャッシュされた結果をサブツリー全体で直接再利用します。 つまり、次の場合にコンポーネントをキャッシュ **しない** でください。 diff --git a/docs/ja/guide/data.md b/docs/ja/guide/data.md index 20dc8373..92cbc94f 100644 --- a/docs/ja/guide/data.md +++ b/docs/ja/guide/data.md @@ -2,11 +2,9 @@ ## データストア -SSR をしているとき、基本的にはアプリケーションの"スナップショット"を描画しています、したがって、アプリケーションがいくつかの非同期データに依存している場合においては、**それらのデータを、描画処理を開始する前にプリフェッチして解決する必要があります**。 +SSR をしているとき、基本的にはアプリケーションの"スナップショット"を描画しています。クライアントサイドのアプリケーションがマウントする前に、コンポーネントから非同期データが、利用可能である必要があります。つまり、それ以外の場合、クライアントアプリケーションは異なる状態を使用して描画するため、ハイドレーションは失敗します。 -もうひとつの重要なことは、クライアントサイドでアプリケーションがマウントされる前に、クライアントサイドで同じデータを利用可能である必要があるということです。そうしないと、クライアントサイドが異なる状態 (state) を用いて描画してしまい、ハイドレーションが失敗してしまいます。 - -この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストア (data store) もしくは "状態コンテナ (state container)" に入っている必要があります。サーバーサイドでは描画する前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HTML に状態を埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれた状態を直接取得できます。 +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり専用のデータストア (data store) もしくは "状態コンテナ (state container)" に入っている必要があります。サーバーサイドでは描画する前にデータをプリフェッチしてストアの中に入れることができます。さらに、アプリケーションの描画が終わった後、シリアライズして HTML にインラインで状態を埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれた状態を直接取得できます。 このような用途として、公式の状態管理ライブラリである [Vuex](https://github.com/vuejs/vuex/) を使っています。では `store.js` ファイルをつくって、そこに id に基づく item を取得するコードを書いてみましょう: @@ -23,9 +21,12 @@ import { fetchItem } from './api' export function createStore () { return new Vuex.Store({ - state: { + // 重要: 状態はモジュールを複数回インスタンス化できるように、 + // 関数でなければなりません + state: () => ({ items: {} - }, + }), + actions: { fetchItem ({ commit }, id) { // store.dispatch() 経由でデータがフェッチされたときにそれを知るために、Promise を返します @@ -34,6 +35,7 @@ export function createStore () { }) } }, + mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) @@ -43,6 +45,10 @@ export function createStore () { } ``` +::: warning +ほとんどの場合、次のサーバサイドの実行においてリークしないよう、 `state` を関数でラップする必要があります。[詳細情報はこちら](./structure.md#avoid-stateful-singletons) +::: + そして `app.js` を更新します: ```js @@ -79,34 +85,68 @@ export function createApp () { フェッチする必要があるデータはアクセスしたルート (route) によって決まります。またそのルートによってどのコンポーネントが描画されるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートで描画されるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に置くのが自然でしょう。 -ルートコンポーネントではカスタム静的関数 `asyncData` が利用可能です。この関数はそのルートコンポーネントがインスタンス化される前に呼び出されるため `this` にアクセスできないことを覚えておいてください。ストアとルートの情報は引数として渡される必要があります: +コンポーネントでは、 `serverPrefetch` オプション (2.6.0 以降で新規追加)を使用します。このオプションは、サーバレンダラによって認識され、そして それを返す Promise が解決されるまで描画を一時停止します。これにより、描画処理中に非同期データを"待つ"ことができます。 + +::: tip +ルートレベルのコンポーネントだけでなく、任意のコンポーネントで `serverPrefetch` を使用できます。 +::: + +これは、`'/item/:id'` ルートで描画される `Item.vue` コンポーネントの例です。コンポーネントインスタンスはこの時点では既に作成されているので、 `this` にアクセスできます: ```html ``` -## サーバーサイドのデータ取得 +::: warning +ロジックが 2 回実行されないようにするために、コンポーネントは `mounted` フックでサーバサイドで描画されているかどうかチェックする必要があります。 +::: + +::: tip +各コンポーネントで同じ `fetchItem()` ロジックが複数回 (`serverPrefetch`、`mounted`、そして `watch` コールバック)繰り返されているのを見つけるかもしれません。そのようなコードをシンプルにするために、あなた自身で抽象化(例えばミックスインまたはプラグイン)することを推奨します。 +::: -`entry-server.js` において `router.getMatchedComponents()` を使ってルートに一致したコンポーネントを取得できます。そしてコンポーネントが `asyncData` を利用可能にしていればそれを呼び出すことができます。そして描画のコンテキストに解決した状態を付属させる必要があります。 +## 最終状態注入 + +これで、描画プロセスがコンポーネント内のデータフェッチを待つことがわかりましたが、それが"完了"したというのをどうやって分かるのでしょうか?それをするために、描画コンテキストに `rendered` コールバックをアタッチする必要があります(これも 2.6 での新機能)。これは描画プロセス全体が終了したときにサーバーレンダラによって呼ばれます。現時点で、ストアは最終的な状態で満たされているはずです。そのコールバック内でコンテキストに状態を注入できます: ```js // entry-server.js @@ -119,27 +159,16 @@ export default context => { 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(() => { - // すべてのプリフェッチのフックが解決されると、ストアには、 - // アプリケーションを描画するために必要とされる状態が入っています。 + // この `rendered` フックは、アプリケーションの描画が終えたときに呼び出されます + context.rendered = () => { + // アプリケーションが描画された後、ストアには、 + // コンポーネントからの状態で満たされています // 状態を context に付随させ、`template` オプションがレンダラに利用されると、 // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます context.state = store.state - resolve(app) - }).catch(reject) + } + + resolve(app) }, reject) }) } @@ -150,104 +179,14 @@ export default context => { ```js // entry-client.js -const { app, router, store } = createApp() +const { app, 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() - } - } -}) +app.$mount('#app') ``` ## ストアコードの分割 @@ -258,14 +197,17 @@ Vue.mixin({ // store/modules/foo.js export default { namespaced: true, + // 重要: 状態は関数でなければならないため、 - // モジュールを複数回インスタン化できます + // モジュールを複数回インスタンス化できます state: () => ({ count: 0 }), + actions: { inc: ({ commit }) => commit('inc') }, + mutations: { inc: state => state.count++ } @@ -279,14 +221,34 @@ export default { +