umi阅读分为5个部分,分别是:
- 启动过程:从umi dev到如何调用umi的核心service
- 实例化过程:Service的实例化过程,以及启动dev之后的编译启动
- 渲染过程:研究umi如何将模块挂接渲染的
- routes渲染过程:从渲染入口研究umi的routes渲染机制
- 插件机制:重点研究umi的插件机制
以下代码以umi的3.5.20版本为例,主要内容以源码+个人解读为主
实例化过程
内容:在上一模块的基础上,发现umi的核心类是Service,接下来内容主要探究Service是干什么,实例化的过程以及调用run()的过程。
找到packages/core,该包index文件导出了一系列的对象,目前我们只关注Service,于是找到packages/core/Serive/Serive,代码较多,一步步来解读。
export default class Service extends EventEmitter{
constructor(opts: IServiceOpts){
// 获取一些环境参数
this.cwd = opts.cwd || process.cwd();
this.pkg = opts.pkg || this.resolvePackage();
this.env = opts.env || process.env.NODE_ENV;
// 注册babel
this.babelRegister = new BabelRegister();
// 获取用户配置(未校验)
const configFiles = opts.configFiles;
this.configInstance = new Config({
cwd: this.cwd,
service: this,
localConfig: this.env === 'development',
configFiles:
Array.isArray(configFiles) && !!configFiles[0]
? configFiles
: undefined,
});
this.userConfig = this.configInstance.getUserConfig();
// 初始化插件
const baseOpts = {
pkg: this.pkg,
cwd: this.cwd,
};
// 通过pathToObj返回后的presets的合并对象
this.initialPresets = resolvePresets({
...baseOpts,
presets: opts.presets || [],
userConfigPresets: this.userConfig.presets || [],
});
通过pathToObj返回后的plugins的合并对象
this.initialPlugins = resolvePlugins({
...baseOpts,
plugins: opts.plugins || [],
userConfigPlugins: this.userConfig.plugins || [],
});
// ...
}
}
首先Serive继承了EventEmitter,EventEmitter是一个事件总线的库,用于事件订阅触发的操作,表明service是也是一个事件总线管理的库。
在获取用户配置步骤,实例化了config实例,传递了相关的serive实例,接着getUserConfig(),这里是根据umi提供的获取配置的各种规则获取到用户定义的当前环境下的配置。
规则:从以下几个文件中加载配置信息,优先级是'.umirc.ts','.umirc.js','config/config.ts','config/config.js'
总:service的过程就是获取相关的配置和初始化插件。
回答:defineConfig的返回值如何传递给Service?
umi根据文件路径规则获取到导出的默认的config。为什么这样做呢?umi框架的一大特点是
约定大于配置,也就是说按照他说明的格式创建文件即可开通和使用功能,比如创建mock文件夹则开启mock数据功能。 好处有:项目代码结构清晰,需要什么知道找到文件夹/文件即可;减少配置的负担。
Service实例化完了,按照启动流程,service会执行service.run(),找到run函数
args._ = args._ || [];
// shift the command itself
if (args._[0] === name) args._.shift();
this.args = args;
// 初始化
await this.init();
// 设置状态
this.setStage(ServiceStage.run);
// 启动onStart插件
await this.applyPlugins({
key: 'onStart',
type: ApplyPluginsType.event,
args: {
name,
args,
},
});
// 运行dev命令
return this.runCommand({ name, args });
}
run主要操作:初始化,设置状态,然后启用onStartplugin,最后运行命令。一一来看。
init是初始化各种插件,并依次调用onPluginReady、modifyDefaultConfig、modifyConfig、modifyPaths,同时初始化了hooks,hooks的作用和来自哪里后续继续研究。
umi的另一特色就是umi本身的功能也是通过插件来实现的,因此内置了许多处理事件的插件,具体的插件使用后续研究。 先看插件调用的写法
import { Plugin, ApplyPluginsType } from 'umi';
// 注册插件
Plugin.register({
apply: { dva: { foo: 1 } },
path: 'foo',
});
Plugin.register({
apply: { dva: { bar: 1 } },
path: 'bar',
});
// 执行插件
// 得到 { foo: 1, bar: 1 }
Plugin.applyPlugins({
key: 'dva',
type: ApplyPluginsType.modify,
initialValue: {},
args: {},
async: false,
});
参数属性包含:
- key,坑位的 key
- type,执行方式类型,详见 ApplyPluginsType
- initialValue,初始值
- args,参数
- async,是否异步执行且返回 Promise
ApplyPluginsType
主要在插件利用,项目代码中一般用不到。
运行时插件执行类型,enum 类型,包含三个属性:
- compose,用于合并执行多个函数,函数可决定前序函数的执行时机
- modify,用于修改值
- event,用于执行事件,前面没有依赖关系
前面执行的plugins,从名字和备注可看出,具体的实现需要研究具体的插件实现。
onPluginReady:event,触发插件准备事件。
modifyDefaultConfig:modify,修改默认配置
modifyConfig:modify,修改配置并校验
modifyPaths:modify,修改outputPath路径
onStart:event,触发启动事件
最后一行代码是runCommand,根据名字是执行命令的,而我们的命令是umi dev
async runCommand({ name, args = {} }: { name: string; args?: any }) {
args._ = args._ || [];
// shift the command itself
if (args._[0] === name) args._.shift();
const command =
typeof this.commands[name] === 'string'
? this.commands[this.commands[name] as string]
: this.commands[name];
const { fn } = command as ICommand;
return fn({ args });
}
按照我们的命令,应该是从this.commands对象中取出了dev的命令函数执行,那找找commands对象。在Service文件中,没有找到commonds包含了dev对象地方,同样从入口文件走过来,代码执行了一系列的plugins,显然,是在那里注册了这些命令和插件。那么接下来找找注册命令的地方,完成umi dev的流程。
v3.umijs.org/zh-CN/plugi… 官网提供了registerCommand的命令,显然是用来注册命令的,那dev应该是通过这个方法注册了命令,那在什么时候注册的呢?往回走packages/umi/src/ServiceWithBuiltIn.ts入口文件实例化service的时候,提供了一个@umijs/preset-built-in的presets,然后在packages/preset-built-in/src/index.ts找到了提供的一系列内置插件和命令
export default function () {
return {
plugins: [
//...
// commands
require.resolve('./plugins/commands/dev/dev'),
],
};
}
显然在初始化插件集的过程中,同时把dev命令注册了进去。
看到插件地址dev文件
// 创建插件的方式,会默认注入一个api对象,可通过api对象创建自己的插件,显然这里提供的也是dev的插件
export default (api: IApi) => {
//...
// 注册dev命令
api.registerCommand({
name: 'dev',
description: 'start a dev server for development',
fn: async function ({ args }) {'
// 具体操作
// 环境参数获取
// 开启热更新
if(watch){
// 监听package.json变化
const unwatchPkg = watchPkg({
cwd: api.cwd,
onChange() {
console.log();
api.restartServer();
},
});
unwatchs.push(unwatchPkg);
// 监听config变化
const unwatchConfig = api.service.configInstance.watch({
userConfig: api.service.userConfig,
onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
// 监听变化,决定是否需要重启服务
}
}
}
// dev 上面是dev的准备工作,下面是dev执行核心
// 1.调用webpack方法,获取webpack实例化编译实例complier
// 2.使用webpack-dev-middleware封装complier,并处理成server能接受的结构
const { bundler, bundleConfigs, bundleImplementor } =
await getBundleAndConfigs({ api, port });
const opts: IServerOpts = bundler.setupDevServerOpts({
bundleConfigs: bundleConfigs,
bundleImplementor,
});
const beforeMiddlewares = [
...(await api.applyPlugins({
key: 'addBeforeMiddewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {},
})),
...(await api.applyPlugins({
key: 'addBeforeMiddlewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {},
})),
];
const middlewares = [
...(await api.applyPlugins({
key: 'addMiddewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {},
})),
...(await api.applyPlugins({
key: 'addMiddlewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {},
})),
];
// 实例化server
server = new Server({
...opts,
compress: true,
https: !!isHTTPS,
headers: {
'access-control-allow-origin': '*',
},
proxy: api.config.proxy,
beforeMiddlewares,
afterMiddlewares: [
...middlewares,
createRouteMiddleware({ api, sharedMap }),
],
...(api.config.devServer || {}),
});
const listenRet = await server.listen({
port,
hostname,
});
return {
...listenRet,
compilerMiddleware: opts.compilerMiddleware,
destroy,
};
})
}
dev命令进行了文件变更监听,初始化编译中间件,实例化服务网络server。
了解上面的内容,需要了解一些前置内容:webpack和webpack-dev-middleware
我们知道umi默认采用的是webpack编译,而webpack-dev-middleware则是实现热更新的基础库,webpack-dev-middleware是一个容器,可以把webpack处理后的文件传递给一个服务器,特点如下:
1.监听资源变更,如果代码变化,则停止提供旧版bundle然后自动打包,直到编译完成
2.快速编译,打包后文件直接写入内存
webpack-dev-middleware的简单使用
const webpack = require("webpack");
const express = require("express");
const WebpackDevMiddleware = require("webpack-dev-middleware");
const app = express();
const compiler = webpack(config);
app.use(WebpackDevMiddleware(compiler,{
//...各种配置
}));
app.listen(8000,function(){
console.log("example app listening on port 8000\n")
})
明白了webpack-dev-middleware的简单使用之后,再来看dev命令中的代码,结构就清晰了,初始化了一个bundler,然后调用budler.setupDevServerOpts来构建server需要的对象,对象中包含了编译中间件。
packages/bundler-webpack/index.ts中的setupDevServerOpts函数
setupDevServerOpts({
bundleConfigs,
bundleImplementor = defaultWebpack,
}: {
bundleConfigs: defaultWebpack.Configuration[];
bundleImplementor?: typeof defaultWebpack;
}): IServerOpts {
// 实例化webpack的complier
const compiler = bundleImplementor.webpack(bundleConfigs);
const { ssr, devServer } = this.config;
// 构建webpackDevMiddleware中间件
const compilerMiddleware = webpackDevMiddleware(compiler, {
// must be /, otherwise it will exec next()
publicPath: '/',
logLevel: 'silent',
// if `ssr` set false, next() into server-side render
...(ssr ? { index: false } : {}),
writeToDisk: devServer && devServer?.writeToDisk,
watchOptions: {
// not watch outputPath dir and node_modules
ignored: this.getIgnoredWatchRegExp(),
},
});
// 其他代码
}
而在packages/server/Server/Server.ts中,可以看到server使用了这个编译中间件
compilerMiddleware: () => {
if (this.opts.compilerMiddleware) {
// @ts-ignore
this.app.use(this.opts.compilerMiddleware);
}
},
webpack热更新的内容比较多,这里只提炼了核心的流程。
参考资料: