ast语法树
有三个操作ast语法树的api
- esprima:将源代码转化为抽象语法树
- estraverse:遍历抽象语法树,修改书上的语法节点
- escodegen:将抽象语法树生成代码
let esprima = require('esprima');
let estraverse = require('estraverse');
let escodegen = require('escodegen');
let sourceCode = 'function ast(){}';
let ast = esprima.parse(sourceCode);
let indent = 0;
let visitor = {
enter(node, parent) {
console.log(node.type);
if (node.type === 'FunctionDeclaration') {
node.id.name = 'newFunction';
}
indent++;
},
leave(node, parent) {
indent--;
console.log(node.type);
}
}
estraverse.traverse(ast,visitor);
//重新生成源代码,经过上面的步骤,已经将函数名转化为newFunction了
let newSourceCode = escodegen.generate(ast);
console.log(newSourceCode);
webpack打包流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler 对象
- 加载所有配置的插件
- 执行对象的 run 方法开始执行编译
- 根据配置中的entry找出入口文件
- 从入口文件出发,调用所有配置的Loader对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
初始化参数
function webpack(options) {
// [ '--mode=development']
const argv = process.argv.slice(2);
// 获取shell配置,返回一个对象 { mode: development }
const shellOptions = argv.reduce((shellOptions, option) => {
let [key, value] = option.split('=');
shellOptions[key.slice(2)] = value;
return shellOptions;
}, {});
// 合并用户配置和shell配置,得到最终的配置对象finalOptions
const finalOptions = { ...options, ...shellOptions };
//2.用上一步得到的参数初始化 Compiler 对象
let compiler = new Compiler(finalOptions);
//3.加载所有配置的插件,调用插件对象的apply方法,并将compiler实例传进去
finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
return compiler;
}
tapable
// webpack中使用的是tapable,这里起到发布订阅的功能
//let { SyncHook } = require('tapable');
class SyncHook {
constructor(args) {
// 参数
this.args = args;
// 事件池
this.taps = [];
}
// 注册事件
tap(name, fn) {
this.taps.push(fn);
}
// 触发事件
call(...args) {
this.taps.forEach((tap) => tap(...args));
}
}
let syncHook = new SyncHook(['name', 'age']);
// 注册事件、等同于on
syncHook.tap('监听器的名字1(没有实际意义)', (name, age) => {
console.log(name, age);
})
syncHook.tap('监听器的名字2(没有实际意义)', (name, age) => {
console.log(name, age);
})
// 触发事件执行、等同于emit
syncHook.call('myName', 13);
编译器Compiler
/**
* 代表整个编译对象,负责整个编译的过程,里面会保存所有的编译的信息
* Compiler类的实例全局唯一
* 会将compiler实例作为参数传递给所有插件的apply方法
*/
class Compiler {
constructor(options) {
this.options = options;
//存的是当前的Compiler上面的所有的钩子
this.hooks = {
//开始编译事件
run: new SyncHook(),
//编译结束事件
done: new SyncHook()
}
}
//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();
}
compile(onCompiled) {
//以后每次开启一次新的编译 ,都会创建一个新的Compilation类的实例
let compilation = new Compilation(this.options);
compilation.build(onCompiled);
}
}
调用compiler的run方法
- 调用compiler.run
const webpack = require('./webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
//4.执行编译器对象的 run 方法开始执行编译 run 方法开始执行编译
compiler.run((err, stats) => {
console.log(err);
console.log(stats.toJson());
});
- Compiler
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(),
done: new SyncHook()
}
}
run(callback) {
// 编译开始,触发run钩子
this.hooks.run.call();
// 编译结束的回调
const onCompiled = (err, stats, fileDependencies) => {
// 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
// TODO...
}
// 进行编译
this.compile(onCompiled);
// 编译结束后触发done钩子
this.hooks.done.call();
}
compile(onCompiled) {
//每次新的新的编译 ,都会创建一个新的Compilation类的实例
let compilation = new Compilation(this.options);
// 开始构建
compilation.build(onCompiled);
}
}
调用compilation的build方法
const path = require('path').posix;
const fs = require('fs');
const types = require('@babel/types');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const baseDir = toUnixPath(process.cwd());
function toUnixPath(filePath) {
return filePath.replace(/\\/g, '/');
}
class Compilation {
constructor(options) {
// 配置对象
this.options = options;
//存放本次编译的所有的模块
this.modules = [];
//当前编译依赖的文件
this.fileDependencies = [];
//里面放置所有的chunk
this.chunks = [];
// 输出资源
this.assets = {};
}
// 构建
build(onCompiled) {
// 5. 根据配置中的entry找出入口文件
let entry = {};
// 兼容entry的值是对象和字符串的情况
if (typeof this.options.entry === 'string') {
entry.main = this.options.entry;
} else {
entry = this.options.entry;
}
// 遍历入口对象 {main: './src/title.js'}
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.assets[filename] = getSource(chunk);
});
}
// 编译完成、触发回调,根据参数生成输出文件
onCompiled(null, {
modules: this.modules,
chunks: this.chunks,
assets: this.assets
}, this.fileDependencies);
}
/**
* 编译模块
* @param {*} name 入口的名称 entry1 entry2
* @param {*} modulePath 模块的路径
*/
buildModule(name, modulePath) {
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
// 读取源代码的内容
let sourceCode = fs.readFileSync(modulePath, 'utf8');
// 匹配此模块需要使用的loader
let { rules } = this.options.module;
let loaders = [];
// 遍历规则数组
rules.forEach(rule => {
//如果正则匹配上了,则把此rule对应的loader添加到loaders数组里
if (modulePath.match(rule.test)) {
loaders.push(...rule.use);
}
});
// 使用loader对文件进行处理(loader顺序=>从右向左),得到最终的代码 sourceCode
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] };
// 将源代码转化为ast语法树
let ast = parser.parse(sourceCode, { sourceType: 'module' });
// 对语法树进行分析处理
traverse(ast, {
CallExpression: ({ node }) => {
if (node.callee.name === 'require') {
let depModuleName = node.arguments[0].value;//./title
//获取当前模块所有的目录 => xxx/xxx/src
let dirname = path.dirname(modulePath);
// xxx/xxx/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;
}
}
/**
* 尝试给当前的路径添加扩展名
* @param {*} modulePath
* @param {*} extensions
*/
function tryExtensions(modulePath, extensions) {
// 如果modulePath存在,不需要添加后缀
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 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}
})();
`;
}
生成文件
class Compiler {
constructor(options) {
this.options = options;
//存的是当前的Compiler上面的所有的钩子
this.hooks = {
run: new SyncHook(), //开始编译的时候触发
done: new SyncHook() //编译结束的时候触发
}
}
//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();
}
compile(onCompiled) {
//以后每次开启一次新的编译 ,都会创建一个新的Compilation类的实例
let compilation = new Compilation(this.options);
compilation.build(onCompiled);
}
}