我想webpack工作量,无论是面试还是对了解webpack的内部实现都很重要,我最近也是学习了一下,所以把自己的心得体会记录一下,希望也能帮助到其他有兴趣学习的同学!
首先我们可以将它分成几步来方便记忆
- 初始化参数
- 初始化Compiler对象
- 获取所有plugin并执行
- 开始编译,执行Compiler对象的run方法
- 根据配置文件获取所有的入口文件
- 根据入口文件调用所有的loader对模块进行编译
- 找出入口文件所依赖的模块,递归本步骤直到所有的入口依赖都经过本步骤的处理
- 根据入口文件和模块的依赖关系,组装成一个个的chunk
- 把chunk生成对应的文件添加到输出列表中
- 确定输出的内容,根据配置确定输出文件的路径和文件名,把文件写入到文件系统
然后我们接下来先看前三步
webpack.js
- 初始化参数
- 初始化Compiler对象
- 加载所有的插件,把compiler对象传递到apply的形参中
const Compiler = require('./Compiler);
function webpack(options) {
// 1. 初始化参数
const argv = process.argv.slice(2);
const shellOptions = argv.reduce((shellOptions, option) => {
const [key, value] = option.split('=');
shellOptions[key] = value;
return shellOptions
}, {});
const finalOptions = {
...options,
...shellOptions
};
// 2. 初始化Compiler对象
const compiler = new Compiler(finalOptions);
// 3. 加载所有的插件,把compiler对象传递到apply的形参中
finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
};
module.exports = webpack;
Compiler.js
- 执行对象的run方法开始编译
- 确定输出内容,根据配置确定输出路径和文件名,把文件写入到文件系统
const fs = require('fs');
const { SyncHook } = require('tapable');
const Compilcation = require('./Compilcation');
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(),
done: new SyncHook()
}
}
// 4. 执行对象的run方法
run(callback) {
// 在这可以执行插件的钩子函数
this.hooks.run.call();
function compiled(err, state, fileDependencies) {
// 10. 确定输出内容,根据配置确定输出路径和文件名,把文件写入到文件系统
for (let filename in stats.assets) {
// 拼接输出路径和文件名
let filePath = path.join(this.options.output.path, filename);
// 调用fs把文件内容写到文件系统
fs.writeFileSync(filePath, stats.assets[filename], "utf8");
}
callback(err, {
toJson: () => stats,
});
// 监听文件变化,如果文件发生变化之后会从新调取编译方法
fileDependencies.forEach(file => {
fs.watch(file, () => {
this.compiler(compiled)
});
});
};
// 每次编译都会创建一个新的compilcation
this.compiler(compiled);
this.hooks.done.call();
}
// 每次编译都会创建一个新的compilcation
compiler(callback) {
const complication = new Compilcation(this.options);
complication.build(callback)
}
}
module.exports = Compiler;
Compilcation.js
- 根据配置中的entry找到所有的入口文件
- 根据入口文件,调用所有的Loader配置对模块进行编译
- 再找出该模块所依赖的模块,递归本步骤直到所有的入口文件依赖的模块都经过了本步骤的处理
- 根据入口和模块的依赖关系组装成一个个包含多个模块的chunk
- 把每个chunk转换成单独的文件添加到输出列表中
const path = require('path');
const baseDir = process.cwd();
const fs = require('fs')
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generatar = require('@babel/generator').default;
class Compilcation {
constructor(options) {
this.options = options;
this.fileDependencies = [];
this.modules = [];
this.chunks = [];
this.assets = {};
}
build(callback) {
// 5. 根据配置中的entry找到所有的入口文件
let entry = {};
if (typeof this.options.entry === 'string') {
entry.main = this.options.entry;
} else {
entry = this.options.entry;
};
for (let entryName in entry) {
let entryPath = path.posix.join(baseDir, entry[entryName]);
this.fileDependencies.push(entryPath);
// 6. 根据入口文件调用所有的配置Loader对模块进行编译
const entryModule = this.buildModule(entryName, entryPath);
// 8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk
let chunk = {
name: entryName,
entryModule,
// 包含就表示入口文件依赖了这个模块
modules: this.modules.filter((item) => item.names.includes(entryName))
};
this.chunks.push(chunk);
}
// 9. 把每个chunk转换成单独的文件添加到输出列表
this.chunks.forEach(chunk => {
// 替换output中的输出文件名称
let filename = this.options.output.filename.replace("[name]", chunk.name);
this.assets[filename] = getSource(chunk);
});
// 调用callback把参数传回
callback(null, {
chunks: this.chunks,
modules: this.modules,
assets: this.assets,
}, this.fileDependencies);
}
buildModule(name, modulePath) {
// 获取到源文件代码
let sourceCode = fs.readFileSync(modulePath, 'utf8');
// 找到webpack.config.js中的module的rules
const { rules } = this.options.module;
let loaders = [];
rules.forEach(rule => {
if (modulePath.match(rule.test)) {
loaders.push(...rule.use);
}
});
// 执行配置的Loader,先引用loader,然后执行传入源代码
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
// 获取当前的模块ID ./src/entry1
let moduleId = './' + path.posix.relative(baseDir, modulePath);
let moudle = {
id: moduleId,
dependencies: [],
names: [name]
};
// 7. 再找出该模块所依赖的模块,递归本步骤直到所有的入口依赖都经过了本步骤的处理
// 把源代码传入,通过解析生成ast语法树
let ast = parser.parse(sourceCode, {sourceType: "module"})
traverse(ast, {
CallExpression: ({ node }) => {
if (node.callee.name === "require") {
// 获取依赖模块的相对路径 wepback打包后不管什么模块,模块ID都是相对于根目录的相对路径 ./src ./node_modules
let depModuleName = node.arguments[0].value; // ./title
// 获取当前模块的所在的目录
let dirname = path.posix.dirname(modulePath); //src
//C:\aproject\zhufengwebpack202108\4.flow\src\title.js
let depModulePath = path.posix.join(dirname, depModuleName);
// 匹配文件扩展名找到对应的文件
let extensions = this.options.resolve.extensions;
depModulePath = tryExtensions(depModulePath, extensions);
// 把找到的对应的依赖文件添加到依赖数组中去
this.fileDependencies.push(depModulePath);
//生成此模块的模块ID
let depModuleId = "./" + path.posix.relative(baseDir, depModulePath);
node.arguments = [types.stringLiteral(depModuleId)]; // ./title => ./src/title.js
//把此模块依赖的模块ID和模块路径放到此模块的依赖数组中
module.dependencies.push({ depModuleId, depModulePath });
}
}
});
// 把ast语法树从新生成为源代码
let { code } = generator(ast);
// 把新生成的源码指向_source属性上
module._source = code;
// 7. 再找出该模块所依赖的模块,递归本步骤直到所有的入口依赖都经过了本步骤的处理
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)
};
});
}
}
// 获取源文件并输出
function getSource(chunk) {
return `
(() => {
var modules = {
${chunk.modules.map(
(module) => `
"${module.id}": (module) => {
${module._source}
},
`
)}
};
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
var exports ={};
${chunk.entryModule._source}
})();
`;
}
module.exports = Compilcation;
webpack.config.js
const RunPlugin = require('./plugins/run-plugin');
const DonePlugin = require('./plugins/done-plugin');
module.exports = {
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
path: path.resolve("dist"),
filename: "[name].js",
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
},
module: {
rules: [
{
test: /.js$/,
use: [
path.resolve(__dirname, 'loaders/loggers1.js'),
path.resolve(__dirname, 'loaders/loggers2.js')
]
}
]
},
plugins: [
new RunPlugin(),
new DonePlugin(),
]
}
自定义插件 - RunPlugin.js
- 插件都是一个类
- 固定都有一个apply的方法,参数是固定的compiler
- compiler实例可以调用hooks方法,来监听事件
class RunPlugin {
apply(compiler) {
compiler.hooks.run.tap('runPlugin', () => console.log('开始编译 runplugin执行'))
}
}
module.exports = Runplugin
自定义插件 - DonePlugin.js
class DonePlugin {
apply(compiler) {
compiler.hooks.run.tap('donePlugin', () => console.log('结束编译 doneplugin执行'))
}
}
module.exports = Runplugin
自定义loader - logger1.js
function loader(source) {
return source + '//logger1'
};
module.exports = loader;
自定义loader - logger2.js
function loader(source) {
return source + '//logger2'
};
module.exports = loader;