工作中的很多项目都是基于 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 配置修改相关。
就分享到这里吧,如有错误欢迎指正。