工作中的很多项目都是基于 umi 开发的,所以最近学了一下 umi 的源码,对这个框架的好感又多了一些~。如果你也感兴趣的话,欢迎跟我一起来学习or温习一下。
这篇文章会带你从项目运行开始切入,循序渐进地了解 umi 核心的部分。
我们创建好 umi 项目之后,第一步一般是使用 yarn start
命令去运行它,执行的是 umi dev
,也就是 umi 命令,所以先来看看 umi 命令是怎么定义的。
下面提到的源码目录在 umi 的源码仓库 /packages 目录下。
umi 命令
umi 命令的定义在 /umi/bin
目录,默认执行 /umi/src/cli.ts
,逻辑是这样的:
1. 参数规范化
使用 yargs-parser
库处理命令行参数,处理 version
、 help
命令的逻辑。
2. 【dev】启动新的进程
这里说一下 dev
和 build
的区别,开发环境会额外启动新的进程来运行服务,并做一些事件监听、处理通信的工作。
这部分代码在 /umi/src/utils/fork.ts
,核心逻辑主要有三部分:
##### 2.1 处理端口号
默认端口是 8000,如果被占用会自动加 1。
##### 2.2 启动新的进程
使用 child_process
的 fork
创建新的子进程,执行的是 /umi/src/forkedDev.ts
,这个文件里的逻辑就是下面的第3步和第4步了。
##### 2.3 处理通信
创建好子进程之后,会监听这个子进程的事件来传递消息,这里会处理两种事件: RESTART
重启 和 UPDATE_PORT
更新端口。
主进程(运行 umi 命令的当前进程)会监听退出等事件从而 kill 掉子进程以及触发插件的 onExit
事件。
3. 初始化 webpack
这部分代码在 /umi/src/initWebpack.ts
。
初始化之前会先去配置文件里找有没有 webpack5
或 mfsu
的配置,有的话初始化 webpack5,没有就初始化 webpack4。
这里提一个重要的点,umi 将类似 webpack 的依赖封装在了 deps
, 之前的 《Umi 4 设计思路 》 演讲里有提到中间商的概念,umi 会做一些预打包的工作来解决版本稳定性的问题。
4. 构造 Service 对象
终于讲到 umi 的核心部分了,代码很短但很重要。
await new Service({ ...params }).run({
name: 'dev', // or 'build'
args,
});
这里有个特别的处理,初始化使用的 Service
做了个小小的封装,默认内置了 preset-built-in
,这个 preset 就是 插件的扩展方法 的实现部分。
import { IServiceOpts, Service as CoreService } from '@umijs/core';
class Service extends CoreService {
constructor(opts: IServiceOpts) {
...
super({
...opts,
presets: [
require.resolve('@umijs/preset-built-in'),
...(opts.presets || []),
],
plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
});
}
}
plugin 是 umi 设计中非常重要的部分,插件化的思想使得 umi 可以轻松自如的控制流程和实现定制,这种思想有一个学术名称 微内核架构
。
微内核架构
这部分就不展开说啦,我也是查资料摘了一些重要的点,重点了解一下核心系统的设计思想, Service
类就是 umi 的核心系统。
核心类 Service
代码在 /core/src/Service/Service.ts
,这个类的结构非常简单,只有两部分:构造函数 和 run()
方法。
constructor 构造函数
初始化阶段的主要工作是收集配置,从这里能看出来作者是如此的心细,环境变量的优先级,配置文件的优先级,就连 page(s)
目录名这么细节的点都安排的明明白白😂。
初始化的属性从图里可以清晰的看到,就不赘述了,这里只列出来了(我觉得)重要的部分。
初始化 presets 和 plugin 的同时,会把所有插件的信息记录下来,也就是 插件注册表
,以便于管理和运行插件。
run()
跑起来~
总结来说,这部分就是按生命周期前进:
- 初始化 presets 和 plugins
- 设置一些钩子
- 最后执行
runCommand()
运行 umi 命令相关的具体逻辑
Service 有9个生命周期,作用是后续插件内执行一些动作的判断依据。
export enum ServiceStage {
uninitialized,
constructor,
init,
initPresets,
initPlugins,
initHooks,
pluginReady,
getConfig,
getPaths,
run,
}
presets 和 plugins 的初始化逻辑是一样的,摘录几行重要的伪代码如下:
const api = this.getPluginAPI({ id, key, service: this })
// 获取 PluginAPI 对象,用来传递给 plugin 本身,也就是插件的实现规范
this.registerPlugin(preset)
// 插件注册表,执行的代码是 this.plugins[preset.id] = preset
const { presets, plugins } = await this.applyAPI({ api, apply: preset.apply })
// 执行插件内部的 apply 方法,即 return apply()(api)
PluginAPI
这个类里定义了 Plugin的核心方法,umi 文档已经介绍的很详细了。
applyPlugin()
插件执行的核心方法,参数中的 type
决定了执行插件的逻辑, add 和 modify 会添加或修改 initialValue,并在运行完后将结果返回,event 顾名思义作为事件存在。
tapable(webpack) 我也还在学习总结中,有优秀的文章欢迎推荐给我~。
runCommand()
这里主要看一下 dev
命令相关的逻辑,代码在 preset-built-in/src/plugins/commands/dev/dev.ts
。
preset-built-in
这是 umi 默认的插件集,实现的功能主要有五部分:
registorMethods
统一注册方法。
route
routes 配置的实现。
写临时文件
src/.umi
目录里的文件生成过程都在这里,包括项目运行的入口、路由、插件等等。配置
umi 的文档 配置 里的实现。
commands
命令具体的实现逻辑,以及 webpack 配置修改相关。
就分享到这里吧,如有错误欢迎指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。