From 897ad1ba74b6065ed1693c40b9ea15bef342e2c7 Mon Sep 17 00:00:00 2001 From: reoring Date: Sat, 27 May 2017 02:18:10 +0900 Subject: [PATCH 1/7] Translate data.md via GitLocalize --- ja/data.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 ja/data.md diff --git a/ja/data.md b/ja/data.md new file mode 100644 index 00000000..c89917f2 --- /dev/null +++ b/ja/data.md @@ -0,0 +1,234 @@ +# データのプリフェッチとステート + +## データストア + +During SSR, we are essentially rendering a "snapshot" of our app, so if the app relies on some asynchronous data, **these data need to be pre-fetched and resolved before we start the rendering process**. + +Another concern is that on the client, the same data 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 before rendering. In addition, we will serialize and inline the state in the HTML. 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({ + 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) + } + } + }) +} +``` + +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 expose a custom static function `asyncData` on our route components. Note because this function will be called before the components are instantiated, it doesn't have access to `this`. The store and route information needs to be passed in as arguments: + +```html + + + +``` + +## Server Data Fetching + +In `entry-server.js` we can get the components matched by a route with `router.getMatchedComponents()`, and call `asyncData` if the component exposes it. Then we need to attach resolved state to the render context. + +```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 }) + } + // call asyncData() on all matched route components + Promise.all(matchedComponents.map(Component => { + if (Component.asyncData) { + return Component.asyncData({ + store, + route: router.currentRoute + }) + } + })).then(() => { + // After all preFetch hooks are resolved, our store is now + // filled with the state needed to render the app. + // 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) + }).catch(reject) + }, reject) + }) +} +``` + +When using `template`, `context.state` will automatically be embedded in the final HTML as `window.__INITIAL__` state. On the client, the store should pick up the state before mounting the application: + +```js +// entry-client.js +const { app, router, store } = createApp() +if (window.__INITIAL_STATE__) { + store.replaceState(window.__INITIAL_STATE__) +} +``` + +## Client Data Fetching + +On the client, there are two different approaches for handling data fetching: + +1. **Resolve data before route navigation:** + + With this strategy, the app will stay on the current view until the data needed by the incoming view has been resolved. The benefit is that the incoming view can directly render the full content when it's ready, but if the data fetching takes a long time, the user will feel "stuck" on the current view. It is therefore recommended to provide a data loading indicator if using this strategy. + + We can implement this strategy on the client by checking matched components and invoking their `asyncData` function inside a global route hook. Note we should register this hook after the initial route is ready so that we don't unnecessarily fetch the server-fetched data again. + +```js + // entry-client.js + // ...omitting unrelated code + router.onReady(() => { + // Add router hook for handling asyncData. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const matched = router.getMatchedComponents(to) + const prevMatched = router.getMatchedComponents(from) + // we only care about none-previously-rendered components, + // so we compare them until the two matched lists differ + let diffed = false + const activated = matched.filter((c, i) => { + return diffed || (diffed = (prevMatched[i] !== c)) + }) + if (!activated.length) { + return next() + } + // this is where we should trigger a loading indicator if there is one + Promise.all(activated.map(c => { + if (c.asyncData) { + return c.asyncData({ store, route: to }) + } + })).then(() => { + // stop loading indicator + next() + }).catch(next) + }) + app.$mount('#app') + }) +``` + +1. **Fetch data after the matched view is rendered:** + + This strategy places the client-side data-fetching logic in a view component's `beforeMount` function. This allows the views to switch instantly when a route navigation is triggered, so the app feels a bit more responsive. However, the incoming view will not have the full data available when it's rendered. It is therefore necessary to have a conditional loading state for each view component that uses this strategy. + + This can be achieved with a client-only global mixin: + +```js + Vue.mixin({ + beforeMount () { + const { asyncData } = this.$options + if (asyncData) { + // assign the fetch operation to a promise + // so that in components we can do `this.dataPromise.then(...)` to + // perform other tasks after data is ready + this.dataPromise = asyncData({ + store: this.$store, + route: this.$route + }) + } + } + }) +``` + +The two strategies are ultimately different UX decisions and should be picked based on the actual scenario of the app you are building. But regardless of which strategy you pick, the `asyncData` function should also be called when a route component is reused (same route, but params or query changed. e.g. from `user/1` to `user/2`). We can also handle this with a client-only global 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() + } + } +}) +``` + +--- + +Phew, that was a lot of code! This is because universal data-fetching is probably the most complex problem in a server-rendered app and we are laying the groundwork for easier further development. Once the boilerplate is set up, authoring individual components will be actually quite pleasant. From bc83b5fdb22c9f847910bd2d82b3e76d4c5db7f0 Mon Sep 17 00:00:00 2001 From: INOUE Takuya Date: Sat, 27 May 2017 02:18:11 +0900 Subject: [PATCH 2/7] Translate data.md via GitLocalize --- ja/data.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/ja/data.md b/ja/data.md index c89917f2..c8ee63d6 100644 --- a/ja/data.md +++ b/ja/data.md @@ -2,13 +2,13 @@ ## データストア -During SSR, we are essentially rendering a "snapshot" of our app, so if the app relies on some asynchronous data, **these data need to be pre-fetched and resolved before we start the rendering process**. +SSR をしているとき、基本的にはアプリケーションの「スナップショット」をレンダリングしています、したがって、アプリケーションがいくつかの非同期データに依存している場合においては、**それらのデータを、レンダリング処理を開始する前にプリフェッチして解決する必要があります**。 -Another concern is that on the client, the same data 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 before rendering. In addition, we will serialize and inline the state in the HTML. The client-side store can directly pick up the inlined state before we mount the app. +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストアもしくは "ステート・コンテナ" に入っている必要があります。サーバーサイドではレンダリングする前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HMTL にステートを埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれたステートを直接取得できます。 -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: +このような用途として、公式のステート管理ライブラリである [Vuex](https://github.com/vuejs/vuex/) を使っています。では `store.js` ファイルをつくって、そこに id に基づく item を取得するコードを書いてみましょう: ```js // store.js @@ -41,7 +41,7 @@ export function createStore () { } ``` -And update `app.js`: +そして `app.js` を更新します: ```js // app.js @@ -67,13 +67,13 @@ export function createApp () { } ``` -## 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 expose a custom static function `asyncData` on our route components. Note because this function will be called before the components are instantiated, it doesn't have access to `this`. The store and route information needs to be passed in as arguments: +ルートコンポーネントではカスタム静的関数 `asyncData` が利用可能です。この関数はそのルートコンポーネントがインスタンス化される前に呼び出されるため `this` にアクセスできないことを覚えておいてください。ストアとルートの情報は引数として渡される必要があります: ```html @@ -96,9 +96,9 @@ export default { ``` -## Server Data Fetching +## サーバーサイドのデータ取得 -In `entry-server.js` we can get the components matched by a route with `router.getMatchedComponents()`, and call `asyncData` if the component exposes it. Then we need to attach resolved state to the render context. +`entry-server.js` において `router.getMatchedComponents()` を使ってルートにマッチしたコンポーネントを取得できます。そしてコンポーネントが `asyncData` を利用可能にしていればそれを呼び出すことができます。そしてレンダリングのコンテキストに解決したステートを付属させる必要があります。 ```js // entry-server.js @@ -134,7 +134,7 @@ export default context => { } ``` -When using `template`, `context.state` will automatically be embedded in the final HTML as `window.__INITIAL__` state. On the client, the store should pick up the state before mounting the application: +`template` を使うと `context.state` は自動的に最終的な HTML に `window.__INITIAL__` というかたちのステートとして埋め込まれます。クライアントサイドでは、アプリケーションがマウントされる前に、ストアがそのステートを取得します: ```js // entry-client.js @@ -144,15 +144,15 @@ if (window.__INITIAL_STATE__) { } ``` -## Client Data Fetching +## クライアントサイドのデータ取得 -On the client, there are two different approaches for handling data fetching: +クライアントサイドではデータ取得について 2つの異なるアプローチがあります: -1. **Resolve data before route navigation:** +1. **ルートのナビゲーションの前にデータを解決する:** - With this strategy, the app will stay on the current view until the data needed by the incoming view has been resolved. The benefit is that the incoming view can directly render the full content when it's ready, but if the data fetching takes a long time, the user will feel "stuck" on the current view. It is therefore recommended to provide a data loading indicator if using this strategy. +この方法では、アプリケーションは、遷移先のビューが必要とするデータが解決されるまで、現在のビューを保ちます。良い点は遷移先のビューがデータの準備が整い次第、フルの内容をダイレクトにレンダリングできることです。しかしながら、データの取得に時間がかかるときは、ユーザーは現在のビューで「固まってしまった」と感じてしまうでしょう。そのため、この方法を用いるときにはローディング・インジケーターを表示させることが推奨されます。 - We can implement this strategy on the client by checking matched components and invoking their `asyncData` function inside a global route hook. Note we should register this hook after the initial route is ready so that we don't unnecessarily fetch the server-fetched data again. +この方法は、クライアントサイドでマッチするコンポーネントをチェックし、グローバルなルートのフック内で `asyncData` 関数を実行することにより実装できます。重要なことは、このフックは初期ルートが ready になった後に登録するということです。そうすれば、サーバーサイドで取得したデータをもう一度無駄に取得せずに済みます。 ```js // entry-client.js @@ -188,11 +188,11 @@ On the client, there are two different approaches for handling data fetching: }) ``` -1. **Fetch data after the matched view is rendered:** +1. **マッチするビューがレンダリングされた後にデータを取得する:** - This strategy places the client-side data-fetching logic in a view component's `beforeMount` function. This allows the views to switch instantly when a route navigation is triggered, so the app feels a bit more responsive. However, the incoming view will not have the full data available when it's rendered. It is therefore necessary to have a conditional loading state for each view component that uses this strategy. +この方法ではビューコンポーネントの `beforeMount` 関数内にクライアントサイドでデータを取得するロジックを置きます。こうすればルートのナビゲーションが発火したらすぐにビューを切り替えられます。そうすればアプリケーションはよりレスポンスが良いと感じられるでしょう。しかしながら、遷移先のビューはレンダリングした時点ではフルのデータを持っていません。したがって、この方法を使うコンポーネントの各々がローディング中か否かの状態を持つ必要があります。 - This can be achieved with a client-only global mixin: +この方法はクライアントサイド限定のグローバルな mixin で実装できます: ```js Vue.mixin({ @@ -211,7 +211,7 @@ On the client, there are two different approaches for handling data fetching: }) ``` -The two strategies are ultimately different UX decisions and should be picked based on the actual scenario of the app you are building. But regardless of which strategy you pick, the `asyncData` function should also be called when a route component is reused (same route, but params or query changed. e.g. from `user/1` to `user/2`). We can also handle this with a client-only global mixin: +これら 2つの方法のどちらを選ぶかは、究極的には異なる UX のどちらを選ぶかの判断であり、構築しようとしているアプリケーションの実際のシナリオに基づいて選択されるべきものです。しかし、どちらの方法を選択したかにかかわらず、ルートコンポーネントが再利用されたとき(つまりルートは同じだがパラメーターやクエリが変わったとき。例えば `user/1` から `user/2`) へ変わったとき)には `asyncData` 関数は呼び出されるようにすべきです。これはクライアントサイド限定のグローバルな mixin でハンドリングできます: ```js Vue.mixin({ @@ -231,4 +231,4 @@ Vue.mixin({ --- -Phew, that was a lot of code! This is because universal data-fetching is probably the most complex problem in a server-rendered app and we are laying the groundwork for easier further development. Once the boilerplate is set up, authoring individual components will be actually quite pleasant. +ふぅ、コードが長いですね。これはどうしてかというと、ユニバーサルなデータ取得は、大抵の場合、サーバーサイドでレンダリングするアプリケーションの最も複雑な問題であり、また、今後、スムーズに開発を進めていくための下準備をしているためです。一旦ひな形が準備できてしまえば、あとは、それぞれのコンポーネントを記述していく作業は、実際のところ実に楽しいものになるはずです。 From 5486068a80864b47f09e2e67e1d4b1e0e6433e52 Mon Sep 17 00:00:00 2001 From: INOUE Takuya Date: Sun, 28 May 2017 18:52:25 +0900 Subject: [PATCH 3/7] Fix typos --- ja/data.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ja/data.md b/ja/data.md index c8ee63d6..5de9d752 100644 --- a/ja/data.md +++ b/ja/data.md @@ -4,9 +4,9 @@ SSR をしているとき、基本的にはアプリケーションの「スナップショット」をレンダリングしています、したがって、アプリケーションがいくつかの非同期データに依存している場合においては、**それらのデータを、レンダリング処理を開始する前にプリフェッチして解決する必要があります**。 -もうひとつの重要なことは、クライアントサイドでアプリケーションがマウとされる前に、クライアントサイドで同じデータを利用可能である必要があるということです。そうしないと、クライアントサイドが異なるステートを用いてレンダリングしてしまい、ハイドレーションが失敗してしまいます。 +もうひとつの重要なことは、クライアントサイドでアプリケーションがマウントされる前に、クライアントサイドで同じデータを利用可能である必要があるということです。そうしないと、クライアントサイドが異なるステートを用いてレンダリングしてしまい、ハイドレーションが失敗してしまいます。 -この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストアもしくは "ステート・コンテナ" に入っている必要があります。サーバーサイドではレンダリングする前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HMTL にステートを埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれたステートを直接取得できます。 +この問題に対応するため、フェッチされたデータはビューコンポーネントの外でも存続している必要があります。つまり特定の用途のデータストアもしくは "ステート・コンテナ" に入っている必要があります。サーバーサイドではレンダリングする前にデータをプリフェッチしてストアの中に入れることができます。さらにシリアライズして HTML にステートを埋め込みます。クライアントサイドのストアは、アプリケーションをマウントする前に、埋め込まれたステートを直接取得できます。 このような用途として、公式のステート管理ライブラリである [Vuex](https://github.com/vuejs/vuex/) を使っています。では `store.js` ファイルをつくって、そこに id に基づく item を取得するコードを書いてみましょう: From c90ee392e70204a97dd44baafdbde22e725ea0dd Mon Sep 17 00:00:00 2001 From: INOUE Takuya Date: Sun, 28 May 2017 19:15:30 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E5=86=85?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E6=97=A5?= =?UTF-8?q?=E6=9C=AC=E8=AA=9E=E3=81=AB=E7=BF=BB=E8=A8=B3=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ja/data.md | 58 ++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/ja/data.md b/ja/data.md index 5de9d752..357b6ee1 100644 --- a/ja/data.md +++ b/ja/data.md @@ -15,8 +15,8 @@ SSR をしているとき、基本的にはアプリケーションの「スナ 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 +// Promise を返すユニバーサルなアプリケーションを想定しています +// また、実装の詳細は割愛します import { fetchItem } from './api' export function createStore () { return new Vuex.Store({ @@ -25,8 +25,8 @@ export function createStore () { }, actions: { fetchItem ({ commit }, id) { - // return the Promise via store.dispatch() so that we know - // when the data has been fetched + // store.dispatch() を使って Promise を返します + // そうすればデータがフェッチされたときにそれを知ることができます return fetchItem(id).then(item => { commit('setItem', { id, item }) }) @@ -51,27 +51,27 @@ 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 } } ``` ## ロジックとコンポーネントとの結び付き -ではデータをプリフェッチするアクションをディスパッチするコードはどこに書けばよいでしょうか? +ではデータをプリフェッチするアクションをディスパッチするコードはどこに置けばよいでしょうか? -フェッチする必要があるデータはアクセスしたルートによって決まります。またそのルートによってどのコンポーネントがレンダリングされるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートでレンダリングされるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に書くのが自然でしょう。 +フェッチする必要があるデータはアクセスしたルートによって決まります。またそのルートによってどのコンポーネントがレンダリングされるかも決まります。実のところ、与えられたルートに必要とされるデータは、そのルートでレンダリングされるコンポーネントに必要とされるデータでもあるのです。したがって、データをフェッチするロジックはルートコンポーネントの中に置くのが自然でしょう。 ルートコンポーネントではカスタム静的関数 `asyncData` が利用可能です。この関数はそのルートコンポーネントがインスタンス化される前に呼び出されるため `this` にアクセスできないことを覚えておいてください。ストアとルートの情報は引数として渡される必要があります: @@ -83,11 +83,11 @@ export function createApp () {