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ず比范した堎合の「おしゃべり」ですが、それほど悪いのでしょうか


皮類圧瞮前バむト圧瞮埌バむト
䌝統的なJSON264170
JSON API771293

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クラむアントアプリケヌションの゜ヌスコヌド、最終バヌゞョン


Source: https://habr.com/ru/post/J318958/


All Articles