技术分享
首页
  • JavaScript

    • 构造函数和原型
    • Cookie和Session
    • Object.create(null)和{}
    • TypeScript配置
    • typescript入门到进阶
  • 框架

    • Vue-Router
    • React基础入门
  • 其它

    • Http协议
    • 跨域问题总结
  • 分析Promise实现
  • Axios源码分析
  • Webpack原理
  • vueRouter源码分析
  • Vue

    • Vite快速搭建Vue3+TypeScript项目
    • Cordova打包Vue项目的问题
    • Vue将汉字转为拼音,取出首字母
    • h5项目问题总结
  • JavaScript

    • new Function
  • 后端

    • Node.js中使用Crypto生成Token
    • Body-Parser处理多层对象的问题
  • 其它

    • 项目Demo汇总
    • Vuepress+Vercel搭建个人站点
    • 项目中能用到的
    • husky规范代码提交
  • Mongoose基础
  • Multer文件上传中间件的使用
  • JavaScript

    • 浅谈两数全等
    • JavaScript进制转换
    • 手写bind,apply,call和new
  • 算法

    • 数组去重和排序
    • 数组扁平化
    • 斐波那契数列
  • JavaScript 数据结构
  • 其它

    • webpack面试题
    • vite面试题
    • svg和canvas的优缺点
    • TypeScript面试题
    • Vue常见面试题
  • 计算机网络

    • 数据链路层
    • 网络层
  • Git的使用
  • Nginx的使用
  • CentOS7安装Nginx
  • 正则表达式
  • SEO搜索引擎优化
  • Serverless介绍
友链
GitHub (opens new window)

刘誉

总有人要赢,为什么不能是我
首页
  • JavaScript

    • 构造函数和原型
    • Cookie和Session
    • Object.create(null)和{}
    • TypeScript配置
    • typescript入门到进阶
  • 框架

    • Vue-Router
    • React基础入门
  • 其它

    • Http协议
    • 跨域问题总结
  • 分析Promise实现
  • Axios源码分析
  • Webpack原理
  • vueRouter源码分析
  • Vue

    • Vite快速搭建Vue3+TypeScript项目
    • Cordova打包Vue项目的问题
    • Vue将汉字转为拼音,取出首字母
    • h5项目问题总结
  • JavaScript

    • new Function
  • 后端

    • Node.js中使用Crypto生成Token
    • Body-Parser处理多层对象的问题
  • 其它

    • 项目Demo汇总
    • Vuepress+Vercel搭建个人站点
    • 项目中能用到的
    • husky规范代码提交
  • Mongoose基础
  • Multer文件上传中间件的使用
  • JavaScript

    • 浅谈两数全等
    • JavaScript进制转换
    • 手写bind,apply,call和new
  • 算法

    • 数组去重和排序
    • 数组扁平化
    • 斐波那契数列
  • JavaScript 数据结构
  • 其它

    • webpack面试题
    • vite面试题
    • svg和canvas的优缺点
    • TypeScript面试题
    • Vue常见面试题
  • 计算机网络

    • 数据链路层
    • 网络层
  • Git的使用
  • Nginx的使用
  • CentOS7安装Nginx
  • 正则表达式
  • SEO搜索引擎优化
  • Serverless介绍
友链
GitHub (opens new window)
  • 分析Promise实现
  • Axios源码分析
  • vueRouter源码分析
  • Webpack原理
    • webpack 的 bundle.js 文件分析
      • 文件准备
    • webpack 的 loader
      • this 上的属性说明
      • 如何编写 loader
    • webpack 的 plugin
      • webpack 的生命周期钩子
      • 如何编写 plugin
    • 实现一个 mini-webpack
      • package.json 注册全局命令
      • 修改 acli.js
      • 实现 Compiler
  • 阅读《重构:改善既有代码的设计》笔记
  • vue源码
  • webpack
  • 可视化
  • koa全家桶
  • 笔记
coderly
2021-05-01

Webpack原理

# Webpack原理

  • loader 和 plugin 作用及编写
  • 实现 mini-webpack,加深对webpack的理解
  • 完整项目地址:mini-webpack (opens new window)

# webpack 的 bundle.js 文件分析

# 文件准备

  • src/index.js
const vars = require('./variable')
const calcs = require('./calc')

console.log('结果:' + calcs.add(1, 2, 3, 4, 5))
console.log('i am ' + vars.name)
1
2
3
4
5
  • src/variable.js
const name = 'coderly'

module.exports = {
  name
}
1
2
3
4
5
  • src/calc.js
module.exports = {
  add(...nums) {
    return nums.reduce((p, c) => p + c, 0)
  }
}
1
2
3
4
5

打包之后的文件(删除无关注释)



































 
 
 
 
 



 
 


(() => {
  var __webpack_modules__ = ({
    "./src/calc.js":
      ((module) => {
        // 做了优化,未使用 __webpack_require__ 将不再作为变量传入
        // 在 module.exports 上添加变量,最终是在 __webpack_module_cache__ 对象中的模块中添加了代码
        eval("module.exports = {\r\n  add(...nums) {\r\n    return nums.reduce((p, c) => p + c, 0)\r\n  }\r\n}\n\n//# sourceURL=webpack://webpack-test/./src/calc.js?");
      }),
    "./src/index.js":
      ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
        // 递归加载 variable.js 和 calc.js
        eval("const vars = __webpack_require__(/*! ./variable */ \"./src/variable.js\")\r\nconst calcs = __webpack_require__(/*! ./calc */ \"./src/calc.js\")\r\n\r\nconsole.log('结果:' + calcs.add(1, 2, 3, 4, 5))\r\nconsole.log('i am ' + vars.name)\n\n//# sourceURL=webpack://webpack-test/./src/index.js?");
      }),
    "./src/variable.js":
      ((module) => {
        eval("const name = 'coderly'\r\n\r\nmodule.exports = {\r\n  name\r\n}\n\n//# sourceURL=webpack://webpack-test/./src/variable.js?");
      })
  });
  // 缓存 - 用来判断文件是否已加载
  var __webpack_module_cache__ = {};

  function __webpack_require__ (moduleId) {

    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // 构建 模块并添加进缓存
    var module = __webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {}
    };

    // 执行 模块对应的代码
    // module 变量传入的是一个引用,做的修改将会直接作用在 __webpack_module_cache__ 对应模块中
    // __webpack_require__ 递归加载文件
    // require 无法在 浏览器环境下使用,所以命名 __webpack_require__
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    return module.exports;
  }
  // 开始加载 入口文件
  var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

说明

  • __webpack_require__ 其实就是 require(因为require 无法在浏览器环境下使用,所以命名 webpack_require)
  • 从入口文件开始加载文件执行
  • 遇到需要 require 的,会先递归执行 __webpack_require__函数,并 return module.exports
  • 以上例分析
    1. 先执行 "./src/index.js" 对应值的代码
    2. 第一行遇到 const vars = webpack_require(/*! ./variable */ "./src/variable.js"),开始执行 "./src/variable.js" 对应值的代码
    3. "./src/variable.js" 对应的代码中 在执行完 __webpack_require__ 函数后,return 了 module.exports,"./src/index.js" 对应代码中获取到了 "./src/variable.js" 导出的函数
    4. 第二行遇到 const calcs = webpack_require(/*! ./calc */ "./src/calc.js")
    5. TODO

# webpack 的 loader

执行顺序: 从下至上,从右至左

# this 上的属性说明

属性 作用 参数 返回值
addContextDependency 添加一个目录作为加载程序结果的依赖项 directory:string ——
addDependency 添加文件作为加载程序结果的依赖项,以使其可观察 file:string ——
async 告诉加载程序运行程序加载程序打算异步回调。返回this.callback —— this.callback
cacheable 设置可缓存标志的函数 flag:boolean ——
callback 可以同步或异步调用以返回多个结果的函数 (err: Error/null, content: string Buffer, sourceMap?: SourceMap, meta?: any)
clearDependencies 删除加载程序结果的所有依赖关系,甚至包括初始依赖关系和其他加载程序的依赖关系 —— ——
context 模块所在的目录。可以用作解析其他模块路径的上下文 —— ——
data 在 pitch 阶段和正常阶段之间共享的 data 对象 —— ——
emitError 发出一个错误,该错误也可以显示在输出中 error: Error ——
emitFile 产生一个文件。这是 webpack 特有的 (name: string, content: Buffer/string, sourceMap: {...}) ——
emitWarning 发出警告,该警告将在输出中显示 warning: Error ——
fs 用于访问 compilation 的 inputFileSystem 属性 —— ——
getOptions 提取给定的加载程序选项 schema:JSON模式参数 ——
getResolve 创建类似于的解析函数this.resolve 函数重载 ——
hot loaders 热模块替换的相关信息 —— ——
importModule 子编译器在构建时编译和执行请求的另一种轻量级解决方案 (request, options, [callback]): Promise ——
loaderIndex 当前loader 在 loader 数组中的索引 (request: string, callback: function(err, source, sourceMap, module)) ——
loadModule 将给定的请求解析为模块,应用所有已配置的 loader,并使用生成的source,sourceMap和模块实例(通常是的实例NormalModule)进行回调 (request: string, callback: function(err, source, sourceMap, module)) ——
loaders loader 数组,可以在 pitch 阶段写入 —— ——
mode webpack 在 哪个模式下运行 —— production/development/none
query 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象; 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串 —— production/development/none
request 解析的请求字符串 —— production/development/none
resolve 解析操作的所有依赖项将作为依赖项自动添加到当前模块 (context: string, request: string, callback: function(err, result: string)) ——
resource request 中的资源部分,包括 query 参数 —— ——
resourcePath 资源文件的路径 —— ——
resourceQuery 资源的 query 参数 —— ——
rootContext 从webpack 4开始,以前 this.options.context 替换为 this.rootContext —— ——
sourceMap 应该生成一个 source map。因为生成 source map 可能会非常耗时,你应该确认 source map 确实有必要请求 —— ——
target 编译的目标。从配置选项中传递过来的 —— web/node
utils 访问 contextify 和 absolutify 实用工具 —— ——
version loader API 版本 —— ——
webpack 如果是由 webpack 编译的,这个布尔值会被设置为真 —— ——
_compilation 访问webpack的当前Compilation对象 —— ——
_compiler 访问webpack的当前Compiler对象 —— ——

# 如何编写 loader

实现 vars-usage-times-loader

  • 统计某个变量在文件中使用到的次数
  • 使用 loader-utils 插件来提取 query
  • 确定 use loader 传参字段
  • webpack.config.js




 
 
 
 




 
 
 
 
 
 





const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  resolveLoader: {
    // 自定义模块解析
    modules: [path.resolve(__dirname, './loader'), 'node_modules'],
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'vars-usage-times-loader',
          options: {
            varname: 'works'
          }
        }
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • loader/vars-usage-times-loader.js







 















 
 





 



const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const loaderUtils = require('loader-utils')

module.exports = function(source) {
  // 获取在 webpack.config.js 中传入的参数
  const options = loaderUtils.getOptions(this)
  // 将文件转换成 ast
  let ast = parser.parse(source)
  // 遍历 ast
  traverse(ast, {
    // 如果是函数调用
    CallExpression(p) {
      // 获取当前函数调用的名字
      let name = p.node.callee.name
      // 只匹配 require 的变量
      if (name === 'require') {
        // 获取 require 文件起的变量名
        let varName = p.parent.id.name
        // 如果该变量名和 传入的相匹配
        if (varName && varName === options.varname) {
          // 开始获取该变量名在当前文件中使用的次数
          let currentBinding = p.scope.getBinding(p.parent.id.name)
          console.log(`${options.varname} 变量在当前文件中变量使用了:${currentBinding.references} 次`)
        }
      }
    }
  })
  // 重新将 ast 生成代码
  source = generator(ast).code
  return source
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

说明:

  • 获取在 webpack.config.js 中传入的参数
  • 将文件转换成 ast
  • 遍历 ast,获取当前函数调用的名字只匹配 require 的变量
  • 获取 require 文件起的变量名,如果该变量名和 传入的相匹配,开始获取该变量名在当前文件中使用的次数
  • 重新将 ast 生成代码

至此一个简单的 loader 插件就完成了

# webpack 的 plugin

  • 一个 JavaScript 命名函数
  • 在插件函数的 prototype 上定义一个 apply 方法
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据
  • 功能完成后调用 webpack 提供的回调

# webpack 的生命周期钩子

官方文档位置 (opens new window)

钩子 作用 参数 类型
environment 在初始化配置文件中的插件之后,在准备编译器环境时调用 —— SyncHook
afterEnvironment environment编译器环境设置完成后,在挂钩之后立即调用 —— SyncHook
entryOption 在处理完webpack选项中的entry配置后调用 context, entry SyncBailHook
afterPlugins 在设置初始内部插件集之后调用 compiler SyncHook
afterResolvers 解析器设置完成后触发 compiler SyncHook
initialize 在初始化编译器对象时调用 —— SyncHook
beforeRun 在运行编译器之前添加一个挂钩 compiler AsyncSeriesHook
run 开始阅读之前,请先钩住编译器records compiler AsyncSeriesHook
watchRun 在触发新的编译之后但实际开始编译之前,在监视模式下执行插件 compiler AsyncSeriesHook
normalModuleFactory 在创建NormalModuleFactory之后调用 normalModuleFactory SyncHook
contextModuleFactory 创建ContextModuleFactory后运行插件 contextModuleFactory SyncHook
beforeCompile 创建编译参数后执行插件 compilationParams AsyncSeriesHook
compile beforeCompile在创建新编译之前,在之后立即调用 compilationParams SyncHook
thisCompilation 在初始化编译时执行,恰好在发出compilation事件之前执行 compilation, compilationParams SyncHook
compilation 创建编译后运行插件 compilation, compilationParams SyncHook
make 在完成编译之前执行 compilation AsyncParallelHook
afterCompile 在完成并密封编译后调用 compilation AsyncSeriesHook
shouldEmit 在释放资产之前调用。应该返回一个布尔值,告诉是否发出 compilation SyncBailHook
emit 在将资产释放到输出目录之前立即执行 compilation AsyncSeriesHook
afterEmit 在将资产释放到输出目录后调用 compilation AsyncSeriesHook
assetEmitted 当资产被放出时执行。提供对有关发出的资产的信息的访问,例如其输出路径和字节内容 file, info AsyncSeriesHook
done 编译完成后执行 stats AsyncSeriesHook
additionalPass 该挂钩使您可以再进行一次构建 —— AsyncSeriesHook
failed 如果编译失败则调用 error SyncHook
invalid 当监视编译无效时执行 fileName, changeTime SyncHook
watchClose 监视编译停止时调用 —— SyncHook
infrastructureLog 当通过infrastructureLoggingoption在配置中启用时,允许使用基础结构日志记录 name, type, args SyncBailHook
log 允许登录到统计启用时,看到stats.logging,stats.loggingDebug和stats.loggingTrace选项。 origin, logEntry SyncBailHook

# tapable 类型说明

  1. 基本的钩子:这个钩子只会简单的调用每个 tap 进去的函数(钩子类名没有waterfall, Bail, 或者 Loop 的)

  2. Waterfall:一个 waterfall 钩子,也会调用每个tap进去的函数;不同的是:它会将前一个函数的返回的值作为形式参数传递到下一个函数

  3. Bail:Bail 钩子允许提前退出,当任何一个 tap 进去的函数返回任意值时,bail 钩子会停止执行其他 tap 的函数(类似 Promise.race())

  4. Loop:当 loop 钩子中的插件有一个返回一个未定义( non-undefined )的值时,钩子将从第一个插件重新启动。直到所有插件返回 undefined

  5. Sync:一个同步钩子只能 tap 同步函数,不然会报错

  6. AsyncSeries:一个 async-series 钩子,可以 tap 同步钩子, 基于回调的钩子和一个基于promise的钩子,它会按顺序的调用每个方法

  7. AsyncParallel:一个 async-parallel 钩子跟上面的 async-series 一样,不同的是它会把异步钩子并行执行(并行执行就是把异步钩子全部一起开启,不按顺序执行)

# webpack 回调参数说明

参数类型 说明
context 基本目录,一个绝对路径,用于从配置中解析入口点和加载器(默认情况下,使用当前目录)
entry 打包入口:每个HTML页面一个入口点;SPA:一个入口点;MPA:多个入口点
compiler webpack 编译对象(只有一个),可在该对象上添加 事件钩子
compilation webpack 在各个生命周期编译构建 bundle 的产物
stats ——

# 如何编写 plugin

官网文档位置 (opens new window)

实现 html-webpack-plugin

  • 确定需要操作的 webpack 生命周期,可读取打包生成的 chunk(emit/afterEmit)
  • 确定需要传入的参数(template/filename ...)
  • 取一个名字(HtmlInjectPlugin)

开始编写

  • webpack.config.js 中
 





 
 
 
 



const HtmlInjectPlugin = require('./HtmlInjectPlugin.js')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  plugins: [
    new HtmlInjectPlugin({
      template: './index.html',
      filename: 'index.html'
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
  • HtmlInjectPlugin.js

cheerio

cheerio 是 jquery 核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对 DOM 进行操作的地方

const cheerio = require('cheerio')
const fs = require('fs')
const path = require('path')

module.exports = class HtmlInjectPlugin {
  constructor(options) {
    // 保存传入的配置
    // 在 webpack.config.js 中 new 对象时传入的参数
    this.options = options
  }
  apply (compiler) {
    // 在 afterEmit 钩子中注册事件
    compiler.hooks.afterEmit.tap('HtmlInjectPlugin', (compilation) => {
      // 获取整个打包输入的目录
      let outputPath = compilation.options.output.path
      // 读取作为模板的 index.html
      let template = fs.readFileSync(path.resolve(process.cwd(), this.options.template), 'utf-8')
      // 将 html 模板 转为 DOM
      let $ = cheerio.load(template)

      // 遍历最终生成的 chunk 文件名
      Object.keys(compilation.assets).forEach((file) => {
        // 根据 chunk 名动态创建 script 标签,并插入到 body 下
        $('body').append($(`<script src="./${file}"></script>`))
      })
      // 将 DOM 转换成 文本并写入 webpack 输入目录中
      fs.writeFileSync(path.join(outputPath, this.options.filename), $.html())
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

整个流程可概括为:

  1. 保存传入的配置
  2. 在 afterEmit 钩子中注册事件
  3. 获取整个打包输入的目录
  4. 读取作为模板的 index.html,并转为 DOM
  5. 遍历最终生成的 chunk 文件名,根据 chunk 名动态创建 script 标签,并插入到 body 下
  6. 将修改后的 DOM 转换成 文本,并写入 webpack 输入目录中

至此一个简单的 webpack plugin 就完成了

# 实现一个 mini-webpack

# package.json 注册全局命令

  • package.json文件中新增
    acli 为使用的命令,将会执行 ./bin/acli.js 文件
"bin": {
  "acli": "./bin/acli.js"
},
1
2
3
  • 在package.json 当前目录下新建 bin/acli.js 文件

    1. 第一行 #!/usr/bin/env node是必须的
    2. 改行的作用是告知使用的环境是 node
#!/usr/bin/env node
console.log('hello world')
1
2
  • 注册全局命令

    1. 方式一:发布包之后使用 npm install xxx -g
    2. 方式二:使用 npm link(将一个任意位置的npm包链接到全局执行环境)
  • 此时就可以在全局使用命令 acli

# 修改 acli.js

  • 读取 webpack.config.js 配置文件
  • 引入 自定义 编辑器
  • 新建对象并开始执行打包任务
#!/usr/bin/env node

const path = require('path')
const fs = require('fs')
// 引入 webpack.config.js 配置文件,执行 acli 命令就是所在工作目录
let config = require(path.resolve('webpack.config.js'))
// 引入自定义 打包工具,等会将会实现
let Compiler = require('../lib/Compiler')

let comipler = new Compiler(config)
comipler.start() // 开始打包构建
1
2
3
4
5
6
7
8
9
10
11

# 实现 Compiler

  • 新建 lib/Compiler.js

保存传入的配置

const path = require('path')

module.exports = class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry || './src/index.js'
    this.root = config.root || process.cwd()
    this.rules = config.module.rules || []
    this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
  }

  start () {
    // TODO
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

添加 babel,从入口文件开始分析
递归读取依赖的文件
将 require 修改更名为 __webpack_require__

const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default

module.exports = class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry || './src/index.js'
    this.root = config.root || process.cwd()
    this.rules = config.module.rules || []
    this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
    this.modules = {} // 保存以读取的模块
  }

  getSource (filePath) {
    return fs.readFileSync(filePath, 'utf-8')
  }

  start () {
    this.depAnalyse(this.entry) // 从入口开始分析
  }

  depAnalyse (entry) {
    let filePath = path.resolve(this.root, entry)
    // 读取文件
    let source = this.getSource(filePath)
    // 转换成 ast,方便分析结构
    let ast = parser.parse(source)
    let root = this.root
    let dependenes = []
    // 遍历 ast 节点
    traverse(ast, {
      CallExpression (p) {
        const { name } = p.node.callee
        if (name === 'require') { // 获取 require 的路径
          let oldPath = p.node.arguments[0].value
          // 将 加载路径修改为从工作目录开始的相对路径
          oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
          // 将改文件中的依赖保存
          dependenes.push(oldPath)
          p.node.arguments[0].value = oldPath
          // 将 require 更名为 __webpack_require__
          p.node.callee.name = '__webpack_require__'
        }
      }
    })
    // 递归解析依赖
    dependenes.forEach((path) => {
      this.depAnalyse(path)
    })
    // 将 ast 还原为代码
    let code = generator(ast).code
    // 保存已加载的代码
    this.modules[entry] = code
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

文件解析完成后打包
使用 ejs 模板来拼接 bundle.js






 




















































 
 

 
 
 
 
 
 
 
 


const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')

module.exports = class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry || './src/index.js'
    this.root = config.root || process.cwd()
    this.rules = config.module.rules || []
    this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
    this.modules = {} // 保存以读取的模块
  }

  getSource (filePath) {
    return fs.readFileSync(filePath, 'utf-8')
  }

  start () {
    this.depAnalyse(this.entry) // 从入口开始分析
  }

  depAnalyse (entry) {
    let filePath = path.resolve(this.root, entry)
    // 读取文件
    let source = this.getSource(filePath)
    // 转换成 ast,方便分析结构
    let ast = parser.parse(source)
    let root = this.root
    let dependenes = []
    // 遍历 ast 节点
    traverse(ast, {
      CallExpression (p) {
        const { name } = p.node.callee
        if (name === 'require') { // 获取 require 的路径
          let oldPath = p.node.arguments[0].value
          // 将 加载路径修改为从工作目录开始的相对路径
          oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
          // 将改文件中的依赖保存
          dependenes.push(oldPath)
          p.node.arguments[0].value = oldPath
          // 将 require 更名为 __webpack_require__
          p.node.callee.name = '__webpack_require__'
        }
      }
    })
    // 递归解析依赖
    dependenes.forEach((path) => {
      this.depAnalyse(path)
    })
    // 将 ast 还原为代码
    let code = generator(ast).code
    // 保存已加载的代码
    this.modules[entry] = code

    // TODO emit之前应该有 loader 处理
    this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
  },
  emit () {
    let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8') // 加载的 index.ejs 文件内容见下方
    let result = ejs.render(template, {
      modules: this.modules,
      entryPath: this.entry
    })
    fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

index.ejs 模板内容

(() => { // webpackBootstrap
  var __webpack_modules__ = ({
    <%for (let key in modules) {%>
      "<%- key%>":
        ((module, exports, __webpack_require__) => {
          eval(`<%- modules[key]%>`);
        }),
    <%}%>
  });

  var __webpack_module_cache__ = {};

  function __webpack_require__ (moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
  }
  var __webpack_exports__ = __webpack_require__(`<%- entryPath%>`);
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

至此,从入口遍历依赖并且生成一个 bundle.js 文件的简单 webpack 功能已经实现了

何时使用loader

  1. 根据我们对 loader 功能的认识:加载文件、转换文件
  2. 加载文件 主要是对非 js 文件的处理,这里我们不做讲解
  3. 转换文件 功能是我们这次将要在 mini-webpack 上实现的
  4. rules 匹配规则顺序是从前向后匹配,如果匹配到了,loader 执行顺序从后往前。















 









































 











 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')

module.exports = class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry || './src/index.js'
    this.root = config.root || process.cwd()
    this.rules = config.module.rules || []
    this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
    this.modules = {} // 保存以读取的模块
    this.cacheLoaders = {} // 缓存加载的 loader
  }

  getSource (filePath) {
    return fs.readFileSync(filePath, 'utf-8')
  }

  start () {
    this.depAnalyse(this.entry) // 从入口开始分析
  }

  depAnalyse (entry) {
    let filePath = path.resolve(this.root, entry)
    // 读取文件
    let source = this.getSource(filePath)
    // 转换成 ast,方便分析结构
    let ast = parser.parse(source)
    let root = this.root
    let dependenes = []
    // 遍历 ast 节点
    traverse(ast, {
      CallExpression (p) {
        const { name } = p.node.callee
        if (name === 'require') { // 获取 require 的路径
          let oldPath = p.node.arguments[0].value
          // 将 加载路径修改为从工作目录开始的相对路径
          oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
          // 将改文件中的依赖保存
          dependenes.push(oldPath)
          p.node.arguments[0].value = oldPath
          // 将 require 更名为 __webpack_require__
          p.node.callee.name = '__webpack_require__'
        }
      }
    })
    // 递归解析依赖
    dependenes.forEach((path) => {
      this.depAnalyse(path)
    })
    // 将 ast 还原为代码
    let code = generator(ast).code
    // 保存已加载的代码
    this.modules[entry] = this.loadAndRunLoaders(code, filePath) // 将源代码交给匹配上的 loader 处理

    this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
  },
  emit () {
    let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8')
    let result = ejs.render(template, {
      modules: this.modules,
      entryPath: this.entry
    })
    fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
  }
  loadAndRunLoaders (code, filePath) {
    for (let i = 0; i < this.rules.length; i++) {
      let { test, use } = this.rules[i]
      if (test.test(filePath)) { // 如果规则匹配上了
        if (Array.isArray(use)) { // 数组
          for (let j = use.length - 1; j >= 0; j--) { // 从后往前执行
            let loader = use[j]
            code = this.cacheLoader(loader, code)
          }
        } else if (typeof use === 'string') {
          code = this.cacheLoader(use, code)
        } else if (typeof use === 'object') {
          code = this.cacheLoader(use.loader, code, use.options)
        }
      }
    }
    return code
  }
  cacheLoader (loader, code, options = undefined) {
    if (!this.cacheLoaders[loader]) {
      this.cacheLoaders[loader] = require(path.resolve(this.root, loader))
    }
    code = this.cacheLoaders[loader].call({ query: options }, code)
    return code
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

何时使用plugin

  1. 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
  2. 根据 plugin 的功能,在 mini-webpack 中我们需要在不用处理阶段抛出事件
  3. 这里主要用 tapable 来实现






 











 
 
 
 
 
 
 
 
 
 
 
 
 
 







 



































 



 






 




























const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
const { SyncHook } = require('tapable')

module.exports = class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry || './src/index.js'
    this.root = config.root || process.cwd()
    this.rules = config.module.rules || []
    this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
    this.modules = {} // 保存以读取的模块
    this.cacheLoaders = {} // 缓存加载的 loader

    // 以同步钩子举例
    this.hooks = {
      entryOption: new SyncHook(), // options 合并时触发
      run: new SyncHook(), // 开始从入口文件分析前触发
      compile: new SyncHook(), // 开始编译
      make: new SyncHook(), // 编译时触发
      emit: new SyncHook(), // 全部文件解析完成
      done: new SyncHook(), // 整个项目构建完成
    }

    if (Array.isArray(config.plugins)) {
      config.plugins.forEach(plusin => plusin.apply(this))
    }
    this.hooks.entryOption.call()
  }

  getSource (filePath) {
    return fs.readFileSync(filePath, 'utf-8')
  }

  start () {
    this.hooks.run.call()
    this.depAnalyse(this.entry) // 从入口开始分析
  }

  depAnalyse (entry) {
    let filePath = path.resolve(this.root, entry)
    // 读取文件
    let source = this.getSource(filePath)
    // 转换成 ast,方便分析结构
    let ast = parser.parse(source)
    let root = this.root
    let dependenes = []
    // 遍历 ast 节点
    traverse(ast, {
      CallExpression (p) {
        const { name } = p.node.callee
        if (name === 'require') { // 获取 require 的路径
          let oldPath = p.node.arguments[0].value
          // 将 加载路径修改为从工作目录开始的相对路径
          oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
          // 将改文件中的依赖保存
          dependenes.push(oldPath)
          p.node.arguments[0].value = oldPath
          // 将 require 更名为 __webpack_require__
          p.node.callee.name = '__webpack_require__'
        }
      }
    })
    // 递归解析依赖
    dependenes.forEach((path) => {
      this.depAnalyse(path)
    })
    // 将 ast 还原为代码
    let code = generator(ast).code
    // 保存已加载的代码
    this.modules[entry] = this.loadAndRunLoaders(code, filePath) // 将源代码交给匹配上的 loader 处理
    this.hooks.compile.call()
    this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
  },
  emit () {
    this.hooks.emit.call()
    let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8')
    let result = ejs.render(template, {
      modules: this.modules,
      entryPath: this.entry
    })
    fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
    this.hooks.done.call()
  }
  loadAndRunLoaders (code, filePath) {
    for (let i = 0; i < this.rules.length; i++) {
      let { test, use } = this.rules[i]
      if (test.test(filePath)) { // 如果规则匹配上了
        if (Array.isArray(use)) { // 数组
          for (let j = use.length - 1; j >= 0; j--) { // 从后往前执行
            let loader = use[j]
            code = this.cacheLoader(loader, code)
          }
        } else if (typeof use === 'string') {
          code = this.cacheLoader(use, code)
        } else if (typeof use === 'object') {
          code = this.cacheLoader(use.loader, code, use.options)
        }
      }
    }
    return code
  }
  cacheLoader (loader, code, options = undefined) {
    if (!this.cacheLoaders[loader]) {
      this.cacheLoaders[loader] = require(path.resolve(this.root, loader))
    }
    code = this.cacheLoaders[loader].call({ query: options }, code)
    return code
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#Webpack
上次更新: 2021/08/14, 14:54:53
vueRouter源码分析
阅读《重构:改善既有代码的设计》笔记

← vueRouter源码分析 阅读《重构:改善既有代码的设计》笔记→

最近更新
01
代码片段
04-22
02
koa全家桶
03-29
03
mocks项目复盘
03-29
更多文章>
Theme by Vdoing | Copyright © 2021-2022 coderly | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式