一、环境搭建

1
2
3
4
5
6
7
$ npm install dva-cli -g

# 创建应用
$ dva new dva-quickstart

# 启动
$ npm start

react项目的推荐目录结构(如果使用dva脚手架创建,则自动生成如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|── /mock/             # 数据mock的接口文件  
|── /src/ # 项目源码目录(我们开发的主要工作区域)
| |── /components/ # 项目组件(用于路由组件内引用的可复用组件)
| |── /routes/ # 路由组件(页面维度)
| | |── route1.js
| | |── route2.js # 根据router.js中的映射,在不同的url下,挂载不同的路由组件
| | └── route3.js
| |── /models/ # 数据模型(可以理解为store,用于存储数据与方法)
| | |── model1.js
| | |── model2.js # 选择分离为多个model模型,是根据业务实体进行划分
| | └── model3.js
| |── /services/ # 数据接口(处理前台页面的ajax请求,转发到后台)
| |── /utils/ # 工具函数(工具库,存储通用函数与配置参数)
| |── router.js # 路由配置(定义路由与对应的路由组件)
| |── index.js # 入口文件
| |── index.less
| └── index.html
|── package.json # 项目信息
└── proxy.config.js # 数据mock配置

使用 antd

1
npm i babel-plugin-import --save

babel-plugin-import 是用来按需加载 antd 的脚本和样式的

  • 编辑 .webpackrc,使 babel-plugin-import 插件生效
1
2
3
4
5
{
+ "extraBabelPlugins": [
+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+ ]
}

二、初识Dva

2.1 Dva的特性

1
dva = React-Router + Redux + Redux-saga
  • 仅有 5 个api,仅有5个主要的api
  • 支持 HMR,支持模块的热更新
  • 支持 SSR (ServerSideRender),支持服务器端渲染
  • 支持 Mobile/ReactNative,支持移动手机端的代码编写
  • 支持TypeScript
  • 支持路由和 Model 的动态加载

2.2 Dva的五个api

2.2.1 app = dva(Opts)

app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)**

opts可以配置所有的hooks

1
2
3
4
5
6
7
8
9
10
11
12
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});

hooks包含如下配置项

1、 onError((err, dispatch) => {})

  • effect 执行错误或 subscription 通过done 主动抛错时触发,可用于管理全局出错状态
  • 注意:subscription 并没有加 try...catch,所以有错误时需通过第二个参数 done 主动抛错
1
2
3
4
5
6
7
app.model({
subscriptions: {
setup({ dispatch }, done) {
done(e)
},
},
})

2、 onAction(fn | fn[])

actiondispatch时触发,用于注册 redux 中间件。支持函数或函数数组格式

  • 例如我们要通过 redux-logger 打印日志
1
2
3
4
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
})

3、 onStateChange(fn)

state 改变时触发,可用于同步 state 到 localStorage,服务器端等

4、 onReducer(fn)

封装 reducer 执行,比如借助 redux-undo 实现 redo/undo

1
2
3
4
5
6
7
8
9
10
11
import undoable from 'redux-undo';
const app = dva({
onReducer: reducer => {
return (state, action) => {
const undoOpts = {};
const newState = undoable(reducer, undoOpts)(state, action);
// 由于 dva 同步了 routing 数据,所以需要把这部分还原
return { ...newState, routing: newState.present.routing };
},
},
})

5、 onEffect(fn)

封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态

6、 onHmr(fn)

热替换相关,目前用于 babel-plugin-dva-hmr

7、 extraReducers

指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer

1
2
3
4
5
6
import { reducer as formReducer } from 'redux-form'
const app = dva({
extraReducers: {
form: formReducer,
},
})

这里比较常用的是,history的配置,一般默认的是hashHistory,如果要配置 historybrowserHistory,可以这样

1
2
3
4
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});

initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在modal里面设置相应的state

2.2.2 app.use(Hooks)

app.use(Hooks):配置 hooks 或者注册插件

这里最常见的就是dva-loading插件的配置

1
2
3
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));

但是一般对于全局的loading我们会根据业务的不同来显示相应不同的loading图标,我们可以根据自己的需要来选择注册相应的插件

2.2.3 app.model(ModelObject)

app.model(ModelObject):这个是你数据逻辑处理,数据流动的地方

2.2.4 app.unmodel(namespace)

取消 model 注册,清理 reducers,effectssubscriptionssubscription 如果没有返回 unlisten 函数,使用 app.unmodel 会给予警告

2.2.5 app.router(Function)

注册路由表,这一操作步骤在dva中也很重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注册路由
app.router(require('./router'))


// 路由文件
import { Router, Route } from 'dva/router';
import IndexPage from './routes/IndexPage'
import TodoList from './routes/TodoList'

function RouterConfig({ history }) {
return (

"/" component={IndexPage} />
'/todoList' components={TodoList}/>
</Router>
)
}
export default RouterConfig

如果我们想解决组件动态加载问题,我们的路由文件也可以按照下面的写法来写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Router, Switch, Route } from 'dva/router'
import dynamic from 'dva/dynamic'

function RouterConfig({ history, app }) {
const IndexPage = dynamic({
app,
component: () => import('./routes/IndexPage'),
})

const Users = dynamic({
app,
models: () => [import('./models/users')],
component: () => import('./routes/Users'),
})

return (


"/" component={IndexPage} />
"/users" component={Users} />
</Switch>
Router>
)
}

export default RouterConfig

其中dynamic(opts)opt包含三个配置项:

  • app: dva 实例,加载 models 时需要
  • models: 返回 Promise 数组的函数,Promise返回 dva model`
  • component:返回 Promise的函数,Promise返回 React Component

2.2.6 app.start

启动应用,即将我们的应用跑起来

2.3 Dva九个概念

2.3.1 State

初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva()opts.initialState

1
2
3
4
5
6
7
8
9
10
// dva()初始化
const app = dva({
initialState: { count: 1 },
});

// modal()定义事件
app.model({
namespace: 'count',
state: 0,
});

2.3.2 Action

表示操作事件,可以是同步,也可以是异步

  • action 的格式如下,它需要有一个 type,表示这个 action 要触发什么操作;payload 则表示这个 action 将要传递的数据
1
2
3
4
{
type: String,
payload: data,
}

我们通过 dispatch 方法来发送一个 action

1
dispatch({ type: 'todos/add', payload: 'Learn Dva' });

其实我们可以构建一个Action 创建函数,如下

1
2
3
4
5
6
7
8
9
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

//我们直接dispatch(addTodo()),就发送了一个action。
dispatch(addTodo())

2.3.3 Model

modeldva 中最重要的概念,ModelMVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import queryString from 'query-string'
import * as todoService from '../services/todo'

export default {
namespace: 'todo',
state: {
list: []
},
reducers: {
save(state, { payload: { list } }) {
return { ...state, list }
}
},
effects: {
*addTodo({ payload: value }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, value)
console.log(data)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
const tempObj = {}
tempObj.title = value
tempObj.id = list.length
tempObj.finished = false
list.push(tempObj)
yield put({ type: 'save', payload: { list }})
},
*toggle({ payload: index }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.finished = !obj.finished
yield put({ type: 'save', payload: { list } })
},
*delete({ payload: index }, { call, put, select }) {
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
list.splice(index, 1)
yield put({ type: 'save', payload: { list } })
},
*modify({ payload: { value, index } }, { call, put, select }) {
const data = yield call(todoService.query, value)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.title = value
yield put({ type: 'save', payload: { list } })
}
},
subscriptions: {
setup({ dispatch, history }) {
// 监听路由的变化,请求页面数据
return history.listen(({ pathname, search }) => {
const query = queryString.parse(search)
let list = []
if (pathname === 'todoList') {
dispatch({ type: 'save', payload: {list} })
}
})
}
}
}

model对象中包含5个重要的属性

state

这里的 state 跟我们刚刚讲的 state 的概念是一样的,只不过她的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的

namespace

model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace

Reducer

key/value 格式定义 reducer,用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。其实一个纯函数

1
2
3
4
5
6
7
8
9
10
namespace: 'todo',
state: {
list: []
},
// reducers 写法
reducers: {
save(state, { payload: { list } }) {
return { ...state, list }
}
}

Effect

用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action交给reducer 的地方

其中它用到了redux-saga,里面有几个常用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// effects 写法
effects: {
*addTodo({ payload: value }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, value)
console.log(data)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
const tempObj = {}
tempObj.title = value
tempObj.id = list.length
tempObj.finished = false
list.push(tempObj)
yield put({ type: 'save', payload: { list }})
},
*toggle({ payload: index }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.finished = !obj.finished
yield put({ type: 'save', payload: { list } })
},
*delete({ payload: index }, { call, put, select }) {
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
list.splice(index, 1)
yield put({ type: 'save', payload: { list } })
},
*modify({ payload: { value, index } }, { call, put, select }) {
const data = yield call(todoService.query, value)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.title = value
yield put({ type: 'save', payload: { list } })
}
}

在项目中最主要的会用到的是 putcall

Subscription

  • key/value 格式定义 subscriptionsubscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action
  • subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的action。在 app.start() 时被执行,数据源可以是当前的时间、当前页面的url、服务器的 websocket 连接、history路由变化等等。
  • 注意:如果要使用 app.unmodel()subscription 必须返回 unlisten 方法,用于取消数据订阅
1
2
3
4
5
6
7
8
9
10
11
12
13
// subscriptions 写法
subscriptions: {
setup({ dispatch, history }) {
// 监听路由的变化,请求页面数据
return history.listen(({ pathname, search }) => {
const query = queryString.parse(search)
let list = []
if (pathname === 'todoList') {
dispatch({ type: 'save', payload: {list} })
}
})
}
}

2.3.4 Router

Router 表示路由配置信息,项目中的 router.js

1
2
3
4
5
6
7
export default function({ history }){
return(

"/" component={App} />
</Router>
);
}

RouteComponent

RouteComponent 表示Router 里匹配路径的 Component,通常会绑定model的数据。如下:

1
2
3
4
5
6
7
8
9
10
11
import { connect } from 'dva';

function App() {
return <div>Appdiv>;
}

function mapStateToProps(state) {
return { todos: state.todos };
}

export default connect(mapStateToProps)(App);

2.4 整体架构

  • 首先我们根据 url 访问相关的 Route-Component,在组件中我们通过 dispatch发送 actionmodel 里面的 effect 或者直接 Reducer
  • 当我们将action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect 会发送相应的 actionreducer,由唯一能改变 statereducer 改变 state ,然后通过connect重新渲染组件。
  • 当我们将action发送给reducer,那直接由 reducer 改变 state,然后通过connect重新渲染组件

2.5 Dva图解

图解一:加入Saga

React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store

使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware就行了, 这里使用redux-saga 这个类库

  • 点击创建 Todo的按钮, 发起一个 type == addTodoaction
  • saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSuccaction, 提示创建成功, 反之则发送 type == addTodoFailaction 即可

图解二:Dva表示法

dva做了 3 件很重要的事情

  • storesaga 统一为一个 model 的概念, 写在一个 js 文件里面
  • 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作
  • model 写法很简约, 类似于 DSL 或者 RoR

三、计数器例子

1
$ dva new myapp

目录结构介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── mock // mock数据文件夹
├── node_modules // 第三方的依赖
├── public // 存放公共public文件的文件夹
├── src // 最重要的文件夹,编写代码都在这个文件夹下
│ ├── assets // 可以放图片等公共资源
│ ├── components // 就是react中的木偶组件
│ ├── models // dva最重要的文件夹,所有的数据交互及逻辑都写在这里
│ ├── routes // 就是react中的智能组件,不要被文件夹名字误导。
│ ├── services // 放请求借口方法的文件夹
│ ├── utils // 自己的工具方法可以放在这边
│ ├── index.css // 入口文件样式
│ ├── index.ejs // ejs模板引擎
│ ├── index.js // 入口文件
│ └── router.js // 项目的路由文件
├── .eslintrc // bower安装目录的配置
├── .editorconfig // 保证代码在不同编辑器可视化的工具
├── .gitignore // git上传时忽略的文件
├── .roadhogrc.js // 项目的配置文件,配置接口转发,css_module等都在这边。
├── .roadhogrc.mock.js // 项目的配置文件
└── package.json // 当前整一个项目的依赖

首先是前端的页面,我们使用 class 形式来创建组件,原例子中是使用无状态来创建的。react 创建组件的各种方式,大家可以看React创建组件的三种方式及其区别

我们先修改route/IndexPage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
render() {
const { dispatch } = this.props;

return (

Highest Record: 1</div>

2div>



</div>
div>
);
}
}

export default connect()(IndexPage);

同时修改样式routes/IndexPage.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.normal {
width: 200px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 0 20px #ccc;
}
.record {
border-bottom: 1px solid #ccc;
padding-bottom: 8px;
color: #ccc;
}
.current {
text-align: center;
font-size: 40px;
padding: 40px 0;
}
.button {
text-align: center;
button {
width: 100px;
height: 40px;
background: #aaa;
color: #fff;
}
}

model 处理state,在页面里面输出 model 中的 state

  • 首先我们在index.js中将models/example.js,即将model下一行的的注释打开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import dva from 'dva';
import './index.css';

// 1. Initialize
const app = dva();

// 2. Plugins
// app.use({});

// 3. Model
app.model(require('./models/example')); // 打开注释

// 4. Router
app.router(require('./router'));

// 5. Start
app.start('#root');

接下来我们进入 models/example.js,将namespace 名字改为 countstate对象加上 recordcurrent 属性。如下

1
2
3
4
5
6
7
8
export default {
namespace: 'count',
state: {
record: 0,
current: 0,
},
...
};

接着我们来到 routes/indexpage.js 页面,通过的 mapStateToProps引入相关的 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;

return (


Highest Record: {count.record} // 将count的record输出
</div>

{count.current}
div>


+
</button>
div>
</div>
);
}
}

function mapStateToProps(state) {
return { count: state.count };
} // 获取state

export default connect(mapStateToProps)(IndexPage);

通过 + 发送 action,通过 reducer 改变相应的 state

  • 首先我们在 models/example.js,写相应的 reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
...
reducers: {
add1(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1 };
},
},
};

在页面的模板 routes/IndexPage.js+ 号点击的时候,dispatch一个 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';

class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;
return (

Highest Record: {count.record}</div>

{count.current}div>



</div>
div>
);
}
}
function mapStateToProps(state) {
return { count: state.count };
}

export default connect(mapStateToProps)(IndexPage);

接下来我们来使用 effect 模拟一个数据接口请求,返回之后,通过 yield put() 改变相应的 state

  • 首先我们替换相应的 models/example.jseffect
1
2
3
4
5
6
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},

这里的 delay,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js ,一般请求接口的函数都会写在 servers 文件夹中

1
2
3
4
5
export function delay(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}

订阅订阅键盘事件,使用 subscriptions,当用户按住 command+up 时候触发添加数字的 action

  • models/example.js 中作如下修改
1
2
3
4
5
6
7
8
9
10
+import key from 'keymaster';
...
app.model({
namespace: 'count',
+ subscriptions: {
+ keyboardWatcher({ dispatch }) {
+ key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
+ },
+ },
});
  • 在这里你需要安装 keymaster 这个依赖
1
npm install keymaster --save
  • 现在你可以按住 command+up 就可以使 current 加1

四、Dva实践

4.1 抽离Model

抽离Model,根据设计页面需求,设计相应的Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// models/users.js
// version1: 从数据维度抽取,更适用于无状态的数据
// version2: 从业务状态抽取,将数据与组件的业务状态统一抽离成一个model
// 新增部分为在数据维度基础上,改为从业务状态抽取而添加的代码
export default {
namespace: 'users',
state: {
list: [],
total: null,
+ loading: false, // 控制加载状态
+ current: null, // 当前分页信息
+ currentItem: {}, // 当前操作的用户对象
+ modalVisible: false, // 弹出窗的显示状态
+ modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
},

// 异步操作
effects: {
*query(){},
*create(){},
*'delete'(){}, // 因为delete是关键字,特殊处理
*update(){},
},

// 替换状态树
reducers: {
+ showLoading(){}, // 控制加载状态的 reducer
+ showModel(){}, // 控制 Model 显示状态的 reducer
+ hideModel(){},
querySuccess(){},
createSuccess(){},
deleteSuccess(){},
updateSuccess(){},
}
}

4.2 设计组件

先设置容器组件的访问路径,再创建组件文件

4.2.1 容器组件

具有监听数据行为的组件,职责是绑定相关联的 model 数据,包含子组件;传入的数据来源于model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component, PropTypes } from 'react';

// dva 的 connect 方法可以将组件和数据关联在一起
import { connect } from 'dva';

// 组件本身
const MyComponent = (props)=>{};

// propTypes属性,用于限制props的传入数据类型
MyComponent.propTypes = {};

// 声明模型传递函数,用于建立组件和数据的映射关系
// 实际表示 将ModelA这一个数据模型,绑定到当前的组件中,则在当前组件中,随时可以取到ModelA的最新值
// 可以绑定多个Model
function mapStateToProps({ModelA}) {
return {ModelA};
}

// 关联 model
// 正式调用模型传递函数,完成模型绑定
export default connect(mapStateToProps)(MyComponent);

4.2.2 展示组件

展示通过 props 传递到组件内部数据;传入的数据来源于容器组件向展示组件的props

1
2
3
4
5
6
7
8
9
import React, { Component, PropTypes } from 'react';

// 组件本身
// 所需要的数据通过 Container Component 通过 props 传递下来
const MyComponent = (props)=>{}
MyComponent.propTypes = {};

// 并不会监听数据
export default MyComponent;

4.2.3 设置路由

1
2
3
4
5
6
7
8
9
10
11
12
// .src/router.js
import React, { PropTypes } from 'react';
import { Router, Route } from 'dva/router';
import Users from './routes/Users';

export default function({ history }) {
return (

"/users" component={Users} />
</Router>
);
};

容器组件雏形

1
2
3
4
5
6
7
8
9
10
// .src/routes/Users.jsx
import React, { PropTypes } from 'react';

function Users() {
return (
User Router Component</div>

);
}

export default Users;

4.2.4 设计容器组件

自顶向下的设计方法:先设计容器组件,再逐步细化内部的展示容器

组件的定义方式

1
2
3
4
5
6
7
8
// 方法一: es6 的写法,当组件设计react生命周期时,可采用这种写法
// 具有生命周期的组件,可以在接收到传入数据变化时,自定义执行方法,有自己的行为模式
// 比如在组件生成后调用xx请求(componentDidMount)、可以自己决定要不要更新渲染(shouldComponentUpdate)等
class App extends React.Component({});

// 方法二: stateless 的写法,定义无状态组件
// 无状态组件,仅仅根据传入的数据更新,修改自己的渲染内容
const App = (props) => ({});

容器组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ./src/routes/Users.jsx
import React, { Component, PropTypes } from 'react';

// 引入展示组件 (暂时都没实现)
import UserList from '../components/Users/UserList';
import UserSearch from '../components/Users/UserSearch';
import UserModal from '../components/Users/UserModal';

// 引入css样式表
import styles from './style.less'

function Users() {

// 向userListProps中传入静态数据
const userSearchProps = {};
const userListProps = {
total: 3,
current: 1,
loading: false,
dataSource: [
{
name: '张三',
age: 23,
address: '成都',
},
{
name: '李四',
age: 24,
address: '杭州',
},
{
name: '王五',
age: 25,
address: '上海',
},
],
};
const userModalProps = {};

return (

{/* 用户筛选搜索框 */}

{/* 用户信息展示列表 */}

{/* 添加用户 & 修改用户弹出的浮层 */}

</div>
);
}

// 很关键的对外输出export;使外部可通过import引用使用此组件
export default Users;

展示组件UserList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// ./src/components/Users/UserList.jsx
import React, { Component, PropTypes } from 'react';

// 采用antd的UI组件
import { Table, message, Popconfirm } from 'antd';

// 采用 stateless 的写法
const UserList = ({
total,
current,
loading,
dataSource,
}) => {
const columns = [{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: (text) => "#">{text}</a>,
}, {
title: '年龄',
dataIndex: 'age',
key: 'age',
}, {
title: '住址',
dataIndex: 'address',
key: 'address',
}, {
title: '操作',
key: 'operation',
render: (text, record) => (


{}}>编辑a>

"确定要删除吗?" onConfirm={()=>{}}>

删除</a>
Popconfirm>
</p>
),
}];

// 定义分页对象
const pagination = {
total,
current,
pageSize: 10,
onChange: ()=>{},
};


// 此处的Table标签使用了antd组件,传入的参数格式是由antd组件库本身决定的
// 此外还需要在index.js中引入antd import 'antd/dist/antd.css'
return (


columns={columns}
dataSource={dataSource}
loading={loading}
rowKey={record => record.id}
pagination={pagination}
/>