
ãã°ã€ã³ããŠãã ãã
ããã¯ãå圢ã®React.jsã¢ããªã±ãŒã·ã§ã³ããŒãããéçºããããšã«é¢ããèšäºã®3çªç®ã®æåŸã®éšåã§ãã ããŒã1ãš2 ã
ãã®ããŒãã§ã¯ïŒ
- redux-dev-toolsãè¿œå ããŸã ã
- APIã«ãªã¯ãšã¹ããè¿œå ããŸãã
- æ¿èªãå®è£
ããŸãã
- ãµãŒããŒåŽã¬ã³ããªã³ã°ããã»ã¹ã§APIãªã¯ãšã¹ãã®å®è¡ãå®è£
ããŸãã
ããã¯ãéçºããã»ã¹ãç°¡çŽ åããéåžžã«äŸ¿å©ãªã©ã€ãã©ãªã§ãã ããã«ãããã°ããŒãã«ç¶æ
ã®å
容ãšãã®å€åããªã¢ã«ã¿ã€ã ã§ç¢ºèªã§ããŸãã ããã«ã redux-dev-toolsã䜿çšãããšãã°ããŒãã«ç¶æ
ã«å¯Ÿããææ°ã®å€æŽããããŒã«ããã¯ãã§ããŸããããã¯ããã¹ãããã³ãããã°äžã«äŸ¿å©ã§ãã ç§ãã¡ã«ãšã£ãŠãããã¯æå¿«ããè¿œå ããåŠç¿ããã»ã¹ãããã€ã³ã¿ã©ã¯ãã£ãã§éæã«ããŸãã
1.1ã å¿
èŠãªããã±ãŒãžãã€ã³ã¹ããŒã«ãã
npm i --save-dev redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
import React from 'react'; import { createDevTools } from 'redux-devtools'; import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; export default createDevTools( <DockMonitor toggleVisibilityKey='ctrl-h' changePositionKey='ctrl-q'> <LogMonitor /> </DockMonitor> );
import DevTools from './DevTools'; export default DevTools;
1.3ã ãããã rootReducer
2çªç®ã®éšåã§ã¯ãã«ãŒãã¬ãã¥ãŒãµãŒã®äœæãconfigureStoreã«é
眮ããŸããã ãããã¯åœŒã®è²¬ä»»ç¯å²ã§ã¯ãªããããå®å
šã«æ£ããããã§ã¯ãããŸããã å°ããªãã¡ã¯ã¿ãªã³ã°ãè¡ãããããredux / reducers / index.jsã«è»¢éããŸãããã
redux /ã¬ãã¥ãŒãµãŒ/ index.js
import { combineReducers } from 'redux'; import counterReducer from './counterReducer'; export default combineReducers({ counter: counterReducer });
redux-dev-toolsã®ããã¥ã¡ã³ãããã configureStoreã«å€æŽãå ããå¿
èŠãããããšãããããŸã ã éçºã«ã®ã¿redux-dev-toolsãå¿
èŠã§ããããšãæãåºããŠãã ããããããã£ãŠãåè¿°ã®æäœãç¹°ãè¿ããŸãã
- configureStore.jsã®ååãconfigureStore.prod.jsã«å€æŽããŸã ã
- configureStore.dev.jsãå®è£
ããŸã ã
- configureStore.jsãå®è£
ããŸã ãããã¯ãã·ã¹ãã ã©ã³ãã¹ã±ãŒãã«å¿ããŠã configureStore.prod.jsãŸãã¯configureStore.dev.jsã䜿çšããŸã ã
mv redux/configureStore.js redux/configureStore.prod.js
import { applyMiddleware, createStore } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; export default function (initialState = {}) { return createStore(rootReducer, initialState, applyMiddleware(thunk)); }
configureTool.dev.jsãDevToolsãšããããªããŒãã®ãµããŒãã§å®è£
ããŸã ã
import { applyMiddleware, createStore, compose } from 'redux'; import thunk from 'redux-thunk'; import DevTools from 'components/DevTools'; import rootReducer from './reducers'; export default function (initialState = {}) { const store = createStore(rootReducer, initialState, compose( applyMiddleware(thunk), DevTools.instrument() ) ); if (module.hot) { module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers').default) ); } return store; }
ConfigureStoreãšã³ããªãã€ã³ã
if (process.env.NODE_ENV === 'production') { module.exports = require('./configureStore.prod'); } else { module.exports = require('./configureStore.dev'); }
ãã¹ãŠæºåå®äºã§ãïŒ webpack-dev-serverãšnodemonãåèµ·åãããã©ãŠã¶ãéããŠãå³åŽã«ã°ããŒãã«ã¹ããŒãã®å
容ãåæ ããããã«ã衚瀺ãããŠããããšã確èªããŸãã ã«ãŠã³ã¿ãŒãå«ãããŒãžãéãã ReduxCounterãã¯ãªãã¯ããŸã ã åæã«ãã¯ãªãã¯ãããã³ã«ãã¢ã¯ã·ã§ã³ãreduxãã¥ãŒã«å
¥åãããã°ããŒãã«ç¶æ
ãã©ã®ããã«å€åããããããããŸãã Revertãã¯ãªãã¯ãããšãæåŸã®ã¢ã¯ã·ã§ã³ãåãæ¶ãããšãã§ãã Commitãã¯ãªãã¯ãããšããã¹ãŠã®ã¢ã¯ã·ã§ã³ãæ¿èªããã³ãã³ãã®çŸåšã®ãã¥ãŒãã¯ãªã¢ã§ããŸãã
æ³šïŒ redux-dev-toolsãè¿œå ããåŸãã³ã³ãœãŒã«ã«æ¬¡ã®ã¡ãã»ãŒãžã衚瀺ãããå ŽåããããŸãïŒ "Reactãã³ã³ãããŒå
ã®ããŒã¯ã¢ãããåå©çšããããšããŸãããããã§ãã¯ãµã ãç¡å¹ã§ãã..." ã ããã¯ãã¢ããªã±ãŒã·ã§ã³ã®ãµãŒããŒåŽãšã¯ã©ã€ã¢ã³ãåŽãç°ãªãã³ã³ãã³ããã¬ã³ããªã³ã°ããããšãæå³ããŸãã ããã¯éåžžã«æªãããšã§ãããã¢ããªã±ãŒã·ã§ã³ã§ã¯é¿ããå¿
èŠããããŸãã ãã ãããã®å Žåãç¯äººã¯redux-dev-toolsã§ãããçç£æ§ã§ã¯ãŸã 䜿çšããªããããäŸå€ãäœæããŠåé¡ã«é¢ããã¡ãã»ãŒãžãå·éã«ç¡èŠã§ããŸãã
æŽæ°ïŒ gialdeynããã³LerayneãŠãŒã¶ãŒã®ãããã§ã次ã®æ¹æ³ã§redux-dev-toolsã§SSRãä¿®æ£ã§ããŸã
src / server.js
<div id="react-view">${componentHTML}</div> +++ <div id="dev-tools"></div>
src / client.js
+++ import DevTools from './components/DevTools'; ... ReactDOM.render(component, document.getElementById('react-view')); +++ ReactDOM.render(<DevTools store={store} />, document.getElementById('dev-tools'));
2.æ°ããæ©èœãè¿œå ãã
次ã®ã·ããªãªãå®è£
ããŸã
- ãŠãŒã¶ãŒã¯ããªã¯ãšã¹ãæéããã¿ã³ãã¯ãªãã¯ããŸãã
- ããŠã³ããŒãã€ã³ãžã±ãŒã¿ã衚瀺ããŸãããã¿ã³ãéã¢ã¯ãã£ãã«ãªããäžèŠãªç¹°ãè¿ããªã¯ãšã¹ããåé¿ããŸãã
- ã¢ããªã±ãŒã·ã§ã³ã¯APIãªã¯ãšã¹ããäœæããŸãã
- ã¢ããªã±ãŒã·ã§ã³ã¯APIããå¿çãåä¿¡ããåä¿¡ããããŒã¿ãã°ããŒãã«ç¶æ
ã«ä¿åããŸãã
- ããŠã³ããŒãã€ã³ãžã±ãŒã¿ãæ¶ãããã¿ã³ãåã³ã¢ã¯ãã£ãã«ãªããŸãã ãŠãŒã¶ãŒãåä¿¡ããããŒã¿ã衚瀺ããŸãã
ããã¯ããªãèšå€§ãªäœæ¥ã§ãã åã
ã®éšåã«çŠç¹ãåœãŠãããã«ãæåã«ãã€ã³ã1ã2ããã³5ãå®è£
ãã3ããã³4ã«å¯ŸããŠã¹ã¿ããäœæããŸãã
2.1ã ã¢ã¯ã·ã§ã³ãè¿œå ãã
[Request Time]ãã¿ã³ãã¯ãªãã¯ããåŸã次ã®ããšãé ã«è¡ãå¿
èŠããããŸãã
- ããŒãã®å€ãfalseããtrueã«å€æŽããŸã ã
- ãªã¯ãšã¹ãããã;
- åçãåãåã£ããã ããŒãã®å€ãtrueããfalseã«æ»ããåä¿¡ããããŒã¿ãŸãã¯ãšã©ãŒã«é¢ããæ
å ±ãä¿åããŸãã
export const TIME_REQUEST_STARTED = 'TIME_REQUEST_STARTED'; export const TIME_REQUEST_FINISHED = 'TIME_REQUEST_FINISHED'; export const TIME_REQUEST_ERROR = 'TIME_REQUEST_ERROR'; function timeRequestStarted() { return { type: TIME_REQUEST_STARTED }; } function timeRequestFinished(time) { return { type: TIME_REQUEST_FINISHED, time }; } function timeRequestError(errors) { return { type: TIME_REQUEST_ERROR, errors }; } export function timeRequest() { return (dispatch) => { dispatch(timeRequestStarted()); return setTimeout(() => dispatch(timeRequestFinished(Date.now())), 1000);
ããã§ã¯ãåã¢ã¯ã·ã§ã³ãã°ããŒãã«ç¶æ
ãå€æŽããå°ããªé¢æ°ã®åœ¢åŒã§é
眮ã ã timeRequestã¯ã·ããªãªãå®å
šã«èª¬æãããããã®é¢æ°ã®çµã¿åããã§ãã ã³ã³ããŒãã³ãããåŒã³åºãã®ã¯ç§ãã¡ã§ãã
2.2ã æéã®çµéãšãšãã«ããŒãžã³ãŒããæŽæ°ããŸã
TimePageããŒãžã®èªã¿èŸŒã¿ã€ã³ãžã±ãŒã¿ãŒããµããŒãããreact-bootstrap-button-loaderãã¿ã³ãè¿œå ããã¯ãªãã¯ã§timeRequesté¢æ°ãåŒã³åºãããã«æããŸãã
npm i --save react-bootstrap-button-loader
ãã¿ã³ãšã¯ãªãã¯ãã³ãã©ãŒãè¿œå ãã
src / components / TimePage / TimePage.jsx
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import PageHeader from 'react-bootstrap/lib/PageHeader'; import Button from 'react-bootstrap-button-loader'; import { timeRequest } from 'redux/actions/timeActions'; const propTypes = { dispatch: PropTypes.func.isRequired }; class TimePage extends Component { constructor() { super(); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.dispatch(timeRequest()); } render() { return ( <div> <PageHeader>Timestamp</PageHeader> <Button onClick={this.handleClick}>!</Button> </div> ); } } TimePage.propTypes = propTypes; export default connect()(TimePage);
ãã¿ã³ããã£ã¹ãããæ©èœã«ã¢ã¯ã»ã¹ããŠã°ããŒãã«ç¶æ
ãå€æŽã§ããããã«ã react-reduxããã®æ¥ç¶ã䜿çšããå¿
èŠããã£ãããšã«æ³šæããŠãã ããã
äœæ¥ã®çµæã確èªããŸãããã©ãŠã¶ã§ãæéãããŒãžãéããããªã¯ãšã¹ãããã¿ã³ãã¯ãªãã¯ããŸãã ã€ã³ã¿ãŒãã§ã€ã¹ã¯ãŸã äœãããŠããŸãããã redux-dev-toolsã§ãæè¿å®è£
ããã¢ã¯ã·ã§ã³ãã©ã®ããã«èµ·åãããããããããŸãã
ã€ã³ã¿ãŒãã§ãŒã¹ã掻æ§åããæãæ¥ãŸããã ã°ããŒãã«ç¶æ
ãæŽæ°ããããžãã¯ãå®è£
ããããšããå§ããŸããã
2.3ã æžéæ©ãå®çŸ
src / redux / reducers / timeReducer.js
import { TIME_REQUEST_STARTED, TIME_REQUEST_FINISHED, TIME_REQUEST_ERROR } from 'redux/actions/timeActions'; const initialState = { time: null, errors: null, loading: false }; export default function (state = initialState, action) { switch (action.type) { case TIME_REQUEST_STARTED: return Object.assign({}, state, { loading: true, errors: null }); case TIME_REQUEST_FINISHED: return { loading: false, errors: null, time: action.time }; case TIME_REQUEST_ERROR: return Object.assign({}, state, { loading: false, errors: action.errors }); default: return state; } }
å¿ããŠã¯ãªããªãéèŠãªãã€ã³ãïŒ reduxä»æ§ã«ããã°ãæž¡ãããç¶æ
ãå€æŽããæš©å©ã¯ãªãããããè¿ãããæ°ãããªããžã§ã¯ããè¿ãå¿
èŠããããŸãã æ°ãããªããžã§ã¯ããäœæããã«ã¯ã Object.assignã䜿çšããŸã ãããã¯ãå
ã®ãªããžã§ã¯ããååŸã ãå¿
èŠãªå€æŽãé©çšããŸãã
ããŠãæ°ããã¬ãã¥ãŒãµãŒãã«ãŒãã¬ãã¥ãŒãµãŒã«è¿œå ããŸãã
src / redux / reducers / index.js
+++ import timeReducer from './timeReducer'; export default combineReducers({ counter: counterReducer, +++ time: timeReducer });
ãã©ãŠã¶ãå床éãã以åã«redux-dev-toolsãã¥ãŒãã¯ãªã¢ããŠãããããªã¯ãšã¹ãããã¿ã³ãã¯ãªãã¯ããŸãã ã€ã³ã¿ãŒãã§ã€ã¹ã¯ãŸã æŽæ°ãããŠããŸãããããªãã¥ãŒãµãŒã®ã³ãŒãã«åŸã£ãŠã¢ã¯ã·ã§ã³ãã°ããŒãã«ç¶æ
ãå€æŽããŸããã€ãŸãããå
éšãã§ãã¹ãŠã®ããžãã¯ãæ£åžžã«æ©èœããããšãæå³ããŸãã ãã€ã³ãã¯å°ãã-ã€ã³ã¿ãŒãã§ã€ã¹ãã埩掻ããããã
2.4ã ããŒãžã³ãŒããæéãã®æŽæ°
src / components / TimePage / TimePage.jsx
const propTypes = { dispatch: PropTypes.func.isRequired, +++ loading: PropTypes.bool.isRequired, +++ time: PropTypes.any }; class TimePage extends Component { ... render() { +++ const { loading, time } = this.props; ... --- <Button onClick={this.handleClick}>!</Button> +++ <Button loading={loading} onClick={this.handleClick}>!</Button> +++ {time && <div>Time: {time}</div>} </div> ); } } +++ function mapStateToProps(state) { +++ const { loading, time } = state.time; +++ return { loading, time }; +++ } --- export default connect()(TimePage); +++ export default connect(mapStateToProps)(TimePage);
ãã©ãŠã¶ã«ç§»åããããªã¯ãšã¹ãããã¿ã³ãã¯ãªãã¯ããŠããã¹ãŠãã·ããªãªã«åŸã£ãŠæ©èœããããšã確èªããŸãã
ãã©ã°ãå®éã®ããã¯ãšã³ãã«äº€æããæãæ¥ãŸãã ã
3. ããã¯ãšã³ããšèªèšŒãšã®çžäºäœçšãè¿œå ãã
泚ïŒãã®äŸã§ã¯ãç§ãRailsã§éçºããéåžžã«ã·ã³ãã«ãªããã¯ãšã³ãã䜿çšããŠããŸã ã https://redux-oauth-backend.herokuapp.comã§å
¥æã§ãããŠãŒã¶ãŒããã°ã€ã³ããŠããå Žåã¯ãµãŒããŒã®ã¿ã€ã ã¹ã¿ã³ããè¿ããããã§ãªãå Žåã¯401ãšã©ãŒãè¿ãmethod / test / testã 1ã€ã ãå«ãŸããŠããŸãã ããã¯ãšã³ãã®ãœãŒã¹ã³ãŒãã¯ã https ïŒ //github.com/yury-dymov/redux-oauth-backend-demoã«ãããŸã ã ããã§ã gem deviseãæ¿èªã«äœ¿çšããŸããããã¯ã railsãšgem devise_token_authã®åæ§ã®åé¡ã解決ããããã®äºå®äžã®æšæºã§ããã Bearer TokenããŒã¹ã®èªèšŒ èæ¡æ¿èªã¡ã«ããºã ãè¿œå ããŸãã çŸåšããã®ã¡ã«ããºã ã¯ã»ãã¥ã¢ãªAPIã®éçºã§æããã䜿çšãããŠããŸãã
ã¯ã©ã€ã¢ã³ãåŽããã¯ãããã¹ãããšããããããããŸãã
- åã®èšäºãããå°ãåéãæ®ã£ãŠããŸãã ãµãŒããŒåŽã¬ã³ããªã³ã°åŸã®ã°ããŒãã«ç¶æ
㯠ãã¯ã©ã€ã¢ã³ãã«ãã£ãŠéä¿¡ãŸãã¯äœ¿çšãããŸããã ä»ããä¿®æ£ããŸãã
- ããã³ããšã³ãèªèšŒãæ
åœããredux-oauthã©ã€ãã©ãªããããžã§ã¯ãã«è¿œå ããå圢ã¹ã¯ãªããçšã«æ§æããŸãã
- ã¹ã¿ãããå®éã«APIãªã¯ãšã¹ããå®è¡ããã³ãŒãã«çœ®ãæããŸãã
- ããã°ã€ã³ããã¿ã³ãšããã°ã¢ãŠãããã¿ã³ãè¿œå ããŸãã
3.1ã ã°ããŒãã«ç¶æ
ãæž¡ããŸã
ã¡ã«ããºã ã¯éåžžã«ç°¡åã§ãã
- ãµãŒããŒããã¹ãŠã®äœæ¥ãå®äºããã¯ã©ã€ã¢ã³ãã®ã³ã³ãã³ããçæããåŸã getStateé¢æ°ãåŒã³åºããŠãçŸåšã®ã°ããŒãã«ç¶æ
ãè¿ããŸãã 次ã«ãã³ã³ãã³ããšã°ããŒãã«ç¶æ
ãHTMLãã³ãã¬ãŒãã«è»¢éããçµæã®ããŒãžãã¯ã©ã€ã¢ã³ãã«æž¡ããŸãã
- ã¯ã©ã€ã¢ã³ãåŽã®JavaScriptã¯ãã°ããŒãã«ãŠã£ã³ããŠãªããžã§ã¯ãããã°ããŒãã«ç¶æ
ãçŽæ¥èªã¿åãã initialStateãšããŠconfigureStoreã«æž¡ããŸã ã
src / server.js
+++ const state = store.getState(); --- return res.end(renderHTML(componentHTML)); +++ return res.end(renderHTML(componentHTML, state)); ... --- function renderHTML(componentHTML, initialState) { +++ function renderHTML(componentHTML, initialState) { <link rel="stylesheet" href="${assetUrl}/public/assets/styles.css"> +++ <script type="application/javascript"> +++ window.REDUX_INITIAL_STATE = ${JSON.stringify(initialState)}; +++ </script> </head>
src / client.js
+++ const initialState = window.REDUX_INITIAL_STATE || {}; --- const store = configureStore(); +++ const store = configureStore(initialState);
ã³ãŒããããããããã«ãã°ããŒãã«ç¶æ
ãå€æ°REDUX_INITIAL_STATEã«æž¡ããŸãã
3.2ã æ¿èªãè¿œå
redux-oauthãã€ã³ã¹ããŒã«ãã
泚ïŒååã¹ã¯ãªããã«ã¯redux-oauthã䜿çšããŸãããã¯ã©ã€ã¢ã³ãåŽã®ã¿ããµããŒãããŸã ã ããŸããŸãªã±ãŒã¹ãšãã¢ã®æ§æäŸã¯ãã©ã€ãã©ãªã®Webãµã€ãã§èŠã€ããããšãã§ããŸãã
泚2ïŒ redux-oauthã¯ã ããŒã«ã«ã¹ãã¬ãŒãžã¡ã«ããºã ãå圢ã®ã·ããªãªã«é©ããŠããªããããèªèšŒã«Cookieã䜿çšããŸãã
npm i --save redux-oauth cookie-parser
Expressã®cookieParserãã©ã°ã€ã³ãæå¹ã«ãã
src / server.js
+++ import cookieParser from 'cookie-parser'; const app = express(); +++ app.use(cookieParser());
ã¢ããªã±ãŒã·ã§ã³ã®ãµãŒããŒåŽã®redux-oauthã®æ§æ
src / server.js
+++ import { getHeaders, initialize } from 'redux-oauth'; app.use((req, res) => { const store = configureStore(); +++ store.dispatch(initialize({ +++ backend: { +++ apiUrl: 'https://redux-oauth-backend.herokuapp.com', +++ authProviderPaths: { +++ github: '/auth/github' +++ }, +++ signOutPath: null +++ }, +++ currentLocation: req.url, +++ cookies: req.cookies })).then(() => match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { ... const state = store.getState(); +++ res.cookie('authHeaders', JSON.stringify(getHeaders(state)), { maxAge: Date.now() + 14 * 24 * 3600 * 1000 }); return res.end(renderHTML(componentHTML, state)); }));
ããã§å€ãã®èå³æ·±ãããšãèµ·ãããŸãïŒ
- redux-oauthããinitializeé¢æ°ãåŒã³åºãå¿
èŠããããŸãããã®é¢æ°ã«ã¯ãçŸåšã®URL ã Cookie ãããã³æ§æïŒAPIã¢ãã¬ã¹ãšäœ¿çšãããOAuthãããã€ããŒïŒãæž¡ããŸã ã
- 転éãããCookieã«èªèšŒããŒã¯ã³ãèŠã€ãã£ãå Žåãã©ã€ãã©ãªã¯ããã¯ãšã³ãã§ãã®æå¹æ§ã確èªããæåããå ŽåããŠãŒã¶ãŒæ
å ±ãã°ããŒãã«ç¶æ
ã§ä¿åããŸãã åæåãå®äºããåŸã«ã®ã¿ãããã«ã¢ããªã±ãŒã·ã§ã³ã³ãŒããå®è¡ãããããšã«æ³šæããŠãã ããã
- HTMLãã¯ã©ã€ã¢ã³ãã«éä¿¡ããåã«ã res.cookieã¡ãœããã䜿çšããŸãã ãã®ã¡ãœããã¯ã SetCookieããããŒãHTTPå¿çã«è¿œå ããå¿
èŠãããããšãæ瀺çã«éç¥ããŸãããã®ããããŒã§ã¯ãæŽæ°ãããèªèšŒããŒã¯ã³ã転éããå¿
èŠããããŸãã ããã¯éåžžã«éèŠãªæé ã§ããæ°ããèªèšŒããŒã¯ã³ã¯ããµãŒããŒããå¿çãåä¿¡ãããšããã«ãã©ãŠã¶ãŒã®Cookieã«ä¿åãããŸãã ãããã£ãŠãã¯ã©ã€ã¢ã³ãåŽã®JavaScriptãããŠã³ããŒããåæåããŸãã¯å€±æããå Žåã§ããæ¿èªãäžæããªãããšãä¿èšŒããŸãã
ããã¥ã¡ã³ãã«ãããšã redux-oauthã¬ãã¥ãŒãµãŒãã«ãŒãã¬ãã¥ãŒãµãŒã«è¿œå ããå¿
èŠããããŸãã
src / redux / reducers / index.js
+++ import { authStateReducer } from 'redux-oauth'; export default combineReducers({ +++ auth: authStateReducer,
3.3ã timeActions.jsã®ã¹ã¿ãã眮ãæãã
src / redux / actions / timeActions.js
import { fetch, parseResponse } from 'redux-oauth'; export function timeRequest() { return (dispatch) => { dispatch(timeRequestStarted()); --- return setTimeout(() => dispatch(timeRequestFinished(Date.now())), 1000);
redux-oauthããã®ãã§ããé¢æ°ã¯ã isomorphic-fetchããã±ãŒãžããã®æ¡åŒµé¢æ°ã§ãã ããã¥ã¡ã³ãã«ãããšã ãã£ã¹ããããä»ããŠåŒã³åºãå¿
èŠããããŸãããã®å Žåãã°ããŒãã«ã¹ããŒãã«ã¢ã¯ã»ã¹ããŠãããããèªèšŒããŒã¯ã³ãèªã¿åãããªã¯ãšã¹ããšãšãã«éä¿¡ã§ããããã§ãã APIãªã¯ãšã¹ãã§ã¯ãªãã ãã§ããé¢æ°ãä»»æã®HTTPãªã¯ãšã¹ãã«äœ¿çšãããå ŽåãèªèšŒããŒã¯ã³ã¯äœ¿çšãããŸãããã€ãŸããå®è¡ã¢ã«ãŽãªãºã ã¯å圢ãã§ããå®è¡ã¢ã«ãŽãªãºã ãš100ïŒ
åäžã«ãªããŸãã
æ³šïŒ isomorphic-fetchã¯ããã©ãŠã¶ãŒç°å¢ãšNodeç°å¢ã®äž¡æ¹ããHTTPèŠæ±ãäœæã§ããã©ã€ãã©ãªã§ãã
ãã©ãŠã¶ãéãããæéãããŒãžã®ããªã¯ãšã¹ãããã¿ã³ãå床ã¯ãªãã¯ããŸãã ããŠãçŸåšã®ã¿ã€ã ã¹ã¿ã³ãã¯è¡šç€ºãããªããªããŸãããã redux-dev-toolsã« 401ãšã©ãŒã«é¢ããæ
å ±ã衚瀺ãããŸããã åœç¶ã®ããšãªãããAPIãäœããè¿ãã«ã¯ãæ¿èªãå¿
èŠã§ãã
3.4ã [ãã°ã€ã³]ãã¿ã³ãš[ãã°ã¢ãŠã]ãã¿ã³ãè¿œå ããŸãã
ååãšããŠãæ¿èªããããŠãŒã¶ãŒã¯ã²ã¹ããããã·ã¹ãã ãæäœããæ©äŒãå€ããªããŸããããã§ãªãå Žåãæ¿èªã®ãã€ã³ãã¯äœã§ããïŒ
æè¡çãªèŠ³ç¹ããèŠããšãããã¯ããŠãŒã¶ãŒãã·ã¹ãã ã«ãã°ã€ã³ããŠãããã©ããã«å¿ããŠãå€ãã®ã³ã³ããŒãã³ãã®å€èŠ³ãšåäœãç°ãªãããšãæå³ããŸãã
ç§ã¯DRYã®ååãç±å¿ã«æ¯æããŠããŸãïŒç¹°ãè¿ããªãã§ãã ããïŒã®ã§ãå°ããªãã«ããŒãäœæããŸãã
src / redux / models / user.js
export function isUserSignedIn(state) { return state.auth.getIn(['user', 'isSignedIn']); }
ããã°ã€ã³ããã¿ã³ãå®è£
ããŸã
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { oAuthSignIn } from 'redux-oauth'; import Button from 'react-bootstrap-button-loader'; import { isUserSignedIn } from 'redux/models/user'; const propTypes = { dispatch: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, provider: PropTypes.string.isRequired, userSignedIn: PropTypes.bool.isRequired }; class OAuthButton extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { const { dispatch, provider } = this.props; dispatch(oAuthSignIn({ provider })); } render() { const { loading, provider, userSignedIn } = this.props; if (userSignedIn) { return null; } return <Button loading={loading} onClick={this.handleClick}>{provider}</Button>; } } OAuthButton.propTypes = propTypes; function mapStateToProps(state, ownProps) { const loading = state.auth.getIn(['oAuthSignIn', ownProps.provider, 'loading']) || false; return { userSignedIn: isUserSignedIn(state), loading }; } export default connect(mapStateToProps)(OAuthButton);
ãã®ãã¿ã³ã¯ããŠãŒã¶ãŒããŸã ãã°ã€ã³ããŠããªãå Žåã«ã®ã¿è¡šç€ºãããŸãã
ããã°ã¢ãŠãããã¿ã³ãå®è£
ããŸã
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { signOut } from 'redux-oauth'; import Button from 'react-bootstrap-button-loader'; import { isUserSignedIn } from 'redux/models/user'; const propTypes = { dispatch: PropTypes.func.isRequired, userSignedIn: PropTypes.bool.isRequired }; class SignOutButton extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { const { dispatch } = this.props; dispatch(signOut()); } render() { if (!this.props.userSignedIn) { return null; } return <Button onClick={this.handleClick}></Button>; } } SignOutButton.propTypes = propTypes; function mapStateToProps(state) { return { userSignedIn: isUserSignedIn(state) }; } export default connect(mapStateToProps)(SignOutButton);
ãã®ãã¿ã³ã¯ããŠãŒã¶ãŒãæ¢ã«ãã°ã€ã³ããŠããå Žåã«ã®ã¿è¡šç€ºãããŸãã
import OAuthButton from './OAuthButton'; import SignOutButton from './SignOutButton'; export { OAuthButton, SignOutButton };
HelloWorldPageããŒãžã«æ¿èªãè¿œå ããŸãã
src / components / HelloWorldPage / HelloWorldPage.jsx
+++ import { OAuthButton, SignOutButton } from 'components/AuthButtons'; +++ <h2></h2> +++ <OAuthButton provider='github' /> +++ <SignOutButton />
ç§ãã¡ã®ä»äºã®çµæã楜ããæã§ãã [ãã°ã€ã³]ãã¿ã³ãã¯ãªãã¯ããGitHubã¢ã«ãŠã³ãã䜿çšããŠèªèšŒãè¡ããŸã...ã·ã¹ãã ã«ããŸãïŒ [ãµã€ã³ã€ã³]ãã¿ã³ã¯æ¶ããŸãããã[ãµã€ã³ã¢ãŠã]ãã¿ã³ã¯è¡šç€ºãããŸããã ã»ãã·ã§ã³ãä¿åãããŠããããšã確èªããŸãããã®ããã«ããŒãžããªããŒãããŸãã [ãã°ã¢ãŠã]ãã¿ã³ã¯æ¶ããŸããã§ãããredux-dev-toolsã§ã¯ããŠãŒã¶ãŒã«é¢ããæ
å ±ãèŠã€ããããšãã§ããŸãã ãããïŒ ãããŸã§ã®ãšããããã¹ãŠãæ©èœããŠããŸãã [æé]ããŒãžã«ç§»åãã[ãªã¯ãšã¹ã]ãã¿ã³ãã¯ãªãã¯ããŠã ã¿ã€ã ã¹ã¿ã³ãã衚瀺ãããŠããããšã確èªããŸããããã¯ãããŒã¿ãè¿ãããµãŒããŒã§ãã
ããã¯çµäºããå¯èœæ§ããããŸãããã¢ããªã±ãŒã·ã§ã³ããç 磚ãããå¿
èŠããããŸãã
4.ã¢ããªã±ãŒã·ã§ã³ã®ãç åã
ããã§ãæ¹åã§ãããã®ïŒ
- [æé]ããŒãžãžã®ãªã³ã¯ã¯ãæ¿èªããããŠãŒã¶ãŒã«ã®ã¿è¡šç€ºããå¿
èŠããããŸãã
- ãŠãŒã¶ãŒããã©ãŠã¶ãŒã§ä¿è·ãããããŒãžã®ã¢ãã¬ã¹ãå
¥åããå ŽåããããèªèšŒããŒãžïŒãã®å Žåã¯HelloWorldPage ïŒã«ãªãã€ã¬ã¯ãããŸãã
- ãŠãŒã¶ãŒããã°ã¢ãŠãããå Žåãã°ããŒãã«ç¶æ
ãããŠãŒã¶ãŒã®ããŒã¿ãåé€ããå¿
èŠããããŸãã
4.1ã ã¢ã¯ã»ã¹ã§ããªãããŒãžãžã®ãªã³ã¯ãåé€ãã
src / components / App / App.jsx
+++ import { connect } from 'react-redux'; +++ import { isUserSignedIn } from 'redux/models/user'; const propTypes = { +++ userSignedIn: PropTypes.bool.isRequired, ... }; ... +++ {this.props.userSignedIn && ( <LinkContainer to='/time'> <NavItem></NavItem> </LinkContainer> +++ )} ... +++ function mapStateToProps(state) { +++ return { userSignedIn: isUserSignedIn(state) }; +++ } --- export default App; +++ export default connect(mapStateToProps)(App);
ãã©ãŠã¶ãŒãéããŠããæéãããŒãžãžã®ãªã³ã¯ããŸã 䜿çšå¯èœã§ããããšã確èªãã HelloWorldPageããŒãžã«ç§»åããŠããçµäºããã¿ã³ãã¯ãªãã¯ãããšããªã³ã¯ããªããªããŸãã
4.2ã å®å
šãªããŒãžãžã®ã¢ã¯ã»ã¹ãå¶éãã
èŠããŠããããã«ã react-routerã©ã€ãã©ãªãŒã¯ã URLãšã¬ã³ããªã³ã°ããå¿
èŠãããããŒãžãšã®å¯Ÿå¿ãæ
åœãããã¹æ§æã¯routes.jsxãã¡ã€ã«ã«ãããŸãã 次ã®ããžãã¯ãè¿œå ããå¿
èŠããããŸãããŠãŒã¶ãŒãæ¿èªãããŠããããå®å
šãªããŒãžãèŠæ±ããå ŽåããããHelloWorldPageã«ãªãã€ã¬ã¯ãããŸãã
ãŠãŒã¶ãŒã«é¢ããæ
å ±ãååŸããã«ã¯ãã°ããŒãã«ã¹ããŒããªããžããªãžã®ãªã³ã¯ãroutes.jsxã«æž¡ãå¿
èŠããããŸãã
src / server.js
--- .then(() => match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { +++ .then(() => match({ routes: routes(store), location: req.url }, (error, redirectLocation, renderProps) => {
src / client.js
<Router history={browserHistory}> --- {routes} +++ {routes(store)} </Router>
src / routes.jsx
import { isUserSignedIn } from 'redux/models/user'; function requireAuth(nextState, transition, cb) { setTimeout(() => { if (!isUserSignedIn(store.getState())) { transition('/'); } cb(); }, 0); } let store; export default function routes(storeRef) { store = storeRef; return ( <Route component={App} path='/'> <IndexRoute component={HelloWorldPage} /> <Route component={CounterPage} path='counters' /> <Route component={TimePage} path='time' onEnter={requireAuth} /> </Route> ); }
ãã¹ãïŒ
- ãã°ã€ã³ããŠããããšã確èªããŠãã ããã
- ãã©ãŠã¶ã®ã¢ãã¬ã¹ããŒã«httpïŒ// localhostïŒ3001 / timeãšå
¥åãã[Enter]ãæŒããšãããŒãž[Time]ã衚瀺ãããŸãã
- ã·ã¹ãã ãããã°ã¢ãŠãããŸãã
- http://localhost:3001/time "Enter" â "HelloWorldPage" â !
: requireAuth setTimeout , . , .
4.3.
src/redux/reducers/timeReducer.js
+++ import { SIGN_OUT } from 'redux-oauth'; +++ case SIGN_OUT: +++ return initialState; default: return state;
action SIGN_OUT , timeReducer initialState , . , .
5. : Server-Side API Requests
redux-oauth Server Side API requests , API . :
- API, ;
- - latency ;
- JavaScript , .
: , , API . redux-oauth .
Proof of Concept .
API
src/server.js
+++ import { timeRequest } from './redux/actions/timeActions'; ... return store.dispatch(initialize({ backend: { apiUrl: 'https://redux-oauth-backend.herokuapp.com', authProviderPaths: { github: '/auth/github' }, signOutPath: null }, cookies: req.cookies, currentLocation: req.url, })) +++ .then(() => store.dispatch(timeRequest())) .then(() => match({ routes: routes(store), location: req.url }, (error, redirectLocation, renderProps) => {
, initialize redux-oauth backend , , timeRequest . .
, , "" F5. timestamp , "" . Dev Tools , Network , , API . , .
: API , .
src/redux/actions/timeActions.js
--- return (dispatch) => { +++ return (dispatch, getState) => { +++ if (!isUserSignedIn(getState())) { +++ return Promise.resolve(); +++ }
, getState , . , .
6.
- React.js . , !
, .
github â https://github.com/yury-dymov/habr-app/tree/v3
Ps , , . äºåã«æè¬ããŸãïŒ