状态管理
redux
学习资源
- redux:https://redux.js.org/
- react-redux:https://github.com/reduxjs/react-redux
原理图
试用redux
- 安装:npm install redux --save
- redux中首先需要了解 store,store具有全局唯一性,所有的数据都在这一个数据源里进行管理,但是本身 redux 与 react 并没有直接的联系,可以单独使用。
- 复杂的项目才需要 redux 来管理数据,简单项目 state + props + context 足矣。
redux中的角色
- Store:状态载体,访问状态、提交状态更新、监听状态更新
- Reducer:状态更新具体执行者,纯函数
- Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作
关键点
- 需要一个 store 来存储数据
- store 里的 state 是放置数据的地方
- 通过 dispatch 一个 action 来提交对数据的修改
- 请求提交到 reducer 函数里,根据传入的 action 和 state,返回新的 state
示例
用一个累加器来举例:
- store.js
import {createStore} from 'redux'
// reducer:具体状态修改执行者
const counterReducer = (state = 0, action) => {
switch(action.type){
case 'incr':
return state + 1;
case 'decr':
return state - 1;
default:
return state;
}
}
export default createStore(counterReducer);
- ReduxTest
import React, { Component } from 'react'
import store from '../store'
export default class ReduxTest extends Component {
render() {
return (
<div>
<p>{store.getState()}</p>
<div>
<button onClick={() => store.dispatch({type: 'decr'})}>-</button>
<button onClick={() => store.dispatch({type: 'incr'})}>+</button>
</div>
</div>
)
}
}
- index.js
import React from 'react';
import ReactDOM from 'react-dom';
import ReduxTest from './components/ReduxTest'
function render() {
ReactDOM.render(<ReduxTest/>, document.querySelector('#root'));
}
render();
store.subscribe(render)
- 涉及知识点:
* createStore
* reducer
* getState
* dispatch
* subscribe
react-redux
- 通过上面的案例可以看出,每次数据修改,都要重新调用 render,这样实在是不太友好。我们希望能用 react 的方式来写,可以安装 react-redux 的支持:npm install react-redux --save
API
- Provider:顶级组件,提供数据
- connect:高阶组件,提供数据和方法
代码
- index.js
import store from './store'
import {Provider} from 'react-redux'
function render() {
ReactDOM.render((
<Provider store={store}>
<ReduxTest/>
</Provider>
), document.querySelector('#root'));
}
render();
- ReduxTest.js
import {connect} from 'react-redux'
class ReduxTest extends Component {
render() {
return (
<div>
<p>{this.props.num}</p>
<div>
<button onClick={() => this.props.decr()}>-</button>
<button onClick={() => this.props.incr()}>+</button>
</div>
</div>
)
}
}
const mapStateToProps = state => ({num: state});
const mapDispatchToProps = dispatch => ({
incr: () => dispatch({type: 'incr'}),
decr: () => dispatch({type: 'decr'})
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReduxTest)
- 很明显,connect用装饰器会更简洁,声明需要 state 里的数据方法
// @connect(mapStateToProps, mapDispatchToProps)
@connect(
state => ({num: state}),
{
incr: () => ({type: 'incr'}),
decr: () => ({type: 'decr'})
}
)
class ReduxTest extends Component {
render() {
return (
<div>
<p>{this.props.num}</p>
<div>
<button onClick={() => this.props.decr()}>-</button>
<button onClick={() => this.props.incr()}>+</button>
</div>
</div>
)
}
}
export default ReduxTest
中间件:redux-thunk、redux-logger
- redux默认只支持同步,实现异步任务(比如延迟、网络请求),需要中间件的支持,比如我们可以使用最简单的redux-thunk,同时还可以使用日志中间件redux-logger。
- 安装:npm install redux-thunk redux-logger --save
- Reducer:纯函数,只承担计算 state 的功能,不适合承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
- View:与state一一对应,可以看作 state 的视觉层,也不适合承担其他功能。
- Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
- 实际的Reducer 和 action store 都需要独立拆分文件。
重构代码
- src下新建文件夹 store,然后store下新建 index.js:
import {createStore, applyMiddleware, combineReducers} from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
// import counterReducer from './counter.redux'
import counter from './counter.redux'
export default createStore(
// reducer模块化
combineReducers({counter}),
applyMiddleware(logger, thunk)
);
- 在 store 下新建文件 counter.redux.js:
// reducer:具体状态修改执行者
// const counterReducer = (state = 0, action) => {
export default (state = 0, action) => {
switch(action.type){
case 'incr':
return state + 1;
case 'decr':
return state - 1;
default:
return state;
}
}
function incr() {
return {type: 'incr'}
}
function decr() {
return {type: 'decr'}
}
function asyncAdd(){
return (dispatch, getState) => {
console.log(getState());
// 模拟异步操作
setTimeout(() => {
dispatch({type: 'incr'})
}, 1000)
}
}
export {incr, decr, asyncAdd}
- 修改 ReduxTest.js:
import React, { Component } from 'react'
import {connect} from 'react-redux'
import {incr, decr, asyncAdd} from '../store/counter.redux'
@connect(
// 注意:这里使用 state 需要带上模块名 counter
state => ({num: state.counter}),
{incr, decr, asyncAdd}
)
class ReduxTest extends Component {
render() {
return (
<div>
<p>{this.props.num}</p>
<div>
<button onClick={() => this.props.decr()}>-</button>
<button onClick={() => this.props.incr()}>+</button>
<button onClick={() => this.props.asyncAdd()}>asyncAdd</button>
</div>
</div>
)
}
}
export default ReduxTest
中间件原理
redux原理
- createStore
export function createStore(reducer, enhancer){
if(enhancer){ // 增强器,作用:将createStore功能增强后返回【增强的createStore】,然后把参数reducer传入
return enhancer(createStore)(reducer);
}
let currentState = {}
let currentListeners = []
function getState(){
return currentState
}
fucntion subscribe(listener){
currentListeners.push(listener)
}
fucntion dispatch(action){
currentState = reducer(currentState, action)
currentListeners.forEach(v => v());
return action;
}
dispatch({type: '@IMOOC/WONIU-REDUX'}) // 初始化 state ==> default
return {getState, subscribe, dispatch}
}
- applyMiddleware
export function applyMiddleware(...middlewares){
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 获得中间件链式 数组
const middlewareChain = middlewares.map(middleware => middleware(midApi))
// compose作用:将链式的中间件,一层一层包装起来
dispatch = compose(...middlewareChain)(store.dispatch)
return {
...store,
dispatch
}
}
}
export function compose(...funcs){
if(funcs.length == 0){
return arg => arg
}
if(funcs.length == 1){
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
浅析Redux 的 store enhancer :https://www.jianshu.com/p/04d3fefea8d7
reduce与redux中compose函数:https://www.jianshu.com/p/c9dfe57c4a4e
- bindActionCreators
function bindActionCreator(creator, dispatch){
return (...args) => dispatch(creator(...args))
}
export function bindActionCreators(creators, dispatch){
return Object.keys(creators).reduce((ret, item) => {
ret[item] = bindActionCreator(creators[item], dispatch)
return ret
}, {})
}
react-redux原理
- connect
import React from 'react'
import ProTypes from 'prop-types'
import {bindActionCreators} from './woniu-redux'
export const connect = (mapStateToProps = state => state, mapDispatchToProps = {}) => (WrapComponent) => {
return class ConnectComponent extends React.Component {
static contextTypes = {
store: ProTypes.object
}
constructor(props, context){
super(props, context)
this.state = {
props: {}
}
}
componentDidMount(){
const {store} = this.context
store.subscribe(() => this.update())
this.update()
}
update(){
const {store} = this.context
const StateProps = mapStateToProps(store.getState())
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
this.setState({
props: {
...this.state.props,
...StateProps,
...dispatchProps
}
})
}
}
render(){
return <WrapComponent {...this.state.props}></WrapComponent>
}
}
- Provider
export class Provider extends React.Component{
static childContextTypes = {
store: ProTypes.object
}
getChildContext(){
return {stote: this.store}
}
constructor(props, context){
super(props, context)
this.store = props.store
}
render() {
return this.props.children
}
}
redux-thunk原理
- thunk
const thunk = ({dispatch, getState}) => next => action => {
if(typeof action === 'function'){
return action(dispatch, getState)
}
return next(action)
}
export default thunk
路由管理
react-router-dom
- 特点:路由也是组件、分布式配置、包含式匹配
- 学习资源:https://reacttraining.com/react-router
- 安装:npm i -S react-router-dom
使用
基本使用
- 知识点:BrowserRouter、路由链接地址(Link)、路由展示(Route)、路由匹配(包含匹配、确切匹配、Switch)、路由重定向、路由传参
- BrowserRouter:
import React, { Component } from "react";
import { BrowserRouter, Link, Route, Switch, Redirect } from "react-router-dom";
export default class RouteSample extends Component {
render() {
return (
<BrowserRouter>
<App />
</BrowserRouter>
);
}
}
- 路由根组件——App:
function App(props) {
return (
<div>
{/* 导航链接 */}
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/foo">Foo</Link></li>
</ul>
{/* 路由配置 */}
<Switch>
{/* exact:确切匹配 */}
<Route exact path="/" path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/detail/:course" component={Detail} />
<Route path="/login" component={Login} />
<Route component={NoMatch} />
</Switch>
</div>
);
}
- Home:
function Home({ location }) {
console.log("接收参数:", location.state && location.state.foo);
return (
<div>
<ul>
<li><Link to="/detail/web">Web</Link></li>
<li><Link to="/detail/python">Python</Link></li>
<li><Link to="/detail/java">Java</Link></li>
</ul>
</div>
);
}
- About:
function About() {
return (
<div>
{/* 显示用户信息和订单 */}
<h2>用户中心</h2>
<div>
<Link to="/about/me">个人信息</Link><br />
<Link to="/about/order">订单</Link>
</div>
<Switch>
<Route path="/about/me" component={() => <div>我的信息</div>} />
<Route path="/about/order" component={() => <div>订单信息</div>} />
{/* 重定向 */}
<Redirect to="/about/me" />
</Switch>
</div>
);
}
- Detail:
// function Detail(props) {
function Detail({ match, history, location }) {
/**
* match 参数获取等路由信息
* history 导航
* location url定位
*/
console.log(match, history, location);
return (
<div>
{/* 获取参数 */}
{match.params.course}
{/* 命令式导航 */}
<button onClick={history.goBack}>返回</button>
{/* <button onClick={() => history.push('/')}>返回首页</button> */}
<button onClick={() => history.push({ pathname: "/", state: { foo: "bar" } })}>返回首页</button>
</div>
);
}
- 404页面:
function NoMatch() {
return <div>404页面</div>;
}
路由守卫
- 需求:完成登录验证,对 About 进行守卫,没有登录则跳到登陆页面,登陆页面点击登陆则跳回About页面
- 修改:
{/* <Route path="/about" component={About} /> */}
<PrivateRoute path="/about" component={About} />
- PrivateRoute:
/**
* 路由守卫:定义可以验证的高阶组件
*/
function PrivateRoute({ component: Component, ...rest }) {
// render 和 component 二选一
return (
<Route {...rest} render={props => auth.isLogin
? (<Component {...props} />)
: (<Redirect to={{pathname: "/login", state: { from:props.location.pathname }}}/>)
}
/>
);
}
- 登陆组件:
// 模拟接口
const auth = {
isLogin: false,
login(cb) {
this.isLogin = true;
setTimeout(cb, 300);
}
};
// 登陆组件
class Login extends Component {
state = { isLogin: false };
login = () => {
auth.login(() => {
this.setState({ isLogin: true });
});
};
render() {
// 回调地址
const from = this.props.location.state.from || '/';
if (this.state.isLogin) {
return <Redirect to={from} />;
}
return (
<div>
<p>请先登陆</p>
<button onClick={this.login}>登陆</button>
</div>
);
}
}
redux改造
- 使用 redux 改造用户登陆验证
- 新建 user.redux.js:
const initialState = {
isLogin: false
}
export default (state = initialState, { type, payload }) => {
switch (type) {
case 'login':
return { isLogin: true }
default:
return state
}
}
export function login(){
return (dispatch) => {
// mock一个异步登陆
setTimeout(() => {
dispatch({type: 'login'})
}, 1000);
}
}
- 在store中导入
import {createStore, applyMiddleware, combineReducers} from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import user from './user.redux'
export default createStore(
combineReducers({counter, user}),
applyMiddleware(logger, thunk)
)
- 登陆组件:
@connect(
state => ({ isLogin: state.user.isLogin }),
{ login }
)
class Login extends Component {
render() {
// 回调地址
const from = this.props.location.state.from || "/";
if (this.props.isLogin) {
return <Redirect to={from} />;
}
return (
<div>
<p>请先登陆</p>
<button onClick={this.props.login}>登陆</button>
</div>
);
}
}
- 路由守卫组件:
@connect(state => ({ isLogin: state.user.isLogin }))
class PrivateRoute extends Component {
render() {
const {isLogin, component: Component, ...rest} = this.props;
return (
<Route {...rest} render={props => isLogin
? (<Component {...props} />)
: (<Redirect to={{pathname: "/login", state: { from: props.location.pathname }}}/>)
}
/>
);
}
}
- 修改BrowserRouter:注入store
export default class RouteSample extends Component {
render() {
return (
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
);
}
}
redux-saga改造
- 概述:redux-saga使副作用(数据获取、浏览器缓存获取)易于管理、执行、测试和失败处理
- 地址:https://github.com/redux-saga/redux-saga
- 安装:npm install --save redux-saga
- 使用:用户登录改造
- 新建 sagas.js:
import {call, put, takeEvery} from 'redux-saga/effects'
const api = {
login(){
return new Promise((resolve, reject) => {
setTimeout(() => {
if(Math.random() > 0.5){
resolve({id: 1, name: 'jerry'})
}else{
reject('用户名或密码错误')
}
}, 1000)
})
}
}
// work saga
function* login(action){
try {
const result = yield call(api.login) // 调用接口
yield put({type: 'login', result}) // dispatch派发action
} catch (error) {
yield put({type: 'login_failure', message: error.message})
}
}
function* mySaga(){
yield takeEvery('login_request', login) // 事件监听
}
export default mySaga
- 在store中导入
import {createStore, applyMiddleware, combineReducers} from 'redux'
import logger from 'redux-logger'
import createSagaMiddleware from 'redux-saga'
import saga from './sagas'
// 1.创建中间件
const mid = createSagaMiddleware();
// 2.应用中间件
const store = createStore(
// reducer模块化
combineReducers({user}),
// applyMiddleware(logger, thunk)
applyMiddleware(logger, mid)
);
// 3.run
mid.run(saga);
// 4.导出
export default store;
- user.redux.js中修改:
// // for redux-thunk
// export function login(){
// return (dispatch) => {
// // mock一个异步登陆
// setTimeout(() => {
// dispatch({type: 'login'})
// }, 1000);
// }
// }
// for saga
export function login(){
return {type: 'login_request'}
}
redux-saga 和 redux-thunk的区别
- thunk可以接收 function 类型的action,saga 则是纯对象 action 的解决方案
- saga使用 generator 解决异步问题,非常容易用同步的方式编写异步代码
ES6的generator函数
简单的说生成器就是带一个星星的函数,生成器函数可以暂停,而普通函数则是默认一路到底执行代码,生成器函数在内部碰到 yield 就可以实现暂停的功能,使用 next 进行迭代。
function* g(){
yield 'a';
yield 'b';
yield 'c';
return 'ending';
}
console.log(g()); // 返回迭代器 Iterator
g()并不执行 g 函数,而是返回迭代器 Iterator 对象,调用 next 实际执行
var gen = g();
console.log(gen.next()); // {value: "a", done: false}
// value:是 yield后面表达式执行的结果;done:false-没执行完,true-执行完
console.log(gen.next()); // {value: "b", done: false}
console.log(gen.next()); // {value: "c", done: false}
console.log(gen.next()); // {value: "ending", done: true}
console.log(gen.next()); // {value: undefined, done: true}
// 使用递归函数执行 生成器函数 中的所有步骤
function next(){
let {value, done} = gen.next() // 启动
console.log(value) // 依次打印输出 a b c ending
if(!done){ // 直到迭代完成
next()
}
}
next()
- 传值
function* say(){
let a = yield '1'
console.log(a)
let b = yield '2'
console.log(b)
}
let it = say()
// 输出 {value: "1", done: false}
// a的值并不是该返回值,而是下次next参数
console.log(it.next())
// 输出 我是被传进来的1
// 输出 {value: "2", done: false}
console.log(it.next('我是被传进来的1'))
// 输出 我是被传进来的2
// 输出 {value: undefined, done: true}
console.log(it.next('我是被传进来的2'))
- 结合Promise使用
// 使用 Generator 顺序执行两次异步操作
function* r(num){
const r1 = yield compute(num);
yield compute(r1);
}
// compute为异步操作,结合Promise使用可以轻松实现异步操作队列
function compute(num){
return new Promise(resolve => {
setTimeout(() => {
const ret = num * num;
console.log(ret); // 输出处理结果
resolve(ret); // 操作成功
}, 1000)
})
}
// 不使用递归函数调用
let it = r(2)
it.next().value.then(num => it.next(num));
// 输出:4 16
// 修改为可处理Promise的next
function next(data){
let {value, done} = it.next(data); // 启动
if(!done) {
value.then(num => {
next(num)
})
}
}
next();
// 输出:4 16
umi
网址:https://umijs.org/zh/guide
介绍
特性
架构图
从源码到上线的生命周期管理
市面上的框架基本都是从源码到构建产物,很少会考虑到各种发布流程,而 umi 则多走了这一步。下图是 umi 从源码到上线的一个流程。
他和 dva、roadhog 是什么关系?
umi 首先会加载用户的配置和插件,然后基于配置或者目录,生成一份路由配置,再基于此路由配置,把 JS/CSS 源码和 HTML 完整地串联起来。用户配置的参数和插件会影响流程里的每个环节。
他和 dva、roadhog 是什么关系?
简单来说,
- roadhog 是基于 webpack 的封装工具,目的是简化 webpack 的配置
- umi 可以简单地理解为 roadhog + 路由,思路类似 next.js/nuxt.js,辅以一套插件机制,目的是通过框架的方式简化 React 开发
- dva 目前是纯粹的数据流,和 umi 以及 roadhog 之间并没有相互的依赖关系,可以分开使用也可以一起使用,个人觉得 umi + dva 是比较搭的
dva
原理图
redux + redux-saga + react-redux + react-router-dom = dva
dva+umi的约定
- src源码
* pages页面
* components组件
* layout布局
* model
- config配置
- mock数据模拟
- test测试等
umi使用
- 全局安装:npm install umi -g
- 新建页面:umi g page index、umi g page about
- 开启服务查看效果:umi dev
访问index页面:http://localhost:8000/
访问about页面:http://localhost:8000/about
动态路由
- 以$开头的文件或目录
- 创建 users/$id.js,内容和其他页面相同,显示一下传参
export default function(props){
return (
<div>
<h1>user id:{props.match.params.id}</h1>
<div>
)
}
嵌套路由
- 创建父组件 umi g page users/_layout
export default function(props) {
return (
<div>
<h1>Page _layout</h1>
<div>{props.children}</div>
</div>
);
}
配置式路由
- 默认路由为声明式,根据pages下面的内容自动生成路由,业务复杂后仍然需要配置路由
- 创建 ./config/config.js
export default {
routes: [
// 相对路径是相对于pages
{ path: '/', component: './index' },
{
path: 'users',
component: './users/_layout',
routes: [
{ path: '/users/', component: './users/index' },
{ path: '/users/:id', component: './users/$id' }
]
},
// 配置 404 页面
{component: './notfound'}
]
}
- 注意:一旦使用了配置式路由,那么声明式路由就失效了。
路由守卫
- 配置 ./config/config.js
export default {
routes: [
... ,
// 路由守卫
{
path: '/about', component: './about',
Routes: ['./routes/PrivateRoute.js'] // 这里的是相对于根目录的,且后缀名不可省略
},
...
]
}
- 创建 ./routes/PrivateRoute.js
import Redirect from 'umi/redirect'
export default props => {
if (Math.random() > 0.5) {
return <Redirect to="/login" />
}
return (
<div>
{props.children}
</div>
)
}
- 创建登录页面:umi g page login,并配置路由 { path: ‘login’, component: ‘./login’}
import styles from './login.css';
export default function() {
return (
<div className={styles.normal}>
<h1>Page login</h1>
</div>
);
}
引入antd
- 安装:npm install antd -S、npm install umi-plugin-react -D
win10 有权限错误,通过管理员权限打开 vscode
- 修改 ./config/config.js
export default {
plugins: [
['umi-plugin-react', {antd: true}]
],
routes: [
...
]
}
- 试用
import {Button} from 'antd'
export default () => {
return <div><Button>登录</Button></div>
}
引入dva
- 软件分层:回顾 react,为了让数据流更易于维护,我们分成了 store、reducer、action 等模块,各司其职软件开发也是一样,如下图是前端代码结构层次图:
- Page:负责与用户直接打交道,渲染页面、接受用户的操作输入,侧重于展示型交互性逻辑。
- Model:负责处理业务逻辑,为 Page 做数据、状态的读写、交换、暂存等。
- Service:负责与 HTTP 接口对接,进行纯粹的数据读写。
- 修改 ./config/config.js 配置引入 dva:
export default {
plugins: [
["umi-plugin-react",
{
antd: true,
dva: true
}
]
],
routes: [
...
{ path: "/goods", component: "./goods" },
...
]
};
- dva 是基于 redux、redux-saga 和 react-router 的轻量级前端框架,以及最佳实践沉淀。核心API如下
* model
- state
- action
- dispatch
- reducers
- effects:处理异步 saga 中间件
- subscriptions 订阅
- router 路由
model
- 新建 src/model/goods.js
export default {
namespace: 'goods', // model的命名空间,区分多个model
state: [], // 初始化状态
effects: {}, // 异步操作
reducers: { // 更新状态
}
}
- 说明:
* namespace:model的命名空间,只能用字符串。一个大型应用可能包含多个model,同过 namespace区分
* state:当前model状态的初始值,表示当前状态
* reducers:用于处理同步操作,可以修改 state,由 action 触发。reducer是一个纯函数,它接收当前的 state 及一个 action 对象。action对象里面可以包含数据体(payload)作为入参,需要返回一个新的 state。
* effects:用于处理异步操作(例如:与服务器交互)和业务逻辑,也是由 action 触发。但是,它不可以修改 state,要通过 触发 action 实现对 state 的间接操作。
* action:是 reducers 及 effects 的触发器,一般是一个对象,形如 {type: 'add', payload: todo},同过 type 属性可以匹配到具体某个 reducer 或者 effect,payload 属性则是数据体,用于传送给 reducer 或 effect。
- 首先安装axios:npm i axios -S
import axios from "axios";
// api
function getGoods() {
return axios.get("/api/goods");
}
export default {
namespace: "goods", // model的名字空间,区分多个model
state: [
// 初始化状态
{ title: "xxx" }
],
effects: {
// 异步操作
*getList(action, { call, put }) {
const res = yield call(getGoods);
// type名字不需要命名空间
yield put({ type: 'initGoods', payload: res.data.result });
}
},
reducers: {
// 更新状态
initGoods(state, action) {
return action.payload;
},
addGood(state, action) {
return [...state, { title: action.payload.title }];
}
}
};
商品页面 goods.js
import { Component } from 'react'
import { connect } from "dva";
import styles from './goods.css';
import { Button, Card } from 'antd';
@connect(
state => ({
loading: state.loading, // 通过loading命名空间获取加载状态
goodsList: state.goods // 获取指定命名空间的模型状态
}),
{
addGood: title => ({
type: "goods/addGood", // action的type需要以命名空间为前缀+reducer名称
payload: { title }
}),
getList: () => ({
type: "goods/getList", // action的type需要以命名空间为前缀+reducer名称
})
}
)
export default class extends Component {
componentDidMount(){
this.props.getList();
}
render() {
console.log(this.props.loading);
if (this.props.loading.models.goods) {
return <div>加载中...</div>
}
return (
<div className={styles.normal}>
<h1>Page goods</h1>
{/* 商品列表 */}
<div>
{this.props.goodsList.map(good => {
return (
<Card key={good.title}>
<div>{good.title}</div>
</Card>
)
})}
</div>
<div>
{/* 添加商品 */}
<Button onClick={() => this.props.addGood('商品' + new Date().getTime())}>添加商品</Button>
</div>
</div>
);
}
}
数据mock
- 新建 src/mock/goods.js:
let data = [{ title: "web全栈" }, { title: "java架构师" }, { title: "python" }];
export default {
// "method url": Object 或 Array
// "get /api/goods": { result: data },
// "method url": (req, res) => {}
"get /api/goods": function(req, res) {
setTimeout(() => {
res.json({ result: data });
}, 1250);
}
};