umi核心代码解读
最近遇到了一个不错的框架——umijs。现在前端开发的一大痛点就是,上手开发一个项目,上来就是一堆的配置,其中最重的可能就是webpack。umijs就是为了解决这个问题,希望能将开发者从无穷无尽的配置中解放出来,只关注业务代码。
优势在哪里?
umijs的优势如何体现呢?
➜ myapp tree
.
└── pages
├── index.css
├── index.js
├── users.css
└── users.js
只有核心的业务代码,没有任何配置信息,什么路由、什么webpack统统没有,但是这时我们直接执行
umi dev
就可以开始开发了。而这就是优势。当然这个并不是 umi
首创的,create-react-app
已经先行一步了,但是umi提供了一种比较通用的解决方案,值得期待,
架构图
总的来说,野心很大,希望一统江湖。
umijs的核心开发者云谦大佬说了,umijs的核心点有以下几点。
- 路由
- 插件
- webpack
- 约定优于配置
而核心中的重点,就是插件,而插件带来的最大的能力就是扩展性。而扩展性重要性是毋庸置疑的,这一点从koa、vscode、webpack等等知名的开源项目中都得到了体现。
插件
umi插件的核心原理就是深入到构建的整个过程中去,hook重要的时间节点,让插件能影响后续的构建流程,从而影响构建结果。
umi插件的标准写法如下:
export default (api, opts) => {
// your plugin code here
};
插件初始化方法会收到两个参数,第一个参数 api
,umi
提供给插件的接口都是通过它暴露出来的。第二个参数 opts
是用户在初始化插件的时候填写的。
API
其中 api
就是前面提到的整个构建过程中的重要时间节点。umi
将这些时间节点做了拆分,详细的可以参考这里 —— 插件开发。个人认为应该分为三个部分。
context
- 功能函数,和构建生命周期并没有太大的关系
- 生命周期hook
context
看下使用场景,简单就不多说了。
export default function(api, options = {}) {
const { paths, findJS } = api; // 取出paths
......
api.addEntryImport(() => {
return {
source: relative(
paths.absTmpDirPath, // 获取tmp目录绝对地址
findJS(paths.absSrcPath, 'hd') || // 获取src绝对地址
join(__dirname, '../template/index.js'),
),
};
});
}
功能函数
看下使用场景,实现都比较单一,不多说。
export default function(api, options = {}) {
const { config, paths } = api;
const { targets } = config;
......
api.onOptionChange(newOpts => {
options = newOpts;
api.rebuildTmpFiles(); // 直接调用功能函数,重新创建临时文件
});
}
生命周期hook
生命周期的 hook
很好理解,在开启一个本地开发服务的时候,会有各个时机,就是要将这些时机能被插件 hook
,进而做一些好玩的事情。
比如:
beforeDevServer:dev server 启动之前。
afterDevServer:dev server 启动之后。
onStart:umi dev 或者 umi build 开始时触发。
拿 afterDevServer
这个看看,在 umi
中是怎么被触发的。
// https://github.com/umijs/umi/blob/master/packages/af-webpack/src/dev.js#L148
server.listen(port, HOST, err => {
if (err) {
console.log(err);
return;
}
if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...\n'));
send({ type: STARTING });
if (afterServer) {
afterServer(server); // server启动以后执行
}
});
// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/plugins/commands/dev/index.js#L119
afterServer(devServer) {
service.applyPlugins('afterDevServer', { // 触发afterDevServer的hook,注意这里并不关心返回值
args: { server: devServer },
});
startWatch();
}
除了以上这种事件类的 hook
之外,还有一种比较常见的 hook
,umi
称之为 应用类 api
。
事件类 hook
能被插件监听到事件发生,然后做一些处理,并不关心返回值。但是如何能让插件方便地将处理后的结果同步的返回给事件发出者,进而影响构建的结果呢?
应用类的 api
主要就是解决这个问题。
随便找一个 addHTMLMeta
看看具体是如何实现的。
// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/html/HTMLGenerator.js#L300
getContent(route) {
......
if (this.modifyMetas) metas = this.modifyMetas(metas, { route }); // 获取metas的时机
......
// insert tags
// insert tags
html = html.replace(
'<head>',
`
<head>
${metas.length ? this.getMetasContent(metas) : ''} // 将meta插入到页面中
${links.length ? this.getLinksContent(links) : ''}
${styles.length ? this.getStylesContent(styles) : ''}
`.trim() + '\n',
);
......
}
// https://github.com/umijs/umi/blob/master/packages/umi-build-dev/src/plugins/commands/getHtmlGenerator.js#L33
modifyMetas(memo, opts = {}) {
const { route } = opts;
// 在获取metas的时机调用 `addHTMLMeta api`,这个api不同于事件型api,是可以同步返回插件的处理结果的,进而会直接影响生成的html文件。
return service.applyPlugins('addHTMLMeta', {
initialValue: memo,
args: { route },
});
}
umi
提供了非常多类似的 api
,这也是 umi
比较强大的地方,这些 api
保证了插件编写的便利性。
applyPlugins
上面提到了很多次applyPlugins函数,单独拿出来看看。自己编写的插件和系统内置的插件都会被 register
,然后在 applyPlugins
的时候被集体按顺序执行。
applyPlugins(key, opts = {}) {
debug(`apply plugins ${key}`);
return (this.pluginHooks[key] || []).reduce((memo, { fn }) => { // reduce函数,见下图
try {
return fn({
memo,
args: opts.args,
});
} catch (e) {
console.error(chalk.red(`Plugin apply failed: ${e.message}`));
throw e;
}
}, opts.initialValue);
}
reduce
函数相信大家都不陌生。
如果 plugin
是基于事件类 api
写的,那就把所有的 plugin
都执行一遍就完事了。
如果 plugin
是基于应用类 api
写的,那也会把所有的 plugin
,并将每次执行的结果 reduce
成最终结果返回给 applyPlugins
的点。
展望
个人还是比较好奇基于构建时期的 hook
做插件到底能 cover
多广的业务开发需求,如果遇到不能解决的,那岂不是有点头疼?