• 2021/01/09 更新
  • 2021/07/27 更新

导航

[[深刻01] 执行上下文](https://juejin.im/post/684490...
[[深刻02] 原型链](https://juejin.im/post/684490...
[[深刻03] 继承](https://juejin.im/post/684490...
[[深刻04] 事件循环](https://juejin.im/post/684490...
[[深刻05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490...
[[深刻06] 隐式转换 和 运算符](https://juejin.im/post/684490...
[[深刻07] 浏览器缓存机制(http缓存机制)](https://juejin.im/post/684490...
[[深刻08] 前端平安](https://juejin.im/post/684490...
[[深刻09] 深浅拷贝](https://juejin.im/post/684490...
[[深刻10] Debounce Throttle](https://juejin.im/post/684490...
[[深刻11] 前端路由](https://juejin.im/post/684490...
[[深刻12] 前端模块化](https://juejin.im/post/684490...
[[深刻13] 观察者模式 公布订阅模式 双向数据绑定](https://juejin.im/post/684490...
[[深刻14] canvas](https://juejin.im/post/684490...
[[深刻15] webSocket](https://juejin.im/post/684490...
[[深刻16] webpack](https://juejin.im/post/684490...
[[深刻17] http 和 https](https://juejin.im/post/684490...
[[深刻18] CSS-interview](https://juejin.im/post/684490...
[[深刻19] 手写Promise](https://juejin.im/post/684490...
[[深刻20] 手写函数](https://juejin.im/post/684490...

[[react] Hooks](https://juejin.im/post/684490...

[[部署01] Nginx](https://juejin.im/post/684490...
[[部署02] Docker 部署vue我的项目](https://juejin.im/post/684490...
[[部署03] gitlab-CI](https://juejin.im/post/684490...

[[源码-webpack01-前置常识] AST形象语法树](https://juejin.im/post/684490...
[[源码-webpack02-前置常识] Tapable](https://juejin.im/post/684490...
[[源码-webpack03] 手写webpack - compiler简略编译流程](https://juejin.im/post/684490...
[[源码] Redux React-Redux01](https://juejin.im/post/684490...
[[源码] axios ](https://juejin.im/post/684490...
[[源码] vuex ](https://juejin.im/post/684490...
[[源码-vue01] data响应式 和 初始化渲染 ](https://juejin.im/post/684490...
[[源码-vue02] computed 响应式 - 初始化,拜访,更新过程 ](https://juejin.im/post/684490...
[[源码-vue03] watch 侦听属性 - 初始化和更新 ](https://juejin.im/post/684490...
[[源码-vue04] Vue.set 和 vm.$set ](https://juejin.im/post/684490...
[[源码-vue05] Vue.extend ](https://juejin.im/post/684490...

[[源码-vue06] Vue.nextTick 和 vm.$nextTick ](https://juejin.im/post/684790...

前置常识

一些单词

compiler:编译

npm link

  • (1) 先把须要link的包在根目录执行:---------------------------------- npm link

    • 通过 npm link 能够把包link到全局
    • 该包须要有 bin/wpack.js
    • 在package.json中设置 bin: { wpack: '门路'}
  • (2) 在须要应用该包的我的项目中的根目录,执行命令:------------------- npm link wpack

    • 则会把wpack包装置到node_modules中
  • (3) 验证

    • 在应用到wpack包的我的项目中,执行命令:------------------------- npx wpack

process.cwd() ------------------------ 当前工作目录

  • process.cwd() 返回 Node.js 过程的当前工作目录
  • process.cwd() === path.resolve()
  • process.cwd()

fs.readFileSync(path[, options]) ----- 读文件

  • 作用:返回path的内容
  • 参数:

    • path:文件名或文件描述符
    • options: 配置项,object|string

      • encoding:编码格局,可选

path.relative(from, to) --------------- from 到 to 的行对门路

  • path.relative() 办法依据当前工作目录返回 ( <font color=red>from</font> ) 到 ( <font color=red>to</font> ) 的 ( <font color=red>相对路径</font> )

path.dirname(path) ------------------ 最初一段的父目录

  • path.dirname() 办法返回 path 的目录名
  • 即 ( <font color=red>返回门路中最初一段文件或者文件夹所在的文件夹,即最初一段文件或文件夹的父目录</font> )

path.extname(path) ------------------ 返回path的扩展名

  • path.extname(path) 返回path的扩展名
  • ext是 extend:扩大

arguments.callee --------------------- 指向以后执行的函数 (严格模式下禁止)

  • arguments.callee --------------------- 指向以后执行的函数 (严格模式下禁止)

AST explorer

源码:require('./a.js')AST:{  "type": "Program",  "start": 0,  "end": 17,  "body": [    { // --------------------------------------- body数组可能蕴含多个statement状态对象      "type": "ExpressionStatement",       "start": 0,      "end": 17,      "expression": {        "type": "CallExpression", // ----------- 调用表达式        "start": 0,        "end": 17,        "callee": { // ------------------------- callee.name = 'require'          "type": "Identifier",          "start": 0,          "end": 7,          "name": "require"        },        "arguments": [ // ---------------------- 参数列表          {            "type": "Literal",            "start": 8,            "end": 16,            "value": "./a.js",            "raw": "'./a.js'"          }        ]      }    }  ],  "sourceType": "module"}

babel相干的AST插件

  • @babel/core

    • 外围文件
  • @babel/parser

    • 将源码string转成AST
  • @babe/traverse

    • 遍历AST
    • enter(path)进入exit(path)退出 等钩子
  • @babel/types

    • 批改,增加,删除等,操作AST
    • 用于 AST 的类 lodash 库,其封装了大量与 AST 无关的办法,大大降低了转换 AST 的老本
    • babelTypes.stringLiteral(modulePath)
  • @bebe/generator

    • 将批改后的AST转换成源码string

const options = loaderUtils.getOptions(this)

<font color=blue>Loader - 编写一个自定义loader </font>

  • ( <font color=red>loader</font> ) 是一个 ( <font color=red>函数</font> ),函数的第一个参数示意 ( <font color=red>该loader匹配的文件的 源代码</font> )
  • loader 不能写成 ( 箭头函数 ),因为须要通过this获取更多的api
  • loader-utils

    • 用来获取 module -> rules 中的 loader的 ( <font color=red>options</font> ) 对象
    • 通过 ( loader-utils ) 中的 ( <font color=red>getOptions</font> ) 来获取 ( options ) 对象
    • 装置: npm install loader-utils -D
    • 应用:const options = loaderUtils.getOptions(this)
    • loader-utils
  • this.callback

    • 第一个参数:err // Error 或者 null
    • 第二个参数:content // string或者buffer,即解决过后的源代码
    • 第三个参数:sourceMap? // 可选,必须是一个能够被这个模块解析的 source map
    • 第四个参数:meta? //可选,即元数据
    • this.callback - webpack官网文档
  • this.async

    • this.async 次要用于解决loader中的异步操作
    • 返回值是: this.callback()
  • 编写好的loader,如何在webpack.config.js中引入?

    • 在根目录中新建 loaders 文件夹,外面寄存 replace-loader.js
    • 单个loader

      module.exports = {...module: {    rules: [{        test: /\.js$/,        use: [{            loader: path.resolve(__dirname, 'loaders/replace-loader'), // 须要用到path模块            options: {                name: 'aaaaa'            }        }]    }]}}
    • 多个loader

      module.exports = {...resolveLoader: { // resolveLoader配置项    modules: ['node_modules', path.resolve(__dirname, 'loaders')]    // 通知 webpack 该去那个目录下找 loader 模块    // 先从node_modules中寻找,再在loaders文件夹中寻找    // modules: ['node_modules', './loaders/']},module: {    rules: [{        test: /\.js$/,        use: [{            loader: 'upper-loader',             options: {                name: 'aaaaa'            }        },{            loader: 'replace-loader',            options: {                name: 'hi!!!!???&&&&'            }            // 间接加载在loaders文件夹中的 replace-loader.js,这里只须要写上loader的名字即可        }]    }]}}
  • 自定义loader实例

    • loaders/replace-loader.js
    const loaderUtils = require('loader-utils')// loader-utils插件// 能够通过loader-utils中的getOptions拿到loader中的options对象module.exports = function(source) {  // source就是该loader匹配的文件的源码    const options = loaderUtils.getOptions(this)  // 通过 loader-utils的getOptions获取options对象  const callback = this.async()  // this.async()用来解决loader中的异步操作, -------- 返回值是:this.callback()  // this.callback(err, content, sourceMap?, meta?)    setTimeout(function() {    const result = source.replace('hello', options.name)    callback(null, result)  }, 1000)}
    • webpack.config.js

      const path = require('path')module.exports = {mode: 'development',entry: {index: './src/index.js'},output: {path: path.resolve(__dirname, 'dist'),filename: 'index.js'},module: {rules: [  // {  //   test: /\.js$/,  //   use: [{  //     loader: path.resolve(__dirname, 'loaders/replace-loader.js'),  //     options: {  //       name: 'woow_wu7'  //     }  //   }]  // }  {    test: /\.js$/,    use: [{      loader: 'replace-loader', // 这里的名字就是 loaders 文件夹中的 replace-loader.js 文件名      options: {        name: 'woow_wu77'      }    }]  }]},resolveLoader: { // 规定加载loader的中央限度在 node_modules 文件夹中,和 './loaders/'文件夹中// 先找 node_modules 再找 './loaders/'modules: ['node_modules', './loaders/']}}

<font color=blue>Compiler - 生命周期钩子函数 </font>

  • entryOption

    • 在 webpack 选项中的 entry 配置项 解决过之后,执行插件
  • afterPlugins

    • 设置完初始插件之后,执行插件
  • run

    • compiler.run() 办法执行时触发 - 开始读取 records 之前,钩入(hook into) compiler
  • compile

    • buildMoudle()执行前触发 - 一个新的编译(compilation)创立之后触发
  • afterCompile

    • buildMoudle()执行后触发
  • emit

    • emitFile() 执行时触发 - 生成资源到 output 目录之前。
  • done

    • 编译实现时触发

<font color=blue>Plugin - 编写一个自定义plugin </font>

  • plugin是一个具备 ( <font color=red>apply</font> ) 办法的类,apply办法参数是 ( <font color=red>compiler</font> ) 调用,并且 compiler 对象可在整个编译生命周期拜访
  • 过程:

    • (1) 在Compiler类所在我的项目装置 tapable
    • (2) 编写plugin类

      • 必须有apply()办法
      • 在办法中调用compiler实例的hooks属性对应的生命周期钩子的tap()等注册办法
    • (3) 在webpack.config.js中的plugins中new注册插件实例

      • 就能够在Compiler类的构造函数中循环plugins,执行apply办法
  • plugin的编写,在plugin中通过tap()注册监听,因为是SyncHook所以tap()注册,还有tapAsync(),tapPromise()等

    class EntryOptionPlugin {apply(compiler) {  compiler.hooks.entryOption.tap('EntryOptionPlugin', function() {    console.log('EntryOptionPlugin')  })}}class AfterPlugin {apply(compiler) {  compiler.hooks.afterPlugins.tap('AfterPlugin', function() {    console.log('AfterPlugin')  })}}class RunPlugin {apply(compiler) {  compiler.hooks.run.tap('RunPlugin', function() {    console.log('RunPlugin')  })}}class CompilePlugin {apply(compiler) {  compiler.hooks.compile.tap('CompilePlugin', function() {    console.log('CompilePlugin')  })}}class AfterCompilePlugin {apply(compiler) {  compiler.hooks.afterCompile.tap('AfterCompilePlugin', function() {    console.log('AfterCompilePlugin')  })}}class EmitPlugin {apply(compiler) {  compiler.hooks.emit.tap('emit', function() {    console.log('emit')  })}}class DonePlugin {apply(compiler) {  compiler.hooks.done.tap('DonePlugin', function() {    console.log('DonePlugin')  })}}
  • 在webpack.config.js中注册插件

    plugins: [  new EntryOptionPlugin(),  new AfterPlugin(),  new RunPlugin(),  new CompilePlugin(),  new AfterCompilePlugin(),  new EmitPlugin(),  new DonePlugin()]
  • 在Compiler类中引入tapable并new出不同的生命周期
  • 在不同的函数执行的不同机会执行tapbale中的调用call()办法,这里用的是SyncHook所以用tap()注册,用call()调用

    class Compiler {constructor(config) {  this.hooks = {    entryOption: new SyncHook(),    afterPlugins: new SyncHook(),    run: new SyncHook(),    compile: new SyncHook(),    afterCompile: new SyncHook(),    emit: new SyncHook(),    done: new SyncHook(),  }}}run() {  // run办法次要做两件事件  // 1. 创立模块的依赖关系  // 2. 发射打包后的文件  this.hooks.run.call()  this.hooks.compile.call()  this.buildModule(path.resolve(this.root, this.entry), true)  // buildModule()的作用:建模块的依赖关系  // 参数:  // 第一个参数:是entry指定门路的绝对路径  // 第二个参数:是否是主模块  this.hooks.afterCompile.call()  console.log(this.modules, this.entryId)  // 发射一个文件,打包后的文件  this.emitFile()  this.hooks.emit.call()  this.hooks.done.call()}

<font color=blue>webpack打包后文件剖析</font>

  • 精简代码,去除 webpack_require 上的无关属性,代码如下

  • 再持续简化

    (function(modules){  var initialMoudles = {}    function __webpack_require__(moduleId)  return __webpack_require__('./src/index.js')})()自执行后,相当于调用 __webpack_require__('./src/index.js'),并且 initialMoudles 成为闭包变量,常驻内存
  • 参数对象 modules

    {  "./src/a.js": function () { eval("") },  "./src/base/b.js": function () { eval("") },  "./src/base/c.js": function () { eval("") },  "./src/index.js": function () { eval("") },}
  • 第一步:

    • 调用 __webpack_require__('./src/index.js')
    • 执行 modules[moduleId].call() 即执行modules参数对象'./src/index'中的eval()源码
  • 第二步

    • 调用 __webpack_require__('./src/a.js')
    • 执行 modules[moduleId].call() 即执行modules参数对象'./src/a.js'中的eval()源码
  • 第三步

    • 调用 __webpack_require__('./src/b.js')
    • 执行 modules[moduleId].call() 即执行modules参数对象'./src/b.js'中的eval()源码
  • 第四步

    • 调用 __webpack_require__('./src/c.js')
    • 执行 modules[moduleId].call() 即执行modules参数对象'./src/c.js'中的eval()源码
  • 直到modules中的所有moudleId对应的源码都执行完

手写webpack - compiler

流程

  • buildModul() - modules对象的赋值过程

    • (1) 将webpack.config.js作为参数传入Compiler类
    • (2) 通过new命令调用Compiler,生成compiler实例,并调用Compiler.prototype上的 run 办法

      • 在new命令执行的时候,遍历webpack.config.js中的plugins数组中的plugin实例上的apply()办法
      • tap => apply()办法中会调用compiler.hooks.钩子函数.tap()注册监听事件
      • call => 在不同的compiler的函数中去call()执行事件,从而在不同生命周期实现监听
    • (3) 在 run 办法中调用 buildModule() 和 emitFile()
    • (4) buildModule() 办法承受webpack.config.js中的 ( 入口文件的绝对路径 ) 和 ( 是否是主模块 ) 为参数
    • (5) 在 buildModule() 中调用 getSource('absolutePath') 办法

      • 参数是模块的绝对路径
      • 通过 fs.readFileSync(path, options) 读取源码
      • 循环webpack.config.js中的module->rules数组->test,用test和absolutePath做正则匹配,匹配胜利的话,就递归调用loader函数解析该文件,并返回该文件,直到moudle->rules->use中的数组成员loader都调用完
    • (6) 在 buildModule() 中调用 parse() 办法解析源码,批改源码,返回源码
    • (7) parse()办法

      • 参数有两个:模块的源码 和 模块文件所在的文件夹门路 - 即文件所在的文件夹
      • 返回值有两个:批改后的模块源码 和 该模块的依赖数组
      • 留神:批改局部(替换require名,moudules中的key要是'./src/xxxxx'的格局,匹配loader并解决源文件)
    • (8) 将 模块的相对路径 和 模块批改后的源码 一一对应作为 modules对象的 key和value值
    • (9) 如果 parse()返回的该模块的依赖数组不为空,则遍历该模块的依赖数组,并递归调用 buildModule 办法,直到最初一个模块没有依赖为止
  • emitFile() - 将源码发射到webpack.config.js指定的目录的过程

    • (1) 装置ejs模板引擎 并编写模板 传入两个参数 entryId 和 modules
    • (2) 获取webpack.config.js中的output对象的path,filename
    • (3) fs.readFileSync()读取ejs模板源文件
    • (4) 将esj.render() 生成能够执行的文件
    • (5) fs.writeFileSync(file, data[, options])将生成的经esj编译后的源文件写入output.path中,文件名是outpt.name
    wpack.js#!  /usr/bin/env node// 一.须要拿到 webpack.config.js 文件const path = require('path')const config = require(path.resolve('webpack.config.js')) // 获取webpack.config.jsconst Compiler = require('../lib/compiler.js')const compiler = new Compiler(config)compiler.run() // 调用run办法,

Compiler - run()办法

  • ( run ) 办法次要做 ( 两件 ) 事件

    • <font color=red>(1) 调用buildModul() -> modules = { } ------------- 依赖关系对象的key和vlue的收集</font>

      • key:所有模块的相对路径
      • value:所有模块的源码
    • <font color=red>(2) 调用emitFile办法 -> 发射打包后的文件到指定的文件夹中</font>
  • 具体流程

    • 在run中调用 ( buildModule ) 办法
    • 在run中调用 ( emitFile ) 办法
  • buildMoudle(moduleAbsolutePath, isEntryModule)
  • buildMoudle()参数

    • moduleAbsolutePath:每个模块的绝对路径
    • isEntryModule:布尔值,是否是入口主模块,入口模块个别是index.js
  • <font color=red>buildmoudle次要做以下几件事件:</font>

    • <font color=red>通过 fs.readFileSync(modulePath, { encoding: 'utf8' }) 读取传入的模块门路对应的源码</font>

      • 留神:这里肯定要用utf8格局,不然@babel/parse解析时会报错
    • 如果是 ( <font color=red>主入口模块</font> ),就用 ( <font color=red>this.entryId</font> ) 来标记主入口模块的门路 (门路须要解决成想要的格局)
    • 调用 <font color=red>parse()</font> 办法

      • 传入:( <font color=red>未修改的源码</font> ) 和 入口文件所在 ( <font color=red>文件夹</font> )
      • 返回:( <font color=red>批改过后的源码</font> ) 和 以后模块的依赖数组,即 ( <font color=red>以后模块require的文件</font> )

        • 批改源码

          • 通过 @babel/parser 将源码转成AST
          • 通过 @babel/traverse 遍历AST,并在遍历过程中通过 @babel/types实现批改,增加,删除等操作
          • 通过 @babel/types 批改,增加,删除AST的各个节点
          • 通过 @babel/generator将批改后的AST转成源码字符串
    • 如果 ( <font color=red>以后模块还有依赖项</font> ),即返回的以后模块的依赖项数组不为空,就 ( <font color=red>递归执行buildMoudle()</font> ) 办法
    • 最终收集所有的模块对应关系到 modules对象中 this.modules[moduleRelativePath] = sourceCode
    buildModule(moduleAbsolutePath, isEntry) {  // 参数  // moduleAbsolutePath:是模块的绝对路径,通过path.resolve(this.root, this.entry)取得  // isEntry:是否是入口主模块  const source = this.getSource(moduleAbsolutePath)  // 读取模块的源文件内容  const moduleRelativePath = './' + path.relative(this.root, moduleAbsolutePath)  // path.relative(from, to)   // path.relative(from, to)办法依据当前工作目录返回 from 到 to 的相对路径  // moduleRelativePath  // 示意模块文件的相对路径  // moduleRelativePath = moduleAbsolutePath - this.root  // console.log(source, moduleRelativePath)  if (isEntry) {    this.entryId = moduleRelativePath    // 如果是主入口,把革新后的形如 ./src/index.js 的文件门路赋值给 entryId  }  const fatherPath = path.dirname(moduleRelativePath)  // fatherPath 即获取 ./src/index.js 的最初一段文件或文件夹的父目录 => ./src  const {sourceCode, dependencies} = this.parse(source, fatherPath).replace(/\\/g, '/');  // parse()次要性能    // 1. 对入口文件源码进行革新    // 2. 返回革新后的源码 和 依赖列表  // 参数:    // 革新前的源码    // 和父门路  // 返回值    // 革新后的源码    // 依赖列表  this.modules[moduleRelativePath] = sourceCode;  // this.modules    // 模块的门路 和 模块的源码一一对应    // key   => moduleRelativePath    // value => sourceCode    dependencies.forEach(dep => { // 附模块的加载 递归加载      this.buildModule(path.join(this.root, dep), false)    })    // 递归依赖数组,将this.modules对象的所有key,vaue收起到一起}

Compiler - run() - buildMoudle() - getSource()办法 - 减少loader解析源码后再给到parse()去转换

  • less-loader

    const less = require('less')const lessLoader = function(source) {const that = this;let res;less.render(source, function(err, content) {  res = content.css.replace(/\n/g, '\\n').replace(/\r/g, '\\r')  // res = that.callback(null, content.css.replace(/\n/g, '\\n'))})return res;}
  • style-loader

    const styleLoader = function(source) {const style = `  const styleElement = document.createElement('style');  styleElement.innerHTML = ${JSON.stringify(source)};  document.head.appendChild(styleElement);`return style}module.exports = styleLoader
  • getSource()办法中增加loader局部的代码

    getSource()办法中增加loader局部的代码getSource(modulePath) {  let content = fs.readFileSync(modulePath, { encoding: 'utf8' })  //  { encoding: 'utf8' } 肯定要用utf8格局  // 不然在@babel/parse中.parse()解析时会报错  const { rules } = this.config.module // 获取rule数组  for(let i = 0; i < rules.length; i++) { // 循环rules数组    const {test, use} = rules[i] // 取出每个对象中的test和use    let reverseIndex = use.length - 1; // use也是一个数组,从后往前,从下往上执行    if (test.test(modulePath)) {      function runLoader() {        const loader = require(use[reverseIndex--])         // 先去use数组中的最初一个,再一次取前一个        // require('absolute path') 引入loader函数                content = loader(content)         // 执行loader函数,返回loader批改后的内容                if (reverseIndex >= 0) { // 循环递归完结条件          runLoader()        }      }      runLoader()    }  }  // content  // fs.readFileSync(modulePath, {encoding: 'utf8'}) 读取模块源码,返沪utf8格局的源码  // 参数:  // modulePath:这里是模块的 绝对路径  return content}

Compiler - run() - buildMoudle() - parse()办法

  parse(source, parentPath) { // AST (解析 -> 遍历 -> 转换 -> 生成)    const dependencies = [] //  依赖数组    // 解析    const AST = babelParser.parse(source)    // 遍历    babelTraverse(AST, {      CallExpression(p) { // 调用表达式,留神这里参数不能写成path,和node的path抵触了        // 批改          // 次要做两件事件          // 1. require() => __webpack_require__()          // 2. require('./a.js') => require('./src/a.js)        const node = p.node        if (node.callee.name === 'require') { // 找到节点中的callee.name是require的办法,批改名字          node.callee.name = '__webpack_require__' // 替换require的名字          let modulePath = node.arguments[0].value;          modulePath = "./" + path.join(parentPath, modulePath).replace(/\\/g, '/') + (path.extname(modulePath) ? '' : '.js');  // 后缀存在就加空字符串即不做操作,不存在加.js          // 例如:modulePath = './' + '/src' + 'index' + '.js'          // 获取require的参数          dependencies.push(modulePath)           // 转换          node.arguments = [babelTypes.stringLiteral(modulePath)] // 把AST中的argumtns中的Literal批改掉 => 批改成最新的modulePath        }      }    })    // 生成    const sourceCode = babelGenerator(AST).code;    // 返回    return {sourceCode, dependencies}  }

Compiler - run() - emitFile()

 emitFile() { // 发射文件    console.log(111111111)    const {path: p, filename} = this.config.output    const main = path.join(p, filename)    // main 示意打包后的文件的门路    const templeteSourceStr = this.getSource(path.join(__dirname, 'main.ejs'))    // 读取模块源文件 main.ejs    const code = ejs.render(templeteSourceStr, {      entryId: this.entryId,      modules: this.modules    })    // 渲染模板    // 模板中有两个参数 entryId 和 modules    this.assets = {}    this.assets[main] = code;    // key:打包后的文件门路    // value: 打包后的文件源码    fs.writeFileSync(main, this.assets[main])    // 写文件按    // fs.writeFileSync(file, data[, options])  }
------main.ejs(function (modules) {   var installedModules = {};  function __webpack_require__(moduleId) {    if (installedModules[moduleId]) {      return installedModules[moduleId].exports;    }    var module = installedModules[moduleId] = {      i: moduleId,      l: false,      exports: {}    };    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);    module.l = true;    return module.exports;  }  return __webpack_require__(__webpack_require__.s = "<%-entryId%>");})  ({    <%for(let key in modules){%>      "<%-key%>":      (function (module, exports, __webpack_require__) {        eval(`<%-modules[key]%>`)      }),    <%}%>  });

Compiler 总文件

const fs = require('fs')const path = require('path')const babelParser = require('@babel/parser')const babelTypes = require('@babel/types')const babelTraverse = require('@babel/traverse').defaultconst babelGenerator = require('@babel/generator').defaultconst ejs = require('ejs')const {SyncHook} = require('tapable')class Compiler {  constructor(config) {    this.config = config // webapck.config.js中的内容,即webpack配置文件模块    this.entryId = null // 入口文件的相对路径    this.modules = {}    // 用来保留所有模块信息    // key:模块的相对路径    // value:模块的源码    this.entry = config.entry.index; // 入口文件门路    this.root = process.cwd(); // 当前工作门路,返回node.js过程的当前工作目录    this.hooks = {      entryOption: new SyncHook(),      afterPlugins: new SyncHook(),      run: new SyncHook(),      compile: new SyncHook(),      afterCompile: new SyncHook(),      emit: new SyncHook(),      done: new SyncHook(),    }    // plugins获取    const plugins = this.config.plugins    if (Array.isArray(plugins)) {      plugins.forEach(plugin => {        plugin.apply(this) // this是compiler实例      })    }    this.hooks.afterPlugins.call()  }  getSource(modulePath) {    let content = fs.readFileSync(modulePath, { encoding: 'utf8' }) // 记得肯定要utf8格局    const { rules } = this.config.module    for(let i = 0; i < rules.length; i++) {      const {test, use} = rules[i]      let reverseIndex = use.length - 1;      if (test.test(modulePath)) {        function runLoader() {          const loader = require(use[reverseIndex--])          content = loader(content)          console.log(content, '6666666666');          if (reverseIndex >= 0) {            runLoader()          }        }        runLoader()      }    }    // content    // fs.readFileSync(modulePath, {encoding: 'utf8'}) 读取模块源码,返沪utf8格局的源码    // 参数:    // modulePath:这里是模块的 绝对路径    return content  }  parse(source, parentPath) { // AST (解析 -> 遍历 -> 转换 -> 生成)    const dependencies = [] //  依赖数组    // 解析    const AST = babelParser.parse(source)    // 遍历    babelTraverse(AST, {      CallExpression(p) { // 调用表达式,留神这里参数不能写成path,和node的path抵触了        // 批改          // 次要做两件事件          // 1. require() => __webpack_require__()          // 2. require('./a.js') => require('./src/a.js)        const node = p.node        if (node.callee.name === 'require') { // 找到节点中的callee.name是require的办法,批改名字          node.callee.name = '__webpack_require__' // 替换require的名字          let modulePath = node.arguments[0].value;          modulePath = "./" + path.join(parentPath, modulePath).replace(/\\/g, '/') + (path.extname(modulePath) ? '' : '.js'); // 后缀存在就加空字符串即不做操作,不存在加.js          // 例如:modulePath = './' + '/src' + 'index' + '.js'          // 获取require的参数          dependencies.push(modulePath)           // 转换          node.arguments = [babelTypes.stringLiteral(modulePath)] // 把AST中的argumtns中的Literal批改掉 => 批改成最新的modulePath        }      }    })    // 生成    const sourceCode = babelGenerator(AST).code;    // 返回    return {sourceCode, dependencies}  }  buildModule(moduleAbsolutePath, isEntry) {    // 参数    // moduleAbsolutePath:是模块的绝对路径,通过path.resolve(this.root, this.entry)取得    // isEntry:是否是入口主模块    const source = this.getSource(moduleAbsolutePath)    // 读取模块的源文件内容    let moduleRelativePath = './' + path.relative(this.root, moduleAbsolutePath).replace(/\\/g, '/');    console.log(path.relative(this.root, moduleAbsolutePath))    // path.relative(from, to)     // path.relative(from, to)办法依据当前工作目录返回 from 到 to 的相对路径    // moduleRelativePath    // 示意模块文件的相对路径    // moduleRelativePath = moduleAbsolutePath - this.root    // console.log(source, moduleRelativePath)    if (isEntry) {      this.entryId = moduleRelativePath      // 如果是主入口,把革新后的形如 ./src/index.js 的文件门路赋值给 entryId    }    const fatherPath = path.dirname(moduleRelativePath)    // fatherPath 即获取 ./src/index.js 的最初一段文件或文件夹的父目录 => ./src    const {sourceCode, dependencies} = this.parse(source, fatherPath)    // parse()次要性能      // 1. 对入口文件源码进行革新      // 2. 返回革新后的源码 和 依赖列表    // 参数:      // 革新前的源码      // 和父门路    // 返回值      // 革新后的源码      // 依赖列表    this.modules[moduleRelativePath] = sourceCode;    // this.modules      // 模块的门路 和 模块的源码一一对应      // key   => moduleRelativePath      // value => sourceCode      dependencies.forEach(dep => { // 附模块的加载 递归加载        this.buildModule(path.join(this.root, dep), false)      })      // 递归依赖数组,将this.modules对象的所有key,vaue收起到一起  }  emitFile() { // 发射文件    const {path: p, filename} = this.config.output    const main = path.join(p, filename)    // main 示意打包后的文件的门路    const templeteSourceStr = this.getSource(path.join(__dirname, 'main.ejs'))    // 读取模块源文件 main.ejs    const code = ejs.render(templeteSourceStr, {      entryId: this.entryId,      modules: this.modules    })    // 渲染模板    // 模板中有两个参数 entryId 和 modules    this.assets = {}    this.assets[main] = code;    // key:打包后的文件门路    // value: 打包后的文件源码    fs.writeFileSync(main, this.assets[main])    // 写文件按    // fs.writeFileSync(file, data[, options])  }  run() {    // run办法次要做两件事件    // 1. 创立模块的依赖关系    // 2. 发射打包后的文件    this.hooks.run.call()    this.hooks.compile.call()    this.buildModule(path.resolve(this.root, this.entry), true)    // buildModule()的作用:建模块的依赖关系    // 参数:    // 第一个参数:是entry指定门路的绝对路径    // 第二个参数:是否是主模块    this.hooks.afterCompile.call()    console.log(this.modules, this.entryId)    // 发射一个文件,打包后的文件    this.emitFile()    this.hooks.emit.call()    this.hooks.done.call()  }}module.exports = Compiler

材料

打包原理: https://www.jianshu.com/p/89b...
打包原理2:https://juejin.im/post/684490...
Webpack Loader:https://juejin.im/post/684490...