旁白(脑补bgm)
经历了蛮荒,小作坊,故事发生在前端工程化时代,传说有这么一个江湖,江湖中有一条有名的街道,名曰"打包街",街上的铺子依稀能看到几个大大的Logo:Grunt,Gulp,FIS,Webpack,Rollup,Snowpack,Vite… 其中以Webpack最为热闹,只见牌匾上书:“Bundle Everything”,一群人来来往往,每个人手里拿着各种资源,排队递交资源,有人兴高采烈拿到了一片绿油油的条码,有人崔头丧气,拿了一堆警告和报错。大家议论纷纷:“您这配置不对呀”,“您这版本搞错了”,“您少包了”,“能快点吗,也忒慢了”,“听说隔壁Vite挺快的,走瞧瞧去”… 熙熙攘攘,好不热闹。
支撑这家店铺运转的两台机器,一个是loaders,一个是plugins。
我们今天就深入到这家铺子,探索他的运行机制,顺着webpack命令,我们来到了node_modules下webpack/bin/webpack.js下,webpack入口文件比较简单,没有复杂的逻辑,没有文件模块间的依赖和跳转,不会给新手绕晕,给想看源码的同学一个信心,循序渐进的肢解掉webpack,了解源码可以学到很多,可以扩宽技术视野,也可以夯实基础,还可以了解它的设计思想。带着这样的问题:一个工具为什么会出现,它的出现解决了什么问题,它是用什么样的思路解决的,有没有更好的解决方案,有哪些优缺点。带着疑问,怀着好奇,我们进入正文…
正文
文件第一行代码是
代码如下(示例):
#!/usr/bin/env node
用维基百科一句话解释如下
使用#!/usr/bin/env 脚本解释器名称是一种常见的在不同平台上都能正确找到解释器的办法
边逻辑的部分,非常简单,下边是简化的代码,省略了主流程,注释用字母标识了,下边会根据字母分析对应模块具体做了什么,整体逻辑就是判断webpack-cli和webpack-command是否已经安装了,然后做对应的事儿(对应下边的e,f,g),下边细说
// a.运行命令
const runCommand = (command, args) => {
};
// b.判断工具包是否被安装工具函数
const isInstalled = packageName => {
};
// c.定义webpack-cli和webpack-command
const CLIs = [{
name: "webpack-cli",
installed: isInstalled("webpack-cli"),
recommended: true,
url: "https://github.com/webpack/webpack-cli",
description: "The original webpack full-featured CLI."
},{
name: "webpack-command",
installed: isInstalled("webpack-command"),
recommended: false,
url: "https://github.com/webpack-contrib/webpack-command",
description: "A lightweight, opinionated webpack CLI."
}];
// d.判断是否被安装
const installedClis = CLIs.filter(cli => cli.installed);
if (installedClis.length === 0) {
// e.没有被安装
} else if (installedClis.length === 1) {
// f.有一个被安装
} else {
// g. 都安装了
}
下边按顺序解释下代码逻辑,正所谓处处留心皆学问,见微知著,看看这些简单的逻辑我们能了解到什么
a处逻辑
a. 注释a下的大致逻辑是开启一个子进程,并监听子进程的错误和退出事件
const runCommand = (command, args) => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true
});
executedCommand.on("error", error => {
reject(error);
});
executedCommand.on("exit", code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};
借助这篇文章(https://zhuanlan.zhihu.com/p/74879045)可以详细了解Node的"多线程"
b处逻辑
b. 在Node.js中,可以使用require.resolve函数来查询某个模块文件的带有完整绝对路径的文件名
c处逻辑
c处定义一个数组对象,储存了两个cli的一些属性,供下边逻辑判断的时候使用
d处逻辑
d在c的基础上通过filter筛选出符合条件的结果
e处逻辑
e处的逻辑是在d的结果返回空的数组,重点讲下这部分
if (installedClis.length === 0) { // e处代码展示
const path = require("path");
const fs = require("fs");
const readLine = require("readline");
let notify =
"One CLI for webpack must be installed. These are recommended choices";
for (const item of CLIs) {
if (item.recommended) {
notify += `\n - ${item.name} (${item.url})\n ${item.description}`;
}
}
console.error(notify);
const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));
const packageManager = isYarn ? "yarn" : "npm";
const installOptions = [isYarn ? "add" : "install", "-D"];
console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)}".`
);
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
questionInterface.question(question, answer => {
questionInterface.close();
const normalizedAnswer = answer.toLowerCase().startsWith("y");
if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);
process.exitCode = 1;
return;
}
const packageName = "webpack-cli";
console.log(
`Installing '${packageName}' (running '${packageManager} ${installOptions.join(
" "
)} ${packageName}')...`
);
runCommand(packageManager, installOptions.concat(packageName))
.then(() => {
require(packageName); //eslint-disable-line
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} // e处代码展示
看for of 循环处,基于c处定义的两个对象的recommended属性,他提示推荐你安装webpack-cli;接着通过yarn.lock是否存在检测你使用的包管理工具
fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));
然后通过readLine的createInterface创建了一个简单的命令行交互的接口实例,通过实例的question方法给你提示一个问题,判断你的输入是否合法(以字母y开头的就合法),不合法就退出主进程,合法执行a处定义的命令,也就是通过yarn/npm安装webpack-cli,成功能后require引入这个包。基本到这儿流程就结束了。到这儿可以说下,如果自己开发过脚手架的话,应该听过,目前主流制作脚手架的工具Yeoman,原理类似,没有接触过的有兴趣可以了解下。
下边再说下f 和g 处的逻辑
f处逻辑
f. 到 installedClis.length === 1 的逻辑分支 ,发现有一个cli了,就找到webpack-cli/package.json 这个路径,执行.bin进入webpack-cli包下
g处逻辑
g处的逻辑就是发现你安装了webpack-cli,也安装了webpack-command,提示你删除一个
总结
-
Node.js 的多进程有兴趣可以了解:child_process, cluster, worker threads…
-
通过require.resolve(packageName) 判断包是否存在
-
readLine模块 引申出脚手架的脚手架Yeoman
-
webpack 和webpack-cli的关系