json-api-normalizer:ReduxとJSON APIを友達にする簡単な方法

JSON API + redux







最近、Webサービスを開発するためのJSON API標準が人気を集めています。 私の意見では、これは非常に成功したソリューションであり、最終的にAPIの開発プロセスを少なくともわずかに標準化し、自転車の次の発明の代わりに、サーバー側とクライアント側の両方のライブラリを使用してデータを交換し、100個のシリアライザーとパーサーを書く代わりに興味深いタスクに焦点を当てます初めて。









JSON APIと一般的なWebサービス



JSON APIは、階層を保持したまま正規化された形式でデータをすぐに提供し、ページネーション、並べ替え、フィルタリングをすぐにサポートするため、本当に気に入っています。







典型的なウェブサービス



{ "id": "123", "author": { "id": "1", "name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "text": "Great job, Bro!", "commenter": { "id": "2", "name": "Nicole" } } ] }
      
      





JSON API



 { "data": [{ "type": "post", "id": "123", "attributes": { "id": 123, "title": "My awesome blog post" }, "relationships": { "author": { "type": "user", "id": "1" }, "comments": { "type": "comment", "id": "324" } } }], "included": [{ "type": "user", "id": "1", "attributes": { "id": 1, "name": "Paul" } }, { "type": "user", "id": "2", "attributes": { "id": 2, "name": "Nicole" } }, { "type": "comment", "id": "324", "attributes": { "id": 324, "text": "Great job, Bro!" }, "relationships": { "commenter": { "type": "user", "id": "2" } } }] }
      
      





JSON APIの主な欠点は、従来のAPIと比較した場合の「おしゃべり」ですが、それほど悪いのでしょうか?







種類 圧縮前(バイト) 圧縮後(バイト)
伝統的なJSON 264 170
JSON API 771 293


gzipの後、サイズの差は大幅に小さくなり、ボリュームが小さい構造化データについて話しているため、パフォーマンスの観点からはすべて問題ありません。







必要に応じて、JSON APIのデータのサイズが従来のJSONよりも小さくなる総合テストを考え出すことができます。たとえば、ブログの投稿とその著者など、別のオブジェクトにリンクするオブジェクトの束を取得すると、著者オブジェクトがJSON APIに表示されます一度だけ、従来のJSONでは各投稿に含まれます。







ここでメリットについて:JSON APIによって返されるデータ構造は常にフラットで正規化されます。つまり、各オブジェクトには複数のネストレベルがありません。 このような表現は、オブジェクトの重複を回避するだけでなく、reduxでデータを操作するためのベストプラクティスと完全に一致します。 最後に、オブジェクトの型指定は最初はJSON APIに組み込まれているため、 normalizrが必要とするように、クライアント側で「スキーマ」を定義する必要はありません。 この機能により、クライアント上のデータの処理を簡素化することができ、まもなく検証できるようになります。







注:以降、reduxは他の多くの状態管理ライブラリに置き換えることができますが、2016年の最新のJavaScriptの調査によると、reduxは他の既存のソリューションよりもはるかに人気があるため、JSでのreduxと状態管理はほとんど同じです同じ。







JSON APIとredux



すぐに使用できるJSON APIは、reduxとの統合に非常に適していますが、より良い方法がいくつかあります。







特に、アプリケーションの場合、データをdata



に分割してincluded



ことは理にかなっていることがあります。これは、要求したデータと取得したデータを分離する必要がある場合があるためです。 ただし、ストアにデータを保存する場合は統一する必要があります。そうしないと、同じオブジェクトの複数のコピーが異なる場所に存在する危険性があり、これはreduxのベストプラクティスに反します。







また、JSON APIはオブジェクトのコレクションを配列の形式で返します。reduxでは、Mapと同様にオブジェクトを操作する方がはるかに便利です。







これらの問題を解決するために、私はjson-api-normalizerライブラリーを開発しました。これは次のことを実行できます。







  1. マージdata



    を実装してincluded



    data



    を正規化します。
  2. オブジェクトのコレクションを配列からid =>



    形式のマップに変換しid =>



  3. JSON APIドキュメントの元の構造を特別なmeta



    オブジェクトに保存します。
  4. 1対多の関係を1つのオブジェクトに結合します。


ポイント3および4について詳しく説明します。







原則として、Reduxはデータをストアに徐々に蓄積します。これにより、パフォーマンスが向上し、オフラインモードの実装が簡素化されます。 ただし、同じデータオブジェクトを使用する場合、特定の画面についてストアから取得するデータを明確に言うことは常に可能とは限りません。 各リクエストのjson-api-normalizerは、ドキュメントのJSON APIの構造を特別なmeta



オブジェクトに格納します。これにより、必要なストアからデータのみを一意に取得できます。







json-api-normalizerは関係の説明を変換します







 { "relationships": { "comments": [{ "type": "comment", "id": "1", }, { "type": "comment", "id": "2", }, { "type": "comment", "id": "3", }] } }
      
      





次のように







 { "relationships": { "comments": { "type": "comment", "id": "1,2,3" } } }
      
      





このような表現は、マージを通じてredux状態を更新する場合により便利です。この場合、コレクション内のオブジェクトの1つとその参照を削除するという難しい問題を解決する必要がないためです。マージプロセスでは、ステップ。 おそらく、このソリューションはすべてのシナリオに最適というわけではないので、オプションを使用して既存の実装をオーバーライドできるリクエストをプルすることができればうれしいです。







実用例



1.ブランクをダウンロードします



JSON APIドキュメントのソースとして、 Phoenix Frameworkで簡単なWebアプリケーションを作成しました。 その実装については詳しく説明しませんが、ソースコードを見て、このようなWebサービスを作成するのがどれほど簡単かを確認することをお勧めします。







クライアントとして、 小さなReactアプリケーションを作成しました







このワークで作業します。 このブランチのgitクローンを作成します。







 git clone https://github.com/yury-dymov/json-api-react-redux-example.git --branch initial
      
      





そして、あなたは次のようになります:









これらはすべて構成されており、そのまま使用できます。







例を実行するには、コンソールに入力します







 npm run webpack-dev-server
      
      





そして、ブラウザhttp://localhost:8050



で開きます。







2. APIと統合する



最初に、APIと対話するreduxミドルウェアを作成します。 多くのreduxアクションでデータを正規化し、同じコードを繰り返すことがないように、json-api-normalizerを使用するのは論理的です。







src / redux /ミドルウェア/ api.js



 import fetch from 'isomorphic-fetch'; import normalize from 'json-api-normalizer'; const API_ROOT = 'https://phoenix-json-api-example.herokuapp.com/api'; export const API_DATA_REQUEST = 'API_DATA_REQUEST'; export const API_DATA_SUCCESS = 'API_DATA_SUCCESS'; export const API_DATA_FAILURE = 'API_DATA_FAILURE'; function callApi(endpoint, options = {}) { const fullUrl = (endpoint.indexOf(API_ROOT) === -1) ? API_ROOT + endpoint : endpoint; return fetch(fullUrl, options) .then(response => response.json() .then((json) => { if (!response.ok) { return Promise.reject(json); } return Object.assign({}, normalize(json, { endpoint })); }), ); } export const CALL_API = Symbol('Call API'); export default function (store) { return function nxt(next) { return function call(action) { const callAPI = action[CALL_API]; if (typeof callAPI === 'undefined') { return next(action); } let { endpoint } = callAPI; const { options } = callAPI; if (typeof endpoint === 'function') { endpoint = endpoint(store.getState()); } if (typeof endpoint !== 'string') { throw new Error('Specify a string endpoint URL.'); } const actionWith = (data) => { const finalAction = Object.assign({}, action, data); delete finalAction[CALL_API]; return finalAction; }; next(actionWith({ type: API_DATA_REQUEST, endpoint })); return callApi(endpoint, options || {}) .then( response => next(actionWith({ response, type: API_DATA_SUCCESS, endpoint })), error => next(actionWith({ type: API_DATA_FAILURE, error: error.message || 'Something bad happened' })), ); }; }; }
      
      





ここですべての「魔法」が発生します。ミドルウェアでデータを受信した後、json-api-normalizerを使用してデータを変換し、チェーンのさらに下に転送します。







注:エラーハンドラを少し終了すると、このコードは実稼働環境でも機能します。







ストア構成にミドルウェアを追加します。







src / redux / configureStore.js



 ... +++ import api from './middleware/api'; export default function (initialState = {}) { const store = createStore(rootReducer, initialState, compose( --- applyMiddleware(thunk), +++ applyMiddleware(thunk, api), DevTools.instrument(), ...
      
      





最初のアクションを作成します:







src / redux / actions / post.js



 import { CALL_API } from '../middleware/api'; export function test() { return { [CALL_API]: { endpoint: '/test', }, }; }
      
      





レデューサーを書きましょう:







src / redux / reducers / data.js



 import merge from 'lodash/merge'; import { API_DATA_REQUEST, API_DATA_SUCCESS } from '../middleware/api'; const initialState = { meta: {}, }; export default function (state = initialState, action) { switch (action.type) { case API_DATA_SUCCESS: return merge( {}, state, merge({}, action.response, { meta: { [action.endpoint]: { loading: false } } }), ); case API_DATA_REQUEST: return merge({}, state, { meta: { [action.endpoint]: { loading: true } } }); default: return state; } }
      
      





レデューサーをreduxストア構成に追加します。







src / redux / reducers / data.js



 import { combineReducers } from 'redux'; import data from './data'; export default combineReducers({ data, });
      
      





モデルレイヤーの準備ができました! これで、ビジネスロジックをUIに関連付けることができます。







src / components / Content.jsx



 import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; import Button from 'react-bootstrap-button-loader'; import { test } from '../../redux/actions/test'; const propTypes = { dispatch: PropTypes.func.isRequired, loading: PropTypes.bool, }; function Content({ loading = false, dispatch }) { function fetchData() { dispatch(test()); } return ( <div> <Button loading={loading} onClick={() => { fetchData(); }}>Fetch Data from API</Button> </div> ); } Content.propTypes = propTypes; function mapStateToProps() { return {}; } export default connect(mapStateToProps)(Content);
      
      





ブラウザでページを開いてボタンをクリックします-Browser DevToolsとRedux DevToolsのおかげで、アプリケーションがJSON API形式でデータを受信し、それらをより便利なプレゼンテーションに変換してreduxストアに保存することがわかります。 いいね! このデータをUIに表示するときが来ました。







3.データを使用します



redux-objectライブラリは、redux-storeのデータをJavaScriptオブジェクトに変換します。 これを行うには、リデューサーのアドレス、オブジェクトのタイプ、およびIDを渡す必要があります。その後、彼女はすべて自分で行います。







 import build, { fetchFromMeta } from 'redux-object'; console.log(build(state.data, 'post', '1')); // ---> post console.log(fetchFromMeta(state.data, '/posts')); // ---> array of posts
      
      





すべてのリンクは、遅延読み込みをサポートするJavaScriptプロパティに変わります。つまり、子オブジェクトは必要な場合にのみ読み込まれます。







 const post = build(state.data, 'post', '1'); // ---> post object; `author` and `comments` properties are not loaded post.author; // ---> user object
      
      





新しいUIコンポーネントをいくつか追加して、ページにデータを表示します。







注:記事のメイントピックから注意をそらさないように、スタイルの操作を意図的に省略しています。







最初に、ストアからデータを取得し、接続関数を介してデータをコンポーネントに渡す必要があります。







src / components / Content.jsx



 import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; import Button from 'react-bootstrap-button-loader'; import build from 'redux-object'; import { test } from '../../redux/actions/test'; import Question from '../Question'; const propTypes = { dispatch: PropTypes.func.isRequired, questions: PropTypes.array.isRequired, loading: PropTypes.bool, }; function Content({ loading = false, dispatch, questions }) { function fetchData() { dispatch(test()); } const qWidgets = questions.map(q => <Question key={q.id} question={q} />); return ( <div> <Button loading={loading} onClick={() => { fetchData(); }}>Fetch Data from API</Button> {qWidgets} </div> ); } Content.propTypes = propTypes; function mapStateToProps(state) { if (state.data.meta['/test']) { const questions = (state.data.meta['/test'].data || []).map(object => build(state.data, 'question', object.id)); const loading = state.data.meta['/test'].loading; return { questions, loading }; } return { questions: [] }; } export default connect(mapStateToProps)(Content);
      
      





ここでは、「/ test」リクエストのメタデータからデータを取得し、識別子を引き出して「question」タイプのオブジェクトを構築します。これを「questions」コレクションのコンポーネントに渡します。







src / components / Question / package.json
 { "name": "Question", "version": "0.0.0", "private": true, "main": "./Question" }
      
      





src / components / Question / Question.jsx



 import React, { PropTypes } from 'react'; import Post from '../Post'; const propTypes = { question: PropTypes.object.isRequired, }; function Question({ question }) { const postWidgets = question.posts.map(post => <Post key={post.id} post={post} />); return ( <div className="question"> {question.text} {postWidgets} </div> ); } Question.propTypes = propTypes; export default Question;
      
      





それらに対する質問と回答を表示します。







src / components / Post / package.json
 { "name": "Post", "version": "0.0.0", "private": true, "main": "./Post" }
      
      





src / components / Post / Post.jsx



 import React, { PropTypes } from 'react'; import Comment from '../Comment'; import User from '../User'; const propTypes = { post: PropTypes.object.isRequired, }; function Post({ post }) { const commentWidgets = post.comments.map(c => <Comment key={c.id} comment={c} />); return ( <div className="post"> <User user={post.author} /> {post.text} {commentWidgets} </div> ); } Post.propTypes = propTypes; export default Post;
      
      





ここでは、回答の著者とコメントを表示します。







src / components / User / package.json
 { "name": "User", "version": "0.0.0", "private": true, "main": "./User" }
      
      





src / components / User / User.jsx



 import React, { PropTypes } from 'react'; const propTypes = { user: PropTypes.object.isRequired, }; function User({ user }) { return <span className="user">{user.name}: </span>; } User.propTypes = propTypes; export default User;
      
      





src / components / Comment / package.json
 { "name": "Comment", "version": "0.0.0", "private": true, "main": "./Comment" }
      
      





src / components / Comment / Comment.jsx



 import React, { PropTypes } from 'react'; import User from '../User'; const propTypes = { comment: PropTypes.object.isRequired, }; function Comment({ comment }) { return ( <div className="comment"> <User user={comment.author} /> {comment.text} </div> ); } Comment.propTypes = propTypes; export default Comment;
      
      





以上です! 何かがうまくいかない場合、あなたのコードを私のプロジェクトのマスターブランチと比較できます







ライブデモはこちらから入手できます。







おわりに



json-api-normalizerおよびredux-objectライブラリが最近登場しました。 外からは非常にシンプルに思えるかもしれませんが、実際、そのような実装に着手する前に、私は年間を通して多くの異なった明白でない熊手を踏むことができました。したがって、これらのシンプルで便利なツールは役に立つと確信していますコミュニティと多くの時間を節約できます。







ディスカッションに参加していただき、またこれらのツールの開発を手伝ってください。







参照資料



  1. JSON API仕様
  2. リポジトリjson-api-normalizer
  3. Reduxオブジェクトリポジトリ
  4. Phoenixフレームワークに実装されたJSON APIベースのサンプルWebサービス
  5. JSON API Webサービスのサンプルソースコード
  6. JSON APIを使用したサンプルReactクライアントアプリケーション
  7. Reactクライアントアプリケーションのソースコード、初期バージョン
  8. Reactクライアントアプリケーションのソースコード、最終バージョン



All Articles