基于前面umi
插件机制的原理可以了解到,umi
是一个插件化的企业级前端框架,它配备了完善的插件体系,这也使得umi具有很好的可扩展性。umi
的全部功能都是由插件完成的,构建功能同样是以插件的形式完成的。下面将从以下两个方面来了解umi
的构建原理。
想了解umi
命令的注册流程,咱们就从umi
生成的项目入手。
从umi
初始化的项目package.json
文件看,umi
执行dev
命令,实际执行的是start:dev
,而start:dev
最终执行的是umi dev
。
"scripts": {
"dev": "npm run start:dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev"
}
根据这里的umi
命令,我们找到node_modules
里的umi文件夹,看下umi文件夹下的package.json
文件:
"name": "umi",
"bin": {
"umi": "bin/umi.js"
}
可以看到,这里就是定义umi
命令的地方,而umi
命令执行的脚本就在bin/umi.js
里。接下来咱们看看bin/umi.js
都做了什么。
#!/usr/bin/env node
require('v8-compile-cache');
const resolveCwd = require('@umijs/deps/compiled/resolve-cwd');
const { name, bin } = require('../package.json');
const localCLI = resolveCwd.silent(`${name}/${bin['umi']}`);
if (!process.env.USE_GLOBAL_UMI && localCLI && localCLI !== __filename) {
const debug = require('@umijs/utils').createDebug('umi:cli');
debug('Using local install of umi');
require(localCLI);
} else {
require('../lib/cli');
}
判断当前是否执行的是本地脚手架,若是,则引入本地脚手架文件,否则引入 lib/cli
。在这里,我们未开启本地脚手架指令,所以是引用的lib/cli
。
// 获取进程的版本号
const v = process.version;
// 通过yParser工具对命令行参数进行处理,此处是将version和help进行了简写
const args = yParser(process.argv.slice(2), {
alias: {
version: ['v'],
help: ['h'],
},
boolean: ['version'],
});
// 若参数中有version值,并且args._[0]为空,此时将version字段赋值给args._[0]
if (args.version && !args._[0]) {
args._[0] = 'version';
const local = existsSync(join(__dirname, '../.local'))
? chalk.cyan('@local')
: '';
console.log(`umi@${require('../package.json').version}${local}`);
// 若参数中无version值,并且args._[0]为空,此时将help字段复制给args._[0]
} else if (!args._[0]) {
args._[0] = 'help';
}
处理完version
和help
后,紧接着会执行一段自执行代码:
(async () => {
try {
// 读取args._中第一个参数值
switch (args._[0]) {
case 'dev':
// 若当前运行环境是dev,则调用Node.js的核心模块child_process的fork方法衍生一个新的Node.js进程。scriptPath表示要在子进程中运行的模块,这里引用的是forkedDev.ts文件。
const child = fork({
scriptPath: require.resolve('./forkedDev'),
});
// ref:
// http://nodejs.cn/api/process/signal_events.html
// https://lisk.io/blog/development/why-we-stopped-using-npm-start-child-processes
process.on('SIGINT', () => {
child.kill('SIGINT');
// ref:
// https://github.com/umijs/umi/issues/6009
process.exit(0);
});
process.on('SIGTERM', () => {
child.kill('SIGTERM');
process.exit(1);
});
break;
default:
// 非dev环境皆执行default中的代码
// 读取args._中的第一个参数,若为build,则认为是要运行生产环境,process.env.NODE_ENV赋值为production
const name = args._[0];
if (name === 'build') {
process.env.NODE_ENV = 'production';
}
// 下面的这块代码和dev子进程中执行的forkedDev.ts文件中的核心代码一模一样,此处不再赘述,接下来我们看forkedDev.ts文件中的内容。
// Init webpack version determination and require hook for build command
initWebpack();
await new Service({
cwd: getCwd(),
pkg: getPkg(process.cwd()),
}).run({
name,
args,
});
break;
}
} catch (e) {
console.error(chalk.red(e.message));
console.error(e.stack);
process.exit(1);
}
})();
forkedDev.ts
文件是专门处理dev环境子进程的脚本。
// 获取命令行参数
const args = yParser(process.argv.slice(2));
// dev环境子进程自执行函数
(async () => {
try {
// 设置环境变量为development
process.env.NODE_ENV = 'development';
// Init webpack version determination and require hook
// initWebpack方法主要是获取用户的配置,并针对webpack5做特殊处理,可参考源码中的注释,此处不再细说。
// 源码中说明如下:
// 1. read user config
// 2. if have webpack5:
// 3. init webpack with webpack5 flag
initWebpack();
// cwd: 返回 Node.js 进程的当前工作目录
// pkg: 获取当前目录 package.json
// 同上段代码,build也是执行的这段代码
// 实例化Service类,并运行service.run方法,启动进程
const service = new Service({
cwd: getCwd(),
pkg: getPkg(process.cwd()),
});
// 执行实例化对象service的run方法,完成命令行的注册
await service.run({
name: 'dev',
args,
});
let closed = false;
// kill(2) Ctrl-C
process.once('SIGINT', () => onSignal('SIGINT'));
// kill(3) Ctrl-\
process.once('SIGQUIT', () => onSignal('SIGQUIT'));
// kill(15) default
process.once('SIGTERM', () => onSignal('SIGTERM'));
function onSignal(signal: string) {
if (closed) return;
closed = true;
// 退出时触发插件中的onExit事件
service.applyPlugins({
key: 'onExit',
type: service.ApplyPluginsType.event,
args: {
signal,
},
});
process.exit(0);
}
} catch (e) {
console.error(chalk.red(e.message));
console.error(e.stack);
process.exit(1);
}
})();
上述源码中,Service
继承自CoreService
,是对CoreService
二次封装。它的核心代码在ServiceWithBuiltIn.ts
文件中,下面我们来看下它都做了哪些处理:
class Service extends CoreService {
constructor(opts: IServiceOpts) {
// 增加全局环境变量字段:UMI_VERSION、UMI_DIR
process.env.UMI_VERSION = require('../package').version;
process.env.UMI_DIR = dirname(require.resolve('../package'));
// 调用父类CoreService的构造函数,注入插件集@umijs/preset-built-in和插件plugins/umiAlias
super({
...opts,
presets: [
require.resolve('@umijs/preset-built-in'),
...(opts.presets || []),
],
plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
});
}
}
export { Service };
在二次封装的Service
中我们看到,它在初始化时注入了一个插件集@umijs/preset-built-in
和一个插件plugins/umiAlias
。plugins/umiAlias
只是修改了webpack中的alias,源码很简单,感兴趣的可前往查看,这里不再赘述。我们看下插件集@umijs/preset-built-in
:
// commands
require.resolve('./plugins/commands/build/build'),
require.resolve('./plugins/commands/build/applyHtmlWebpackPlugin'),
require.resolve('./plugins/commands/config/config'),
require.resolve('./plugins/commands/dev/dev'),
require.resolve('./plugins/commands/dev/devCompileDone/devCompileDone'),
require.resolve('./plugins/commands/dev/mock/mock'),
require.resolve('./plugins/commands/generate/generate'),
require.resolve('./plugins/commands/help/help'),
require.resolve('./plugins/commands/plugin/plugin'),
require.resolve('./plugins/commands/version/version'),
require.resolve('./plugins/commands/webpack/webpack')
在preset-built-in
目录的入口文件index.ts
中可以看出,它引入众多插件,这些插件都是umi
内置的核心插件,这里我们只关注命令行的插件注入。
前面讲到,Service
调用了CoreService
的构造函数,在构造函数中,将传入的插件集@umijs/preset-built-in
和插件plugins/umiAlias
都进行了初始化。
// 初始化插件集,opts.persets即包括传入的@umijs/preset-built-in
this.initialPresets = resolvePresets({
...baseOpts,
presets: opts.presets || [],
userConfigPresets: this.userConfig.presets || [],
});
// 初始化插件,opts.plugins即包括传入的plugins/umiAlias
this.initialPlugins = resolvePlugins({
...baseOpts,
plugins: opts.plugins || [],
userConfigPlugins: this.userConfig.plugins || [],
});
至于插件集和插件是如何实现初始化注册的,请参看本公众号上篇文章[UMI3源码解析系列之插件化架构核心] 。
插件集和插件初始化完成后,也就完成了Service
的实例化过程。还记得上面dev
的核心脚本?再来回顾下源码:
// 实例化Service类,并运行service.run方法,启动进程
const service = new Service({
cwd: getCwd(),
pkg: getPkg(process.cwd()),
});
// 执行实例化对象service的run方法,完成命令行的注册
await service.run({
name: 'dev',
args,
});
进程启动需要两步:1、实例化Service
,2、调用Service
的run
方法。
上面已经完成了Service
的实例化,接下来我们看下run
方法的调用。
async run({ name, args = {} }: { name: string; args?: any }) {
args._ = args._ || [];
// shift the command itself
if (args._[0] === name) args._.shift();
this.args = args;
// ///////////////////////////////////////
// 第1步:调用init方法,初始化presets和plugins
// 这里完成了所有插件集和插件的初始化
// ///////////////////////////////////////
await this.init();
logger.debug('plugins:');
logger.debug(this.plugins);
// /////////////////////////////
// 第2步:设置生命周期状态为run运行时
// /////////////////////////////
this.setStage(ServiceStage.run);
// /////////////////////
// 第3步:触发onStart hook
// /////////////////////
await this.applyPlugins({
key: 'onStart',
type: ApplyPluginsType.event,
args: {
name,
args,
},
});
// ///////////////////////////
// 第4步:执行命令脚本函数
// 插件准备完成后,开始执行命令脚本
// ///////////////////////////
return this.runCommand({ name, args });
}
此时,我们已完成umi
所有内置命令行的插件注册。
插件注册完成后,立即调用了runCommand
方法,来执行命令的脚本函数。
async runCommand({ name, args = {} }: { name: string; args?: any }) {
assert(this.stage >= ServiceStage.init, `service is not initialized.`);
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];
assert(command, `run command failed, command ${name} does not exists.`);
const { fn } = command as ICommand;
return fn({ args });
}
在runCommand
方法中,从this.commands
集合中获取当前命令名,这里对command
做了格式上的统一。获取到的command
是个ICommand
类型的对象,从中获取fn
属性,并直接调用,从而完成命令行脚本的执行。
下面我们以dev
为例,看下每个命令的核心实现逻辑。
如上所述,umi
内置的核心插件都通过插件集@umijs/preset-built-in
注入,我们找到插件集中dev的命令文件,即:
require.resolve('./plugins/commands/dev/dev')
umi
注册命令是通过registerCommand
核心方法完成的,我们来看下dev
文件的registerCommand
方法做了什么:
api.registerCommand({
name: 'dev',
description: 'start a dev server for development',
fn: async function ({ args }) {}
});
先来看下registerCommand
方法,包括3部分内容:
dev
从 UMI命令注册 小节我们了解到,最终在执行runCommand
方法时,实际是在执行每个命令插件的fn
方法。那么,我们就来看看dev
命令的fn
具体是怎么实现的。
fn: async function ({ args }) {
// 获取默认端口号
const defaultPort =
// @ts-ignore
process.env.PORT || args?.port || api.config.devServer?.port;
// 为全局变量 port 赋值,若项目配置指定了端口号,则优先采用,否则端口号默认为:8000
port = await portfinder.getPortPromise({
port: defaultPort ? parseInt(String(defaultPort), 10) : 8000,
});
// @ts-ignore
// 设置全局hostname。优先读取配置中的host配置,若无,则默认赋值为:0.0.0.0
// 补充一个知识点:0.0.0.0表示什么?
// 在IPV4中,0.0.0.0地址被用于表示一个无效的,未知的或者不可用的目标。
// 如果一个主机有两个IP地址,192.168.1.11 和 172.16.1.11 ,那么使用这两个IP地址访问本地服务都可以,这也就是启动项目时,控制台打印的Network对应的本机IP地址。
hostname = process.env.HOST || api.config.devServer?.host || '0.0.0.0';
console.log(chalk.cyan('Starting the development server...'));
// 若进程采用的是IPC通道衍生,需通过 process.send() 方法通知父进程更新端口号
// 若进程不是采用IPC通道衍生,则不需要发送
process.send?.({ type: 'UPDATE_PORT', port });
// enable https, HTTP/2 by default when using --https
// 设置环境变量HTTPS值
const isHTTPS = process.env.HTTPS || args?.https;
// 清理过期的缓存文件,即 .cache 文件夹中的所有文件
cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath!,
});
// 是否开启监听 package.json 变化
const watch = process.env.WATCH !== 'none';
// generate files
// 执行 onGenerateFiles 插件,生成临时文件
const unwatchGenerateFiles = await generateFiles({ api, watch });
if (unwatchGenerateFiles) unwatchs.push(unwatchGenerateFiles);
// 若开启热更新,执行如下逻辑:
if (watch) {
// watch pkg changes
// 通过 chokidar 库,开启 package.json 文件监听任务
const unwatchPkg = watchPkg({
cwd: api.cwd,
onChange() {
console.log();
api.logger.info(`Plugins in package.json changed.`);
api.restartServer();
},
});
unwatchs.push(unwatchPkg);
// watch config change
// 同样通过 chokidar 库,开启对配置文件的监听任务
const unwatchConfig = api.service.configInstance.watch({
userConfig: api.service.userConfig,
onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
if (pluginChanged.length) {
console.log();
api.logger.info(
`Plugins of ${pluginChanged
.map((p) => p.key)
.join(', ')} changed.`,
);
api.restartServer();
}
if (valueChanged.length) {
let reload = false;
let regenerateTmpFiles = false;
const fns: Function[] = [];
const reloadConfigs: string[] = [];
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;
reloadConfigs.push(key);
}
if (typeof onChange === 'function') {
fns.push(onChange);
}
});
if (reload) {
console.log();
api.logger.info(`Config ${reloadConfigs.join(', ')} changed.`);
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运行的准备工作,下面则是核心的dev操作
// dev
// 获取实例化后的 bundler 和 配置
const { bundler, bundleConfigs, bundleImplementor } =
await getBundleAndConfigs({ api, port });
// 调用实例化后的 bundler 的 setupDevServerOpts 方法,这个方法做了如下几件事:
// 1. 调用webpack方法,获取webpack的编译器实例 compiler
// 2. 编译器实例 compiler 通过 webpack-dev-middleware 封装器,将webpack处理过的文件封装成 server 能接收的格式
// 3. 通过调用 sockjs 的 sockWrite 方法,实现热更新
// 4. 处理服务类 Server 实例化时需要的 onListening 和 onConnection 函数
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,并传入bundler.setupDevServerOpts处理过的 compilerMiddleware、onListening、onConnection
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 || {}),
});
// 启动实例化后的server
const listenRet = await server.listen({
port,
hostname,
});
return {
...listenRet,
compilerMiddleware: opts.compilerMiddleware,
destroy,
};
}
至此,dev
命令的核心运行脚本已解读完毕。
以上,通过命令注册原理和 dev 命令注册流程的源码解读,我们已经了解到 UMI 是怎么实现命令注册的。实际上,还是通过插件的形式实现了,再次印证了 UMI 一切皆插件的设计思想。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/2MsgPh9AZtUoJpsLLdOynQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。