dva 入门:手把手教你写应用
sorrycc opened this issue · 38 comments
本文已迁移至 https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/getting-started.md
我们将基于 dva 完成一个简单 app,并熟悉他的所有概念。
最终效果:
这是一个测试鼠标点击速度的 App,记录 1 秒内用户能最多点几次。顶部的 Highest Record 纪录最高速度;中间的是当前速度,给予即时反馈,让用户更有参与感;下方是供点击的按钮。
看到这个需求,我们可能会想:
- 该如何创建应用?
- 创建完后,该如何一步步组织代码?
- 开发完后,该如何构建、部署和发布?
在代码组织部分,可能会想:
- 如何写 Component ?
- 如何写样式?
- 如何写 Model ?
- 如何 connect Model 和 Component ?
- 用户操作后,如何更新数据到 State ?
- 如何处理异步逻辑? (点击之后 +1,然后延迟一秒 -1)
- 如何处理路由?
以及:
- 不想每次刷新 Highest Record 清 0,想通过 localStorage 记录,这样刷新之后还能保留 Highest Record。该如何处理?
- 希望同时支持键盘的点击测速,又该如何处理?
我们可以带着这些问题来看这篇文章,但不必担心有多复杂,因为全部 JavaScript 代码只有 70 多行。
安装 dva-cli
你应该会更希望关注逻辑本身,而不是手动敲入一行行代码来构建初始的项目结构,以及配置开发环境。
那么,首先需要安装的是 dva-cli 。dva-cli 是 dva 的命令行工具,包含 init、new、generate 等功能,目前最重要的功能是可以快速生成项目以及你所需要的代码片段。
$ npm install -g dva-cli
安装完成后,可以通过 dva -v
查看版本,以及 dva -h
查看帮助信息。
创建新应用
安装完 dva-cli 后,我们用他来创建一个新应用,取名 myApp
。
$ dva new myApp --demo
注意:--demo
用于创建简单的 demo 级项目,正常项目初始化不加要这个参数。
然后进入项目目录,并启动。
$ cd myApp
$ npm start
几秒之后,会看到这样的输出:
proxy: listened on 8989
livereload: listening on 35729
📦 173/173 build modules
webpack: bundle build is now finished.
(如需关闭 server,请按 Ctrl-C.)
在浏览器里打开 http://localhost:8989/ ,正常情况下,你会看到一个 "Hello Dva" 页面。
定义 model
接到需求之后推荐的做法不是立刻编码,而是先以上帝模式做整体设计。
- 先设计 model
- 再设计 component
- 最后连接 model 和 component
这个需求里,我们定义 model 如下:
app.model({
namespace: 'count',
state: {
record : 0,
current: 0,
},
});
namespace 是 model state 在全局 state 所用的 key,state 是默认数据。然后 state 里的 record 表示 highest record
,current
表示当前速度。
完成 component
完成 Model 之后,我们来编写 Component 。推荐尽量通过 stateless functions 的方式组织 Component,在 dva 的架构里我们基本上不需要用到 state 。
import styles from './index.less';
const CountApp = ({count, dispatch}) => {
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
</div>
</div>
);
};
注意:
- 这里先
import styles from './index.less';
,再通过styles.xxx
的方式声明 css classname 是基于 css-modules 的方式,后面的样式部分会用上 - 通过 props 传入两个值,
count
和dispatch
,count
对应 model 上的 state,在后面 connect 的时候绑定,dispatch
用于分发 action dispatch({type: 'count/add'})
表示分发了一个{type: 'count/add'}
的 action,至于什么是 action,详见:Actions@redux.js.org
更新 state
更新 state 是通过 reducers 处理的,详见 Reducers@redux.js.org。
reducer 是唯一可以更新 state 的地方,这个唯一性让我们的 App 更具可预测性,所有的数据修改都有据可查。reducer 是 pure function,他接收参数 state 和 action,返回新的 state,通过语句表达即 (state, action) => newState
。
这个需求里,我们需要定义两个 reducer,count/add
和 count/minus
,分别用于计数的增和减。值得注意的是 count/add
时 record 的逻辑,他只在有更高的记录时才会被记录。
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
+ reducers: {
+ add(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};
+ },
+ },
});
注意:
{ ...state }
里的...
是对象扩展运算符,类似Object.extend
,详见:对象的扩展运算符add(state) {}
等同于add: function(state) {}
绑定数据
还记得之前的 Component 里用到的 count 和 dispatch 吗? 会不会有疑问他们来自哪里?
在定义了 Model 和 Component 之后,我们需要把他们连接起来。这样 Component 里就能使用 Model 里定义的数据,而 Model 中也能接收到 Component 里 dispatch 的 action 。
这个需求里只要用到 count
。
function mapStateToProps(state) {
return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);
这里的 connect 来自 react-redux。
定义路由
接收到 url 之后决定渲染哪些 Component,这是由路由决定的。
这个需求只有一个页面,路由的部分不需要修改。
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
注意:
history
默认是 hashHistory 并且带有_k
参数,可以换成 browserHistory,也可以通过配置去掉_k
参数。
现在刷新浏览器,如果一切正常,应该能看到下面的效果:
添加样式
默认是通过 css modules 的方式来定义样式,这和普通的样式写法并没有太大区别,由于之前已经在 Component 里 hook 了 className,这里只需要在 index.less
里填入以下内容:
.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;
}
}
效果如下:
异步处理
在此之前,我们所有的操作处理都是同步的,用户点击 + 按钮,数值加 1。
现在我们要开始处理异步任务,dva 通过对 model 增加 effects 属性来处理 side effect(异步任务),这是基于 redux-saga 实现的,语法为 generator。(但是,这里不需要我们理解 generator,知道用法就可以了)
在这个需求里,当用户点 + 按钮,数值加 1 之后,会额外触发一个 side effect,即延迟 1 秒之后数值 1 。
app.model({
namespace: 'count',
+ effects: {
+ *add(action, { call, put }) {
+ yield call(delay, 1000);
+ yield put({ type: 'minus' });
+ },
+ },
...
+function delay(timeout){
+ return new Promise(resolve => {
+ setTimeout(resolve, timeout);
+ });
+}
注意:
*add() {}
等同于add: function*(){}
- call 和 put 都是 redux-saga 的 effects,call 表示调用异步函数,put 表示 dispatch action,其他的还有 select, take, fork, cancel 等,详见 redux-saga 文档
- 默认的 effect 触发规则是每次都触发(
takeEvery
),还可以选择takeLatest
,或者完全自定义take
规则
刷新浏览器,正常的话,就应该已经实现了最开始需求图里的所有要求。
订阅键盘事件
在实现了鼠标测速之后,怎么实现键盘测速呢?
在 dva 里有个叫 subscriontions 的概念,他来自于 elm。
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
dva 中的 subscriptions 是和 model 绑定的。
+import key from 'keymaster';
...
app.model({
namespace: 'count',
+ subscriptions: {
+ keyboardWatcher({ dispatch }) {
+ key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
+ },
+ },
});
这里我们不需要手动安装 keymaster 依赖,在我们敲入 import key from 'keymaster';
并保存的时候,dva-cli 会为我们安装 keymaster
依赖并保存到 package.json
中。输出如下:
use npm: tnpm
Installing `keymaster`...
[keymaster@*] installed at node_modules/.npminstall/keymaster/1.6.2/keymaster (1 packages, use 745ms, speed 24.06kB/s, json 2.98kB, tarball 15.08kB)
All packages installed (1 packages installed from npm registry, use 755ms, speed 23.93kB/s, json 1(2.98kB), tarball 15.08kB)
📦 2/2 build modules
webpack: bundle build is now finished.
所有代码
index.js
import dva, { connect } from 'dva';
import { Router, Route } from 'dva/router';
import React from 'react';
import styles from './index.less';
import key from 'keymaster';
const app = dva();
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(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};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'count/minus' });
},
},
subscriptions: {
keyboardWatcher(dispatch) {
key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
},
},
});
const CountApp = ({count, dispatch}) => {
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
</div>
</div>
);
};
function mapStateToProps(state) {
return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
app.start('#root');
// ---------
// Helpers
function delay(timeout){
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}
构建应用
我们已在开发环境下进行了验证,现在需要部署给用户使用。敲入以下命令:
$ npm run build
输出:
> @ build /private/tmp/dva-quickstart
> atool-build
Child
Time: 6891ms
Asset Size Chunks Chunk Names
common.js 1.18 kB 0 [emitted] common
index.js 281 kB 1, 0 [emitted] index
index.css 353 bytes 1, 0 [emitted] index
该命令成功执行后,编译产物就在 dist 目录下。
下一步
通过完成这个简单的例子,大家前面的问题是否都已经有了答案? 以及是否熟悉了 dva 包含的概念:model, router, reducers, effects, subscriptions ?
还有其他问题?可以关注 dva repo 了解更多细节。
(完)
如果定义了多个model,model之间如何通信?
问个题外话
dva-cli 会为我们安装 keymaster 依赖并保存到 package.json 中
这个怎么做到的?
我 fork 了一份的,因为要优先用内部的 tnpm 作为安装工具。
- onClick 的 dispatch 放在 Component 里面,keyUp 的 dispatch 写在 model 里面: 事件订阅放在两个地方?
- namespace 已经声明了,还要在每个方法见面加前缀么?
- 如果想在页面加载时 load 一组数据该怎么写?
- effect 里面参数如何传递?
- 将 elm-lang 的 subscription,effect 的概念直接引入,是否有什么特殊考虑? 如果和大家已经熟知的 redux 概念差异较大,是否会增加理解和学习成本?
希望文档中能够补充一下。
@ibigbug 都是很好的问题,说下我的理解。
问:onClick 的 dispatch 放在 Component 里面,keyUp 的 dispatch 写在 model 里面: 事件订阅放在两个地方?
keyUp 可以当做是事件,也可以当做是数据源。作为事件,放 Component 比较合适;作为数据源,放 model 的 subscription 则更合适。
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。keyUp 的事件绑定可以把他理解成是订阅了键盘事件的数据源。
而如果把 keyUp 放在 React Component,按我理解,应该是要放在 componentWillMount 之类的生命周期里。我不喜欢这么做,因为:
- 现在的 React Component 都要求 stateless,不包含 state 和声明周期管理,只涉及 props 和 view
- 逻辑分散,散落在各个 Component 里
问:namespace 已经声明了,还要在每个方法见面加前缀么?
这是一个非常纠结的点。
我也希望把前缀去掉,但其实并不止需要去 key 里的前缀,reducers, effects 和 subscriptions 里 dispatch/put/take 的时候也要处理。
考虑一下原因,最终决定不去前缀:
- redux 生态圈的接受度,保留 namespace prefix 最主要是为了和 redux 的使用习惯保持一致
- 技术问题,effects 层(redux-saga) 里不好实现 namespace prefix 的功能
问:如果想在页面加载时 load 一组数据该怎么写?
在 subscriptions 里写,因为 component 已经是 stateless 了。可以参考:https://github.com/dvajs/dva/blob/master/examples/user-dashboard/src/models/users.js#L20-L31
问:effect 里面参数如何传递?
通过 action 传递数据,effects 里可以去到 action 对象。
比如:
const effects = {
*['a'](action) {
console.log(action.payload); // 1
},
};
dispatch({type: 'a', payload: 1});
问:将 elm-lang 的 subscription,effect 的概念直接引入,是否有什么特殊考虑? 如果和大家已经熟知的 redux 概念差异较大,是否会增加理解和学习成本?
subscription、effect 和 redux 的 reducer 并不冲突,他们解决的是不同的问题。至于为何要引入,主要是认同 elm 的概念,前端的好多库(包括 redux)都是从 elm 里借的概念;另外,用 redux 我们通常不会仅用 redux 本身,还需要各种插件的配合,而 subscription + effect + reducer 则很好的解决了我目前能想到的所有项目问题。
dva 在设计上会尽量保证 redux 用户的低迁移/使用成本,对 redux 用户友好。比如 api 设计上没有做过度封装。
subscriptions: [
function(dispatch) { // error dispatch 找不到
key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
},
]
为啥 阿里都喜欢用 less 嘞
有几个疑问,希望可以帮忙解答下
-
关于effect的归属问题
如果有一个effect需要同时修改多个model,这个effect应该归属于哪个model呢?
-
关于subscription的归属问题
同上,一个数据源修改多个model的情况
-
subscription如何禁用
假如监听了键盘事件,但在某个特定条件下想禁用这个subscription,相关逻辑是写在subscription内部,还是可以从外部禁用呢?比如当另一个model的某个字段变成true时,停止监听键盘事件,该如何实现?
关于effect的归属问题
effect 只能归属一个 model,修改其他 model 要 dispatch 该 model 的 action
subscription如何禁用
用 model 内部变量,比如:
const isEnable = true;
export default {
namespace: 'keyboard',
subscriptions: {
setup({ dispatch }) {
key('ctrl+up', () => {
if (isEnable) { /* do something with dispatch */ }
});
},
},
effects: {
*enable() {
isEnable = true;
},
*disable() {
isEnable = false;
},
}
};
@sorrycc 感谢~
webpack打包工具怎么加载字体文件就报错。该怎么配置。可以出个教程讲讲你这个webpack跟我们的不一样
blog开头贴的地址404
@Plortinus 感谢提醒,已更新。
如果存在页面上存在一个列表,列表里面每一项都对应于一个相同的model(每一项对应之前定义好的一个组件实例,类似于TodoList里面的每一个Todo,但是这个Todo也是进行了封装了,并且使用了Todo model,不是Plain Object),此时,这样的情况如何处理?
dva new myApp
cd myApp
npm start
后:
err with request :Error: socket hang up /
err with request :Error: socket hang up /favicon.ico
访问 http://localhost:8000 呢?
On Wed, Nov 9, 2016 at 9:29 AM, shenxiuqiang notifications@github.com
wrote:
dva new myApp
cd myApp
npm start
后:
err with request :Error: socket hang up /
err with request :Error: socket hang up /favicon.ico—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#8 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AACJOGzQcFJLiYBI6_NyOvSFqstHFUn_ks5q8SIHgaJpZM4JU09Y
.
@spiroo 报什么错? https://github.com/dvajs/dva/issues 这里提 issue 吧。
最终打包想按需加载模块,不全在一个脚本里,应如何处理才好
按步骤写好了index.less,再index.js里也有写好import和className相关语句,但网页上没有出现样式。用浏览器开发工具查看页面代码,已看到css已出现,但在div里却没出现,求赐教
同上,完成后,css无效~
@yelgogogo .roadhogrc 里的配置要把CSSModules打开...
请问写mock时用post方法如何获取到body传过来的参数
module.exports = {
'POST /getList'(req, res) {
const page = qs.parse(req.query);
console.log(qs.parse(req.query))
console.log(qs.parse(req.body))
},
貌似并不会自动安装 keymaster 啊。。
如何实现嵌套的路由,像/products/edit/1234或者/products/view/1234,
{ path: '/',
component: App,
getIndexRoute(nextState, cb) {
require.ensure([], (require) => {
cb(null, {component: require('./routes/Home/')});
}, 'home');
},
childRoutes: [
{
path: 'products',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./routes/Product/index'));
}, 'products');
},
childRoutes: [
{
path: 'edit',
getComponent(nextState, cb){
require.ensure([], (require) => {
cb(null, require('./routes/Product/edit'));
}, 'productEdit');
},
},
],
},
],
}
我import key from 'keymaster'后,并没有自动安装啊,运行报错说Module not found: 'keymaster' in ...
监听页面加载完成之后 立刻执行 函数?或者在render之后执行函数,如何做呢?
reducers 和 effects 同名的action 方法,effects的动作会覆盖 reducers 的动作吗?我的本地测试显示只执行了 effects 里面的 add 方法
其中 “完成 component” 小节中 dispatch 方法是如何传递的
请问,为什么component dispatch type 'count/add' 的actioner, 只执行了effects的add,而reducers的add确没有被执行呢?
@Lifedance dva2.0还是某个版本后就不会同时触发effects和reducers了
作为后端码农,我竟然仔细了阅读了每一个issues,感谢并膜拜~
每次修改完代码后编译构建都很慢,请问如何做优化!
model 什么时候会用到哇,感觉放在页面的state 里面好像就不需要model存放state了