soda-x / blog

Here is my blog
754 stars 37 forks source link

初识 Dva #2

Open soda-x opened 7 years ago

soda-x commented 7 years ago

近期,我们在内部做了一个类似 IDE 性质的应用,基于 electrondva,由于之前一直只关注 node 相关的开发者工具,并未太多接触 React 等内容,所以这段时间过的有点煎熬同时也很兴奋,煎熬来源于非舒适区,而兴奋来源于发现基于 dva + electron 给开发者工具带来了更多的可能性。

此次开发 IDE 项目组织方式已由 sorrycc 同学整理成脚手架 dva-boilerplate-electron

初识 dva 是此次总结的第一篇文章,第二篇文章我会记录下在 electron 中的相关沉淀。

回归正题,如何在几天内玩好 React、Dva、Electron。


React 基础知识

什么是 React o.o ?

React 的核心目的是创建 UI 组件,也就是说它是 MVC 架构中的 V 层,所以 React 和你的技术架构并没有关系。

打个比方来说在 AngularJS 1.x 中它通过扩展 html 标签,注入一些结构性的东西(比如 Controllers, Services),所以 AngularJS 1.x 是会侵入到你整个技术的架构,从某些方面来说这些抽象确实能解决一些业务问题,但由此而来的是塔缺乏了灵活性。

React 这种仅仅关注在 Components 的库,给了开发者非常强的灵活度,因为我不并不会被束缚在某一个技术架构。

Components 在各个生命周期内发生了什么 ?

the-component-lifecycle

总结来讲

从最上层来说 React Component 生命周期可以落入到以下三个环节:

LifeCircle

在这三个类别下分别对应着一些 React 的抽象方法,这些方法都是在组件特定生命周期中的钩子,这些钩子会在组件整个生命周期中执行一次或者多次。明白了这些钩子的调用时机,可以有助于更好的书写组件。

比如:

componentWillMount: 在组件 render 之前执行且永远只执行一次。

componentDidMount: 组件加载完毕之后立即执行,并且此时才在 DOM 树中生成了对应的节点,因此我们通过 this.getDOMNode() 来获取到对应的节点。

等等详细请看 文档

component 的几种创建方式

import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom'

var SayHi = React.createClass({
  getInitialState(){
    return {};
  },
  getDefaultProps(){
      return { from: 'pigcan' };
  }
  propTypes:{
    name: PropTypes.string.isRequired,
  },
  render(){
    var name=this.props.name;
    return(
      <p>{from} says: hello {name}! </p>
    );
  }
})

ReactDOM.render(
  <SayHi name='pigcan'/>,
  document.getElementById('demo')
)
import React, { Component, PropTypes } from 'react';
import { Popover, Icon } from 'antd';

class PreviewQRCodeBar extends Component { // 组件的声明方式
  constructor(props) { // 初始化的工作放入到构造函数
    super(props); // 在 es6 中如果有父类,必须有 super 的调用用以初始化父类信息

    this.state = { // 初始 state 设置方式
      visible: false,
    };
  }
  // 因为是类,所以属性与方法之间不必添加逗号
  hide() {
    this.setState({
      visible: false,
    });
  }

  handleVisibleChange(visible) {
    this.setState({ visible });
  }

  render() {
    const { dataurl } = this.props;
    return (
      <Popover
        placement="rightTop"
        content={<img src={dataurl} alt="二维码" />}
        trigger="click"
        visible={this.state.visible}
        onVisibleChange={this.handleVisibleChange.bind(this)} // 通过 .bind(this) 来绑定
      >
        <Icon type="qrcode" />
      </Popover>
    );
  }
}
// 在 react 写法中,直接通过 propTypes {key:value} 来约定
PreviewQRCodeBar.proptypes = {
  dataurl: PropTypes.string.isRequired,
};

// 在 ES6 类声明中无法设置 props 只能在类的驻外使用 defaultProps 属性来完成默认值的设定
// 而在 react 中则通过 getDefaultProps(){} 方法来设定
PreviewQRCodeBar.defaults = {
  // obj
}

export default PreviewQRCodeBar;
import React, { PropTypes } from 'react';

// 组件无 state,pure function
const PreviewDevToolWebview = ({ remoteUrl }) => // 箭头函数,结构赋值
  <webview className={devToolWebview.devToolWebview} src={remoteUrl} />;

PreviewDevToolWebview.proptype = {
  remoteUrl: PropTypes.string.isRequired,
};

export default PreviewDevToolWebview;

// 此类组件不支持 ref 属性,没有组件生命周期的相关的时候和方法,仅支持 propTypes
// 此类组件用以简单呈现数据

如果想了解更多的基础


Flux 又是什么鬼

简而言之 Flux 是一种架构思想,和 MVC 一样,用以解决软件结构的问题,如上所说 React 只是涉及了 UI 层所以在搭建大型应用时必须要有与之配套的应用架构。在 React 社区大家普遍使用 Flux 架构的思想来搭建应用,目前 flux 前端框架

Flux 中最为显著的特点就是它的单向数据流,核心目的是为了在多组件交互时能避免数据的污染。

flux

在 flux 模式中 Store 层是所有数据的权利中心,任何数据的变更都需要发生在 store 中,Store 层发生的数据变更随后都会通过事件的方式广播给订阅该事件的 View,随后 View 会根据接受到的新的数据状态来更新自己。任何想要变更 Store 层数据都需要调用 Action,而这些 Action 则由 Dispatcher 集中调度,在使用 Actions 时需要确保每个 action 对应一个数据更新,并同一时刻只触发一个 action。

说一说我个人的感受,在以往 MVC 架构中,某一个 Model 的数据可能被多个 View 共享,而每个 View 在通常情况下都会有自己的 Controller 层来代理 Model 和 View,那样子很显著的一个问题就出现了,任何一个 Controller 都可能会引发 Model 的数据更新,在现实中我们的应用通常拥有更为复杂的 UI 层,所以使用稍有不当我们的数据流将乱如麻,在调试中我们也会越来越难以调试,因为我们很难确定数据变更发生的确切位置。

dva 中的数据流

pic

如何来理解呢?

在 web 应用中,数据的改变通常发生在用户交互行为或者浏览器行为(如路由跳转等),当此类行为改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致。


dva 的基本概念

简而言之 dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装

什么是 dva

dva 的基本概念有哪些?

以下内容基本摘自 Dva Concepts

dva - Model

State

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(immutable data)。

Action

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。 需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。

dispatch({
  type: 'user/add', // 如果在 model 外调用,需要添加 namespace
  payload: {}, // 需要传递的信息
});

以上调用函数内的对象就是一个 action。

dispatch 函数

用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

dva - Reducer

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数

app.model({
  namespace: 'todos', //model 的 namespace
  state: [], // model 的初始化数据
  reducers: {
    // add 方法就是 reducer,可以看到它其实非常简单就是把老的 state 和接收到的数据处理下,返回新的 state
    add(state, { payload: todo }) {
      return state.concat(todo);
    },
  },
};

dva - Effect

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作,Effects 的最终流向是通过 Reducers 改变 State

核心需要关注下 put, call, select。

app.model({
  namespace: 'todos',
  effects: {
    *addRemote({ payload: todo }, { put, call, select }) {
      const todos = yield select(state => state.todos); // 这边的 state 来源于全局的 state,select 方法提供获取全局 state 的能力,也就是说,在这边如果你有需要其他 model 的数据,则完全可以通过 state.modelName 来获取
      yield call(addTodo, todo); // 用于调用异步逻辑,支持 promise 。
      yield put({ type: 'add', payload: todo }); // 用于触发 action 。这边需要注意的是,action 所调用的 reducer 或 effects 来源于本 model 那么在 type 中不需要声明命名空间,如果需要触发其他非本 model 的方法,则需要在 type 中声明命名空间,如 yield put({ type: 'namespace/fuc', payload: xxx });
    },
  },
});

dva - Subscription

Subscriptions 是一种从 获取数据的方法,它来自于 elm。

Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent(dispatch) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

dva - Router

这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。

dva 实例提供了 router 方法来控制路由,使用的是react-router

import { Router, Route } from 'dva/router';
app.router(({history}) =>
  <Router history={history}>
    <Route path="/" component={HomePage} />
  </Router>
);

详见 react-router

dva - Route Components

在 dva 中我们通常以页面维度来设计 Container Components。

所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components)。

通过 connect 绑定数据

比如:

import { connect } from 'dva';
function App() {}

function mapStateToProps(state, ownProps) { // 该方法名已经非常形象的说明了 connect 的作用在于 State -> Props 的转换,同时自动注册一个 dispatch 的方法,用以触发 action
  return {
    users: state.users,
  };
}
export default connect(mapStateToProps)(App);

然后在 App 里就有了 dispatchusers 两个属性。


好了,如上就是 dva 中的一些核心概念,起初看的时候可能一下子接收到的信息量颇大,但是不要着急,后续业务中的使用会让你对于如上概念越来越清晰。

那么如何来启动一个 dva 应用呢

// Install dva-cli
$ npm install dva-cli -g

// Create app and start
$ dva new myapp
$ cd myapp
$ npm install
$ npm start

Done o.o

让我们来一窥 dva 项目 src 目录结构,尝试来明白整体的代码的组织方式

.
├── assets
│   └── yay.jpg
├── components
│   └── Example.js
├── index.css
├── index.html
├── index.js
├── models
│   └── example.js
├── router.js
├── routes
│   ├── IndexPage.css
│   └── IndexPage.js
├── services
│   └── example.js
├── tests
│   └── models
│       └── example-test.js
└── utils
    └── request.js

assets: 我们可以把项目 assets 资源丢在这边 components: 纯组件,在 dva 应用中 components 目录中应该是一些 logicless 的 component, logic 部分均由对应的 route-component 来承载。在安装完 dva-cli 工具后,我们可以通过 dva g component componentName 的方式来创建一个 component。 index.css: 首页样式 index.html: 首页 index.js: dva 应用启动 五部曲,这点稍后再展开 models: 该目录结构用以存放 model,在通常情况下,一个 model 对应着一个 route-component,而 route-component 则对应着多个 component,当然这取决于你如何拆分,个人偏向于尽可能细粒度的拆分。在安装完 dva-cli 工具后,我们可以通过 dva g model modelName 的方式来创建一个 model。该 model 会在 index.js 中自动注册。 router.js: 页面相关的路由配置,相应的 route-component 的引入 routes: route-component 存在的地方,在安装完 dva-cli 工具后,我们可以通过 dva g route route-name 的方式去创建一个 route-component,该路由配置会被自动更新到 route.js 中。route-component 是一个重逻辑区,一般业务逻辑全部都在此处理,通过 connect 方法,实现 model 与 component 的联动。 services: 全局服务,如发送异步请求 tests: 测试相关 utils: 全局类公共函数

dva 的五部曲

import './index.html';
import './index.css';
import dva from 'dva';

// 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');

好了,以上便是五部曲,看了 dva 官方文档的可能说还少一步

// 4. Connect components and models
const App = connect(mapStateToProps)(Component);

原因是在实际业务中,我们的 connect 行为通常在 route-component 中进行设置。


以上。

对了,人为新增 model 后记得 model 要在 index.js 中予以注册,当然使用脚手架功能并不存在这个问题。 XD。

xiaoluoboding commented 7 years ago

@pigcan 很期待第二篇关于Electron和dva结合的文章呐。

broadviewcheemi commented 7 years ago

楼主您好,我是电子工业出版社博文视点的编辑,请问您有计划出版前端方面的图书吗

soda-x commented 7 years ago

@broadviewcheemi 您好,感谢,不过资历尚浅,暂无计划。

broadviewcheemi commented 7 years ago

@pigcan 感谢您的回复,如果将来有计划,欢迎随时联系我

zhangxiaoru commented 7 years ago

刚学dva,effects里的方法是dispatch调用的吗,怎么判断dispatch调用reducer还是effects里的方法啊,谢谢

soda-x commented 7 years ago

@zhangxiaoru

确切说 dispatch 用以触发 action,而 action 是一个对象,该对象中会用来描述 需要调用的 reducer 或者 effects,以及调用传递的数据。

怎么判断dispatch调用reducer还是effects里的方法啊

没明白你问的什么,触发一个 action 时的 type 你在写的时候不就已经知道是 reducer 还是 effects 么。

huangshenghao commented 7 years ago

66666

martinwithyou commented 6 years ago

你说的啥。。。。可以再加一个demo。。。。ok

QYEtermal96 commented 6 years ago

请问那个图可以再详细一点吗?同时state不是只根据model中的reducer更改吗?

OwnGhy commented 6 years ago

请教一下dva测试reducer和effetc怎么写呐,官网没有找到详细的说明,但是看你上面给出的目录里面有tests,而我的dva-cli初始化的项目却没有呢?

PleaseTryAgain commented 5 years ago

我想问下关于reducer的内容:

reducers:{ add(state, action){ return { ...state, name: action.payload } } }

和 下面这种写法有什么区别么?

reducers:{ add(state, action){ state.name = action.payload; return state } }

缩进不起作用。 主要就是 返回一个新的state 和 再原state上做修改,然后返回有什么区别? 我两个都试了试好像没什么区别,官方文档上是说返回一个新的state @pigcan

soda-x commented 5 years ago

@PleaseTryAgain

主要是怕有引用问题吧

OwnGhy commented 5 years ago

从我的理解的来说,reducer并不是我们去改变state的地方,reducer应该是纯函数,只负责计算state,不负责存储state,如果在reducer里面直接修改state就相当于去存储state了。

PleaseTryAgain commented 5 years ago

@PleaseTryAgain

主要是怕有引用问题吧

官方是这样说的: 这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用 但是我在时间旅行上看不出来有什么差别。。。。