因为心想着让毕设真正能够落地,过年放假这段时间一直在研究 Ant Design Pro 的源码,力图寻找一种对新人友好、开发体验优良的模式。然后从 pro 研究到 roadhog 与 dva 再从 roadhog 与dva 研究到 umi ,总算有些满意的结果。在此进行一下分析与总结。
本文的顺序便是基于前端开发必会涉及几个环节的来梳理 Ant Design Pro 、 umi 的开发逻辑。
从这几块来讲:组织架构、app 启动、路由、布局、页面、数据模型、mock。
组织架构
先来看看 Ant Design Pro (下称 pro )的官方提供的文件组织结构:
1 | ├── mock |
如果将抛开 model 、 components 、services 这些业务组件,组成一个最小可运行的页面,最关键的内容如下:
1 | ├── src |
再来看看 umi 的最核心的目录结构。
1 | ├── src // 源码目录,可选,把里面的内容直接移到外面即可 |
如果大家仔细看的话umi 与 pro 的核心目录结构其实基本一致。
layout
文件夹控制全局布局,routes
在 pro 中是具体的业务界面,而在 umi 中则是改用 pages
这个词。这对于使用者来说会更加直观。( routes
这样的词其实并不易于理解。我一开始用 pro 的时候花了老半天才理解清楚 routes 到底意味着什么。)
但是粗看的话 umi 比 pro 少了不少文件,比如入口的以下这两个文件:
1 | ├── src |
那么这两个文件到底是干什么的?就要进入第二部分「启动方式」细说。
启动方式
pro 中 index.js
是整个 app 的控制文件,采用 dva
的写法。最小可用代码为:
1 | import dva from 'dva'; |
而这个启动 app 的重要文件在 umi 中为什么看似会没有呢?实际上,这个文件隐藏在了 .umi
这个临时文件夹下
1 | // .umi 的目录结构 |
如果打开 umi.js
1 | // pages/.umi/umi.js |
可以看到,这两个文件的思路基本保持一致,创建 history、model(后文会有创建 model 的版本) 再引入 router, 最后启动 app。而 umi 采用了更加原生的方式来启动 app ,而不是之前那样直接封死到 dva ,这保证了 app 启动时的扩展性。就如下文会提到的 umi-plugin-dva 。
那 router 是什么?它有什么用?让我们往下看到第三部分:路由。
路由
先来看看 pro 的 src
目录下的路由是干什么的。删掉个别不大相关的代码后,router.js
的代码如下
1 | import React from 'react'; |
总体来说有以下几个特征:
- 传出一个整体路由控制的配置函数(给 dva 加载路由);
- 利用
getRouterData
获得目前 app 的路由数据routerData
; - 利用
routerData
中的两个参数得到两种不同的页面布局UserLayout
和BasicLayout
; - 利用
Switch
匹配不同路由控制跳转; - 利用
ConnectedRouter
传递 history props 给子组件。
那我们再来看下 .umi 文件夹下面的 router.js
是怎么样的。
1 | // pages/.umi/router.js |
可以看到,umi 的 router.js
与 pro当中的基本一致,不一样的就是其中的 Route
完全由 umi 基于 pages
文件夹或者路由配置文件表生成,而不是 getRouteData
。
那 getRouteData
里面有什么?这个部分还是看一下官方文档的介绍吧~
目前在脚手架中,除了顶层路由,其余路由列表都是自动生成,其中最关键的就是中心化配置文件
src/common/router.js
,它的主要作用有两个:
- 配置路由相关信息。如果只考虑生成路由,你只需要指定每条配置的路径及对应渲染组件。
- 输出路由数据,并将路由数据(routerData)挂载到每条路由对应的组件上。
这样我们得到一个基本的路由信息对象,它的结构大致是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 {
'/dashboard/analysis': {
component: DynamicComponent(),
name: '分析页',
},
'/dashboard/monitor': {
component: DynamicComponent(),
name: '监控页',
},
'/dashboard/workplace': {
component: DynamicComponent(),
name: '工作台',
},
}
大家看到没有?其实 umi 在这里尝试解决的问题是路由信息的配置。如果页面简单还好说,如果页面数量达到几十乃至上百,这么多的路由要怎么去一一配置?比如看下 pro 默认提供的路由。
1 | '/': { |
如果按这样的方式一个一个去配置和维护路由,我想头发都要掉光了吧。那既然能够在 src/common/router.js
中直接采用数据化的方式来配置路由,何不直接将路由与文件的结构直接一一映射呢?umi 在路由上做的其实就是这件事——基于文件结构自动生成配置路由。(是不是回到了前端最初的模样?hhh)
另外,pro 的 router.js
在布局上可以自己引入不同的页面布局,而 umi 默认引入 layout
文件夹中的 index.js
文件作为整体的布局。
1 | //pro - router.js |
这会导致如果需要存在不同的页面布局,这里就很难调整。而这也是未来 umi 需要解决的一个棘手的问题。
我想细心的朋友可能会想:哎,如果这里是配置了页面布局的路由,那在 pro 中具体的业务逻辑的页面组件到底是怎么实现路由的?
至于这个问题,就一起来看下一个章节吧~
布局
pro 当中 layout 的文件夹包含了不同种的页面布局。默认有有四种不同的布局。一种是通用布局框架 BasicLayout
,一种是登录页面框架 UserLoginLayout
,,还有一种是页头布局框架 PageHeaderLayout
,一种是空白页面框架BlankLayout
。
其他布局框架我个人觉得没有太多可以说的,主要是 BasicLayout
的需要进行简单的介绍。
1 | class BasicLayout extends React.PureComponent { |
这个页面中除了布局元素以外,还通过redirectData.map
、 getRoutes
函数功能耦合了路由的跳转。那 getRoutes
函数是干嘛的?关于 getRouteData
引用官方的一段介绍:
为了帮助自动生成路由,在
src/utils/utils.js
中提供了工具函数getRoutes
,它接收两个参数:当前路由的 match 路径及路由信息 routerData,主要完成两个工作:
- 筛选路由信息,筛选的算法为只保留当前 match.path 下最邻近的路由层级(更深入的层级留到嵌套路由中自行渲染),举个例子 (每条为一个 route path):
1
2
3
4
5
6
7 > // 当前 match.path 为 /
> /a // 没有更近的层级,保留
> /a/b // 存在更近层级 /a,去掉
> /c/d // 没有更近的层级,保留
> /c/e // 没有更近的层级,保留
> /c/e/f // 存在更近层级 /c/e,去掉
>
- 自动分析路由 exact 参数,除了下面还有嵌套路由的路径,其余路径默认设为 exact。
经过
getRoutes
处理之后的路由数据就可直接用于生成路由列表:
1
2
3
4
5
6
7
8
9
10 > // src/layouts/BasicLayout.js
> getRoutes(match.path, routerData).map(item => (
> <Route
> key={item.key}
> path={item.path}
> component={item.component}
> exact={item.exact}
> />
> ))
>
简单的来说就是根据上文提过的 getRouteData
在 BasicLayout
模板页中自动生成对应的路由,挂载上相应的页面组件。
而这种方式赋予了 pro 嵌套路由的能力,提升了页面类型的灵活性,但是又一方面耦合了页面布局与路由,个人感觉可能在写法上会稍微有点点绕。
而 umi 的页面布局则非常简单粗暴
1 | export default function(props) { |
可以看到,layout 是一个无状态函数,只负责布局状态的呈现,并不再负责路由的跳转。这样就把布局与路由的关系完全解耦了。
(实际上我在这边躺坑躺了好久,因为一开始没有给 layout 传进去路由的 history 参数,直接用上 pro 的 layout 一直报错。后来才知道只有在 Switch 里面的组件才能够获得 history 参数。最后用 withRouter 把 Layout 包起来才解决…)
不过随着而来的一个问题就是 layout 的设置问题,因为与路由解耦之后似乎就很难用一种灵活的方式来配置 Layout ,比如识别 /user
跳转到完全不同的登陆页面中。(至少作为一名小萌新并想不到什么好方法…)
页面
讲完了布局那么接下来是页面了。在 pro 当中,其实每个 routes 的文件夹就是一个对应的业务界面。因为页面更多的就是一些业务的界面逻辑,其实并没有什么好说的。在这边主要对比一下pro 和 umi 页面的创建和挂载的步骤。
pro
,在 pro 中如果需要新建一个页面,除了在在这个 routes 下面写好对应的业务代码之外,你还需要把这个页面的路由添加到common/router.js
中,添加的参数有两个:一个路由地址,另一个是加载的页面组件。格式我在前面中说过,大概如下图所示:
1 | '/form/basic-form': { // 路由地址 |
如果希望在菜单栏中呈现,还需要在对应的 menu.js
添加对应的菜单信息,这里不再细讲。
umi
umi 中如官网所述,只需要在 pages
文件夹下建立对应的页面文件即可。非常简单方便。
那么接下来开始介绍模型的部分。
模型
Pro 数据模型采用了 dva ,数据模型放在 models
这个文件夹下。默认情况下会直接引入各个模型文件,因为其中有一个 index.js
的文件,能够将其他所有的模型文件全部引入。
因此,只需要在 models
里面添加对应的模型文件即可。非常简便,轻松,易于用户理解。
那 umi 如果需要用 dva 呢?很简单。首先安装 umi-dva-plugin
插件。
1 | yarn add umi-dva-plugin |
然后在项目的根目录添加.umirc.js
添加以下内容:
1 | export default{ |
接下来直接在 models
文件夹下写 model 就好啦。
如果你关心这个插件到底做了什么。那么可以往下看一看,不然就跳过吧~
实际上,采用 umi-dva-plugin
会生成一个 dvaContainer.js
的临时文件。dvaContainer
会自动引入 models 下面的文件。
1 | // .umi/dvaContainer.js |
同时在 umi.js
中,umi 会引入 dvaContainer.js
从而实现 dva 的使用。
1 | //.umi/umi.js |
可以看到,到目前为止 umi 的启动文件的设计思路和 pro 中 src/index.js
就一模一样了。
mock
基本开发的环节基本说完了, 接下来说说 pro 和 umi 的数据 mock。
在 pro 中采用 .roadhogrc.mock.js
文件来统一 mock 请求。
1 | // pro - .roadhogrc.mock.js |
这里规定的格式为:
1 | { |
方法既是 RESTful 那一套请求方法, 路由不再多说,函数可以是(req,res)=>{...}
这种请求返回函数,也可以直接返回已经准备好的 mock 数据。
在 umi 中,也是采用和 pro 一样的约定方式。也就是说,只要将.roadhogrc.mock.js
直接改成.umirc.mock.js
就可以在 umi 里面使用了。唯一需要注意下的,便是 resquest.js
的下面这句引用
1 | import store from '../index'; |
需要改成:
1 | const store = window.g_app._store; |
如何评价
先说说 pro ,pro 提炼出来的业务开发模式个人认为非常高效。它可以让开发者专注于业务页面的开发,不用关注或者更少去关注中间过程的实现,从而更加聚焦于业务逻辑而不是繁琐的配置。
当理解 pro 之后,再去研究 umi,你会发现 umi 其实是对 pro 的一次升级,意图让开发体验变得更加极致。
如果要我来一句话介绍的话,umi 就是一套零配置,引导开发者进行最佳实践的前端框架。「约定高于配制」的思想在 umi 身上体现得淋漓尽致。
和 parcel.js
比什么优点?我觉得 umi 约定了一套好用、高效的开发模式,这是单纯的 webpack、parcel 这样的工具无法比拟的。你可以不再使用一些很低效的方式把自己搞的一团乱,而是使用一种更加高效、更加直觉化的模式,从而解放开发者,提高生产力。
只要文档完善,我相信 umi 将会是对新手非常友好的一套开发框架。因为这就仿佛回到了网站开发最初的样子:在根目录添加一个 index.html 就能在浏览器里访问,但是却拥有了更加灵活的控制和更强大的实现能力。
当然约定与配置的比重必须有所权衡,不然真的就会是「用起来爽歪歪,改起来火葬场」了。