この記事では、独自のReduxアプリケーションをビルドする方法を学びます。
- 全粒サーバーレンダリング
- 高度なルーティング、Rich Omega-3
- 油っぽい非同期データの読み込み
- 滑らかで機能的な仕上げ
これがあなたがこの人生で望むもののように見えるなら、カットの下に進んでください、そうでないなら、気にしないでください。
お伝えしたいのですが...これは非常に小さなチュートリアルではありませんので、手を伸ばして厄介な道の準備をしてください。腕や脚を突き出さないでください...
停止して、待って、Reduxとは何ですか?
ああ、私はあなたが尋ねてうれしいです!
ReduxはDanil Abramovの新しいFluxフレームワークで、多くの不必要な困難を取り除きます。 ここでこのフレームワークが作成された理由を読むか、一般的にTLを参照してください :DR Reduxはアプリケーションの状態を1か所に保持し、この状態と対話するための最小限でありながら非常に強力な方法を定義します(状態orig。) 。
従来の Flux フレームワークに精通している場合、最大の違いは、Vaults (Stores orig。)の不足と、Reducer (Reducers orig。)の存在です。
Reduxでは、アプリケーションの状態全体が別々のリポジトリに分割されるのではなく、1つの場所(Reduxのインスタンス)に存在します(これは少し同型に抵抗できます)。
「Reducer」は、状態がどのように変化するかの説明であり、本質的には何も変化せず、次のようになります。
function exampleReducer(state, action) { return state.changedBasedOn(action) }
どうやって? 少し後で見るでしょう。
Reduxはフレームワークというよりもソリューションのようなものだと思うでしょう。 これは、すべての最高のFluxアイデアを含む最小ベースです。 この記事は、あなたがそれをうまく使い、繁栄し、最小限の突然変異で進むことをあなたに教えることを願っています。
自分を快適にする
WebpackとBabelを使用してアプリケーションをバンドルします。なぜなら、私たちはクールで、スマートで、楽しく、そしてコードをその場でリロードして最新のES6 / 7チップを提供できるからです。
まず、ディレクトリを作成し、そこにいくつかのファイルを配置する必要があります。
事前に準備したpackage.jsonは次のとおりです。
package.json
{ "name": "isomorphic-redux", "version": "1.0.0", "description": "Basic isomorphic redux application", "main": "index.js", "scripts": { "start": "NODE_PATH=$NODE_PATH:./shared node .", "dev": "npm run start & webpack-dev-server --progress --color" }, "author": "<your-name> <<your-email>>", "license": "MIT", "dependencies": { "axios": "^0.5.4", "express": "^4.13.2", "immutable": "^3.7.4", "object-assign": "^3.0.0", "react": "^0.13.3", "react-redux": "^0.2.2", "react-router": "^1.0.0-beta3", "redux": "^1.0.0-rc" }, "devDependencies": { "babel": "^5.8.20", "babel-eslint": "^4.0.5", "babel-loader": "^5.3.2", "eslint": "^1.0.0", "eslint-plugin-react": "^3.1.0", "react-hot-loader": "^1.2.8", "webpack": "^1.10.5", "webpack-dev-server": "^1.10.1" } }
同様に
webpack.config.js
var path = require('path'); var webpack = require('webpack'); module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/only-dev-server', './client' ], output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, resolve: { modulesDirectories: ['node_modules', 'shared'], extensions: ['', '.js', '.jsx'] }, module: { loaders: [ { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['react-hot', 'babel'] } ] }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], devtool: 'inline-source-map', devServer: { hot: true, proxy: { '*': 'http://localhost:' + (process.env.PORT || 3000) } } };
および.babelrc (「ES7」シュガー用)
{ "optional": ["es7.decorators", "es7.classProperties", "es7.objectRestSpread"] }
率直に言って、これらのファイルはそれほど注目に値するものではなく、合理的な開発環境を作成しただけです。
OK、
npm i
を実行してすべての依存モジュールをダウンロードする必要があり、開始できます。
シーモアをください
アプリケーションの基本構造は次のようになります。
client/ shared/ index.js server.jsx
コードの主要部分はすべて
shared
ディレクトリにありますが、クライアント部分とサーバー部分を分離するには、いくつかの接着コードが必要です。
index.js
'use strict'; require('babel/register')({}); var server = require('./server'); const PORT = process.env.PORT || 3000; server.listen(PORT, function () { console.log('Server listening on', PORT); });
server.jsxを実行するファイルだけなので、ES6 / JSXを使用できます。
サーバーの機能はExpressによって実行されます。これは、サーバーがより単純であり、既に知っている可能性があるためです。
server.jsx
import express from 'express'; const app = express(); app.use((req, res) => { const HTML = ` <DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Isomorphic Redux Demo</title> </head> <body> <div id="react-view"></div> <script type="application/javascript" src="/bundle.js"></script> </body> </html> `; res.end(HTML); }); export default app;
かなり標準的なでたらめ。 グローバルミドルウェアでExpressサーバーをセットアップしますが、何も処理せず、存在の空白を調べるのに役立つ空のWebページのみを処理します。 それを修正しましょう。
についてのルーチン
おそらく、エクスプレスルーティングとテンプレートを使用することは簡単だと思います。 残念ながら、サーバーとクライアント間でできるだけ多くのロジックを共有したいので、あなたは間違っています。 クライアントとサーバーでルーティングできるため、 React Routerを使用します。
そのため、React Routerが埋め込まれるルートコンポーネント
shared/components/index.jsx
があります。 このようにして、アプリケーション全体(キャップや地下室など)に美学を追加することができます。素晴らしいSPAに適したアーキテクチャです。
共有/コンポーネント/index.jsx
import React from 'react'; export default class AppView extends React.Component { render() { return ( <div id="app-view"> <h1>Todos</h1> <hr /> {this.props.children} </div> ); } }
ここの子はコンポーネントツリーに変わり、依存関係を持つ魔法の後にルーターが提供します。 ここには特別なものはありません。すべてをそのまま印刷するだけです。
次に、ルートを定義する必要があります
shared / routes.jsx
import React from 'react'; import { Route } from 'react-router'; import App from 'components'; export default ( <Route name="app" component={App} path="/"> </Route> );
ここでは、 '/'パスに沿って
components/index
を表示するようにReact Routerに指示します。 いいですね!
次に、サーバーで同じことを行います。
server.jsx
import React from 'react'; import { Router } from 'react-router'; import Location from 'react-router/lib/Location'; import routes from 'routes'; app.use((req, res) => { const location = new Location(req.path, req.query); Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); const InitialComponent = ( <Router {...routeState} /> ); const componentHTML = React.renderToString(InitialComponent); const HTML = `...`; res.end(HTML); }); });
ここで、いくつかの新しいおもちゃをインポートし、表現する要求を転送するようにルーターに指示します。
routeState
変数に戻り、要求されたルートを表示できることを願っています。 次に、 Reactの smart
renderToString
メソッドを使用して、コンポーネントをHTML文字列で出力します。これは、前に作成したreact-view divでクライアントに提供します。
<div id="react-view">${componentHTML}</div>
npm start
を実行
npm start
http:// localhost:3000 /ルートがHTMLに挿入されたことがわかります。
いくつかのエラーがコンソールに表示され、ルーターに複数の値が必要であることを示しています。 これは、クライアントが
bundle.js
をダウンロードしようとしているためですが、まだwebpackのエントリポイントを設定していないため、これはそのようなゴミです。
見た目は素晴らしく、あなたの脳はすでに壊れていると思いますが、今は静的なページがあります。 Reactからすべてのジューシーなパルプを入手するには、クライアントにルーティングを実装する必要があります。
だから、
client/index.jsx
を開いて何かを書いてください:
クライアント/ index.jsx
import React from 'react'; import { Router } from 'react-router'; import { history } from 'react-router/lib/BrowserHistory'; import routes from 'routes'; React.render( <Router children={routes} history={history} />, document.getElementById('react-view') );
Routerコンポーネントをreact-view divに挿入するようにReactに指示し、適切なパラメーターを渡しました。 サーバー部分には表示されなかった履歴オブジェクト、React Router構成の必要な部分(直接表示された場合)、およびURLの外観について説明します。 そして、私たちはきれいな施設を求めています! したがって、 BrowserHistoryでHTML5 History APIを使用しますが、古いブラウザーではHashHistoryを使用して、アドレスから
/#
URLを取得できます。
これで、
npm run dev
アプリケーションを
npm run dev
でき、Webpackがbundle.jsを処理します。 あまりおもしろそうではありませんが、 http:// localhost:8080 /にアクセスすると、エラーなく動作するはずです。 ルーティングが完了し、Reduxアクションの準備ができました。
削減、再利用、リデュース
ReduxはFluxに非常によく似ていますが、以前にストレージの代わりにリデューサーが使用されることを述べました。 最初に、Todoシートを変更する簡単な手順をいくつか作成します。
共有/アクション/ TodoActions.js
export function createTodo(text) { return { type: 'CREATE_TODO', text, date: Date.now() } } export function editTodo(id, text) { return { type: 'EDIT_TODO', id, text, date: Date.now() }; } export function deleteTodo(id) { return { type: 'DELETE_TODO', id }; }
ご覧のとおり、Reduxでは、 アクションクリエーターは、順番にフォーマットされたオブジェクトを返す単なる関数です。 魔法ではありません。それを処理するために減速機が必要です。
共有/レデューサー/ TodoReducer.js
import { List } from 'immutable'; const defaultState = new List(); export default function todoReducer(state = defaultState, action) { switch(action.type) { case 'CREATE_TODO': return state.concat(action.text); case 'EDIT_TODO': return state.set(action.id, action.text); case 'DELETE_TODO': return state.delete(action.id); default: return state; } }
すべてが再び単純です。 ここで、 Immutable Listオブジェクトを使用して、不変の状態をリポジトリに格納し(より大きなアプリケーションではより複雑になる可能性があります)、アクションに応じて状態の新しいバージョンを返すことができます。
Reduxはそれほど頑固ではなく、レデューサーからの期待は2つだけです。
- 署名
(state, action) => newState
です。 - レデューサーは渡された状態を変更しませんが、新しいバージョンを返します。
ご覧のとおり、後者はImmuatable.jsに適しています
ここでは単純なスイッチ構造を使用しますが、気に入らない場合は、空白を書くことをためらわないでください。
Reduxは単一のリデューサーの使用に限定されません。それらを取得するために、reducer
reducers/index.js
作成できます。
export { default as todos } from './TodoReducer';
全員が持っているので、実際には必要ありませんが、将来必要になります。
IIIiiii ...アクション!
レデューサーとアクションについて話すのは良いことですが、アプリはそれについて何も知りません! それを変える時が来ました。
Reduxインスタンスをコンポーネントツリーにプッシュして、これらすべてを処理し、これらすべてをリンクする必要があります!
NPMの
react-redux
は、これに役立ついくつかの
react-redux
。
server.jsx
import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from 'reducers'; app.use((req, res, next) => { const location = new Location(req.path, req.query); const reducer = combineReducers(reducers); const store = createStore(reducer); Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); const InitialView = ( <Provider store={store}> {() => <Router {...routeState} /> } </Provider> );
リクエストごとにReduxストレージコンポーネントのインスタンスを作成し、
Provider
ルートコンポーネントをラップすることにより、コンポーネントツリー全体(アクセスが必要な場合は<component> .context.reduxとして利用可能)に転送し
Provider
。
クライアントに初期状態を与えて、保管施設を水素化できるようにする必要もあります。
Reduxの状態を尋ねるだけです:
const initialState = store.getState();
そして、HTMLテンプレートに数行追加します。
<title>Redux Demo</title> <script type="application/javascript"> window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}; </script>
その後、
window.__INITIAL_STATE__
介してクライアントの状態にアクセスできるようになります。
今やらなければならないことは、すべてをImmutable.jsコレクションに変換することです。 そして、新しいリポジトリをインスタンス化するときにそれらをReduxに渡します。
クライアント/ index.jsx
import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from 'reducers'; import { fromJS } from 'immutable'; let initialState = window.__INITIAL_STATE__; // Transform into Immutable.js collections, // but leave top level keys untouched for Redux Object .keys(initialState) .forEach(key => { initialState[key] = fromJS(initialState[key]); }); const reducer = combineReducers(reducers); const store = createStore(reducer, initialState); React.render( <Provider store={store}> {() => <Router children={routes} history={history} /> } </Provider>, document.getElementById('react-view'); );
これは、サーバーから渡された状態でストレージをハイドレイトすることを除いて、サーバーの状態を初期化することと同じです。
本当にアプリケーションの完成に近づいています。すべてのポイントを接続するために、いくつかのコンポーネントが残っています。
すべてのポイントを接続する
3つのコンポーネントを使用して、少し冗長な情報を表示します(ほとんどの場合そうです)が、これは「スマート」コンポーネントと「ダム」コンポーネントのReduxの違いを示します。これは大規模アプリケーションで非常に重要です。
スマートコンポーネントはReduxストレージイベントをサブスクライブし(たとえば、 @ connectorデコレータ構文を使用)、プロパティを介して他のコンポーネントにツリーをプッシュします。 ツリーのどこにでも配置できますが、より複雑なアプリケーションを開発する場合、通常は最下層に到達します。
ここでは1つだけを使用します
共有/コンポーネント/ Home.jsx
import React from 'react'; import TodosView from 'components/TodosView'; import TodosForm from 'components/TodosForm'; import { bindActionCreators } from 'redux'; import * as TodoActions from 'actions/TodoActions'; import { connect } from 'react-redux'; @connect(state => ({ todos: state.todos })) export default class Home extends React.Component { render() { const { todos, dispatch } = this.props; return ( <div id="todo-list"> <TodosView todos={todos} {...bindActionCreators(TodoActions, dispatch)} /> <TodosForm {...bindActionCreators(TodoActions, dispatch)} /> </div> ); } }
次に、2つの「ダム」コンポーネントを作成しますが、まず、ここで何が起こるかを見てみましょう。
デコレータに慣れていない場合( @connectorセクション)、最善の方法は、これがコンポーネントのmixinと同じであると理解することです。 おそらく、Pythonなどの他の言語でも同様の構成体を見たことがあるでしょう。
そうでない場合、javascriptでは、これらは何らかの方法で他の関数(ここでは「クラス」)を変更する単なる関数です。
@connectデコレータは、別のコンポーネント(
<Connector>
)でクラスをラップし、コンポーネントのプロパティとして状態の要求された部分にアクセスできるため、
todos
を使用できます。 また、Reduxから
dispatch
関数へのアクセスを提供します。これにより、次のようにアクションを処理できます。
dispatch(actionCreator());
最後に、Reduxの
bindActionCreators
関数を使用して、関連するアクションクリエーターを転送します。
つまり、子コンポーネントでは、アクション作成者を
dispatch()
関数でラップすることなく直接呼び出すことができます。
見て
コンポーネント/ TodosView.jsx
import React from 'react'; export default class TodosView extends React.Component { handleDelete = (e) => { const id = Number(e.target.dataset.id); // Equivalent to `dispatch(deleteTodo())` this.props.deleteTodo(id); } handleEdit = (e) => { const id = Number(e.target.dataset.id); const val = this.props.todos.get(id).text // For cutting edge UX let newVal = window.prompt('', val); this.props.editTodo(id, newVal); } render() { return ( <div id="todo-list"> { this.props.todos.map( (todo, index) => { return ( <div key={index}> <span>{todo}</span> <button data-id={index} onClick={this.handleDelete}> X </button> <button data-id={index} onClick={this.handleEdit}> Edit </button> </div> ); }) } </div> ); } }
ここでは、アクション作成者に関連付けられている削除ボタンと変更ボタンの隣に、リポジトリ内の各todo要素を表示します。
また、クラスの定義では「矢印」関数を使用します。そのコンテキストはクラスのコンストラクターに関連付けられています(これらの関数はエグゼキューターからコンテキストを継承するため)。 ES6クラスの通常の関数(レンダーなど)を使用する場合は、それらをコンテキストに関連付ける必要があります。
React.createClassを使用して問題を回避し、ミックスインを使用することもできますが、クリーンさと一貫性のためにES6クラスを使用することを好みます。
最後に、定義します
コンポーネント/ TodosForm.jsx
import React from 'react'; export default class TodosForm extends React.Component { handleSubmit = () => { let node = this.refs['todo-input'].getDOMNode(); this.props.createTodo(node.value); node.value = ''; } render() { return ( <div id="todo-form"> <input type="text" placeholder="type todo" ref="todo-input" /> <input type="submit" value="OK!" onClick={this.handleSubmit} /> </div> ); } }
これは、リポジトリにtodoを追加するだけの「ダム」コンポーネントでもあります。
次に、ルートを決定する必要があります
shared / routes.jsx
import Home from 'components/Home'; export default ( <Route name="app" component={App} path="/"> <Route component={Home} path="home" /> </Route> );
そして、 http:// localhost:8080 / homeに移動して、動作中のアプリケーションを見てください
最後のフロンティア:非同期アクション
私はあなたが考えていることを知っています。
これは不可能です。
そして、私はそれが可能であると言います!
Reduxのもう1つの優れた機能は、ディスパッチャからミドルウェアを判別することです。これにより、アクションを(非同期で)変更できます。 Reduxスレッドから、特定のシグネチャを持つ関数で機能することに気づいたと思います。
Reduxミドルウェアを使用して、アプリケーションでのアクションを簡単にし、アクションクリエーターを同期させます。これにより、美しくて素晴らしいES6の約束を使用できるようになります。
共有/ lib / promiseMiddleware.js
export default function promiseMiddleware() { return next => action => { const { promise, type, ...rest } = action; if (!promise) return next(action); const SUCCESS = type; const REQUEST = type + '_REQUEST'; const FAILURE = type + '_FAILURE'; next({ ...rest, type: REQUEST }); return promise .then(res = > { next({ ...rest, res, type: SUCCESS }); return true; }) .catch(error => { next({ ...rest, error, type: FAILURE }); // Another benefit is being able to log all failures here console.log(error); return false; }); }; }
つまり、アクションの「約束」キーを定義するだけで、自動的に解決済みまたは拒否済みの状態になります。
状態の変化を追跡する必要がある場合は、オプションで自動生成された<TYPE> _REQUESTおよび<TYPE> _FAILUREのレデューサーを追跡することもできます。
そして、それらを使用するには、 client / index.jsxおよびserver.jsxの数行を変更する必要があります
... import { applyMiddleware } from 'redux'; import promiseMiddleware from 'lib/promiseMiddleware'; ... const store = applyMiddleware(promiseMiddleware)(createStore)(reducer);
また、 リデューサーとともにinitialStateを転送することを忘れないでください
そして今、私たちはアクション作成者のためにcreateTodoマジックアクションを書くことができます。例えば
import request from 'axios'; const BACKEND_URL = 'https://webtask.it.auth0.com/api/run/wt-milomord-gmail_com-0/redux-tutorial-backend?webtask_no_cache=1'; export function createTodo(text) { return { type: 'CREATE_TODO', promise: request.post(BACKEND_URL, { text }) } }
減速機にわずかな変更を加えた後。
return state.concat(action.res.data.text);
これで、Todoが外部データベースに保存されました。 アプリケーションの開始時にそれらをロードする場合は、アクションのgetTodos作成者を追加するだけです。
export function getTodos() { return { type: 'GET_TODOS', promise: request.get(BACKEND_URL) } }
減速機で彼を捕まえます
case 'GET_TODOS': return state.concat(action.res.data);
そして、 TodosViewが初期化されたときに呼び出すことができます
componentDidMount() { this.props.getTodos(); }
ミドルウェアが初期リクエストまたは失敗の可能性に対してアクションを呼び出す場合、これらすべてをリデューサーでキャッチし、それぞれブート時またはエラー時にアプリケーションの状態を更新する方法を確認できます。
待って...そして、我々は状態の水分補給を破らなかったのですか?
はい それを修正しましょう!
問題は、非同期アクションを追加したが、状態がクライアントに送信される前に非同期アクションが完了するのを待たないことです。 クライアントにロード画面を表示できるので、それは重要ではないと思うかもしれません。 サーバーにぶら下がるよりはましですか?
まあ、それは多くの要因に依存します。 サーバーレンダリングの主な利点は、バックエンド(同じデータセンターにある可能性があります!)との良好な接続を保証できることです。 たとえば、ユーザーが失われたモバイル接続を介してサイトをダウンロードしようとする場合、サーバーが自分のさまざまなリソースから取得するよりも、サーバーが初期状態を取得するのを待つ方がはるかに優れています。
現在の状況でこの問題を解決することはそれほど難しくありません。 これはいくつかの方法で行うことができますが、私がする道は理想からはほど遠いです:
アクションクリエーターの配列として、コンポーネントに必要なデータを決定します。 クラス定義で静的プロパティを使用できます。
static needs = [ TodoActions.getTodos ]
すべてのpromise呼び出しをキャッチし、データを収集して送信する関数も必要です
shared / lib / fetchComponentData.js
export default function fetchComponentData(dispatch, components, params) { const needs = components.reduce( (prev, current) => { return (current.needs || []) .concat((current.WrappedComponent ? current.WrappedComponent.needs : []) || []) .concat(prev); }, []); const promises = needs.map(need => dispatch(need(params))); return Promise.all(promises); }
前述のスマートコンポーネントはコネクタコンポーネントにラップされるため、 WrappedComponentキーも確認する必要があることに注意してください。
次に、すべてのデータが揃ったときにのみ応答するようにサーバーを構成します。
import fetchComponentData from 'lib/fetchComponentData'; Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); function renderView() { // ... Rest of the old code goes here return HTML; } // Check this is rendering *something*, for safety if(routeState) fetchComponentData(store.dispatch, routeState.components, routeState.params) .then(renderView) .then(html => res.end(html)) .catch(err => res.end(err.message)); });
呼び出しが繰り返されるのを避けるために、 onComponentMountからアクション作成者を削除し、
npm run dev
を再起動してサーバー上の変更を更新してください。
何を学びましたか?
なんて美しい世界!
さらに多くのことができます。 多数のルートがあるアプリケーションでは、おそらくReact Router onLeaveハンドラーを使用して、コンポーネントが必要とするすべてのデータ( ここなど )をロードし、非同期APIで他のアクションをキャッチします。
これらすべてにもかかわらず、素晴らしい機能的な未来への壮大な探求を楽しんでいただければ幸いです。
Githubで最終結果を確認し、 ここで Reduxの詳細を読むこともできます。