webpack 编译流程
- 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
Shell优先级比较高。即最终:mode=’development’
- 用上一步得到的参数初始化Compiler对象
- 加载所有配置的插件
- 执行对象的run方法开始执行编译
- 根据配置中的entry找出入口文件
- 从入口文件出发,调用所有配置的Loader对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个Chunk转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
细化编译模块:
- 从入口文件出发,这里用到fs的读取文件系统,将入口文件经过babel转换成es5的代码得倒entrySource
- 构建模块 :一个对象module{id:entry,source:entrySource},在webpack中,每个文件就是一个模块,包括css,图片等
- 把入口文件转成抽象语法树,分析里面的import和require依赖,编译入口文件内依赖的模块
- 根据入口文件和模块之前的依赖关系,组装成一个个包含多个模块的chunk
输出 : 把每个chunk转换成一个单独的文件加入到输出列表 chunk={name:’main’,modules}
debugger.js
const webpack = require('./webpack');const webpackOptions = require('./webpack.config');//compiler代表整个编译过程.const compiler = webpack(webpackOptions);//调用它的run方法可以启动编译compiler.run((err,stats)=>{console.log(err);let result = stats.toJson({files:true,//产出了哪些文件assets:true,//生成了那些资源chunk:true,//生成哪些代码块module:true,//模块信息entries:true //入口信息});console.log(JSON.stringify(result,null,2));});
webpack.config.js
const path = require('path');const RunPlugin = require('./plugins/run-plugin');const DonePlugin = require('./plugins/done-plugin');const AssetPlugin = require('./plugins/assets-plugin');module.exports = {mode:'development',devtool:false,context:process.cwd(),//上下文目录, ./src .默认代表根目录 默认值其实就是当前命令执行的时候所在的目录entry:{entry1:'./src/entry1.js',entry2:'./src/entry2.js'},output:{path:path.join(__dirname,'dist'),filename:'[name].js'},resolve:{extensions:['.js','.jsx','.json']},module:{rules:[{test:/\.js$/,use:[path.resolve(__dirname,'loaders','logger1-loader.js'),path.resolve(__dirname,'loaders','logger2-loader.js')]}]},plugins:[new RunPlugin(),new DonePlugin(),new AssetPlugin()]}
./webpack.js
const Compiler = require('./Compiler');function webpack(options){//1. 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象let shellConfig= process.argv.slice(2).reduce((shellConfig,item)=>{//item= --mode=developmentlet [key,value] = item.split('=');shellConfig[key.slice(2)]=value;return shellConfig;},{});let finalConfig = {...options,...shellConfig};//2. 用上一步得到的参数初始化Compiler对象let compiler = new Compiler(finalConfig);//3. 加载所有配置的插件let {plugins} = finalConfig;for(let plugin of plugins){plugin.apply(compiler);}return compiler;}module.exports = webpack;
Compiler.js
代表整个编译过程
let {SyncHook} = require('tapable');let Complication = require('./Complication');let path = require('path');let fs = require('fs');class Compiler{constructor(options){this.options = options;this.hooks = {run:new SyncHook(),//开始启动编译 刚刚开始emit:new SyncHook(['assets']),//会在将要写入文件的时候触发done:new SyncHook()//将会在完成编译的时候触发 全部完成}}//4. 执行Compiler对象的run方法开始执行编译run(callback){this.hooks.run.call();//触发run钩子//5. 根据配置中的entry找出入口文件this.compile((err,stats)=>{this.hooks.emit.call(stats.assets);// 把assets给emit钩子//11. 拓展:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统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});});//监听入口的文件变化,如果文件变化了,重新再开始编译/* Object.values(this.options.entry).forEach(entry=>{fs.watchFile(entry,()=>this.compile(callback));}); *///中间是我们编译流程this.hooks.done.call();//编译之后触发done钩子}compile(callback){let complication = new Complication(this.options);complication.build(callback);}}
Complication.js
代表一次编译,代表一次生产过程
const path = require('path');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 = toUnitPath(process.cwd());//\function toUnitPath(filePath) {return filePath.replace(/\\/g, '/');}class Complication {constructor(options) {this.options = options;//webpack4 数组 webpack5 set:为了防止重复this.entries = [];//存放所有的入口this.modules = [];// 存放所有的模块this.chunks = [];//存放所的代码块this.assets = {};//所有产出的资源this.files = [];//所有产出的文件}build(callback) {//5. 根据配置中的entry找出入口文件let entry = {};if (typeof this.options.entry === 'string') {entry.main = this.options.entry;} else {entry = this.options.entry;}//entry={entry1:'./src/entry1.js',entry2:'./src/entry2.js'}for (let entryName in entry) {//5.获取 entry1的绝对路径let entryFilePath = toUnitPath(path.join(this.options.context, entry[entryName]));//6.从入口文件出发,调用所有配置的Loader对模块进行编译let entryModule = this.buildModule(entryName, entryFilePath);//this.modules.push(entryModule);//8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunklet chunk = {name: entryName, entryModule, modules: this.modules.filter(item => {return item.name === entryName || item.extraNames.includes(entryName);})};this.entries.push(chunk);this.chunks.push(chunk);}//9. 再把每个Chunk转换成一个单独的文件加入到输出列表this.chunks.forEach(chunk => {let filename = this.options.output.filename.replace('[name]', chunk.name);// this.assets就是输出列表 key输出的文件名 值就是输出的内容this.assets[filename] = getSource(chunk);});// 10.正常流程 在确定好输出内容之后,根据配置确定的输出路径喝文件名,把文件内容写入文件系统this.files=Object.keys(this.assest)for(let fileName in this.assest){let filePath = path.join(this.option.output.path,fileName)fs.writeFileSync(filePath,this.assest[fileName],'utf8')}callback(null, {toJson:()=>{return {entries: this.entries,chunks: this.chunks,modules: this.modules,files: this.files,assets: this.assets}}});// 11.拓展//callback(null,this.assets),最终在compile拿到}//name=名称,modulePath=模块的绝对路径buildModule(name, modulePath) {//6. 从入口文件出发,调用所有配置的Loader对模块进行编译//1.读取模块文件的内容let sourceCode = fs.readFileSync(modulePath, 'utf8');//console.log('entry1');let rules = this.options.module.rules;let loaders = [];///寻找匹配的loaderfor (let i = 0; i < rules.length; i++) {let { test } = rules[i];//如果此rule的正则和模块的路径匹配的话if (modulePath.match(test)) {loaders = [...loaders, ...rules[i].use];}}sourceCode = loaders.reduceRight((sourceCode, loader) => {return require(loader)(sourceCode);}, sourceCode);/* for(let i=loaders.length-1;i>=0;i--){let loader = loaders[i];sourceCode = require(loader)(sourceCode);} *///console.log('entry1');//2//1//console.log(sourceCode);//7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理//获得当前模块模块ID ./src/index.jslet moduleId = './' + path.posix.relative(baseDir, modulePath);let module = { id: moduleId, dependencies: [], name, extraNames: [] };// 把源代码转化成语法树let ast = parser.parse(sourceCode, { sourceType: 'module' });//遍历语法树traverse(ast, {//遍历语法树,拦截CallExpression节点// 什么是CallExpression,比如const title=require('./src/title1.js')就是一个 //CallExpressionCallExpression: ({ node }) => {if (node.callee.name === 'require') {//依赖的模块的相对路径let moduleName = node.arguments[0].value;//./title1//获取当前模块的所有的目录let dirname = path.posix.dirname(modulePath);// ///C:/aproject/zhufengwebpack202106/4.flow/src/title1let depModulePath = path.posix.join(dirname, moduleName);let extensions = this.options.resolve.extensions;depModulePath = tryExtensions(depModulePath, extensions);//已经包含了拓展名了//得到依赖的模块ID C:/aproject/zhufengwebpack202106/4.flow/src/title1.js//相对于项目根目录 的相对路径 ./src/title1.jslet depModuleId = './' + path.posix.relative(baseDir, depModulePath);//require('./title1');=>require('./src/title1.js');node.arguments = [types.stringLiteral(depModuleId)];//依赖的模块绝对路径放到当前的模块的依赖数组里module.dependencies.push({ depModuleId, depModulePath });}}});let { code } = generator(ast);module._source = code;//模块源代码指向语法树转换后的新生成的源代码//7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理module.dependencies.forEach(({ depModuleId, depModulePath }) => {let depModule = this.modules.find(item => item.id === depModuleId);if (depModule) {depModule.extraNames.push(name);} else {let dependencyModule = this.buildModule(name, depModulePath);this.modules.push(dependencyModule);}});return module;}}function getSource(chunk) {return `(() => {var modules = ({${chunk.modules.map(module => `"${module.id}":(module,exports,require)=>{${module._source}}`).join(',')}});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}})();})();`}// 匹配后缀function tryExtensions(modulePath, extensions) {extensions.unshift('');for (let i = 0; i < extensions.length; i++) {let filePath = modulePath + extensions[i];// ./title.jsif (fs.existsSync(filePath)) {return filePath;}}throw new Error(`Module not found`);}module.exports = Complication;
plugins\assets-plugin.js
拓展:在写入文件系统之前我们触发一个插件触发一个钩子,修改assets,往里面加一个文件

plugins\assets-plugin.js
class AssetPlugin{
apply(compiler){
//对于代码执行来说没有用,但是对于阅读代码的人来说可以起到提示的作用
compiler.hooks.emit.tap('AssetPlugin',(assets)=>{
debugger
assets['assets.md']= Object.keys(assets).join('\n');
});
}
}
module.exports = AssetPlugin;
其他
热更新:监听文件变化-》重新编译-〉通知浏览器变化
loader其实是一个函数,接收一个参数(源代码),对源代码进行转换,顺序是从下往上,从右往左
后一个loader执行结果传递给前一个。。。一次类推,最终将处理好的源代码传给webpack
调用插件这里设计tapable这个库

hook.tap :注册监听,相当于button.addeventlistenter…..
hook.call:触发监听,相当于button.trigger
插件其实就是一个类
