diff --git a/docs/LANGS.md b/docs/LANGS.md index 85c0a3a43..ec8d59bae 100644 --- a/docs/LANGS.md +++ b/docs/LANGS.md @@ -2,4 +2,5 @@ * [2.0 - 简体中文](zh-cn/) * [2.0 - Français](fr/) * [2.0 - Русский](ru/) +* [2.0 - 日本語](ja/) * [1.0 Docs](old/) diff --git a/docs/en/getters.md b/docs/en/getters.md index 66bab0498..b4ab7941e 100644 --- a/docs/en/getters.md +++ b/docs/en/getters.md @@ -1,4 +1,3 @@ - # Getters Sometimes we may need to compute derived state based on store state, for example filtering through a list of items and counting them: diff --git a/docs/ja/README.md b/docs/ja/README.md new file mode 100644 index 000000000..f8a898044 --- /dev/null +++ b/docs/ja/README.md @@ -0,0 +1 @@ +{% include "./SUMMARY.md" %} diff --git a/docs/ja/SUMMARY.md b/docs/ja/SUMMARY.md new file mode 100644 index 000000000..962951abb --- /dev/null +++ b/docs/ja/SUMMARY.md @@ -0,0 +1,22 @@ +# Vuex + +> 注意: これは vuex@2.x のドキュメントです + +- [1.0のドキュメントをお探しですか?](https://github.com/vuejs/vuex/tree/1.0/docs/ja) +- [リリースノート](https://github.com/vuejs/vuex/releases) +- [インストール](installation.md) +- [Vuex とは何か?](intro.md) +- [Vuex 入門](getting-started.md) +- コアコンセプト + - [ステート](state.md) + - [ゲッター](getters.md) + - [ミューテーション](mutations.md) + - [アクション](actions.md) + - [モジュール](modules.md) +- [アプリケーションの構造](structure.md) +- [プラグイン](plugins.md) +- [厳格モード](strict.md) +- [フォームの扱い](forms.md) +- [テスト](testing.md) +- [ホットリローディング](hot-reload.md) +- [API リファレンス](api.md) diff --git a/docs/ja/actions.md b/docs/ja/actions.md new file mode 100644 index 000000000..89083abec --- /dev/null +++ b/docs/ja/actions.md @@ -0,0 +1,174 @@ +# アクション + +アクションはミューテーションと似ていますが、下記の点で異なります: + +- アクションは、状態を変更するのではなく、ミューテーションをコミットします。 +- アクションは任意の非同期処理を含むことができます。 + +シンプルなアクションを登録してみましょう: + +``` js +const store = new Vuex.Store({ + state: { + count: 0 + }, + mutations: { + increment (state) { + state.count++ + } + }, + actions: { + increment (context) { + context.commit('increment') + } + } +}) +``` + +アクションハンドラはストアインスタンスのメソッドやプロパティのセットと同じものを呼び出せるコンテキストオブジェクトを受け取ります。したがって `context.commit` を呼び出すことでミューテーションをコミットできます。あるいは `context.state` や `context.getters` で、状態やゲッターにアクセスできます。なぜコンテキストオブジェクトがストアインスタンスそのものではないのかは、後ほど[モジュール](modules.md)で説明します。 + +実際にはコードを少しシンプルにするために ES2015 の[引数分割束縛(argument destructuring)](https://github.com/lukehoban/es6features#destructuring)がよく使われます(特に `commit` を複数回呼び出す必要があるとき): + +``` js +actions: { + increment ({ commit }) { + commit('increment') + } +} +``` + +### アクションのディスパッチ + +アクションは `store.dispatch` がトリガーとなって実行されます: + +``` js +store.dispatch('increment') +``` + +これは一見ばかげて見えるかもしれません。つまり、カウントをインクリメントしたいときに、どうして直接 `store.commit('increment')` を呼び出してミューテーションをコミットしないのか、と。**ミューテーションは同期的でなければならない**というのを覚えていますか?アクションはそうではありません。アクションの中では**非同期**の操作を行うことができます。 + +``` js +actions: { + incrementAsync ({ commit }) { + setTimeout(() => { + commit('increment') + }, 1000) + } +} +``` + +アクションはペイロード形式とオブジェクトスタイルのディスパッチをサポートします: + +``` js +// ペイロードを使ってディスパッチする +store.dispatch('incrementAsync', { + amount: 10 +}) + +// オブジェクトを使ってディスパッチする +store.dispatch({ + type: 'incrementAsync', + amount: 10 +}) +``` + +より実践的な例として、ショッピングカートをチェックアウトするアクションを挙げます。このアクションは**非同期な API の呼び出し**と、**複数のミューテーションのコミット**をします: + +``` js +actions: { + checkout ({ commit, state }, products) { + // 現在のカート内の商品を保存する + const savedCartItems = [...state.cart.added] + // チェックアウトのリクエストを送信し、楽観的にカート内をクリアする + commit(types.CHECKOUT_REQUEST) + // shop API は成功時のコールバックと失敗時のコールバックを受け取る + shop.buyProducts( + products, + // 成功時の処理 + () => commit(types.CHECKOUT_SUCCESS), + // 失敗時の処理 + () => commit(types.CHECKOUT_FAILURE, savedCartItems) + ) + } +} +``` + +一連の非同期の処理を実行しつつ、ミューテーションのコミットによってのみ副作用(状態の変更)を与えていることに注意してください。 + +### コンポーネント内でのアクションのディスパッチ + +`this.$store.dispatch('xxx')` でコンポーネント内でアクションをディスパッチできます。あるいはコンポーネントのメソッドを `store.dispatch` にマッピングする `mapActions` ヘルパーを使うこともできます(ルートの `store` の注入が必要です): + +``` js +import { mapActions } from 'vuex' + +export default { + // ... + methods: { + ...mapActions([ + 'increment' // this.increment() を this.$store.dispatch('increment') にマッピングする + ]), + ...mapActions({ + add: 'increment' // this.add() を this.$store.dispatch('increment') にマッピングする + }) + } +} +``` + +### アクションを構成する + +アクションはしばしば非同期処理を行いますが、アクションが完了したことをどうやって知れば良いのでしょう?そしてもっと重要なことは、さらに複雑な非同期処理を取り扱うために、どうやって複数のアクションを構成させるかということです。 + +まず知っておくべきことは `store.dispatch` がトリガーされたアクションハンドラによって返された Promise を処理できることと、`store.dispatch` もまた Promise を返すことです。 + +``` js +actions: { + actionA ({ commit }) { + return new Promise((resolve, reject) => { + setTimeout(() => { + commit('someMutation') + resolve() + }, 1000) + }) + } +} +``` + +すると次のようにできます: + +``` js +store.dispatch('actionA').then(() => { + // ... +}) +``` + +また別のアクションで下記のように書くと: + +``` js +actions: { + // ... + actionB ({ dispatch, commit }) { + return dispatch('actionA').then(() => { + commit('someOtherMutation') + }) + } +} +``` + +最終的に JavaScript の機能として近く導入される [async / await](https://tc39.github.io/ecmascript-asyncawait/) を使用することで、次のようにアクションを組み合わせることができます: + +``` js +// getData() と getOtherData() が Promise を返すことを想定している + +actions: { + async actionA ({ commit }) { + commit('gotData', await getData()) + }, + async actionB ({ dispatch, commit }) { + await dispatch('actionA') // actionA が完了するのを待機する + commit('gotOtherData', await getOtherData()) + } +} +``` + +> `store.dispatch` で異なるモジュール内の複数のアクションハンドラをトリガーすることができます。そのようなケースでは、全てのトリガーされたハンドラが解決されたときに解決する Promise が戻り値として返ってくることになります。 diff --git a/docs/ja/api.md b/docs/ja/api.md new file mode 100644 index 000000000..cc1c21163 --- /dev/null +++ b/docs/ja/api.md @@ -0,0 +1,179 @@ +# API リファレンス + +### Vuex.Store + +``` js +import Vuex from 'vuex' + +const store = new Vuex.Store({ ...options }) + ``` + +### Vuex.Store コンストラクタオプション + +- **state** + + - 型: `Object` + + ストアのための ルートステートオブジェクトです。 + + [詳細](state.md) + +- **mutations** + + - 型: `{ [type: string]: Function }` + + ストアにミューテーションを登録します。ハンドラ関数は第一引数に `state` を常に受け取り(モジュール内で定義されていれば、モジュールのローカルステートを受け取り)、指定されていれば第二引数に `payload` を受け取ります。 + + [詳細](mutations.md) + +- **actions** + + - 型: `{ [type: string]: Function }` + + ストアにアクションを登録します。ハンドラ関数は次のプロパティを持つ `context` オブジェクトを受け取ります。: + + ``` js + { + state, // store.state と同じか、モジュール内にあればローカルステート + rootState, // store.state と同じ。ただしモジュール内に限る + commit, // store.commit と同じ + dispatch, // store.dispatch と同じ + getters // store.getters と同じ + } + ``` + + [詳細](actions.md) + +- **getters** + + - type: `{ [key: string]: Function }` + + ストアにゲッターを登録します. ゲッター関数は次の引数を受け取ります: + + ``` + state, // モジュール内で定義されていればモジュールのローカルステート + getters, // store.getters と同じ + rootState // store.state と同じ + ``` + + 登録されたゲッターは `store.getters` 上に公開されます。 + + [詳細](getters.md) + +- **modules** + + - 型: `Object` + + サブモジュールを含む次のような形式のオブジェクトはストアにマージされます。 + + ``` js + { + key: { + state, + mutations + actions?, + getters?, + modules? + + }, + ... + } + ``` + + 各モジュールは、ルートオプションに似た `state` と `mutations` を含むことができます。モジュールの状態は、モジュールのキーを使って、ストアのルートステートに結合されます。モジュールのミューテーションとゲッターは、第一引数としてルートステートの代わりに、モジュールのローカルステートだけを受け取り、モジュールのアクションの `context.state` もローカルステートを指すようになります。 + + [詳細](modules.md) + +- **plugins** + + - 型: `Array` + + プラグイン関数の配列は、ストアに適用されます。このプラグインは、ストアだけを引数として受け取り、外部への永続化、ロギング、デバッギングのために、ミューテーションを監視するか、または、 websocket や observable のような外から渡されるデータのためにミューテーションをディスパッチします。 + + [詳細](plugins.md) + +- **strict** + + - 型: `Boolean` + - デフォルト: `false` + + Vuex ストアを厳格モードにします。厳格モードでは、ミューテーションハンドラ以外で、 Vuex の状態の変更を行うと、エラーが投げられます。 + + [詳細](strict.md) + +### Vuex.Store インスタンスプロパティ + +- **state** + + - type: `Object` + + ルートステート、読み取り専用です。 + +- **getters** + + - type: `Object` + + 登録されているゲッターを公開します。読み取り専用です。 + +### Vuex.Store インスタンスメソッド + +- **`commit(type: string, payload?: any) | commit(mutation: Object)`** + + ミューテーションをコミットします。[詳細](mutations.md) + +- **`dispatch(type: string, payload?: any) | dispatch(action: Object)`** + + アクションをディスパッチします。すべてのトリガーされたアクションハンドラを解決するPromiseを返します。[詳細](actions.md) + +- **`replaceState(state: Object)`** + + ストアのルートステートを置き換えます。これは、ステートのハイドレーションやタイムトラベルのためだけに利用すべきです。 + +- **`watch(getter: Function, cb: Function, options?: Object)`** + + リアクティブにゲッター関数の返す値を監視します。値が変わった場合は、コールバックを呼びます。ゲッターはストアの状態のみを引数として受け取ります。 Vue の`vm.$watch`メソッドと同じオプションをオプションのオブジェクトとして受け付けます。 + + 監視を止める場合は、ハンドラ関数の返り値を関数として呼び出します。 + +- **`subscribe(handler: Function)`** + + ストアへのミューテーションを購読します。`handler` は、全てのミューテーションの後に呼ばれ、引数として、ミューテーション ディスクリプタとミューテーション後の状態を受け取ります。 + + ``` js + store.subscribe((mutation, state) => { + console.log(mutation.type) + console.log(mutation.payload) + }) + ``` + + プラグインの中でもっともよく利用されます。[詳細](plugins.md) + +- **`registerModule(path: string | Array, module: Module)`** + + 動的なモジュールを登録します。[詳細](modules.md#dynamic-module-registration) + +- **`unregisterModule(path: string | Array)`** + + 動的なモジュールを解除します。[詳細](modules.md#dynamic-module-registration) + +- **`hotUpdate(newOptions: Object)`** + + 新しいアクションとミューテーションをホットスワップします。[詳細](hot-reload.md) + +### コンポーネントをバインドするヘルパー + +- **`mapState(map: Array | Object): Object`** + + ストアのサブツリーを返すコンポーネントの computed オプションを作成します。[詳細](state.md#the-mapstate-helper) + +- **`mapGetters(map: Array | Object): Object`** + + ゲッターの評価後の値を返すコンポーネントの computed オプションを作成します。[詳細](getters.md#the-mapgetters-helper) + +- **`mapActions(map: Array | Object): Object`** + + アクションをディスパッチするコンポーネントの methods オプションを作成します。[詳細](actions.md#dispatching-actions-in-components) + +- **`mapMutations(map: Array | Object): Object`** + + ミューテーションをコミットするコンポーネントの methods オプションを作成します。[詳細](mutations.md#commiting-mutations-in-components) diff --git a/docs/ja/book.json b/docs/ja/book.json new file mode 100644 index 000000000..6e622745c --- /dev/null +++ b/docs/ja/book.json @@ -0,0 +1,19 @@ +{ + "gitbook": "2.x.x", + "plugins": ["edit-link", "prism", "-highlight", "github"], + "pluginsConfig": { + "edit-link": { + "base": "https://github.com/vuejs/vuex/tree/dev/docs", + "label": "Edit This Page" + }, + "github": { + "url": "https://github.com/vuejs/vuex/" + } + }, + "links": { + "sharing": { + "facebook": false, + "twitter": false + } + } +} diff --git a/docs/ja/forms.md b/docs/ja/forms.md new file mode 100644 index 000000000..828c6f34f --- /dev/null +++ b/docs/ja/forms.md @@ -0,0 +1,56 @@ +# フォームの扱い + +厳格モードで Vuex を使用するとき、Vuex に属する状態の一部で `v-model` を使用するのは少しトリッキーです: + +``` html + +``` + +`obj` がストアからオブジェクトを返す算出プロパティ (computed property) と仮定すると、`v-model` は input でユーザーが入力するとき、直接 `obj.message` を変更します。厳格モードでは、この変更は明示的に Vuex のミューテーションハンドラ内部で処理されていないため、エラーを投げます。 + +それに対処するための "Vuex way" は、`` の値をバインディングし、`input` または `change` イベントでアクションを呼び出すことです: + +``` html + +``` +``` js +// ... +computed: { + ...mapState({ + message: state => state.obj.message + }) +}, +methods: { + updateMessage: ({ dispatch }, e) => { + this.$store.commit('updateMessage', e.target.value) + } +} +``` + +ミューテーションのハンドラは以下のようになります: + +``` js +// ... +mutations: { + updateMessage (state, message) { + state.obj.message = message + } +} +``` + +### 双方向算出プロパティ + +確かに、上記の例は単純な `v-model` と ローカルステートよりもかなり冗長で、`v-model` のいくつかの有用な機能が使えません。代わりに、セッターで双方向算出プロパティを使うアプローチがあります。 + +``` js +computed: { + message: { + get () { + return this.$store.state.obj.message + }, + set (value) { + this.$store.commit('updateMessage', value) + } + } +} +``` diff --git a/docs/ja/getters.md b/docs/ja/getters.md new file mode 100644 index 000000000..6b4177a25 --- /dev/null +++ b/docs/ja/getters.md @@ -0,0 +1,91 @@ +# ゲッター + +例えば項目のリストをフィルタリングしたりカウントするときのように、ストアの状態を算出したいときがあります。 + +``` js +computed: { + doneTodosCount () { + return this.$store.state.todos.filter(todo => todo.done).length + } +} +``` + +もしこの関数を複数のコンポーネントで利用したくなったら、関数をコピーするか、あるいは関数を共用のヘルパーに切り出して複数の場所でインポートする必要があります。しかし、どちらも理想的とはいえません。 + +Vuex を利用するとストア内に "ゲッター" を定義することができます(ストアのための算出プロパティだと考えてください)。ゲッターはステート(状態)を第1引数として受け取ります: + +``` js +const store = new Vuex.Store({ + state: { + todos: [ + { id: 1, text: '...', done: true }, + { id: 2, text: '...', done: false } + ] + }, + getters: { + doneTodos: state => { + return state.todos.filter(todo => todo.done) + } + } +}) +``` + +ゲッターは `store.getters` オブジェクトから取り出されます: + +``` js +store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] +``` + +ゲッターは第2引数として他のゲッターを受け取ります: + +``` js +getters: { + // ... + doneTodosCount: (state, getters) => { + return getters.doneTodos.length + } +} +``` + +``` js +store.getters.doneTodosCount // -> 1 +``` + +どのコンポーネントの内部でも簡単にゲッターを利用することができます: + +``` js +computed: { + doneTodosCount () { + return this.$store.getters.doneTodosCount + } +} +``` + +### `mapGetters` ヘルパー + +`mapGetters` ヘルパーはストアのゲッターをローカルの算出プロパティにマッピングさせます: + +``` js +import { mapGetters } from 'vuex' + +export default { + // ... + computed: { + // ゲッターを、スプレッド演算子(object spread operator)を使って computed に組み込む + ...mapGetters([ + 'doneTodosCount', + 'anotherGetter', + // ... + ]) + } +} +``` + +ゲッターを異なる名前でマッピングさせたいときはオブジェクトを使います: + +``` js +...mapGetters({ + // this.doneCount を store.getters.doneTodosCount にマッピングさせる + doneCount: 'doneTodosCount' +}) +``` diff --git a/docs/ja/getting-started.md b/docs/ja/getting-started.md new file mode 100644 index 000000000..be6caf94c --- /dev/null +++ b/docs/ja/getting-started.md @@ -0,0 +1,44 @@ +# Vuex 入門 + +Vuex アプリケーションの中心にあるものは**ストア**です。"ストア" は、基本的にアプリケーションの**状態(state)**を保持するコンテナです。単純なグローバルオブジェクトとの違いが 2つあります。 + +1. Vuex ストアはリアクティブです。Vue コンポーネントがストアから状態を取り出すとき、もしストアの状態が変化したら、ストアはリアクティブかつ効率的に更新を行います。 + +2. ストアの状態を直接変更することはできません。明示的に**ミューテーションをコミットする**ことによってのみ、ストアの状態を変更します。これによって、全ての状態の変更について追跡可能な記録を残すことが保証され、ツールでのアプリケーションの動作の理解を助けます。 + +### シンプルなストア + +> **注意:** 私たちは、このドキュメントのコード例に ES2015 のシンタックスを利用しています。 もし触れたことがなければ、[ぜひ触れてください](https://babeljs.io/docs/learn-es2015/)! + +Vuex を[インストール](installation.md) してから、ストアをつくってみましょう。Vuex ストアの作成は、とても簡単です。ストアオブジェクトの初期状態と、いくつかのミューテーションを準備するだけです。 + +``` js +// モジュールシステムを利用しているときはあらかじめ Vue.use(Vuex) を呼び出していることを確認しておいてください + +const store = new Vuex.Store({ + state: { + count: 0 + }, + mutations: { + increment (state) { + state.count++ + } + } +}) +``` + +これで `store.state` でストアオブジェクトの状態を参照でき、また `store.commit` メソッドで状態の変更を行うことができます。 + +``` js +store.commit('increment') + +console.log(store.state.count) // -> 1 +``` + +そして `store.state.count` を直接変更する代わりにミューテーションをコミットする理由は、状態の変更を明確に追跡したいからです。このシンプルな規約は、あなたのコードの意図をさらに明確にし、コードを読んだ時にアプリケーションの状態の変更について、論理的に考えることができるようにします。加えて、私たちに全ての変更のログを取ったり、状態のスナップショットを取ったり、タイムトラベルデバッグを行うようなツールを実装する余地を与えてくれます。 + +ストアオブジェクトの状態はリアクティブなので、ストアの状態をコンポーネント内で使うには算出プロパティ内でただ状態を返せば良いです。コンポーネントメソッドでミューテーションをコミットすることによって状態の変更を行います。 + +こちらが [Vuex を使った最も基本的なカウンターアプリの例](https://jsfiddle.net/yyx990803/n9jmu5v7/)です。 + +これから Vuex のコアコンセプトについて詳しく説明していきます。まずは[状態(state)](state.md)からはじめましょう。 diff --git a/docs/ja/hot-reload.md b/docs/ja/hot-reload.md new file mode 100644 index 000000000..034cecfaa --- /dev/null +++ b/docs/ja/hot-reload.md @@ -0,0 +1,44 @@ +# ホットリローディング + +Vuex は Webpack の [Hot Module Replacement API](https://webpack.github.io/docs/hot-module-replacement.html) を使用することで、アプリケーションの開発を行っている間のミューテーション、モジュール、アクション、ゲッターのホットリローディングをサポートします。Browserify では [browserify-hmr](https://github.com/AgentME/browserify-hmr/) プラグインを使用することができます。 + +ミューテーションとモジュールのホットリローディングのために、`store.hotUpdate()` API メソッドを利用する必要があります: + +``` js +// store.js +import Vue from 'vue' +import Vuex from 'vuex' +import mutations from './mutations' +import moduleA from './modules/a' + +Vue.use(Vuex) + +const state = { ... } + +const store = new Vuex.Store({ + state, + mutations, + modules: { + a: moduleA + } +}) + +if (module.hot) { + // ホットモジュールとしてアクションとモジュールを受け付けます + module.hot.accept(['./mutations', './modules/a'], () => { + // 更新されたモジュールをインポートする + // babel 6 のモジュール出力のため、ここでは .default を追加しなければならない + const newActions = require('./actions').default + const newMutations = require('./mutations').default + // 新しいアクションとミューテーションにスワップ + store.hotUpdate({ + mutations: newMutations, + modules: { + a: newModuleA + } + }) + }) +} +``` + +ホットリローディングを試したい場合は、[counter-hot example](https://github.com/vuejs/vuex/tree/dev/examples/counter-hot)をチェックアウトしてください。 \ No newline at end of file diff --git a/docs/ja/images/flow.png b/docs/ja/images/flow.png new file mode 100644 index 000000000..fd9b97d59 Binary files /dev/null and b/docs/ja/images/flow.png differ diff --git a/docs/ja/images/vuex.png b/docs/ja/images/vuex.png new file mode 100644 index 000000000..018129947 Binary files /dev/null and b/docs/ja/images/vuex.png differ diff --git a/docs/ja/installation.md b/docs/ja/installation.md new file mode 100644 index 000000000..510b5139a --- /dev/null +++ b/docs/ja/installation.md @@ -0,0 +1,44 @@ +# インストール + +### 直接ダウンロードする / CDN + +[https://unpkg.com/vuex](https://unpkg.com/vuex) + + +[Unpkg.com](https://unpkg.com) で NPM ベースの CDN リンクが提供されています。上記リンクは常に NPM の最新のリリースを指します。`https://unpkg.com/vuex@2.0.0` のような URL によって特定のバージョン/タグを利用することもできます。 + + +Vue のあとで `vuex` を取り込むと自動的に Vuex が導入されます: + +``` html + + +``` + +### NPM + +``` bash +npm install vuex +``` + +モジュールシステムで利用される場合、 `Vue.use()` によって Vuex を明示的に導入する必要があります: + +``` js +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) +``` + +グローバルなスクリプトタグを利用する場合にはこのようにする必要はありません。 + +### 開発版ビルド + +最新の開発版ビルドを利用したい場合には、 Github から直接クローンし `vuex` を自身でビルドする必要があります。 + +``` bash +git clone https://github.com/vuejs/vuex.git node_modules/vuex +cd node_modules/vuex +npm install +npm run build +``` diff --git a/docs/ja/intro.md b/docs/ja/intro.md new file mode 100644 index 000000000..4507b61a3 --- /dev/null +++ b/docs/ja/intro.md @@ -0,0 +1,66 @@ +# Vuex とは何か? + +Vuex は Vue.js アプリケーションのための **状態管理パターン + ライブラリ**です。 +これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 +また Vue 公式の[開発ツール拡張](https://github.com/vuejs/vue-devtools)と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。 + +### "状態管理パターン"とはなんですか? + +単純な Vue で作られたカウンターアプリをみてみましょう: + +``` js +new Vue({ + // state + data () { + return { + count: 0 + } + }, + // view + template: ` +
{{ count }}
+ `, + // actions + methods: { + increment () { + this.count++ + } + } +}) +``` + +これはいくつかの要素をアプリ自身に含んでいます: + +- **状態**、これは私達のアプリを動かす信頼できる情報源(the source of truth)です。 +- **ビュー**、これは**状態**のただの宣言的なマッピングです。 +- **アクション**、これは**ビュー**からのユーザー入力に反応して、状態の変更を可能にする方法です。 + +これらは"単方向データフロー"のコンセプトの極めてシンプルな責務です: + +

+ +

+ +しかし、単純さは、**共通の状態を共有する複数のコンポーネントを持ったときに**、すぐに破綻します: + +- 複数のビューが同じ状態に依存することがあります。 +- 異なるビューからのアクションで、同じ状態を変更する必要があります。 + +一つ目は、プロパティ (props) として深く入れ子になったコンポーネントに渡すのは面倒で、兄弟コンポーネントでは単純に機能しません。二つ目は、親子のインスタンスを直接参照したり、イベントを介して複数の状態のコピーを変更、同期することを試みるソリューションに頼っていることがよくあります。これらのパターンは、いずれも脆く、すぐにメンテナンスが困難なコードに繋がります。 + +では、コンポーネントから共有している状態を抽出し、それをグローバルシングルトンで管理するのはどうでしょうか? これにより、コンポーネントツリーは大きな "ビュー" となり、どのコンポーネントもツリー内のどこにあっても状態にアクセスしたり、アクションをトリガーできます! + +さらに、状態管理に関わる概念を定義、分離し、特定のルールを敷くことで、コードの構造と保守性を向上させることができます。 + +これが Vuex の背景にある基本的なアイディアであり、[Flux](https://facebook.github.io/flux/docs/overview.html)、 [Redux](http://redux.js.org/) そして [The Elm Architecture](https://guide.elm-lang.org/architecture/)から影響を受けています。 +他のパターンと異なるのは、Vuex は効率的な更新のために、Vue.js の粒度の細かいリアクティビティシステムを利用するよう特別に調整して実装されたライブラリだということです。 + +![vuex](./images/vuex.png) + +### いつ、Vuexを使うべきでしょうか? + +Vuex は、共有状態の管理に役立ちますが、さらに概念やボイラープレートのコストがかかります。これは、短期的生産性と長期的生産性のトレードオフです。 + +もし、あなたが大規模な SPA を構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。単純な [グローバルイベントバス](http://jp.vuejs.org/v2/guide/components.html#%E8%A6%AA%E5%AD%90%E9%96%93%E4%BB%A5%E5%A4%96%E3%81%AE%E9%80%9A%E4%BF%A1) が必要なだけかもしれません。しかし、中規模から大規模の SPA を構築する場合は、Vue コンポーネントの外の状態をどうやってうまく扱うか考える絶好の機会です。Vuex は自然な次のステップとなるでしょう。これは Redux の作者、Dan Abramov からの良い引用です: + +> Flux ライブラリは眼鏡のようなものです: それらが必要になったときに知るのです。 diff --git a/docs/ja/modules.md b/docs/ja/modules.md new file mode 100644 index 000000000..31f962e1c --- /dev/null +++ b/docs/ja/modules.md @@ -0,0 +1,138 @@ +# モジュール + +単一ステートツリーを使うため、アプリケーションの全ての状態は、一つの大きなストアオブジェクトに内包されます。しかしながら、アプリケーションが大きくなるにつれて、ストアオブジェクトは膨れ上がってきます。 + +そのような場合に役立てるため Vuex ではストアを**モジュール**に分割できるようになっています。それぞれのモジュールは、モジュール自身の状態(state)、ミューテーション、アクション、ゲッター、モジュールさえも内包できます(モジュールをネストできます)- トップからボトムまでフラクタル構造です: + +``` js +const moduleA = { + state: { ... }, + mutations: { ... }, + actions: { ... }, + getters: { ... } +} + +const moduleB = { + state: { ... }, + mutations: { ... }, + actions: { ... } +} + +const store = new Vuex.Store({ + modules: { + a: moduleA, + b: moduleB + } +}) + +store.state.a // -> moduleA のステート +store.state.b // -> moduleB のステート +``` + +### モジュールのローカルステート + +モジュールのミューテーションやゲッターの中では、渡される第1引数は**モジュールのローカルステート**です。 + +``` js +const moduleA = { + state: { count: 0 }, + mutations: { + increment: (state) { + // state はモジュールのローカルステート + state.count++ + } + }, + + getters: { + doubleCount (state) { + return state.count * 2 + } + } +} +``` + +同様に、モジュールのアクションの中では `context.state` はローカルステートにアクセスでき、ルートのステートは `context.rootState` でアクセスできます: + +``` js +const moduleA = { + // ... + actions: { + incrementIfOdd ({ state, commit }) { + if (state.count % 2 === 1) { + commit('increment') + } + } + } +} +``` + +また、モジュールのゲッターの中では、ルートのステートは第3引数でアクセスできます: + +``` js +const moduleA = { + // ... + getters: { + sumWithRootCount (state, getters, rootState) { + return state.count + rootState.count + } + } +} +``` + +### 名前空間 + +特筆すべきこととして、モジュール内部のアクションやミューテーション、ゲッターは依然として**グローバル名前空間**の下に登録されます。これにより、複数のモジュールを同一のミューテーションやアクションタイプに反応させることができます。接頭語や接尾語を付けることで名前の衝突を回避できますし、再利用可能でかつどこで使われるか分からない Vuex のモジュールを書いているのならば、そうすべきです。例えば `todos` モジュールを作りたいときは以下のようにします: + +``` js +// types.js + +// ゲッター、アクション、ミューテーションの名前を定数として定義し、 +// それらにモジュール名である `todos` を接頭語として付ける +export const DONE_COUNT = 'todos/DONE_COUNT' +export const FETCH_ALL = 'todos/FETCH_ALL' +export const TOGGLE_DONE = 'todos/TOGGLE_DONE' +``` + +``` js +// modules/todos.js +import * as types from '../types' + +// 接頭語を付けたゲッター、アクション、ミューテーションを定義する +const todosModule = { + state: { todos: [] }, + + getters: { + [types.DONE_COUNT] (state) { + // ... + } + }, + + actions: { + [types.FETCH_ALL] (context, payload) { + // ... + } + }, + + mutations: { + [types.TOGGLE_DONE] (state, payload) { + // ... + } + } +} +``` + +### 動的にモジュールを登録する + +ストアが作られた**後**に `store.registerModule` メソッドを使って、モジュールを登録できます: + +``` js +store.registerModule('myModule', { + // ... +}) +``` + +モジュールのステートには `store.state.myModule` でアクセスします。 + +動的なモジュール登録があることで、他の Vue プラグインが、モジュールをアプリケーションのストアに付属させることで、状態の管理に Vuex を活用できることができます。例えば [`vuex-router-sync`](https://github.com/vuejs/vuex-router-sync) ライブラリは、動的に付属させたモジュール内部でアプリケーションのルーティングのステートを管理することで vue-router と vuex を統合しています。 + +`store.unregisterModule(moduleName)` を呼び出せば、動的に登録したモジュールを削除できます。ただしストア作成(store creation)の際に宣言された、静的なモジュールはこのメソッドで削除できないことに注意してください。 diff --git a/docs/ja/mutations.md b/docs/ja/mutations.md new file mode 100644 index 000000000..328f07e59 --- /dev/null +++ b/docs/ja/mutations.md @@ -0,0 +1,186 @@ +# ミューテーション + +実際に Vuex のストアの状態を変更できる唯一の方法は、ミューテーションをコミットすることです。Vuex のミューテーションはイベントにとても近い概念です: 各ミューテーションは**タイプ**と**ハンドラ**を持ちます。ハンドラ関数は Vuex の状態(state)を第1引数として取得し、実際に状態の変更を行います: + +``` js +const store = new Vuex.Store({ + state: { + count: 1 + }, + mutations: { + increment (state) { + // 状態を変更する + state.count++ + } + } +}) +``` + +直接ミューテーションハンドラを呼び出すことはできません。この mutations オプションは、どちらかいうと "タイプが `increment` のミューテーションがトリガーされたときに、このハンドラが呼ばれる" といったイベント登録のようなものです。ミューテーションハンドラを起動するためにはミューテーションのタイプを指定して **store.commit** を呼び出す必要があります: + +``` js +store.commit('increment') +``` + +### 追加の引数を渡してコミットする + +`store.commit` に追加の引数を渡すこともできます。この追加の引数は、特定のミューテーションに対する**ペイロード**と呼びます: + +``` js +// ... +mutations: { + increment (state, n) { + state.count += n + } +} +``` + +``` js +store.commit('increment', 10) +``` + +ほとんどの場合、ペイロードはオブジェクトにすべきです。そうすることで複数のフィールドを含められるようになり、またミューテーションがより記述的に記録されるようになります: + +``` js +// ... +mutations: { + increment (state, payload) { + state.count += payload.amount + } +} +``` + +``` js +store.commit('increment', { + amount: 10 +}) +``` + +### オブジェクトスタイルのコミット + +また `type` プロパティを持つオブジェクトを使って、ミューテーションをコミットすることもできます: + +``` js +store.commit({ + type: 'increment', + amount: 10 +}) +``` + +オブジェクトスタイルでコミットするとき、オブジェクト全体がペイロードとしてミューテーションハンドラに渡されます。したがってハンドラの例は上記と同じです: + +``` js +mutations: { + increment (state, payload) { + state.count += payload.amount + } +} +``` + +### サイレントコミット + +> 注意: この機能は開発ツール内にミューテーション・フィルタが実装された後に非推奨になる予定です。 + +デフォルトでは全てのコミットされたミューテーションはプラグイン(開発ツール等)に送られます。しかし場合によっては、プラグインに全ての状態の変化を記録して欲しくないこともあるでしょう。あるいは、短い間隔やポーリングでのストアへの複数のコミットも、常に追跡する必要はないでしょう。こうしたケースでは、`store.commit` に第3引数を渡すことで、特定のミューテーションをプラグインに気付かせないようにすること("silence")ができます: + +``` js +store.commit('increment', { + amount: 1 +}, { silent: true }) + +// オブジェクトスタイルのコミット +store.commit({ + type: 'increment', + amount: 1 +}, { silent: true }) +``` + +### Vue のリアクティブなルールに則ったミューテーション + +Vuex ストアの状態は Vue によってリアクティブになっているので、状態を変更すると、状態を監視している Vue コンポーネントは自動的に更新されます。これは Vuex のミューテーションは、通常の Vue と動作させているときと同じく、リアクティブな値に関する注意が必要であることを意味します: + +1. あらかじめ全ての必要なフィールドによって、ストアの初期状態を初期化することが望ましいです + +2. 新しいプロパティをオブジェクトに追加するとき、以下のいずれかが必要です: + + - `Vue.set(obj, 'newProp', 123)` を使用する。あるいは + + - 全く新しいオブジェクトで既存のオブジェクトを置き換える。例えば、stage-3 の[スプレッドシンタックス(object spread syntax)](https://github.com/sebmarkbage/ecmascript-rest-spread) を使用して、次のように書くことができます: + + ``` js + state.obj = { ...state.obj, newProp: 123 } + ``` + +### ミューテーション・タイプに定数を使用する + +いろいろな Flux 実装において、ミューテーション・タイプに定数を使用することが共通して見られるパターンです。これはコードに対してリントツールのようなツールを利用できるという利点があり、また単一ファイルに全ての定数を設定することによって、共同で作業する人に、アプリケーション全体で何のミューテーションが可能であるかを一目見ただけで理解できるようにします: + +``` js +// mutation-types.js +export const SOME_MUTATION = 'SOME_MUTATION' +``` + +``` js +// store.js +import Vuex from 'vuex' +import { SOME_MUTATION } from './mutation-types' + +const store = new Vuex.Store({ + state: { ... }, + mutations: { + // 定数を関数名として使用できる ES2015 の算出プロパティ名(computed property name)機能を使用できます + [SOME_MUTATION] (state) { + // 状態を変更する + } + } +}) +``` + +定数を使用するかどうかは好みの問題です。多くの開発者による大規模なプロジェクトで役に立ちますが、完全にオプションなので、もしお気に召さなければ使用しなくても構いません。 + +### ミューテーションは同期的でなければならない + +ひとつの重要なルールを覚えておきましょう。それは**ミューテーションハンドラ関数は同期的でなければならない**ということです。なぜか?次の例で考えてみましょう: + +``` js +mutations: { + someMutation (state) { + api.callAsyncMethod(() => { + state.count++ + }) + } +} +``` + +いま、開発ツールのミューテーションのログを見ながら、アプリケーションのデバッグを行っていることを想像してください。全てのミューテーションをログに記録するためには、ミューテーションの前後の状態のスナップショットを捕捉することが必要です。しかし、上の例にあるミューテーション内の非同期コールバックは、それを不可能にします: そのコールバックは、ミューテーションがコミットされた時点ではまだ呼び出されていません。そして、コールバックが実際にいつ呼び出されるかを、開発ツールは知る術がありません。いかなる状態変更でも、コールバック内で起きる場合は本質的に追跡不可能です。 + +### コンポーネント内におけるミューテーションのコミット + +`this.$store.commit('xxx')` と書くか、もしくはコンポーネントのメソッドを `store.commit` にマッピングする `mapMutations` ヘルパーを呼び出すこと(ルートの `store` の注入が必要)で、コンポーネント内でミューテーションをコミットできます: + +``` js +import { mapMutations } from 'vuex' + +export default { + // ... + methods: { + ...mapMutations([ + 'increment' // this.increment() を this.$store.commit('increment') にマッピングする + ]), + ...mapMutations({ + add: 'increment' // this.add() を this.$store.commit('increment') にマッピングする + }) + } +} +``` + +### アクションへ向けて + +状態変更を非同期に組み合わせることは、プログラムの動きを予測することを非常に困難にします。例えば、状態を変更する非同期コールバックを持った 2つのメソッドを両方呼び出しとき、それらがいつ呼び出されたか、どちらが先に呼び出されたかを、どうやって知ればよいのでしょう?これがまさに、状態変更と非同期の 2つの概念を分離したいという理由です。Vuex では**全てのミューテーションは同期的に行う**という作法になっています: + +``` js +store.commit('increment') +// "increment" ミューテーションによる状態変更は、この時点で行われるべきです +``` + +非同期的な命令を扱うために[アクション](actions.md)を見てみましょう。 diff --git a/docs/ja/plugins.md b/docs/ja/plugins.md new file mode 100644 index 000000000..d1b9b0627 --- /dev/null +++ b/docs/ja/plugins.md @@ -0,0 +1,120 @@ +# プラグイン + +Vuex ストア は、各ミューテーションへのフックを公開する `plugins` オプションを受け付けます。 Vuex プラグインは、単一の引数としてストアを受けつけるただの関数です: + +``` js +const myPlugin = store => { + // ストアが初期化されたときに呼ばれます + store.subscribe((mutation, state) => { + // それぞれのミューテーションの後に呼ばれます + // ミューテーションは { type, payload } の形式で提供されます + }) +} +``` + +そして、このように利用することができます: + +``` js +const store = new Vuex.Store({ + // ... + plugins: [myPlugin] +}) +``` + +### プラグイン内でのミューテーションのコミット + +プラグインは直接、状態を変更できません。これはコンポーネントに似ています。プラグインはコンポーネント同様に、ミューテーションのコミットによる変更のトリガーだけで状態を変更できます。 + +ミューテーションのコミットによるストアとデータソースの同期をプラグインで実現できます。 websocket データソースとストアを例にします (これは不自然な例です。実際には、さらに複雑なタスクのために `createPlugin` 関数は、追加でいくつかのオプションを受け取れます): + +``` js +export default function createWebSocketPlugin (socket) { + return store => { + socket.on('data', data => { + store.commit('RECEIVE_DATA', data) + }) + store.subscribe((mutation) => { + if (mutation.type === 'UPDATE_DATA') { + socket.emit('update', mutation.payload) + } + }) + } +} +``` + +``` js +const plugin = createWebSocketPlugin(socket) + +const store = new Vuex.Store({ + state, + mutations, + plugins: [plugin] +}) +``` + +### 状態のスナップショットを撮る + +時々、状態の"スナップショット"を撮って、ミューテーション前後の状態を比較したくなることがあるでしょう。それを実現するために、状態オブジェクトのディープコピーを行う必要があります: + +``` js +const myPluginWithSnapshot = store => { + let prevState = _.cloneDeep(store.state) + store.subscribe((mutation, state) => { + let nextState = _.cloneDeep(state) + + // 以前の状態と以後の状態を比較... + + // 次のミューテーションのために状態を保存 + prevState = nextState + }) +} +``` + +**状態のスナップショットを撮るプラグインはアプリケーションの開発の間だけ使われるべきです。** Webpack や Browserify を使っていれば、ビルドツールにそれを処理させることができます: + +``` js +const store = new Vuex.Store({ + // ... + plugins: process.env.NODE_ENV !== 'production' + ? [myPluginWithSnapshot] + : [] +}) +``` + +上のように記述すれば、プラグインはデフォルトで利用されることになります。本番環境( production ) では、 `process.env.NODE_ENV !== 'production'` を `false` に置き換えるために、 Webpack では[DefinePlugin](https://webpack.github.io/docs/list-of-plugins.html#defineplugin) 、 Browserify では[envify](https://github.com/hughsk/envify) が必要になります。 + +### ビルトインロガープラグイン + +> もし、あなたが [vue-devtools](https://github.com/vuejs/vue-devtools) を使っている場合は、これは不要でしょう。 + +Vuex には、一般的なデバッグに利用する用途の備え付けのロガープラグインがあります。 + +```js +import createLogger from 'vuex/dist/logger' + +const store = new Vuex.Store({ + plugins: [createLogger()] +}) +``` + +`createLogger` 関数はいくつかのオプションを受け取ります: + +``` js +const logger = createLogger({ + collapsed: false, // ログ出力されたミューテーションを自動で展開します + transformer (state) { + // ロギングの前に、状態を変換します + // 例えば、特定のサブツリーのみを返します + return state.subTree + }, + mutationTransformer (mutation) { + // ミューテーションは、{ type, payload } の形式でログ出力されます + // 任意の方法でそれをフォーマットできます + return mutation.type + } +}) +``` + +ロガーファイルは、他にも `