UMI源码学习【1】——从umi dev到启动流程
现在工作用的框架以及脚手架主要都是基于umi开发的,学习一下umi源码还是很有必要的,空闲之余学习了一下umi源码
以下分析完全基于自己理解, 基于umi 3.0版本,如有错误,还请斧正
UMI源码学习系列
UMI源码学习【1】——从umi dev到启动流程
UMI源码学习【2】——插件机制
个人认为,学习源码要从入口开始一步一步向下摸索,就像自己平时用umi工程,启动的时候运行下面的命令,我就会很好奇 umi dev之后的流程,然后顺着这个流程往下,其实整个umi的流程就大致熟悉了,然后再对每一个细小的点深入研究一下。
cross-env UMI_ENV=ronghe_dev WATCH_IGNORED=none umi dev
加载cli文件
umi入口就是加载了umi.js,其中加载了cli.js
new Service
cli中很操作简单,核心就是new Service();根据环境变量,开发模式以及生产模式有点区别,生产模式直接new Service();开发模式加载forkedDev,在forkedDev中依旧是new Service(),然后加了一些退出时触发插件中的onExit事件。
(async () => {
try {
switch (args._[0]) {
case 'dev':
const child = fork({
scriptPath: require.resolve('./forkedDev'),
});
// ref:
// http://nodejs.cn/api/process/signal_events.html
process.on('SIGINT', () => {
child.kill('SIGINT');
});
process.on('SIGTERM', () => {
child.kill('SIGTERM');
});
break;
default:
const name = args._[0];
if (name === 'build') {
process.env.NODE_ENV = 'production';
}
await new Service({
cwd: getCwd(),
}).run({
name,
args,
});
break;
}
} catch (e) {
console.error(chalk.red(e.message));
console.error(e.stack);
process.exit(1);
}
})();
其中Service类来自这个文件
import { Service } from './ServiceWithBuiltIn';
在ServiceWithBuiltIn中,集成了了核心类@umijs/core中的service,且引入了一些预设以及插件,我们的dev命令就是在@umijs/preset-built-in中进行注册的。
@umijs/preset-built-in中准备了许多的插件这个后面细讲,很重要。
import { dirname, join } from 'path';
import { IServiceOpts, Service as CoreService } from '@umijs/core';
class Service extends CoreService {
constructor(opts: IServiceOpts) {
process.env.UMI_VERSION = require('../package').version;
process.env.UMI_DIR = dirname(require.resolve('../package'));
super({
...opts,
presets: [
require.resolve('@umijs/preset-built-in'),
...(opts.presets || []),
],
plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
});
}
}
export { Service };
Service
service.js是整个umi源码的核心这个文件要反复多看几遍
上面可以看到主要就是new Service(),看一下service对象主要有那些功能
源码位置:core/src/Service/Service.js中
service主要操作这里不细说,介绍一下大概,后面细说每一个模块。
service加载环境变量、加载预设、加载插件核心就是加载插件。
dev命令
找到源码中preset-built-in
dev命令其实就是注册了一个插件,主要操作就是启动开发服务器,产生一些文件(.umi中文件),监听文件变化重启服务器。
export default (api: IApi) => {
const {
env,
paths,
utils: { chalk, portfinder },
} = api;
api.registerCommand({
name: 'dev',
description: 'start a dev server for development',
fn: async function({ args }) {
const defaultPort =
process.env.PORT || args?.port || api.config.devServer?.port;
port = await portfinder.getPortPromise({
port: defaultPort ? parseInt(String(defaultPort), 10) : 8000,
});
console.log(chalk.cyan('Starting the development server...'));
process.send?.({ type: 'UPDATE_PORT', port });
cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath!,
});
const watch = process.env.WATCH !== 'none';
// generate files
const unwatchGenerateFiles = await generateFiles({ api, watch });
if (unwatchGenerateFiles) unwatchs.push(unwatchGenerateFiles);
// watch config change
if (watch) {
const unwatchConfig = api.service.configInstance.watch({
userConfig: api.service.userConfig,
onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
if (pluginChanged.length) {
api.restartServer();
}
if (valueChanged.length) {
let reload = false;
let regenerateTmpFiles = false;
const fns: Function[] = [];
valueChanged.forEach(({ key, pluginId }) => {
const { onChange } = api.service.plugins[pluginId].config || {};
if (onChange === api.ConfigChangeType.regenerateTmpFiles) {
regenerateTmpFiles = true;
}
if (!onChange || onChange === api.ConfigChangeType.reload) {
reload = true;
}
if (typeof onChange === 'function') {
fns.push(onChange);
}
});
if (reload) {
api.restartServer();
} else {
api.service.userConfig = api.service.configInstance.getUserConfig();
// TODO: simplify, 和 Service 里的逻辑重复了
// 需要 Service 露出方法
const defaultConfig = await api.applyPlugins({
key: 'modifyDefaultConfig',
type: api.ApplyPluginsType.modify,
initialValue: await api.service.configInstance.getDefaultConfig(),
});
api.service.config = await api.applyPlugins({
key: 'modifyConfig',
type: api.ApplyPluginsType.modify,
initialValue: api.service.configInstance.getConfig({
defaultConfig,
}) as any,
});
if (regenerateTmpFiles) {
await generateFiles({ api });
} else {
fns.forEach(fn => fn());
}
}
}
},
});
unwatchs.push(unwatchConfig);
}
// delay dev server 启动,避免重复 compile
// https://github.com/webpack/watchpack/issues/25
// https://github.com/yessky/webpack-mild-compile
await delay(500);
// dev
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: {},
});
const middlewares = await api.applyPlugins({
key: 'addMiddewares',
type: api.ApplyPluginsType.add,
initialValue: [],
args: {},
});
const server = new Server({
...opts,
compress: true,
headers: {
'access-control-allow-origin': '*',
},
proxy: api.config.proxy,
beforeMiddlewares,
afterMiddlewares: [
...middlewares,
createRouteMiddleware({ api, sharedMap }),
],
...(api.config.devServer || {}),
});
const hostname =
process.env.HOST || api.config.devServer?.host || '0.0.0.0';
const listenRet = await server.listen({
port,
hostname,
});
return {
...listenRet,
destroy,
};
},
});
};
总结
至此通过顺着umi dev命令,我们大致了解了umi的整个启动流程
cross-env UMI_ENV=ronghe_dev WATCH_IGNORED=none umi dev
可以发现主要就是插件的注册, 核心文件就是service.ts,dev build help等命令就是注册了一个插件。后面会说的的路由、内置方法等都是以插件方式注册进来的。
service中插件是如何注册的?
serice中命令是如何注册的?
不同插件都可以使用的内置命令是如何累计执行的?
接下来直接进入umi 核心——插件机制
此文自动发布于:github issues