Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dva 入门:手把手教你写应用 #8

Open
sorrycc opened this issue Jul 26, 2016 · 38 comments
Open

dva 入门:手把手教你写应用 #8

sorrycc opened this issue Jul 26, 2016 · 38 comments
Labels

Comments

@sorrycc
Copy link
Owner

sorrycc commented Jul 26, 2016

本文已迁移至 https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/getting-started.md

我们将基于 dva 完成一个简单 app,并熟悉他的所有概念。

最终效果:

这是一个测试鼠标点击速度的 App,记录 1 秒内用户能最多点几次。顶部的 Highest Record 纪录最高速度;中间的是当前速度,给予即时反馈,让用户更有参与感;下方是供点击的按钮。

看到这个需求,我们可能会想:

  1. 该如何创建应用?
  2. 创建完后,该如何一步步组织代码?
  3. 开发完后,该如何构建、部署和发布?

在代码组织部分,可能会想:

  1. 如何写 Component ?
  2. 如何写样式?
  3. 如何写 Model ?
  4. 如何 connect Model 和 Component ?
  5. 用户操作后,如何更新数据到 State ?
  6. 如何处理异步逻辑? (点击之后 +1,然后延迟一秒 -1)
  7. 如何处理路由?

以及:

  1. 不想每次刷新 Highest Record 清 0,想通过 localStorage 记录,这样刷新之后还能保留 Highest Record。该如何处理?
  2. 希望同时支持键盘的点击测速,又该如何处理?

我们可以带着这些问题来看这篇文章,但不必担心有多复杂,因为全部 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

接到需求之后推荐的做法不是立刻编码,而是先以上帝模式做整体设计。

  1. 先设计 model
  2. 再设计 component
  3. 最后连接 model 和 component

这个需求里,我们定义 model 如下:

app.model({
  namespace: 'count',
  state: {
    record : 0,
    current: 0,
  },
});

namespace 是 model state 在全局 state 所用的 key,state 是默认数据。然后 state 里的 record 表示 highest recordcurrent 表示当前速度。

完成 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>
  );
};

注意:

  1. 这里先 import styles from './index.less';,再通过 styles.xxx 的方式声明 css classname 是基于 css-modules 的方式,后面的样式部分会用上
  2. 通过 props 传入两个值,countdispatchcount 对应 model 上的 state,在后面 connect 的时候绑定,dispatch 用于分发 action
  3. 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/addcount/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};
+   },
+ },
});

注意:

  1. { ...state } 里的 ... 是对象扩展运算符,类似 Object.extend,详见:对象的扩展运算符
  2. 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>
);

注意:

  1. 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);
+  });
+}

注意:

  1. *add() {} 等同于 add: function*(){}
  2. call 和 put 都是 redux-saga 的 effects,call 表示调用异步函数,put 表示 dispatch action,其他的还有 select, take, fork, cancel 等,详见 redux-saga 文档
  3. 默认的 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 了解更多细节。

(完)

@sorrycc sorrycc changed the title dva 入门:手把手教你用写应用 dva 入门:手把手教你写应用 Jul 26, 2016
@jerexyz
Copy link

jerexyz commented Jul 26, 2016

如果定义了多个model,model之间如何通信?

@sorrycc
Copy link
Owner Author

sorrycc commented Jul 26, 2016

@jerexyz 通过 action。dva 封装自 redux,通信机制也等同于 redux 。

@codering
Copy link

问个题外话

dva-cli 会为我们安装 keymaster 依赖并保存到 package.json 中

这个怎么做到的?

@sorrycc
Copy link
Owner Author

sorrycc commented Jul 27, 2016

@codering https://github.com/ericclemmons/npm-install-webpack-plugin

@codering
Copy link

@sorrycc 源码没看到用到这个依赖

@sorrycc
Copy link
Owner Author

sorrycc commented Jul 27, 2016

我 fork 了一份的,因为要优先用内部的 tnpm 作为安装工具。

@ibigbug
Copy link

ibigbug commented Jul 31, 2016

  1. onClick 的 dispatch 放在 Component 里面,keyUp 的 dispatch 写在 model 里面: 事件订阅放在两个地方?
  2. namespace 已经声明了,还要在每个方法见面加前缀么?
  3. 如果想在页面加载时 load 一组数据该怎么写?
  4. effect 里面参数如何传递?
  5. 将 elm-lang 的 subscription,effect 的概念直接引入,是否有什么特殊考虑? 如果和大家已经熟知的 redux 概念差异较大,是否会增加理解和学习成本?

希望文档中能够补充一下。

@sorrycc
Copy link
Owner Author

sorrycc commented Aug 1, 2016

@ibigbug 都是很好的问题,说下我的理解。


问:onClick 的 dispatch 放在 Component 里面,keyUp 的 dispatch 写在 model 里面: 事件订阅放在两个地方?

keyUp 可以当做是事件,也可以当做是数据源。作为事件,放 Component 比较合适;作为数据源,放 model 的 subscription 则更合适。

Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。keyUp 的事件绑定可以把他理解成是订阅了键盘事件的数据源。

而如果把 keyUp 放在 React Component,按我理解,应该是要放在 componentWillMount 之类的生命周期里。我不喜欢这么做,因为:

  1. 现在的 React Component 都要求 stateless,不包含 state 和声明周期管理,只涉及 props 和 view
  2. 逻辑分散,散落在各个 Component 里

问:namespace 已经声明了,还要在每个方法见面加前缀么?

这是一个非常纠结的点。

我也希望把前缀去掉,但其实并不止需要去 key 里的前缀,reducers, effects 和 subscriptions 里 dispatch/put/take 的时候也要处理。

考虑一下原因,最终决定不去前缀:

  1. redux 生态圈的接受度,保留 namespace prefix 最主要是为了和 redux 的使用习惯保持一致
  2. 技术问题,effects 层(redux-saga) 里不好实现 namespace prefix 的功能

详见:dvajs/dva#10 (comment)

问:如果想在页面加载时 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 设计上没有做过度封装。

@janjon
Copy link

janjon commented Aug 14, 2016

subscriptions: [
    function(dispatch) {  //  error  dispatch 找不到
      key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
    },
  ]

为啥 阿里都喜欢用 less 嘞

@sorrycc
Copy link
Owner Author

sorrycc commented Aug 14, 2016

@janjon 抱歉,api 变更了,这里换成 function({ dispatch }) {}

@fengzilong
Copy link

fengzilong commented Sep 27, 2016

有几个疑问,希望可以帮忙解答下

  1. 关于effect的归属问题

    如果有一个effect需要同时修改多个model,这个effect应该归属于哪个model呢?

  2. 关于subscription的归属问题

    同上,一个数据源修改多个model的情况

  3. subscription如何禁用

    假如监听了键盘事件,但在某个特定条件下想禁用这个subscription,相关逻辑是写在subscription内部,还是可以从外部禁用呢?比如当另一个model的某个字段变成true时,停止监听键盘事件,该如何实现?

@sorrycc
Copy link
Owner Author

sorrycc commented Sep 27, 2016

关于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;
    },
  }
};

@fengzilong
Copy link

@sorrycc 感谢~

@wangyinwei1
Copy link

webpack打包工具怎么加载字体文件就报错。该怎么配置。可以出个教程讲讲你这个webpack跟我们的不一样

@Plortinus
Copy link

blog开头贴的地址404

@sorrycc
Copy link
Owner Author

sorrycc commented Oct 18, 2016

@Plortinus 感谢提醒,已更新。

@ian4hu
Copy link

ian4hu commented Oct 21, 2016

如果存在页面上存在一个列表,列表里面每一项都对应于一个相同的model(每一项对应之前定义好的一个组件实例,类似于TodoList里面的每一个Todo,但是这个Todo也是进行了封装了,并且使用了Todo model,不是Plain Object),此时,这样的情况如何处理?

@shenxiuqiang
Copy link

dva new myApp
cd myApp
npm start
后:
err with request :Error: socket hang up /
err with request :Error: socket hang up /favicon.ico

@sorrycc
Copy link
Owner Author

sorrycc commented Nov 9, 2016

访问 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
Copy link

spiroo commented Nov 9, 2016

@sorrycc 请问,如果我想给.normal添加一个背景图片,直接在样式表里写background:url('xxx.png');报错,请问怎么解决?

@sorrycc
Copy link
Owner Author

sorrycc commented Nov 9, 2016

@spiroo 报什么错? https://github.com/dvajs/dva/issues 这里提 issue 吧。

@sorrycc sorrycc added the DvaJS label Dec 9, 2016
@liyswei
Copy link

liyswei commented Jan 5, 2017

最终打包想按需加载模块,不全在一个脚本里,应如何处理才好

@cyaolong
Copy link

按步骤写好了index.less,再index.js里也有写好import和className相关语句,但网页上没有出现样式。用浏览器开发工具查看页面代码,已看到css已出现,但在div里却没出现,求赐教

@yelgogogo
Copy link

同上,完成后,css无效~

@jiyifun
Copy link

jiyifun commented May 25, 2017

@yelgogogo .roadhogrc 里的配置要把CSSModules打开...

@wuxiaolines
Copy link

请问写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))

  },

@DrakeXiang
Copy link

貌似并不会自动安装 keymaster 啊。。

@ANDYGE
Copy link

ANDYGE commented Jun 27, 2017

如何实现嵌套的路由,像/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');
              },
            },
          ],
        },
],
}

@koreadragon
Copy link

我import key from 'keymaster'后,并没有自动安装啊,运行报错说Module not found: 'keymaster' in ...

@wangfengcool
Copy link

监听页面加载完成之后 立刻执行 函数?或者在render之后执行函数,如何做呢?

@joshle
Copy link

joshle commented Sep 20, 2017

reducers 和 effects 同名的action 方法,effects的动作会覆盖 reducers 的动作吗?我的本地测试显示只执行了 effects 里面的 add 方法

@kcshan
Copy link

kcshan commented Oct 17, 2017

@joshle 会,先执行reducers里面的方法,然后执行effects,俩个不能同名

@yinqiao
Copy link

yinqiao commented Dec 24, 2017

其中 “完成 component” 小节中 dispatch 方法是如何传递的

@Lifedance
Copy link

请问,为什么component dispatch type 'count/add' 的actioner, 只执行了effects的add,而reducers的add确没有被执行呢?

@DrakeXiang
Copy link

@Lifedance dva2.0还是某个版本后就不会同时触发effects和reducers了

@cookiespiggy
Copy link

作为后端码农,我竟然仔细了阅读了每一个issues,感谢并膜拜~

@big-tutu
Copy link

每次修改完代码后编译构建都很慢,请问如何做优化!

@Sessionking
Copy link

model 什么时候会用到哇,感觉放在页面的state 里面好像就不需要model存放state了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests