关于javascript:源码webpack03-手写webpack-compiler简单编译流程

35次阅读

共计 20523 个字符,预计需要花费 52 分钟才能阅读完成。

  • 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.js
    const 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').default
const babelGenerator = require('@babel/generator').default
const 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…

正文完
 0