调试
方法一:
1、node –inspect-brk ./node_modules/webpack-cli/bin/cli.js
2、在Chrome中打开一个新窗口,打开控制台,再刷新,在控制台左上角就会出现绿色六角形,代表node脚本,点这个icon,就会打开新的控制台
3、点左侧的文件,会自动进入断点
方法二:
通过配置文件launch.json,然后再启动调试
流程概况
1.初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
2.用上一步得到的参数初始化 Compiler 对象
3.加载所有配置的插件
4.执行对象的 run 方法开始执行编译
5.根据配置中的entry找出入口文件
6.从入口文件出发,调用所有配置的Loader对模块进行编译
7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
9.再把每个 Chunk 转换成一个单独的文件加入到输出列表
10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
\
假设我们有debugger.js,内容如下:
const webpack = require('./webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
//4.执行compiler对象的 run 方法开始执行编译
compiler.run((err, stats) => {
console.log(err);
console.log(stats.toJson());
});
复制代码
控制台执行:
node debugger.js title=zhufeng age=10 –mode=development
在执行过程中,进程的任意位置console.log(process.argv),得到的结果为:
[
'node可执行文件的绝对路径',
'debugger.js的绝对路径',
'title=zhufeng',
'age=10',
'--mode=development'
]
复制代码
处理参数 初始化Compiler 加载插件
const Compiler = require('./Compiler');
function webpack(options) {
//1.初始化参数:从配置文件和 Shell 语句中读取并合并参数, 得出最终的配置对象
const argv = process.argv.slice(2);//[ '--mode=development']
const shellOptions = argv.reduce((shellOptions, option) => {
let [key, value] = option.split('=');
shellOptions[key.slice(2)] = value;
return shellOptions;
}, {});
const finalOptions = { ...options, ...shellOptions };
//2.用上一步得到的参数初始化 Compiler 对象
let compiler = new Compiler(finalOptions);
//3.加载所有配置的插件
finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
return compiler;
}
module.exports = webpack;
复制代码
执行对象的 run 方法
这一步在自己写的构建初始化文件中,在本例中就是debugger.js
const webpack = require('./webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
//4.执行compiler对象的 run 方法开始执行编译
compiler.run((err, stats) => {
console.log(err);
console.log(stats.toJson());
});
复制代码
根据配置中的entry找出入口文件
Compiler.compile的调用和一个Compilation对象有一一映射关系
这一步在Compilation对象中,从代码中可以看到当entry只给了一个字符串时,会将其转换为一个main入口
class Compilation {
constructor(options) {
this.options = options;
}
build(onCompiled) {
//5.根据配置中的entry找出入口文件
let entry = {};
//兼容entry的值是对象和字符串的情况
if (typeof this.options.entry === 'string') {
entry.main = this.options.entry;
} else {
entry = this.options.entry;
}
复制代码
从入口文件出发,调用所有配置的Loader对模块进行编译
class Compilation {
constructor(options) {
this.options = options;
}
build(onCompiled) {
//5.根据配置中的entry找出入口文件
let entry = {};
//兼容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.join(baseDir, entry[entryName]);
this.fileDependencies.push(entryPath);
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
let entryModule = this.buildModule(entryName, entryPath);
}
onCompiled(null, {}, this.fileDependencies);
}
buildModule(name, modulePath) {
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
//6.1读取源代码的内容
let sourceCode = fs.readFileSync(modulePath, 'utf8');
//6.2匹配此模块需要使用的loader
let { rules } = this.options.module;
let loaders = [];
rules.forEach(rule => {
//如果正则匹配上了,则把此rule对应的loader添加到loaders数组里
if (modulePath.match(rule.test)) {
loaders.push(...rule.use);
}
});
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
复制代码
再寻找依赖模块,递归处理
基本思路:通过遍历ast语法树,拦截到当前文件中的require调用,找到依赖后再进行编译
通常来讲,为了避免耦合,我们不会在import或require里加上文件后缀:
let title = require('./title');
复制代码
所以我们需要在webpack的配置文件中添加resolve参数来告诉webpack通过什么后缀名来识别
module.exports = {
// ...
entry: {
// ...
},
output: {
// ...
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
},
复制代码
webpack遇到每个文件时,会挨个尝试加上这些扩展名去寻找对应文件是否存在(通过方法tryExtensions)
文件和模块一一映射,每个模块都有一个标识:moduleId,是相对于项目根目录的相对路径
每个模块也都有一个对象module来描述其信息
module = {
id: moduleId,
dependencies: [],
// 表示此模块添几个入口依赖了,入口的名称 [entry1,entry2]
names: [name]
}
复制代码
\
// 需要增加babel依赖,用来对代码进行转换以及分析
const types = require('@babel/types');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
class Compilation {
constructor(options) {
// ...
}
build(onCompiled) {
// ...
}
buildModule(name, modulePath) {
// ...
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
//7.再找出该模块依赖的模块,再递归本步骤(buildModule)直到所有入口依赖的文件都经过了本步骤的处理
//"./src/title.js" 每个模块都有一个ID, id: './src/entry1.js',
//模块ID就是相对于项目根目录的相对路径
let moduleId = "./" + path.relative(baseDir, modulePath);
//创建一个模块对象,moduleId是相对于项目根目录的相对路径 dependencies表示此模块依赖的模块
//names表示此模块添几个入口依赖了,入口的名称 [entry1,entry2]
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;//./title
//获取当前模块所有的目录 C:\aproject\zhufengwebpack202111\4.flow\src
let dirname = path.dirname(modulePath);
//获取依赖的模块的绝对路径 C:/aproject/zhufengwebpack202111/4.flow/src/title
let depModulePath = path.join(dirname, depModuleName);
//获取当前支持扩展名
let extensions = this.options.resolve.extensions;
//获取依赖的模块的绝对路径
depModulePath = tryExtensions(depModulePath, extensions);
//把此依赖文件添加到依赖数组里,当文件变化了,会重新启动编译 ,创建一个新的Compilation
this.fileDependencies.push(depModulePath);
//获取依赖模块的模块iD,也就是相对于根目录的相对路径
let depModuleId = './' + path.relative(baseDir, depModulePath);
//修改AST语法对,把require方法的参数变成依赖的模块ID
node.arguments = [types.stringLiteral(depModuleId)];
//把依赖信息添加到依赖数组里
module.dependencies.push({ depModuleId, depModulePath });
}
}
});
let { code } = generator(ast);
module._source = code;
module.dependencies.forEach(({ depModuleId, depModulePath }) => {
let buildedModule = this.modules.find(module => module.id === depModuleId);
if (buildedModule) {
//title这个module.names = [entry1,entry2];
buildedModule.names.push(name);
} else {
let depModule = this.buildModule(name, depModulePath);
this.modules.push(depModule);
}
});
return module;
}
}
复制代码
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
这一步是在所有module编译完之后进行的,所以应该在build方法的最后,这里的规则是一个entry对应一个chunk:
class Compilation {
constructor(options) {
this.options = options;
this.modules = [];//存放本次编译的所有的模块
//当前编译依赖的文件
this.fileDependencies = [];
//里面放置所有的代码块
this.chunks = [];
this.assets = {};
this.hooks = {
chunkAsset: new SyncHook(["chunk", "filename"])
}
}
build(onCompiled) {
//5.根据配置中的entry找出入口文件
let entry = {};
//兼容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.join(baseDir, entry[entryName]);
this.fileDependencies.push(entryPath);
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
let entryModule = this.buildModule(entryName, entryPath);
//8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
let chunk = {
name: entryName,//代码块名称是入口名称
entryModule,///入口模块
//这个入口代码块中包含哪些模块
modules: this.modules.filter(module => module.names.includes(entryName))
}
this.chunks.push(chunk);
//9.再把每个 Chunk 转换成一个单独的文件加入到输出列表
this.chunks.forEach(chunk => {
let filename = this.options.output.filename.replace('[name]', chunk.name);
this.hooks.chunkAsset.call(chunk, filename);
this.assets[filename] = getSource(chunk);
});
}
onCompiled(null, {
modules: this.modules,
chunks: this.chunks,
assets: this.assets
}, this.fileDependencies);
}
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}
})();
`;
}
复制代码
最终生成文件
这一步是所有模块拼接好之后进行的,在最后的onCompiled的回调中:
class Compiler {
constructor(options) {
this.options = options;
//存的是当前的Compiler上面的所有的钩子
this.hooks = {
run: new SyncHook(), //开始编译的时候触发
done: new SyncHook(), //编译结束的时候触发
compilation: new SyncHook(["compilation", "params"]),
}
}
//4.执行对象的 run 方法开始执行编译
run(callback) {
//在执行Compiler的run方法开头触发run这个钩子
this.hooks.run.call();
const onCompiled = (err, stats, fileDependencies) => {
//10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
for (let filename in stats.assets) {
let filePath = path.join(this.options.output.path, filename);
fs.writeFileSync(filePath, stats.assets[filename], 'utf8');
}
callback(null, {
toJson: () => stats
});
fileDependencies.forEach(fileDependency => {
fs.watch(fileDependency, () => this.compile(onCompiled));
});
}
this.compile(onCompiled);
//编译过程....
this.hooks.done.call();
}
复制代码