当前umi版本为4.0.0,由于本人水平有限,如有错误望指正。
学习umi使用可以前往Umi 4 开发实战小册
前言
下载项目后,在根目录执行pnpm install,调试时执行pnpm dev,然后就可以运行examples目录下的测试项目调试了
应用代表用户项目
阅读本文前,期望您已经了解了如下几个插件
esbuild
这个插件都比较熟悉,不作介绍
joi
配置校验工具,官网
pirates
用于处理node加载(require) es6、ts文件
详情可以看阿宝哥文章如何为 Node.js 的 require 函数添加钩子?
在工程中如下使用,就可以在
// /packages/utils/src/register.ts
let registered = false // 不重复添加
// 用于解析完后移除addHook
let revert:() => void = () => {}
const COMPILE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
const HOOK_EXTS = [...COMPILE_EXTS, '.mjs'];
// implementor为解析方式,一般用esbuild
// exts需要处理的后缀名集合
export function register(opts: { implementor: any; exts?: string[] }) {
files = [];
if (!registered) {
revert = addHook(
// 这里基本上就是返回esbuild编译后的内容
(code, filename) =>
transform({ code, filename, implementor: opts.implementor }),
{
ext: opts.exts || HOOK_EXTS,
ignoreNodeModules: true,
},
);
registered = true;
}
}
tapable
一套优雅的发布订阅插件模式
源码中Service类(/packages/core/src/service/service.ts)的applyPlugins方法就应用了其事件流程
chokidar
chokidar.watch(paths, [options])
用于监听文件变化,解决了fs.watch和fs.watchFile的不足,并且 API 简单易用且监听效率更加高效
vite、webpack-dev-server、vscode等多个有名的库都使用了它
scripts
这个umi-scripts指令是umi内部的指令,bin文件中执行的逻辑大概就是运行umi-scripts [指令],就可以执行scripts下文件名的ts脚本了。
例如umi-scripts turbo --cmd build就是执行scripts/turbo.ts
umi
内容不是很多,稍微提一下,执行顺序:从上至下依次执行
/cli/cli.ts
当我们的umi项目执行umi [指令]时,会执行文件下的run函数
这里校验了node版本、设置了一些环境信息,然后如果是dev,则执行dev函数,否则直接运行new Service().run2(),由于dev中也会创建Service,我们跳过这块
/cli/dev.ts、/cli/fork.ts
开启进程去执行forkedDev
/cli/forkedDev.ts
创建服务,并运行,并在进程关闭前调用onExit方法出发插件的一些行为
service.applyPlugins({
key: 'onExit',
args: {
signal,
},
})
/service/service.ts
在构造函数中初始化了默认preset:'@umijs/preset-umi',默认的插件join(cwd, 'plugin.ts')(即项目根目录下允许创建自定义插件),默认配置文件位置DEFAULT_CONFIG_FILES,应用的启动目录env.APP_ROOT || process.cwd()
run2为run的前置函数,目前适配了版本和帮助指令的简写
@umijs/core
该库大概做了以下几件事
- Config读取配置文件内容,用户配置>系统配置
- getPaths获取用户项目路径
- 初始化插件,preset也是插件的一种
- 读取插件的配置信息,schema、default、onChange
applyPlugins广播核心插件方法modifyConfig处理schema用户配置modifyDefaultConfig处理default用户配置modifyPaths处理getPaths的路径modifyAppData处理应用属性onCheck启动应用前的最后检查onStart通知应用即将启动
- onStart后执行dev或build等的fn函数
执行run函数
首先获取pkg即应用的package.json内容,后续又可能获取其中安装的插件,用于自动加载插件,但我目前的版本没有开启
// dependencies
// ...Object.keys(opts.pkg.devDependencies || {})
// .concat(Object.keys(opts.pkg.dependencies || {}))
// .filter(Plugin.isPluginOrPreset.bind(null, type)),
然后构造Config
/config/config.ts Config
- 构造函数:记录了cwd、环境类型(生产、开发、测试)、主配置文件位置(通过Config.getMainConfigFile函数遍历配置文件直到匹配上)
- utils的
getAbsFiles用于获取文件的绝对路径 getUserConfig:通过Config.getConfigFiles返回配置文件列表(为什么是列表,因为有.umirc.ts.umirc.dev.ts.umirc.local.ts),然后通过Config.getUserConfig获取配置文件的内容并合并当前环境下的所有配置Config.getUserConfig:用到了之前提到的pirates库辅助获取配置文件内容,文件解析用的esbuildgetConfig:对getUserConfig进行了封装,添加了校验配置是否合法的逻辑Config.validateConfig:校验内容是否合法,用到了之前提到的joi库,每一项配置都匹配一个schema(schema来源于插件的api.describe)watch:使用chokidar.watch监听配置文件变化,并做了深度比对,回调文件变化的部分
/service/path.ts getPaths
然后调用getPaths获取部分绝对路径配置,其中包含src目录、page目录、node_modules目录、构建产物dist目录、自动生成的.umi文件目录位置等。既然都是单独获取,那么我们就可以通过插件修改,比如调整缓存文件.umi的位置、打包产物的位置
const paths = getPaths({
cwd: this.cwd,
env: this.env,
prefix: this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME,
});
/service/plugin.ts Plugin
随后调用Plugin.getPluginsAndPresets方法初始化插件
const { plugins, presets } = Plugin.getPluginsAndPresets({
cwd: this.cwd,
pkg,
plugins: [require.resolve('./generatePlugin')].concat(
this.opts.plugins || [], // 这里包含了应用根目录的plugin.ts插件
),
presets: [require.resolve('./servicePlugin')].concat(
this.opts.presets || [], // 这里在我们的流程里是preset-umi插件
),
userConfig: this.userConfig,
prefix, // 用于读取环境变量中注册的插件
});
getPluginsAndPresets:我们可以看到该函数中,读取了初始化传入的插件、环境变量中注册的插件、用户配置文件的插件。然后构造Plugin实例- 构造函数:初始化了插件的id(不可变)、key(key在后续可以修改),注册了apply函数用于返回插件的内容(这里又用到了
pirates来读取) merge:用于重设插件的key、config、enableBy。对注册schema和default有用
读取插件中的presets、plugins
这里类似于深度遍历presets中包含的presets。比如我每个项目都要注册相同的10个插件,但我不想一一注册,于是可以注册一个presets,返回一个插件集。preset-umi和max等其实就是一个插件集
while (presets.length) {
await this.initPreset({
preset: presets.shift()!,
presets,
plugins: presetPlugins,
});
}
将其余非preset插件注册
plugins.unshift(...presetPlugins);
while (plugins.length) {
await this.initPlugin({ plugin: plugins.shift()!, plugins });
}
这里涉及到两个函数initPreset、initPlugin,initPreset内部调用了initPlugin,initPlugin内部创建了PluginAPI,下面我们来看下PluginAPI
/service/pluginAPI.ts PluginAPI
该类的作用是暴露插件api,让我们自定义插件更加得心应手。
- 构造函数:记录service和当前注册的插件,调整日志输出(log时自带插件前缀)
describe:插件中调用时会- 重置插件key、config、enableBy
- schema传入库joi,配置校验
- schema配置用于校验.umirc的每一项属性是否符合schema规则
registerCommand:插件中调用时用作添加指令,最后指令内容会添加到service.commands上,例如dev。alias参数是别名,如果有再重复注册即可,例如version的别名是v。
api.registerCommand({
name: 'dev',
})
registerGenerator:注册微生成器,允许插件生成代码或配置等,绑定在service.generators上register:订阅hook到service.hooks上,会在事件广播时消费registerMethod:在service.pluginMethods上注册全局函数,允许其它插件调用。如果只传了名称,则改名称的函数调整为注册函数,注册的函数将绑定到service.hooks上。用以下两个例子区分
api.registerMethod({name: 'a', fn() {}})
// 可以直接调用
api.a()
api.registerMethod({name: 'a'})
// 依赖收集
api.a(() => {console.log('1')})
api.a(() => {console.log('2')})
// 广播发布
api.applyPlugins({key: 'a'})
registerPresets/registerPlugins为插件添加插件,不会绑定到service上,暂时不知道作用skipPlugins:设定需要跳过的插件,在hook执行时判断是否跳过hookproxyPluginAPI:代理PluginAPI,用于暴露更多插件api给自定义插件使用。例如service.pluginMethods等
initPlugin方法
了解了PluginAPI,让我们回到initPlugin方法,它主要做了如下几件事
- 将plugin绑定到PluginAPI对象上
- 配置代理,暴露更多属性
- 执行插件函数传入代理对象
proxyPluginAPI,让插件可以做更多事情 - 绑定插件到
service.keyToPluginMap上,方便跳过等其它操作 - 给调用方递归创建插件
回到run函数内,下面循环将api.describe执行后调整的插件配置给绑定到service上
for (const id of Object.keys(this.plugins)) {
const { config, key } = this.plugins[id];
if (config.schema) this.configSchemas[key] = config.schema;
if (config.default !== undefined) {
this.configDefaults[key] = config.default;
}
this.configOnChanges[key] = config.onChange || ConfigChangeType.reload;
}
至此,插件初始化及相关内容已完成
/service/servicePlugin.ts
插件通过registerMethod函数注册核心可以广播的事件
开始事件广播
上面提到过,允许我再复制一遍^_^
modifyConfig处理schema用户配置modifyDefaultConfig处理default用户配置modifyPaths处理getPaths的路径modifyAppData处理应用属性onCheck启动应用前的最后检查onStart通知应用即将启动
最后执行如下代码,表示执行指令的fn函数
let ret = await command.fn({ args });
applyPlugins
分三种类型:新增、编辑、事件发布
通过异步串行瀑布钩子给每个依次钩子执行,并将结果返回给下一个钩子
/service/generatePlugin.ts
微生成器的启动指令
结语
至此@umijs/core大部分内容已解读完成,后续有时间再一一解读umi的其它内容