ps: 最近在学react和redux。用了几天时间看了下,个人觉得react本身比较易懂,但是涉及到的各种中间件和api让人脑阔疼(正好前段时间用vue+koa写了个简易的博客,对中间件还算理解)。在看到redux的时候,被这个real world的栗子难住了(个人水平太low)还2天过年,心思也不在这了。本来想放弃了,后来想想年后就辞职找工作了(心芳芳)新的风暴已经出现,怎么能够停滞不前。硬着头皮看下代码吧!
栗子目录是酱紫的....,咱们剥洋葱(衣服)一样,慢慢深入。

根目录下的index.js ( Root模板,store数据和逻辑 )
import React from 'react' import { render } from 'react-dom' import { BrowserRouter as Router } from 'react-router-dom' import Root from './containers/Root' import configureStore from './store/configureStore' const store = configureStore(); render( <Router> <Root store={store} /> // 由顶层传入store,谨记redux三大原则(单一store,store改变必须由dispatch来通知,reducer最好是pure函数
) </Router>, document.getElementById('root') );
/containers/Root ( 就2行代码,根据运行环境切换输出 )
if (process.env.NODE_ENV === 'production') { module.exports = require('./Root.prod') } else { module.exports = require('./Root.dev') }
/containers/Root.dev.js
import React from 'react' import PropTypes from 'prop-types' // props类型检查 import { Provider } from 'react-redux' // 包装函数,用于将redux和react链接起来 import DevTools from './DevTools' // 可视化调试工具,用法简单 import { Route } from 'react-router-dom' // 路由 import App from './App' import UserPage from './UserPage' import RepoPage from './RepoPage' const Root = ({ store }) => ( <Provider store={store}> <div> <Route path="/" component={App} /> <Route path="/:login/:name" component={RepoPage} /> <Route path="/:login" component={UserPage} /> <DevTools /> </div> </Provider> ) Root.propTypes = { store: PropTypes.object.isRequired, } export default Root
/containers/APP.js ( 顶层逻辑,react是单向数据流,由高流向低( 父->子->孙 ) )
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import Explore from '../components/Explore'
import { resetErrorMessage } from '../actions'
class App extends Component {
static propTypes = {
// Injected by React Redux
errorMessage: PropTypes.string,
resetErrorMessage: PropTypes.func.isRequired,
inputValue: PropTypes.string.isRequired,
// Injected by React Router
children: PropTypes.node // 由路由注入?怎么个注入法? 貌似是<tmp><h1>hello wrold</h1></tmp>
};
handleDismissClick = e => {
this.props.resetErrorMessage();
e.preventDefault()
};
handleChange = nextValue => {
this.props.history.push(`/${nextValue}`)
};
renderErrorMessage() {
const { errorMessage } = this.props;
if (!errorMessage) {
return null
}
return (
<p style={{ backgroundColor: '#e99', padding: 10 }}>
<b>{errorMessage}</b>
{' '}
<button onClick={this.handleDismissClick}>
Dismiss
</button>
</p>
)
}
render() {
const { children, inputValue } = this.props;
return (
<div>
<Explore value={inputValue}
onChange={this.handleChange} />
<hr />
{this.renderErrorMessage()}
{children}
</div>
)
}
}
const mapStateToProps = (state, ownProps) => ({ // 订阅Store,每当state变化时就会重新渲染。第一个参数是Store,第二个参数是当前组件的props对象,props改变也会重新渲染
errorMessage: state.errorMessage,
inputValue: ownProps.location.pathname.substring(1) // 必须要ownProps.location.pathname才能拿到path吗,反正path变化会触发渲染,类似Vue的watch router
});
export default withRouter(connect(mapStateToProps, { // withRouter可以包装任何自定义组件,无需一级级传递react-router属性,就可以拿到需要的路由信息
resetErrorMessage
})(App)
/components/Explore.js ( 上面的App.js引入了2个模块,actions和Explore,我们先看下Explore )
/* eslint-disable no-undef */ import React, { Component } from 'react' import PropTypes from 'prop-types' const GITHUB_REPO = 'https://github.com/reactjs/redux'; export default class Explore extends Component { static propTypes = { value: PropTypes.string.isRequired, // 这里有2个props,都是App传过来的 onChange: PropTypes.func.isRequired }; componentWillReceiveProps(nextProps) { // 生命周期钩子事件,当组件接收props时触发 if (nextProps.value !== this.props.value) { this.setInputValue(nextProps.value) } } getInputValue = () => { return this.input.value }; setInputValue = (val) => { // Generally mutating DOM is a bad idea in React components, // but doing this for a single uncontrolled field is less fuss // than making it controlled and maintaining a state for it. this.input.value = val }; handleKeyUp = (e) => { if (e.keyCode === 13) { this.handleGoClick() } }; handleGoClick = () => { this.props.onChange(this.getInputValue()) }; render() { return ( <div> <p>Type a username or repo full name and hit 'Go':</p> // 可以看到这个组件就干了1件事,调用父组件的onChange事件,而这个onChange事件是用来改变路由的 <input size="45" ref={(input) => this.input = input} defaultValue={this.props.value} onKeyUp={this.handleKeyUp} /> <button onClick={this.handleGoClick}> Go! </button> <p> Code on <a href={GITHUB_REPO} target="_blank" rel="noopener noreferrer">Github</a>. </p> <p> Move the DevTools with Ctrl+W or hide them with Ctrl+H. </p> </div> ) } }
App.js是按需引入的actions的resetErrorMessage函数
export const resetErrorMessage = () => ({
type: RESET_ERROR_MESSAGE
});
// 就是dispatch一个RESET_ERROR_MESSAGE,我们来看看这个action的对应的reducer是怎么样的
const errorMessage = (state = null, action) => {
const { type, error } = action;
if (type === ActionTypes.RESET_ERROR_MESSAGE) {
return null
} else if (error) {
return error
}
return state
};
// 这个简单,就是返回对应的错误信息或者null
App.js到此结束,下面看另一个大组件RepoPage.js
/* eslint-disable no-undef */ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { loadRepo, loadStargazers } from '../actions' // 这里可以看到引入了5个模块,actions2个,子组件3个。 import Repo from '../components/Repo' import User from '../components/User' import List from '../components/List' const loadData = props => { const { fullName } = props; props.loadRepo(fullName, [ 'description' ]); props.loadStargazers(fullName) }; class RepoPage extends Component { static propTypes = { repo: PropTypes.object, fullName: PropTypes.string.isRequired, name: PropTypes.string.isRequired, owner: PropTypes.object, stargazers: PropTypes.array.isRequired, stargazersPagination: PropTypes.object, loadRepo: PropTypes.func.isRequired, loadStargazers: PropTypes.func.isRequired }; componentWillMount() { loadData(this.props) } componentWillReceiveProps(nextProps) { if (nextProps.fullName !== this.props.fullName) { loadData(nextProps) } } handleLoadMoreClick = () => { this.props.loadStargazers(this.props.fullName, true) }; renderUser(user) { return <User user={user} key={user.login} /> } render() { const { repo, owner, name } = this.props; if (!repo || !owner) { return <h1><i>Loading {name} details...</i></h1> } const { stargazers, stargazersPagination } = this.props; return ( <div> <Repo repo={repo} owner={owner} /> <hr /> <List renderItem={this.renderUser} items={stargazers} onLoadMoreClick={this.handleLoadMoreClick} loadingLabel={`Loading stargazers of ${name}...`} {...stargazersPagination} /> </div> ) } } const mapStateToProps = (state, ownProps) => { const login = ownProps.match.params.login.toLowerCase(); const name = ownProps.match.params.name.toLowerCase(); const { pagination: { stargazersByRepo }, entities: { users, repos } } = state; const fullName = `${login}/${name}`; const stargazersPagination = stargazersByRepo[fullName] || { ids: [] }; const stargazers = stargazersPagination.ids.map(id => users[id]); return { fullName, name, stargazers, stargazersPagination, repo: repos[fullName], owner: users[login] } }; export default withRouter(connect(mapStateToProps, { loadRepo, loadStargazers })(RepoPage))
/actions/index.js(所有dispatch的type)
import { CALL_API, Schemas } from '../middleware/api' // 这里引入了另一个模块
export const USER_REQUEST = 'USER_REQUEST';
export const USER_SUCCESS = 'USER_SUCCESS';
export const USER_FAILURE = 'USER_FAILURE';
// Fetches a single user from Github API.
// Relies on the custom API middleware defined in ../middleware/api.js.
const fetchUser = login => ({
[CALL_API]: {
types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ],
endpoint: `users/${login}`,
schema: Schemas.USER
}
});
// Fetches a single user from Github API unless it is cached.
// Relies on Redux Thunk middleware.
export const loadUser = (login, requiredFields = []) => (dispatch, getState) => {
const user = getState().entities.users[login];
if (user && requiredFields.every(key => user.hasOwnProperty(key))) {
return null
}
return dispatch(fetchUser(login))
};
export const REPO_REQUEST = 'REPO_REQUEST';
export const REPO_SUCCESS = 'REPO_SUCCESS';
export const REPO_FAILURE = 'REPO_FAILURE';
// Fetches a single repository from Github API.
// Relies on the custom API middleware defined in ../middleware/api.js.
const fetchRepo = fullName => ({
[CALL_API]: {
types: [ REPO_REQUEST, REPO_SUCCESS, REPO_FAILURE ],
endpoint: `repos/${fullName}`,
schema: Schemas.REPO
}
});
// Fetches a single repository from Github API unless it is cached.
// Relies on Redux Thunk middleware.
export const loadRepo = (fullName, requiredFields = []) => (dispatch, getState) => {
const repo = getState().entities.repos[fullName];
if (repo && requiredFields.every(key => repo.hasOwnProperty(key))) {
return null
}
return dispatch(fetchRepo(fullName))
};
export const STARRED_REQUEST = 'STARRED_REQUEST';
export const STARRED_SUCCESS = 'STARRED_SUCCESS';
export const STARRED_FAILURE = 'STARRED_FAILURE';
// Fetches a page of starred repos by a particular user.
// Relies on the custom API middleware defined in ../middleware/api.js.
const fetchStarred = (login, nextPageUrl) => ({
login,
[CALL_API]: {
types: [ STARRED_REQUEST, STARRED_SUCCESS, STARRED_FAILURE ],
endpoint: nextPageUrl,
schema: Schemas.REPO_ARRAY
}
});
// Fetches a page of starred repos by a particular user.
// Bails out if page is cached and user didn't specifically request next page.
// Relies on Redux Thunk middleware.
export const loadStarred = (login, nextPage) => (dispatch, getState) => {
const {
nextPageUrl = `users/${login}/starred`,
pageCount = 0
} = getState().pagination.starredByUser[login] || {};
if (pageCount > 0 && !nextPage) {
return null
}
return dispatch(fetchStarred(login, nextPageUrl))
};
export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST';
export const STARGAZERS_SUCCESS = 'STARGAZERS_SUCCESS';
export const STARGAZERS_FAILURE = 'STARGAZERS_FAILURE';
// Fetches a page of stargazers for a particular repo.
// Relies on the custom API middleware defined in ../middleware/api.js.
const fetchStargazers = (fullName, nextPageUrl) => ({
fullName,
[CALL_API]: {
types: [ STARGAZERS_REQUEST, STARGAZERS_SUCCESS, STARGAZERS_FAILURE ],
endpoint: nextPageUrl,
schema: Schemas.USER_ARRAY
}
});
// Fetches a page of stargazers for a particular repo.
// Bails out if page is cached and user didn't specifically request next page.
// Relies on Redux Thunk middleware.
export const loadStargazers = (fullName, nextPage) => (dispatch, getState) => {
const {
nextPageUrl = `repos/${fullName}/stargazers`,
pageCount = 0
} = getState().pagination.stargazersByRepo[fullName] || {};
if (pageCount > 0 && !nextPage) {
return null
}
return dispatch(fetchStargazers(fullName, nextPageUrl))
};
export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE';
// Resets the currently visible error message.
export const resetErrorMessage = () => ({
type: RESET_ERROR_MESSAGE
});
/middleware/api.js(异步请求数据)
import { normalize, schema } from 'normalizr'
import { camelizeKeys } from 'humps'
const getNextPageUrl = response => {
const link = response.headers.get('link');
if (!link) {
return null
}
const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1);
if (!nextLink) {
return null
}
return nextLink.trim().split(';')[0].slice(1, -1)
};
const API_ROOT = 'https://api.github.com/';
const callApi = (endpoint, schema) => {
const fullUrl = (endpoint.indexOf(API_ROOT) === -1) ? API_ROOT + endpoint : endpoint;
return fetch(fullUrl)
.then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json)
}
console.error(response.headers);
const camelizedJson = camelizeKeys(json);
const nextPageUrl = getNextPageUrl(response);
return Object.assign({},
normalize(camelizedJson, schema),
{ nextPageUrl }
)
})
)
};
const userSchema = new schema.Entity('users', {}, {
idAttribute: user => user.login.toLowerCase()
});
const repoSchema = new schema.Entity('repos', {
owner: userSchema
}, {
idAttribute: repo => repo.fullName.toLowerCase()
});
// Schemas for Github API responses.
export const Schemas = {
USER: userSchema,
USER_ARRAY: [userSchema],
REPO: repoSchema,
REPO_ARRAY: [repoSchema]
};
export const CALL_API = 'Call API';
export default store => next => action => {
const callAPI = action[CALL_API];
if (typeof callAPI === 'undefined') {
return next(action)
}
let { endpoint } = callAPI;
const { schema, types } = callAPI;
if (typeof endpoint === 'function') {
endpoint = endpoint(store.getState())
}
if (typeof endpoint !== 'string') {
throw new Error('Specify a string endpoint URL.')
}
if (!schema) {
throw new Error('Specify one of the exported Schemas.')
}
if (!Array.isArray(types) || types.length !== 3) {
throw new Error('Expected an array of three action types.')
}
if (!types.every(type => typeof type === 'string')) {
throw new Error('Expected action types to be strings.')
}
const actionWith = data => {
const finalAction = Object.assign({}, action, data);
delete finalAction[CALL_API];
return finalAction
};
const [ requestType, successType, failureType ] = types;
next(actionWith({ type: requestType }));
return callApi(endpoint, schema).then(
response => next(actionWith({
response,
type: successType
})),
error => next(actionWith({
type: failureType,
error: error.message || 'Something bad happened'
}))
)
}