1

工作中的很多项目都是基于 umi 开发的,所以最近学了一下 umi 的源码,对这个框架的好感又多了一些~。如果你也感兴趣的话,欢迎跟我一起来学习or温习一下。

这篇文章会带你从项目运行开始切入,循序渐进地了解 umi 核心的部分。


我们创建好 umi 项目之后,第一步一般是使用 yarn start 命令去运行它,执行的是 umi dev,也就是 umi 命令,所以先来看看 umi 命令是怎么定义的。

下面提到的源码目录在 umi 的源码仓库 /packages 目录下。

umi 命令

umi 命令的定义在 /umi/bin 目录,默认执行 /umi/src/cli.ts,逻辑是这样的:

image.png

1. 参数规范化

使用 yargs-parser库处理命令行参数,处理 versionhelp 命令的逻辑。

2. 【dev】启动新的进程

这里说一下 devbuild 的区别,开发环境会额外启动新的进程来运行服务,并做一些事件监听、处理通信的工作。

这部分代码在 /umi/src/utils/fork.ts,核心逻辑主要有三部分:

##### 2.1 处理端口号

默认端口是 8000,如果被占用会自动加 1。

##### 2.2 启动新的进程

使用 child_processfork 创建新的子进程,执行的是 /umi/src/forkedDev.ts,这个文件里的逻辑就是下面的第3步和第4步了。

##### 2.3 处理通信

创建好子进程之后,会监听这个子进程的事件来传递消息,这里会处理两种事件: RESTART 重启 和 UPDATE_PORT 更新端口。

主进程(运行 umi 命令的当前进程)会监听退出等事件从而 kill 掉子进程以及触发插件的 onExit 事件。

3. 初始化 webpack

这部分代码在 /umi/src/initWebpack.ts

初始化之前会先去配置文件里找有没有 webpack5mfsu 的配置,有的话初始化 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 的核心系统。

image.png

核心类 Service

代码在 /core/src/Service/Service.ts ,这个类的结构非常简单,只有两部分:构造函数 和 run() 方法。

constructor 构造函数

image.png

初始化阶段的主要工作是收集配置,从这里能看出来作者是如此的心细,环境变量的优先级,配置文件的优先级,就连 page(s) 目录名这么细节的点都安排的明明白白😂。

初始化的属性从图里可以清晰的看到,就不赘述了,这里只列出来了(我觉得)重要的部分。

初始化 presets 和 plugin 的同时,会把所有插件的信息记录下来,也就是 插件注册表,以便于管理和运行插件。

run()

跑起来~

image.png

总结来说,这部分就是按生命周期前进:

  1. 初始化 presets 和 plugins
  2. 设置一些钩子
  3. 最后执行 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 文档已经介绍的很详细了。

image.png

applyPlugin()

插件执行的核心方法,参数中的 type 决定了执行插件的逻辑, add 和 modify 会添加或修改 initialValue,并在运行完后将结果返回,event 顾名思义作为事件存在。

tapable(webpack) 我也还在学习总结中,有优秀的文章欢迎推荐给我~。

image.png

runCommand()

这里主要看一下 dev 命令相关的逻辑,代码在 preset-built-in/src/plugins/commands/dev/dev.ts

image.png

preset-built-in

image.png

这是 umi 默认的插件集,实现的功能主要有五部分:

  1. registorMethods

    统一注册方法。

  2. route

    routes 配置的实现。

  3. 写临时文件

    src/.umi 目录里的文件生成过程都在这里,包括项目运行的入口、路由、插件等等。

  4. 配置

    umi 的文档 配置 里的实现。

  5. commands

    命令具体的实现逻辑,以及 webpack 配置修改相关。


就分享到这里吧,如有错误欢迎指正。

参考

《蚂蚁前端研发最佳实践》文字稿

《Umi 4 设计思路 - 云谦》视频 & 文字版

《umi源码》知乎专栏

umi源码-幕布


Jing
156 声望18 粉丝