[源码-webpack03] 手写webpack - compiler简单编译流程

5,868 阅读19分钟
  • 2021/01/09 更新

  • 2021/07/27 更新 Compiler.png

导航

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

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

前置知识

一些单词

compiler:编译

(1) webpack.DefinePlugin 和 cross-env 和 mode 三者的区别?

  • 2022/03/27
 webpack.DefinePlugin 和 cross-env 和 mode 的区别???????????
 ---
 
 - cross-env
   - 作用:设置的是 ( node的环境 ) 中的环境变量 process.env,也就是说只能在webpack.config.js中访问到
   - 例子:`{ scripts: { "test:dev": "cross-env NODE_ENV=development OTHER_ENV=other webpack serve --config build/webpack.config.js" }}`
   - 语法:cross-env可以设置多个node环境的环境变量,只需要空格隔开就行,如上
   - 安装:npm install cross-env
 - webpack.DefinePlugin()
   - 设置的是 ( 浏览器环境 ) 中的环境变量,也就是说可以在各个js文件中使用到 webpack.DefinePlugin() 中定义的环境变量
   - 注意点:
     - 如果环境变量的值是一个字符串,那么需要用 JSON.stringify('"string"') 进行转译,所以为了安全保证,将所有数据类型都进行JSON.stringify来处理
     - 如果 webpack.DefinedPlugin({'process.env.NODE_ENV': xxxx}),那么在浏览器环境中也能访问到process.env.NODE_ENV,这就是webpack.config.js中的mode属性需要干的事情
 - mode
   - mode的作用是,mode的值将会作为 webpack.DefinedPlugin({'process.env.NODE_ENV': JSON.stringify('mode的值')})
   - development,production,none
 - 总结区别
   - cross-env定义的环境变量,只能在node环境中被访问到,即 webpack.config.js 中被访问到
   - webpack.DefinePlugin()定义的环境变量,只能在浏览器环境中被访问到,即只能在各个module模块中去使用,不能在webpack.config.js中使用
   - mode指定的值,mode的值将会作为 webpack.DefinedPlugin({'process.env.NODE_ENV': JSON.stringify('mode的值')}),从而能在浏览器环境中访问,即module中访问
 - 实践案例1
   - 结果:如果webpack.config.js中的 mode="development",并且在 build命令时执行的命令 cross-env NODE_ENV="production",随便在模块js中打印process.env.NODE_ENV输出的是'development'
   - 原因:说明浏览器环境中的process.env.NODE_ENV是通过webpack.config.js 中的 mode属性 设置的,而webpack.config.js中的process.env.NODE_ENV是通过cross-env来设置的
   - 本质:mode的作用是,mode的值将会作为 webpack.DefinedPlugin({'process.env.NODE_ENV': JSON.stringify('mode的值')}),从而能在浏览器环境中访问,即module中访问
   - 所以:如何同步?可以将 mode 设置为 ( mode:process.env.NODE_ENV ) 这样 ( node 和 浏览器 中的环境变量就同步了 ),因为 ( cross-env将webpack.config.js中的环境变量设置为了对应的值,而mode=process.env.NODE_ENV,mode有设置了webpack.DefinePlugin()中的process.env.NODE_ENV,用于在浏览器环境中使用)
 - 实践案例2
   - 设置不同的环境对应的后端服务器地址,详见examples/main.js
   - 源码地址:https:github.com/woow-wu7/8-divine/blob/main/examples/main.js
   - 源码地址2: https://github.com/woow-wu7/7-compiler/blob/main/webpack.config.js

(2) hash chunk-hash content-hash 的区别 ?

  • 2022/03/27
hash chunk-hash content-hash 的区别?
---

- hash
  - 作用:只要项目中有文件修改,整个项目构建的hash都会改变,并且全部文件都共用相同的hash
  - 弊端:如果只修改了一个文件,整个文件的缓存都将失效,因为真个文件的hash都改变了
- chunkhash
  - 相对于hash,chunkhash的影响范围较小
  - 原理:
    - 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值
    - 不同入口打包生成的chunk的hash不一样
  - 测试
    - 请使用 cnpm run build 进行 chunkhash 的测试,main和other的js文件的hash值就不一样
  - 例子:
    - 策略:比如一个项目有6个组件,123打包为一个thunk1输出一组js/css,456打包为另一个thunk2输出另一组js/css
    - 结果: 如果使用chunkhash,打包完成后chunk1的hash和chunk2的hash就不一样,改动了123,456的chunk2的hash就不会变,缓存仍然有效
- contenthash
  - 1. 影响范围最小,在hash,chunkhash,contenthash三者中
  - 2. 遇到问题
    - 使用chunkhash,如果index.css被index.js引用了,那么 ( css文件和js文件 ) 就会 ( 共用相同的chunkhash值 )
    - 如果index.js更改了代码,css文件就算内容没有任何改变,由于是该模块发生了改变,导致css文件会重复构建
  - 3. 解决方法
    - 使用 ( mini-css-extract-plugin ) 里的 ( contenthash ) 值,保证即使css文件所处的模块里就算其他文件内容改变,只要css文件内容不变,那么不会重复构建
- 总结
  - hash(任何一个文件修改,整个打包所有文件的hash都会改变): - 是根据整个项目构建,要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值
  - chunkhash(只影响到不同entry划分的chunk):chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的代码块(chunk),生成对应的哈希值,某文件变化时只有该文件对应代码块(chunk)的hash会变化
  - contentHash(即使是相同chunk的js和css,改动js只会影响对应的js而不会影响到css):每一个代码块(chunk)中的js和css输出文件都会独立生成一个hash,当某一个代码块(chunk)中的js源文件被修改时,只有该代码块(chunk)输出的js文件的hash会发生变化
- 使用
  - 在哪些地方可以使用到 hash chunkhash contenthash
  - 凡是在 webpack.config.js 中具有 ( filename ) 属性的地方都可以使用 ( 占位符的方式 [hash] ) 使用到这几种hash

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() 方法根据当前工作目录返回 ( from ) 到 ( to ) 的 ( 相对路径 )

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

  • path.dirname() 方法返回 path 的目录名
  • 即 ( 返回路径中最后一段文件或者文件夹所在的文件夹,即最后一段文件或文件夹的父目录 )

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)

Loader - 编写一个自定义loader

  • ( loader ) 是一个 ( 函数 ),函数的第一个参数表示 ( 该loader匹配的文件的 源代码 )

  • loader 不能写成 ( 箭头函数 ),因为需要通过this获取更多的api

  • loader-utils

    • 用来获取 module -> rules 中的 loader的 ( options ) 对象
    • 通过 ( loader-utils ) 中的 ( getOptions ) 来获取 ( 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/']
      }
    }
    

Compiler - 生命周期钩子函数

  • entryOption
    • 在 webpack 选项中的 entry 配置项 处理过之后,执行插件
  • afterPlugins
    • 设置完初始插件之后,执行插件
  • run
    • compiler.run() 方法执行时触发 - 开始读取 records 之前,钩入(hook into) compiler
  • compile
    • buildMoudle()执行前触发 - 一个新的编译(compilation)创建之后触发
  • afterCompile
    • buildMoudle()执行后触发
  • emit
    • emitFile() 执行时触发 - 生成资源到 output 目录之前。
  • done
    • 编译完成时触发

image.png

Plugin - 编写一个自定义plugin

  • plugin是一个具有 ( apply ) 方法的类,apply方法参数是 ( compiler ) 调用,并且 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()

  }

webpack打包后文件分析

  • 精简代码,去除 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 ) 方法主要做 ( 两件 ) 事情

    • (1) 调用buildModul() -> modules = { } ------------- 依赖关系对象的key和vlue的收集
      • key:所有模块的相对路径
      • value:所有模块的源码
    • (2) 调用emitFile方法 -> 发射打包后的文件到指定的文件夹中
  • 具体流程

    • 在run中调用 ( buildModule ) 方法
    • 在run中调用 ( emitFile ) 方法
  • buildMoudle(moduleAbsolutePath, isEntryModule)

  • buildMoudle()参数

    • moduleAbsolutePath:每个模块的绝对路径
    • isEntryModule:布尔值,是否是入口主模块,入口模块一般是index.js
  • buildmoudle主要做以下几件事情

    • 通过 fs.readFileSync(modulePath, { encoding: 'utf8' }) 读取传入的模块路径对应的源码
      • 注意:这里一定要用utf8格式,不然@babel/parse解析时会报错
    • 如果是 ( 主入口模块 ),就用 ( this.entryId ) 来标记主入口模块的路径 (路径需要处理成想要的格式)
    • 调用 parse() 方法
      • 传入:( 未修改的源码 ) 和 入口文件所在 ( 文件夹 )
      • 返回:( 修改过后的源码 ) 和 当前模块的依赖数组,即 ( 当前模块require的文件 )
        • 修改源码
          • 通过 @babel/parser 将源码转成AST
          • 通过 @babel/traverse 遍历AST,并在遍历过程中通过 @babel/types完成修改,添加,删除等操作
          • 通过 @babel/types 修改,添加,删除AST的各个节点
          • 通过 @babel/generator将修改后的AST转成源码字符串
    • 如果 ( 当前模块还有依赖项 ),即返回的当前模块的依赖项数组不为空,就 ( 递归执行buildMoudle() ) 方法
    • 最终收集所有的模块对应关系到 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

资料

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