# 分析 webpack 源码,理解原理
# 分析
惯例,把源代码回退到初始化版本,查看代码理念。可以看到项目结构如下:
webpack
├─/bin
│ └─webpack.js
├─/examples
├─/lib
│ ├─buildDeps.js
│ ├─parse.js
│ ├─resolve.js
│ ├─templateAsync.js
│ ├─templateSingle.js
│ ├─webpack.js
│ ├─writeChunk.js
│ └─writeSource.js
├─/test
├─.gitignore
├─package.json
├─README.md
├─require-polyfill.js
└─require-polyfill.web.js
然后从 package.json 中可以看到"bin": "./bin/webpack.js",
,代表 webpack 的 cli 在这里,而"main": "lib/webpack.js",
这应则是主要入口。
我们先从 cli 入手,根据使用方法来分析 webpack 的原理。
# cli
先安装此包,查看用法:
npm install . -g
+ webpack@0.1.0
在命令行中输入命令 webpack:
Usage: C:\\...\\nodejs\\node.exe C:\\...\\webpack\\bin\\webpack.js <input> <output>
Options:
--single Disable Code Splitting [boolean] [default: false]
--min Minimize it with uglifyjs [boolean] [default: false]
--filenames Output Filenames Into File [boolean] [default: false]
--options Options JSON File [string]
--script-src-prefix Path Prefix For JavaScript Loading [string]
--libary Stores the exports into this variable [string]
Not enough non-option arguments: got 0, need at least 1
可以看到,webpack 显示出了 node 的位置,以及本地 cli 文件的位置,至少需要一个必需的参数 input。
下方的 options 分别为一下用途:
选项 | 描述 | 类型 | 默认值 |
---|---|---|---|
single | 禁用代码拆分 | boolean | 默认关闭 |
min | 用 uglifyjs 最小化 | boolean | 默认关闭 |
filenames | 将文件名输出到文件中 | boolean | 默认关闭 |
options | 选项 JSON 文件 | string | |
script-src-prefix | JavaScript 加载的路径前缀 | string | |
libary | 将导出存储到此变量中 | string |
打开 bin 中的 webpack.js 查看:
var argv = require("optimist")
.usage("Usage: $0 <input> <output>")
.boolean("single")
.describe("single", "Disable Code Splitting")
.default("single", false)
.boolean("min")
.describe("min", "Minimize it with uglifyjs")
.default("min", false)
.boolean("filenames")
.describe("filenames", "Output Filenames Into File")
.default("filenames", false)
.string("options")
.describe("options", "Options JSON File")
.string("script-src-prefix")
.describe("script-src-prefix", "Path Prefix For JavaScript Loading")
.string("libary")
.describe("libary", "Stores the exports into this variable")
.demand(1).argv;
这里就是对命令参数的要求,即表格中的内容。
var input = argv._[0],
output = argv._[1];
if (input && input[0] !== "/" && input[1] !== ":") {
input = path.join(process.cwd(), input);
}
if (output && output[0] !== "/" && input[1] !== ":") {
output = path.join(process.cwd(), output);
}
输入参数以后,会和当前 node 项目的根目录拼接,相当于是制定一个入口文件,以及一个出口文件。最少只要 input 参数,说明出口文件有默认位置。
var options = {};
if (argv.options) {
options = JSON.parse(fs.readFileSync(argv.options, "utf-8"));
}
if (argv["script-src-prefix"]) {
options.scriptSrcPrefix = argv["script-src-prefix"];
}
if (argv.min) {
options.minimize = true;
}
if (argv.filenames) {
options.includeFilenames = true;
}
if (argv.libary) {
options.libary = argv.libary;
}
这里是对选项参数进行剖析,存在 options 对象里。
var webpack = require("../lib/webpack.js");
if (argv.single) {
//...
} else {
//...
}
这里引入了 webpack 源文件,然后根据 single 选项分为了两种情况,下面对两种情况分别分析,根据选项含义,首先是禁用了代码拆分的情况:
webpack(input, options, function(err, source) {
if (err) {
console.error(err);
return;
}
if (output) {
fs.writeFileSync(output, source, "utf-8");
} else {
process.stdout.write(source);
}
});
这里直接调用了 webpack 方法,传入了 input 和 options,然后有一个回调,其中发生错误将直接打印,执行完成后判断有没有出口文件,如果存在就直接写入,如果不存在就输入一个 source,这里大概是默认的输出位置。
output = output || path.join(process.cwd(), "js", "web.js");
if (!options.outputDirectory) options.outputDirectory = path.dirname(output);
if (!options.output) options.output = path.basename(output);
if (!options.outputPostfix) options.outputPostfix = "." + path.basename(output);
var outExists = path.existsSync(options.outputDirectory);
if (!outExists) fs.mkdirSync(options.outputDirectory);
webpack(input, options, function(err, stats) {
if (err) {
console.error(err);
return;
}
console.log(stats);
});
这里是启用代码拆分的情况,先对出口做了定义,如果存在就使用,不存在就输出到./js/web.js。之后开始判断在 options 中有没有对出口文件夹,出口文件名,以及出口文件后缀。然后判断这个出口文件夹是否存在,不存在就创建一个。然后调用 webpack,回调变成了输出一个状态。
这里有一个问题,existsSync 方法是 fs 模块的内容,这里写成了 path,因此在这里会报错,改过来就好了。
# lib
从上面看出,主要的内容围绕在 webpack 方法,这个方法在引入的./lib/webpack.js 文件中,整体结构如下(这里对所有方法进行了简化处理,之后再详细分析):
var buildDeps = require("./buildDeps");
var path = require("path");
var writeChunk = require("./writeChunk");
var fs = require("fs");
var templateAsync = require("fs").readFileSync(
path.join(__dirname, "templateAsync.js")
);
var templateSingle = require("fs").readFileSync(
path.join(__dirname, "templateSingle.js")
);
/**
* ...
*/
module.exports = function(context, moduleName, options, callback) {
//...
};
function uglify(input, filename) {
//...
}
function stringify(str) {
//...
}
可以看到,这里的核心都在导出的方法上,分析先从导出模块开始。
if (typeof moduleName === "object") {
callback = options;
options = moduleName;
moduleName = "./" + path.basename(context);
context = path.dirname(context);
}
if (typeof moduleName === "function") {
callback = moduleName;
options = {};
moduleName = "./" + path.basename(context);
context = path.dirname(context);
}
if (!callback) {
callback = options;
options = {};
}