DvaJS入门课
奔跑的瓜牛 人气:11
不管是Vue还是React,他们都没解决组件间的通信和数据流问题。当然,这个说法不是很准确,准确的说法是他们都没很好的处理这些问题。我们是可以用一些烂手段去解决这个问题,但是当应用比较大、数据多的时候,这将变的非常难以维护。因此,我们还需要引入别的库去优雅的解决这个问题。比如:Vue中的Vuex,React中的dva。如果已经掌握Vuex,dva也能很快上手。
### React没有解决的问题(Dva的由来)
React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。
如果开发大应用,还需要解决一个问题。
- 通信:组件之间如何通信?
- 数据流:数据如何和视图串联起来?路由和数据如何绑定?如何编写异步逻辑?等等
### 介绍
dva 首先是一个基于 [redux](https://github.com/reduxjs/redux) 和 [redux-saga](https://github.com/redux-saga/redux-saga) 的数据流方案,然后为了简化开发体验,dva 还额外内置了 [react-router](https://github.com/ReactTraining/react-router) 和 [fetch](https://github.com/github/fetch),所以也可以理解为一个轻量级的应用框架。
> dva = React-Router + Redux + Redux-saga
### 特性
- **易学易用**,仅有 6 个 api,对 redux 用户尤其友好,[配合 umi 使用](https://umijs.org/guide/with-dva.html)后更是降低为 0 API
- **elm 概念**,通过 reducers, effects 和 subscriptions 组织 model
- **插件机制**,比如 [dva-loading](https://github.comhttps://img.qb5200.com/download-x/dvajshttps://img.qb5200.com/download-x/dva/tree/master/packageshttps://img.qb5200.com/download-x/dva-loading) 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
- **支持 HMR**,基于 [babel-plugin-dva-hmr](https://github.comhttps://img.qb5200.com/download-x/dvajs/babel-plugin-dva-hmr) 实现 components、routes 和 models 的 HMR
### 构建项目
1. 安装dva-cli
```bash
npm install dva-cli -g
```
2. 创建新应用
```bash
dva new dva-quickstart
```
3. 启动项目
然后我们 `cd` 进入 `dva-quickstart` 目录,并启动开发服务器:
```bash
$ cd dva-quickstart
$ npm start
```
4. 使用 antd通过 npm 安装 `antd` 和 `babel-plugin-import` 。`babel-plugin-import` 是用来按需加载 antd 的脚本和样式的,详见 [repo](https://github.com/ant-design/babel-plugin-import) 。
```bash
$ npm install antd babel-plugin-import --save
```
编辑 `.webpackrc`,使 `babel-plugin-import` 插件生效。
```diff
{
+ "extraBabelPlugins": [
+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+ ]
}
```
5. 定义路由我们要写个应用来先显示产品列表。首先第一步是创建路由,路由可以想象成是组成应用的不同页面。
新建 route component `routes/Products.js`,内容如下:
```javascript
import React from 'react';
const Products = (props) => (
```
然后在浏览器里打开 http://localhost:8000/#/products ,你应该能看到前面定义的 `h2` 标签。
6. 编写UI Component随着应用的发展,你会需要在多个页面分享 UI 元素 (或在一个页面使用多次),在 dva 里你可以把这部分抽成 component 。
我们来编写一个 `ProductList` component,这样就能在不同的地方显示产品列表了。
新建 `components/ProductList.js` 文件:
```javascript
import React from 'react';
import PropTypes from 'prop-types';
import { Table, Popconfirm, Button } from 'antd';
const ProductList = ({ onDelete, products }) => {
const columns = [{
title: 'Name',
dataIndex: 'name',
}, {
title: 'Actions',
render: (text, record) => {
return (
onDelete(record.id)}>
);
},
}];
return (
);
};
ProductList.propTypes = {
onDelete: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
};
export default ProductList;
```
7. 定义 Model完成 UI 后,现在开始处理数据和逻辑。
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
新建 model `models/products.js` :
```javascript
export default {
namespace: 'products',
state: [],
reducers: {
'delete'(state, { payload: id }) {
return state.filter(item => item.id !== id);
},
},
};
```
这个 model 里:
- `namespace` 表示在全局 state 上的 key
- `state` 是初始值,在这里是空数组
- `reducers` 等同于 redux 里的 reducer,接收 action,同步更新 state
然后别忘记在 `index.js` 里载入他:
```diff
// 3. Model
+ app.model(require('./models/products').default);
```
8. connect 起来到这里,我们已经单独完成了 model 和 component,那么他们如何串联起来呢?
dva 提供了 connect 方法。如果你熟悉 redux,这个 connect 就是 react-redux 的 connect 。
编辑 `routes/Products.js`,替换为以下内容:
```javascript
import React from 'react';
import { connect } from 'dva';
import ProductList from '../components/ProductList';
const Products = ({ dispatch, products }) => {
function handleDelete(id) {
dispatch({
type: 'productshttps://img.qb5200.com/download-x/delete',
payload: id,
});
}
return (
);
};
// export default Products;
export default connect(({ products }) => ({
products,
}))(Products);
```
最后,我们还需要一些初始数据让这个应用 run 起来。编辑 `index.js`:
```diff
- const app = dva();
+ const app = dva({
+ initialState: {
+ products: [
+ { name: 'dva', id: 1 },
+ { name: 'antd', id: 2 },
+ ],
+ },
+ });
```
刷新浏览器,应该能看到以下效果:
![img](https://zos.alipayobjects.com/rmsportal/GQJeDDeUCSTRMMg.gif)
### Dva 概念
#### 数据流向
数据的改变发生通常是通过:
- 用户交互行为(用户点击按钮等)
- 浏览器行为(如路由跳转等)触发的
当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。所以,在 dva 中,数据流向非常清晰简明。
![img](https://zos.alipayobjects.com/rmsportal/PPrerEAKbIoDZYr.png)
#### Models
##### 核心概念
- State:一个对象,保存整个应用状态
- View:React 组件构成的视图层
- Action:一个对象,描述事件
- connect 方法:一个函数,绑定 State 到 View
- dispatch 方法:一个函数,发送 Action 到 State
##### State
> type State = any
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);**操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系**,这样才能保证 State 的独立性,便于测试和追踪变化。
在 dva 中你可以通过 dva 的实例属性 `_store` 看到顶部的 state 数据,但是通常你很少会用到:
```javascript
const app = dva();
console.log(app._store); // 顶部的 state 数据
```
##### Action
> type AsyncAction = any
Action 是**一个普通 javascript 对象**,它是**改变 State 的唯一途径**。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,**最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据**。action 必须带有 `type` 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 `dispatch` 函数;需要注意的是 `dispatch` 是在组件 connect Models以后,通过 props 传入的。
```text
dispatch({
type: 'add',
});
```
##### dispatch 函数
> type dispatch = (a: Action) => Action
dispatching function 是一个用于触发 action 的函数,**action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的**。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
```javascript
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
```
##### Reducer
> type Reducer = (state: S, action: A) => S
Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。
Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中:
```javascript
[{x:1},{y:2},{z:3}].reduce(function(prev, next){
return Object.assign(prev, next);
})
//return {x:1, y:2, z:3}
```
在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是[纯函数](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md),所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用[immutable data](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#reasonable),这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。
##### Effect
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。
dva 为了控制副作用的操作,底层引入了[redux-sagas](http://superraytin.github.io/redux-saga-in-chinese)做异步流程控制,由于采用了[generator的相关概念](http://www.ruanyifeng.com/blog/2015/04/generator.html),所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于 **纯函数**,如果你想了解更多可以阅读[Mostly adequate guide to FP](https://github.com/MostlyAdequate/mostly-adequate-guide),或者它的中文译本[JS函数式编程指南](https://www.gitbook.com/book/llh911001/mostly-adequate-guide-chinesehttps://img.qb5200.com/download-x/details)。
```js
effects: {
*addAfter1Second (action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'add' });
},
}
```
##### Subscription
Subscriptions 是一种从**源**获取数据的方法,它来自于 elm。
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
```javascript
import key from 'keymaster';
...
app.model({
namespace: 'count',
subscriptions: {
keyEvent({dispatch}) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
}
});
```
#### Router
这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 [History API](http://mdn.beonex.com/en/DOM/window.history.html) 可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是[react-router](https://github.com/reactjs/react-router)。
```javascript
import { Router, Route } from 'dva/router';
app.router(({history}) =>
);
```
#### Route Components
在[组件设计方法](https://github.comhttps://img.qb5200.com/download-x/dvajshttps://img.qb5200.com/download-x/dva-docs/blob/master/v1/zh-cn/tutorial/04-组件设计方法.md)中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。
所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在`/routes/`目录下,而`/components/`目录下则是纯组件(Presentational Components)。
##### dva 应用的最简结构
```js
const app = dva();
// 注册 Model
app.model({
namespace: 'count',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000); // 和 Server 层交互
yield put({ type: 'add' });
},
},
subscriptions: {
keyEvent({dispatch}) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
// setup在组件渲染之前会触发,setup一般是直接出发effects里面的方法
setup({ dispatch, history }) {
dispatch({ type: 'fetch'}); //如果是当前model可以直接写方法名
},
}
});
// 注册视图
app.router(() => );
// 启动应用
app.start('#root');
```
![img](https://zos.alipayobjects.com/rmsportal/pHTYrKJxQHPyJGAYOzMu.png)
### 总结
dva 首先是一个基于 [redux](https://github.com/reduxjs/redux) 和 [redux-saga](https://github.com/redux-saga/redux-saga) 的数据流方案,主要**解决了组件通信和数据流问题**。然后为了简化开发体验,dva 还额外内置了 [react-router](https://github.com/ReactTraining/react-router) 和 [fetch](https://github.com/github/fetch)。同时,dva 插件机制可以更好的扩展功能,综上,dva 可以理解为一个轻量级的应用框架。
List of Products
); export default Products; ``` 添加路由信息到路由表,编辑 `router.js` : ```diff + import Products from './routes/Products'; ... +List of Products
加载全部内容