使用蚂蚁金服的Antd框架有一年多了,现在感觉越用越顺手。但Antd只是个UI框架,这些强大功能的背后是Umi
和Dva
的功劳,路由和Model经过简单的配置就可以使用,在一个组件中还可以实现多个Model的注入。这种丝滑程度,让我不禁想起了SpringBoot。Umi+Dva的确实是一套成熟的前端解决方案,它融合了很多技术和理念,对于刚使用的人来说,学习成本还是挺高的,不像Vue那样容易上手。
简单来说
umi :可以理解为 roadhog + 路由,并辅以一套插件机制,目的是通过框架的方式简化 React 开发
dva :可以理解为 redux + redux-saga,数据流解决方案,和 umi 没有依赖关系,可以独立使用
roadhog 是基于 webpack 的封装工具,目的是简化 webpack 的配置,是一个包含
dev
、build
和test
的命令行工具
Umi介绍
Umi是蚂蚁金服的底层前端框架,特别是从3.0版本开始,它们的slogan就改成了插件化的企业级前端应用框架,插件这个概念让整个umi框架的扩展性大大提高,插件让框架支持各种功能扩展和业务需求。可以说插件就是umi的灵魂,官网文档描述Umi的6大特性:
- 🎉 可扩展,Umi 实现了完整的生命周期,并使其插件化,Umi 内部功能也全由插件完成。此外还支持插件和插件集,以满足功能和垂直域的分层需求。
- 📦 开箱即用,Umi 内置了路由、构建、部署、测试等,仅需一个依赖即可上手开发。并且还提供针对 React 的集成插件集,内涵丰富的功能,可满足日常 80% 的开发需求。
- 🐠 企业级,经蚂蚁内部 3000+ 项目以及阿里、优酷、网易、飞猪、口碑等公司项目的验证,值得信赖。
- 🚀 大量自研,包含微前端、组件打包、文档工具、请求库、hooks 库、数据流等,满足日常项目的周边需求。
- 🌴 完备路由,同时支持配置式路由和约定式路由,同时保持功能的完备性,比如动态路由、嵌套路由、权限路由等等。
- 🚄 面向未来,在满足需求的同时,我们也不会停止对新技术的探索。比如 dll 提速、modern mode、webpack@5、自动化 external、bundler less 等等
约定大于配置
约定大于配置的思想,简化了很多搭建项目时的工作,在使用umi框架的时候,它很多东西都是约定式的。所谓约定式就是指,按照约定好的方式开发,就能达到某种效果,中间的过程由框架帮我们完成,一般的我们通过umi创建的工程,目录结构如下
.
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
└── src/ // 源码目录,可选
├── layouts/index.js // 全局布局
├── locales // 国际化
├── models // model层
├── services // service层
├── pages/ // 页面目录,里面的文件即路由
├── .umi/ // dev 临时目录,需添加到 .gitignore
├── .umi-production/ // build 临时目录,会自动删除
├── document.ejs // HTML 模板
├── 404.js // 404 页面
├── page1.js // 页面 1,任意命名,导出 react 组件
├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
└── page2.js // 页面 2,任意命名
├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
├── global.js // 可以在这里加入 polyfill
├── app.js // 运行时配置文件
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json
在Umi的约定下
- 建一个 locales 目录,就拥有了国际化
- 建一个 models 目录,就拥有了数据流
- 建一个 mock 目录,就拥有了数据 mock
这看起来是非常黑盒非常酷的,用这种方式其实对于团队代码风格的统一是非常有好处的,直接在框架层面就约束了大家的目录组织模式,便于团队维护。
但是缺点也是挺明显的,灵活性不如配置式的高,因为只能按特定的模式来开发,如果原本约定的方式不满足业务需求,就需要额外开发umi插件来魔改原本的功能。而且约定式的开发是相对其他框架来说很特别的一点,对新上手的同学来说需要时间去通读官网文档了解约定式的规则。总的来说有舍有得,我还是倾向于约定的方式,整个项目结构简单清晰
umi 缺点就是无法暴露原始的webpack.config.json 文件,只能按照官方文档上特定的方法进行修改,而文档却并不完善,项目进行顺利时大家笑嘻嘻,但如果碰到棘手的问题,就不得不去深入了解umi这个黑盒的原理。启动
umi dev
后,大家会发现 pages 下多了个.umi
的目录。这是啥?这是 umi 的临时目录,可以在这里做一些验证,但请不要直接在这里修改代码,umi 重启或者 pages 下的文件修改都会重新生成这个文件夹下的文件
插件体系
umi 区别于其他前端开发框架和工具的核心就是它的插件机制,基于 umi 的插件机制,你可以获得扩展项目的编译时和运行时的能力。通过插件支持的功能也会变得更强大,我们针对功能的需要可以去使用修改代码打包配置,修改启动代码,约定目录结构,修改 HTML 等更丰富接口。
可以说插件体系是 Umi 最重要的基建,因为包括 Umi内部实现也是全部由插件构成,Umi通过插件体系实现了技术收敛,把大家常用的技术栈进行整理,让大家只用 Umi 就可以完成 80% 的日常工作。3.x版本重构了整个插件体系。从图看出 presets插件集 和 plugins插件 垂直分层,插件组合成不同的插件集来处理不同的业务场景。而umi本身已经内置了一个针对react应用的插件集(@umijs/preset-react),正是这个插件集中的插件引入了react项目中常用的一些功能如antd,数据流,国际化等。
umi的配置
根目录下的 .umirc.js 是整个应用的配置文件,上面提到的插件也可以在这里配置。配置项可以写在 .umirc.js 文件中,也可以写在 config/config.js 文件中,二者只有一个生效 。我个人倾向于用config.js,关于更详细配置,看官网
Dva介绍
一个轻量级的前端数据流应用框架。通过 reducers, effects 和 subscriptions 组织 model,简化 redux 和 redux-saga 引入的概念
React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。如果开发大应用,还需要解决一个问题。
- 通信:组件之间如何通信?
- 数据流:数据如何和视图串联起来?路由和数据如何绑定?如何编写异步逻辑?
目前流行的数据流方案有:
到底哪一种架构最合适 React ?目前最流行的数据流方案,截止 2017.1,最流行的社区 React 应用架构方案如下。
- 路由: React-Router
- 架构: Redux
- 异步操作: Redux-saga
缺点:要引入多个库,项目结构复杂。dva 是体验技术部开发的 React 应用框架,dva将上面三个 React 工具库包装在一起,简化了 API,让开发 React 应用更加方便和快捷。dva = React-Router + Redux + Redux-saga。
Action
Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type
属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch
函数;需要注意的是 dispatch
是在组件 connect Models以后,通过 props 传入的。
方式一(推荐): 使用dispatch调用models里面effects/reducer声明的方法
this.props.dispatch({
type: 'system/testFunc', //type,命名空间/effects方法名
payload: params, //payload,参数
});
方式二:dispatch带callback回调函数作为第三个参数
//组件中
this.props.dispatch({
type: 'system/testFunc',
payload: params,
callback: (res) => {
if (!!res) {
//do sth
}
}
});
//model中
*testFunc({ payload, callback }, { call, put }){
const { data } = yield call(services.rightSave, payload);
if (data && data.code == 0) {
!!callback && && callback(data);
}
}
connect
方式一:通过connet函数
import { connect } from 'dva';
function mapStateToProps(state) { //state是项目所有的models
const { selectList } = state.system; //获取namespace命名空间为system的models数据state
const { organizationList } = state.global; //全局获取namespace命名空间为global的models数据state
return {
selectList,
organizationList
};
}
export default connect(mapStateToProps)(System); //connect组件
//或者直接解构
export default connect(({ system, global: { organizationList } }) => ({ ...system, organizationList }))(System);
方式二:es6注解方式引入,只能用于class继承组件
@connect(({ system, global: {organizationList} }) => ({
...system,
organizationList
}))
class System extends React.Component{render(){return { }}
在@connect注解中可以注入想使用的任何model,注入后这些model中的数据可以在组件的props参数中取到