webpack工作流
如何调试
- 创建个js文件,然后在内部引入webpack和配置文件,可以在文件中写debugger,然后执行小爬虫即可
const webpack = require("./webpack");
const webpackConfig = require("./webpack.config");
debugger;
const compiler = webpack(webpackConfig);
compiler.run((err, stats) => {
if (err) {
console.log(err);
} else {
console.log(
stats.toJson({
files: true,
assets: true,
chunks: true,
modules: true,
})
);
}
});
loader
- 因为webpack只识别js和json,所以我们需要将其他它不认识的文件类型转换成它认识的
- 我们想对某些文件进行处理,也可以使用loader
- loader的本质是一个函数,它最大的作用是
修改语法树的
- loader从后到前依次,后面的会拿到前面的执行结果执行
自定义一个玩具loader
...
module: {
rules: [
{
test: /\.js$/,
use: [
path.resolve(__dirname, "loaders/loader2.js"),
path.resolve(__dirname, "loaders/loader1.js"),
],
},
],
},
...
function loader1(source) {
return "偷梁换柱";
}
module.exports = loader1;
function loader2(source) {
return source + "第二步骤";
}
module.exports = loader2;
让它识别其他文件
- 不认识的文件也能打包,编译不报错,一旦执行就挂了
- 随便自定义搞一个格式,写一段内容,然后编译下
asdadasd
import xx from "./xx.alg";
console.log(xx);
{
test: /\.alg$/,
use: path.resolve(__dirname, "loaders/loader1.js"),
},
function loader1(source) {
return `module.exports = "${source}"`;
}
module.exports = loader1;
(() => {asdadasd})
plugins
- plugins就是我们在webpack中使用的插件集合,有多个,所以使用数组形式存储
- 规范:
- 都有一个apply方法,该方法接收一个compiler形参,代表当前webpack进程对象
- 可以通过compiler.hooks.xxx.tap向进程中指定的钩子注册事件,方法接收两个参数,name与callback,name没用,callback则为事件回调
- 相同周期的钩子看谁先注册,那么谁执行
- 事件的挂载是在启动前全部挂载,后续到某个生命周期,依次执行对应的钩子
class DonePlugin {
apply(compiler) {
compiler.hooks.done.tap("DonePlugin", () => {
console.log("done:结束编译");
});
}
}
module.exports = DonePlugin;
class RunPlugin {
apply(compiler) {
compiler.hooks.run.tap("RunPlugin", () => {
console.log("run:开始编译");
});
}
}
module.exports = RunPlugin;
const RunPlugin = require("./plugins/run-plugin");
const DonePlugin = require("./plugins/done-plugin");
...
plugins: [
new DonePlugin(),
new RunPlugin(),
]
...
const compiler = {
hooks: {
done: [()=>{'结束'}],
run: [()=>{'开始'}],
},
};
编译流程
- 初始化参数,合并命令行参数与配置文件参数,得到配置对象,命令行参数高于配置文件
- 用得到的参数初始化
compiler对象
- 加载所有配置的插件,将
compiler传递给apply方法,然后apply内部会按照插件逻辑向compiler的生命周期事件池子中注册事件
- 执行
compiler对象的run方法,开始编译
- 根据配置中的entry找到入口文件
- 从入口文件出发,调用所有配置的loader对模块进行编译
- 找出该模块依赖的模块,再递归本步骤知道所有入口依赖的文件都经过了本步骤的处理
- 将每个入口与模块之间的依赖关系组装成一个个包含多个模块的chunk
- 再把每个chunk转换成一个单独的文件加入到输出列表
- 确定好输出内容后,根据配置确定出处的路径和文件名,将文件内容写入到文件系统
- 在编译过程中,在特定的时间,会按照顺序执行注册的插件(
生命周期钩子)
实现
- 初始化参数,合并命令行参数与配置文件行参数
- 初始化Compiler实例,将合并好的参数传递过去
- Compiler的事件注册依赖于tapable库,在this.hooks上创建事件池,当前实现了run与done
- 每个plugin都有一个apply方法,会接收compiler实例
- 依次执行plugin的apply方法,在compiler的hooks上的周期池子中注册事件
- 执行compiler的run方法开始编译
- run方法接收一个函数,函数第一个参数为异常,第二个参数为本次进程内容
- 先执行run钩子
- 执行编译,编译依赖于Compilation模块,每次编译都会创建一个新的Compilation实例
- Compilation中会读取babel,然后先编译入口文件,然后将入口文件变为ast语法树,找到导入节点,进行递归编译,最后整合obj然后返回
- 编译完成后,将内容写入文件中
- 将依赖模块交给fs.watch监听,后续修改内容可重新编译
- 执行done钩子
webpack.config.js
const path = require("path");
const Run1Plugin = require("./plugins/run-plugin");
const DonePlugin = require("./plugins/done-plugin");
module.exports = {
mode: "development",
devtool: false,
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
clean: true,
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
module: {
rules: [],
},
plugins: [
new DonePlugin(),
new Run1Plugin(),
],
};
webpack.js
const Compiler = require("./Compiler");
function webpack(options) {
const argv = process.argv.slice(2);
const shellOptions = argv.reduce((shellOptions, options) => {
const [key, value] = options.split("=");
shellOptions[key.slice(2)] = value;
return shellOptions;
}, {});
const finalOptions = {
...options,
...shellOptions,
};
const compiler = new Compiler(finalOptions);
const { plugins } = finalOptions;
for (let plugin of plugins) {
plugin.apply(compiler);
}
return compiler;
}
module.exports = webpack;
Compiler.js
const { SyncHook } = require("tapable");
const Compilation = require("./Compilation");
const fs = require("fs");
const path = require("path");
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(),
done: new SyncHook(),
};
this.fileDependencies = new Set();
}
run(callback) {
this.hooks.run.call();
const onCompiled = (err, stats, fileDependencies) => {
console.log("编译");
for (let filename in stats.assets) {
let filePath = path.join(this.options.output.path, filename);
fs.writeFileSync(filePath, stats.assets[filename], "utf8");
}
callback(err, { toJson: () => stats });
for (let fileDependency of fileDependencies) {
if (!this.fileDependencies.has(fileDependency)) {
fs.watch(fileDependency, () => this.compile(onCompiled));
this.fileDependencies.add(fileDependency);
}
}
};
this.compile(onCompiled);
this.hooks.done.call();
}
compile(callback) {
let compilation = new Compilation(this.options, this);
compilation.build(callback);
}
}
module.exports = Compiler;
Compilations.js
const path = require("path");
const fs = require("fs");
const parser = require("@babel/parser");
const types = require("@babel/types");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const baseDir = normalizePath(process.cwd());
function normalizePath(path) {
return path.replace(/\\/g, "/");
}
class Compilation {
constructor(options, compiler) {
this.options = options;
this.compiler = compiler;
this.modules = [];
this.chunks = [];
this.assets = {};
this.files = [];
this.fileDependencies = new Set();
}
build(callback) {
let entry = {};
if (typeof this.options.entry === "string") {
entry.mian = this.options.entry;
} else {
entry = this.options.entry;
}
for (let entryName in entry) {
let entryFilePath = path.posix.join(baseDir, entry[entryName]);
this.fileDependencies.add(entryFilePath);
let entryModule = this.buildModule(entryName, entryFilePath);
let chunk = {
name: entryName,
entryModule,
modules: this.modules.filter((module) =>
module.names.includes(entryName)
),
};
this.chunks.push(chunk);
}
this.chunks.forEach((chunk) => {
const filename = this.options.output.filename.replace(
"[name]",
chunk.name
);
this.files.push(filename);
this.assets[filename] = getSource(chunk);
});
callback(
null,
{
modules: this.modules,
chunks: this.chunks,
assets: this.assets,
files: this.files,
},
this.fileDependencies
);
}
buildModule(name, modulePath) {
let sourceCode = fs.readFileSync(modulePath, "utf8");
let { rules } = this.options.module;
let loaders = [];
rules.forEach((rule) => {
if (modulePath.math(rule.test)) {
let use = rule.use;
if (!Array.isArray(use) && use) {
use = [use];
}
if (use.length > 0) {
loaders.push(...use);
}
}
});
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
let moduleId = "./" + path.posix.relative(baseDir, modulePath);
let module = { id: moduleId, dependencies: [], names: [name] };
let ast = parser.parse(sourceCode, { sourceType: "module" });
traverse(ast, {
CallExpression: ({ node }) => {
if (node.callee.name === "require") {
let depModuleName = node.arguments[0].value;
let depModulePath;
if (depModuleName.startsWith(".")) {
const currentDir = path.posix.dirname(modulePath);
depModulePath = path.posix.join(currentDir, depModuleName);
const extensions = this.options.resolve.extensions;
depModulePath = tryExtensions(depModulePath, extensions);
} else {
depModulePath = require.resolve(depModuleName);
}
this.fileDependencies.add(depModulePath);
let depModuleId = "./" + path.posix.relative(baseDir, depModulePath);
node.arguments = [types.stringLiteral(depModuleId)];
module.dependencies.push({
depModuleId,
depModulePath,
});
}
},
});
let { code } = generator(ast);
module._source = code;
module.dependencies.forEach(({ depModuleId, depModulePath }) => {
let existModule = this.modules.find(
(module) => module.id === depModuleId
);
if (existModule) {
existModule.names.push(name);
} else {
let depModule = this.buildModule(name, depModulePath);
this.modules.push(depModule);
}
});
return module;
}
}
function tryExtensions(modulePath, extensions) {
if (fs.existsSync(modulePath)) {
return modulePath;
}
for (let i = 0; i < extensions.length; i++) {
let filePath = modulePath + extensions[i];
if (fs.existsSync(filePath)) {
return filePath;
}
}
throw new Error(`${modulePath}不存在,模块路径错误`);
}
function getSource(chunk) {
return `(() => {
var __webpack_modules__ = {
${chunk.modules.map(
(module) => `
"${module.id}": (module) => {
${module._source}
},`
)}
};
var __webpack_module_cache__ = {};
function require(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, require);
return module.exports;
}
var __webpack_exports__ = {};
(() => {
${chunk.entryModule._source}
})();
})();
`;
}
module.exports = Compilation;
debugger.js
- 引入我们的webpack
- 然后node debugger.js即可