æè¿ã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ã©ã€ãã©ãªãŒãéçºããŸãããããã¯æ¬¡ã®ããšãå®è¡ã§ããŸãã
- ããŒãž
data
ãå®è£
ããŠincluded
data
ãæ£èŠåããŸãã - ãªããžã§ã¯ãã®ã³ã¬ã¯ã·ã§ã³ãé
åãã
id =>
圢åŒã®ãããã«å€æãid =>
ã - JSON APIããã¥ã¡ã³ãã®å
ã®æ§é ãç¹å¥ãª
meta
ãªããžã§ã¯ãã«ä¿åããŸãã - 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
ãããŠãããªãã¯æ¬¡ã®ããã«ãªããŸãïŒ
- ReactãšReactDOM
- Reduxããã³Redux DevTools
- Webpack
- ãšã¹ãªã³ã
- ããã«
- Webã¢ããªã±ãŒã·ã§ã³ã®ãšã³ããªãã€ã³ãã2ã€ã®ã³ã³ããŒãã³ããã«ã¹ã¿ãã€ãºãããã¢ã»ã³ããªãåäœããeslintæ§æãããã³reduxã¹ãã¢ã®åæå
- ã¢ããªã±ãŒã·ã§ã³ã§äœ¿çšããããã¹ãŠã®ã³ã³ããŒãã³ãã®ã¹ã¿ã€ã«ã
ãããã¯ãã¹ãŠæ§æãããŠããããã®ãŸãŸäœ¿çšã§ããŸãã
äŸãå®è¡ããã«ã¯ãã³ã³ãœãŒã«ã«å
¥åããŸã
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ã䜿çšããŠããŒã¿ãå€æãããã§ãŒã³ã®ããã«äžã«è»¢éããŸãã
泚ïŒãšã©ãŒãã³ãã©ãå°ãçµäºãããšããã®ã³ãŒãã¯å®çšŒåç°å¢ã§ãæ©èœããŸãã
ã¹ãã¢æ§æã«ããã«ãŠã§ã¢ãè¿œå ããŸãã
... +++ 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'));
ãã¹ãŠã®ãªã³ã¯ã¯ãé
延èªã¿èŸŒã¿ããµããŒãããJavaScriptããããã£ã«å€ãããŸããã€ãŸããåãªããžã§ã¯ãã¯å¿
èŠãªå Žåã«ã®ã¿èªã¿èŸŒãŸããŸãã
const post = build(state.data, 'post', '1');
æ°ãã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" }
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ã©ã€ãã©ãªãæè¿ç»å ŽããŸããã å€ããã¯éåžžã«ã·ã³ãã«ã«æãããããããŸããããå®éããã®ãããªå®è£
ã«çæããåã«ãç§ã¯å¹ŽéãéããŠå€ãã®ç°ãªã£ãæçœã§ãªãçæãèžãããšãã§ããŸããããããã£ãŠããããã®ã·ã³ãã«ã§äŸ¿å©ãªããŒã«ã¯åœ¹ã«ç«ã€ãšç¢ºä¿¡ããŠããŸãã³ãã¥ããã£ãšå€ãã®æéãç¯çŽã§ããŸãã
ãã£ã¹ã«ãã·ã§ã³ã«åå ããŠããã ãããŸããããã®ããŒã«ã®éçºãæäŒã£ãŠãã ããã
åç
§è³æ
- JSON APIä»æ§
- ãªããžããªjson-api-normalizer
- Reduxãªããžã§ã¯ããªããžããª
- Phoenixãã¬ãŒã ã¯ãŒã¯ã«å®è£
ãããJSON APIããŒã¹ã®ãµã³ãã«WebãµãŒãã¹
- JSON API WebãµãŒãã¹ã®ãµã³ãã«ãœãŒã¹ã³ãŒã
- JSON APIã䜿çšãããµã³ãã«Reactã¯ã©ã€ã¢ã³ãã¢ããªã±ãŒã·ã§ã³
- Reactã¯ã©ã€ã¢ã³ãã¢ããªã±ãŒã·ã§ã³ã®ãœãŒã¹ã³ãŒããåæããŒãžã§ã³
- Reactã¯ã©ã€ã¢ã³ãã¢ããªã±ãŒã·ã§ã³ã®ãœãŒã¹ã³ãŒããæçµããŒãžã§ã³