webpack 编译流程

  1. 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象

Shell优先级比较高。即最终:mode=’development’
image.png

  1. 用上一步得到的参数初始化Compiler对象
  2. 加载所有配置的插件
  3. 执行对象的run方法开始执行编译
  4. 根据配置中的entry找出入口文件
  5. 从入口文件出发,调用所有配置的Loader对模块进行编译
  6. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  7. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  8. 再把每个Chunk转换成一个单独的文件加入到输出列表
  9. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
image.png
细化编译模块:

  • 从入口文件出发,这里用到fs的读取文件系统,将入口文件经过babel转换成es5的代码得倒entrySource
  • 构建模块 :一个对象module{id:entry,source:entrySource},在webpack中,每个文件就是一个模块,包括css,图片等
  • 把入口文件转成抽象语法树,分析里面的import和require依赖,编译入口文件内依赖的模块
  • 根据入口文件和模块之前的依赖关系,组装成一个个包含多个模块的chunk

输出 : 把每个chunk转换成一个单独的文件加入到输出列表 chunk={name:’main’,modules}

debugger.js

  1. const webpack = require('./webpack');
  2. const webpackOptions = require('./webpack.config');
  3. //compiler代表整个编译过程.
  4. const compiler = webpack(webpackOptions);
  5. //调用它的run方法可以启动编译
  6. compiler.run((err,stats)=>{
  7. console.log(err);
  8. let result = stats.toJson({
  9. files:true,//产出了哪些文件
  10. assets:true,//生成了那些资源
  11. chunk:true,//生成哪些代码块
  12. module:true,//模块信息
  13. entries:true //入口信息
  14. });
  15. console.log(JSON.stringify(result,null,2));
  16. });

webpack.config.js

  1. const path = require('path');
  2. const RunPlugin = require('./plugins/run-plugin');
  3. const DonePlugin = require('./plugins/done-plugin');
  4. const AssetPlugin = require('./plugins/assets-plugin');
  5. module.exports = {
  6. mode:'development',
  7. devtool:false,
  8. context:process.cwd(),//上下文目录, ./src .默认代表根目录 默认值其实就是当前命令执行的时候所在的目录
  9. entry:{
  10. entry1:'./src/entry1.js',
  11. entry2:'./src/entry2.js'
  12. },
  13. output:{
  14. path:path.join(__dirname,'dist'),
  15. filename:'[name].js'
  16. },
  17. resolve:{
  18. extensions:['.js','.jsx','.json']
  19. },
  20. module:{
  21. rules:[
  22. {
  23. test:/\.js$/,
  24. use:[
  25. path.resolve(__dirname,'loaders','logger1-loader.js'),
  26. path.resolve(__dirname,'loaders','logger2-loader.js')
  27. ]
  28. }
  29. ]
  30. },
  31. plugins:[
  32. new RunPlugin(),
  33. new DonePlugin(),
  34. new AssetPlugin()
  35. ]
  36. }

./webpack.js

  1. const Compiler = require('./Compiler');
  2. function webpack(options){
  3. //1. 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
  4. let shellConfig= process.argv.slice(2).reduce((shellConfig,item)=>{
  5. //item= --mode=development
  6. let [key,value] = item.split('=');
  7. shellConfig[key.slice(2)]=value;
  8. return shellConfig;
  9. },{});
  10. let finalConfig = {...options,...shellConfig};
  11. //2. 用上一步得到的参数初始化Compiler对象
  12. let compiler = new Compiler(finalConfig);
  13. //3. 加载所有配置的插件
  14. let {plugins} = finalConfig;
  15. for(let plugin of plugins){
  16. plugin.apply(compiler);
  17. }
  18. return compiler;
  19. }
  20. module.exports = webpack;

Compiler.js

代表整个编译过程

  1. let {SyncHook} = require('tapable');
  2. let Complication = require('./Complication');
  3. let path = require('path');
  4. let fs = require('fs');
  5. class Compiler{
  6. constructor(options){
  7. this.options = options;
  8. this.hooks = {
  9. run:new SyncHook(),//开始启动编译 刚刚开始
  10. emit:new SyncHook(['assets']),//会在将要写入文件的时候触发
  11. done:new SyncHook()//将会在完成编译的时候触发 全部完成
  12. }
  13. }
  14. //4. 执行Compiler对象的run方法开始执行编译
  15. run(callback){
  16. this.hooks.run.call();//触发run钩子
  17. //5. 根据配置中的entry找出入口文件
  18. this.compile((err,stats)=>{
  19. this.hooks.emit.call(stats.assets);// 把assets给emit钩子
  20. //11. 拓展:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
  21. for(let filename in stats.assets){
  22. let filePath = path.join(this.options.output.path,filename);
  23. fs.writeFileSync(filePath,stats.assets[filename],'utf8');
  24. }
  25. callback(null,{
  26. toJson:()=>stats
  27. });
  28. });
  29. //监听入口的文件变化,如果文件变化了,重新再开始编译
  30. /* Object.values(this.options.entry).forEach(entry=>{
  31. fs.watchFile(entry,()=>this.compile(callback));
  32. }); */
  33. //中间是我们编译流程
  34. this.hooks.done.call();//编译之后触发done钩子
  35. }
  36. compile(callback){
  37. let complication = new Complication(this.options);
  38. complication.build(callback);
  39. }
  40. }

Complication.js

代表一次编译,代表一次生产过程

  1. const path = require('path');
  2. const fs = require('fs');
  3. const types = require('babel-types');
  4. const parser = require('@babel/parser');
  5. const traverse = require('@babel/traverse').default;
  6. const generator = require('@babel/generator').default;
  7. const baseDir = toUnitPath(process.cwd());//\
  8. function toUnitPath(filePath) {
  9. return filePath.replace(/\\/g, '/');
  10. }
  11. class Complication {
  12. constructor(options) {
  13. this.options = options;
  14. //webpack4 数组 webpack5 set:为了防止重复
  15. this.entries = [];//存放所有的入口
  16. this.modules = [];// 存放所有的模块
  17. this.chunks = [];//存放所的代码块
  18. this.assets = {};//所有产出的资源
  19. this.files = [];//所有产出的文件
  20. }
  21. build(callback) {
  22. //5. 根据配置中的entry找出入口文件
  23. let entry = {};
  24. if (typeof this.options.entry === 'string') {
  25. entry.main = this.options.entry;
  26. } else {
  27. entry = this.options.entry;
  28. }
  29. //entry={entry1:'./src/entry1.js',entry2:'./src/entry2.js'}
  30. for (let entryName in entry) {
  31. //5.获取 entry1的绝对路径
  32. let entryFilePath = toUnitPath(path.join(this.options.context, entry[entryName]));
  33. //6.从入口文件出发,调用所有配置的Loader对模块进行编译
  34. let entryModule = this.buildModule(entryName, entryFilePath);
  35. //this.modules.push(entryModule);
  36. //8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  37. let chunk = {
  38. name: entryName, entryModule, modules: this.modules.filter(item => {
  39. return item.name === entryName || item.extraNames.includes(entryName);
  40. })
  41. };
  42. this.entries.push(chunk);
  43. this.chunks.push(chunk);
  44. }
  45. //9. 再把每个Chunk转换成一个单独的文件加入到输出列表
  46. this.chunks.forEach(chunk => {
  47. let filename = this.options.output.filename.replace('[name]', chunk.name);
  48. // this.assets就是输出列表 key输出的文件名 值就是输出的内容
  49. this.assets[filename] = getSource(chunk);
  50. });
  51. // 10.正常流程 在确定好输出内容之后,根据配置确定的输出路径喝文件名,把文件内容写入文件系统
  52. this.files=Object.keys(this.assest)
  53. for(let fileName in this.assest){
  54. let filePath = path.join(this.option.output.path,fileName)
  55. fs.writeFileSync(filePath,this.assest[fileName],'utf8')
  56. }
  57. callback(null, {
  58. toJson:()=>{
  59. return {
  60. entries: this.entries,
  61. chunks: this.chunks,
  62. modules: this.modules,
  63. files: this.files,
  64. assets: this.assets
  65. }
  66. }
  67. });
  68. // 11.拓展
  69. //callback(null,this.assets),最终在compile拿到
  70. }
  71. //name=名称,modulePath=模块的绝对路径
  72. buildModule(name, modulePath) {
  73. //6. 从入口文件出发,调用所有配置的Loader对模块进行编译
  74. //1.读取模块文件的内容
  75. let sourceCode = fs.readFileSync(modulePath, 'utf8');//console.log('entry1');
  76. let rules = this.options.module.rules;
  77. let loaders = [];///寻找匹配的loader
  78. for (let i = 0; i < rules.length; i++) {
  79. let { test } = rules[i];
  80. //如果此rule的正则和模块的路径匹配的话
  81. if (modulePath.match(test)) {
  82. loaders = [...loaders, ...rules[i].use];
  83. }
  84. }
  85. sourceCode = loaders.reduceRight((sourceCode, loader) => {
  86. return require(loader)(sourceCode);
  87. }, sourceCode);
  88. /* for(let i=loaders.length-1;i>=0;i--){
  89. let loader = loaders[i];
  90. sourceCode = require(loader)(sourceCode);
  91. } */
  92. //console.log('entry1');//2//1
  93. //console.log(sourceCode);
  94. //7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  95. //获得当前模块模块ID ./src/index.js
  96. let moduleId = './' + path.posix.relative(baseDir, modulePath);
  97. let module = { id: moduleId, dependencies: [], name, extraNames: [] };
  98. // 把源代码转化成语法树
  99. let ast = parser.parse(sourceCode, { sourceType: 'module' });
  100. //遍历语法树
  101. traverse(ast, {
  102. //遍历语法树,拦截CallExpression节点
  103. // 什么是CallExpression,比如const title=require('./src/title1.js')就是一个 //CallExpression
  104. CallExpression: ({ node }) => {
  105. if (node.callee.name === 'require') {
  106. //依赖的模块的相对路径
  107. let moduleName = node.arguments[0].value;//./title1
  108. //获取当前模块的所有的目录
  109. let dirname = path.posix.dirname(modulePath);// /
  110. //C:/aproject/zhufengwebpack202106/4.flow/src/title1
  111. let depModulePath = path.posix.join(dirname, moduleName);
  112. let extensions = this.options.resolve.extensions;
  113. depModulePath = tryExtensions(depModulePath, extensions);//已经包含了拓展名了
  114. //得到依赖的模块ID C:/aproject/zhufengwebpack202106/4.flow/src/title1.js
  115. //相对于项目根目录 的相对路径 ./src/title1.js
  116. let depModuleId = './' + path.posix.relative(baseDir, depModulePath);
  117. //require('./title1');=>require('./src/title1.js');
  118. node.arguments = [types.stringLiteral(depModuleId)];
  119. //依赖的模块绝对路径放到当前的模块的依赖数组里
  120. module.dependencies.push({ depModuleId, depModulePath });
  121. }
  122. }
  123. });
  124. let { code } = generator(ast);
  125. module._source = code;//模块源代码指向语法树转换后的新生成的源代码
  126. //7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  127. module.dependencies.forEach(({ depModuleId, depModulePath }) => {
  128. let depModule = this.modules.find(item => item.id === depModuleId);
  129. if (depModule) {
  130. depModule.extraNames.push(name);
  131. } else {
  132. let dependencyModule = this.buildModule(name, depModulePath);
  133. this.modules.push(dependencyModule);
  134. }
  135. });
  136. return module;
  137. }
  138. }
  139. function getSource(chunk) {
  140. return `
  141. (() => {
  142. var modules = ({
  143. ${chunk.modules.map(module => `
  144. "${module.id}":(module,exports,require)=>{
  145. ${module._source}
  146. }
  147. `).join(',')
  148. }
  149. });
  150. var cache = {};
  151. function require(moduleId) {
  152. var cachedModule = cache[moduleId];
  153. if (cachedModule !== undefined) {
  154. return cachedModule.exports;
  155. }
  156. var module = cache[moduleId] = {
  157. exports: {}
  158. };
  159. modules[moduleId](module, module.exports, require);
  160. return module.exports;
  161. }
  162. var exports = {};
  163. (() => {
  164. ${chunk.entryModule._source}
  165. })();
  166. })()
  167. ;
  168. `
  169. }
  170. // 匹配后缀
  171. function tryExtensions(modulePath, extensions) {
  172. extensions.unshift('');
  173. for (let i = 0; i < extensions.length; i++) {
  174. let filePath = modulePath + extensions[i];// ./title.js
  175. if (fs.existsSync(filePath)) {
  176. return filePath;
  177. }
  178. }
  179. throw new Error(`Module not found`);
  180. }
  181. module.exports = Complication;

plugins\assets-plugin.js
拓展:在写入文件系统之前我们触发一个插件触发一个钩子,修改assets,往里面加一个文件
image.png
image.png
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;

结果
image.png

其他

热更新:监听文件变化-》重新编译-〉通知浏览器变化
loader其实是一个函数,接收一个参数(源代码),对源代码进行转换,顺序是从下往上,从右往左
后一个loader执行结果传递给前一个。。。一次类推,最终将处理好的源代码传给webpack

image.png

调用插件这里设计tapable这个库

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