Webpack基础打包原理

350 阅读4分钟

这是一篇非常基础的打包原理

开始前准备

  • 先来看个目录结构

还是图片好,目录树可真丑

  • 写一个webpack的基础配置,只需要主入口和输出配置
/* webpack.config.js */
module.exports = {
  'entry': './src/index.js',
  'output': {
    'filename': 'index.js',
    'path': path.resolve(__dirname, 'bundle')
  }
}
  • 主入口是index.js,引入了a.js和b.js两个文件
/* src/index.js */
const name = require('./a.js')
const age = require('./b/b.js')

console.log('index.js')
console.log(name, age)
/* src/a.js */
module.exports = 'Rskmin'
/* src/b/b.js */
module.exports = 19

开始

从打包好的文件入手,我去除了这次用不到的部分。只留下了最重要的__webpack_require__方法

(function (modules) {
  // 模块
  var installedModules = {};

  function __webpack_require__(moduleId) {
    // 1.判断是否有该模块的缓存
    if (installedModules[moduleId]) {// 如果有就直接返回缓存内容
      return installedModules[moduleId].exports;
    }
    // 2.如果没有就创建这个模块的缓存
    var module = installedModules[moduleId] = {
      i: moduleId,
      // 缓存状态
      l: false,
      // 缓存内容
      exports: {}
    };
    // 3.执行模块内部表达式,获取计算结果并存入缓存。同时传入`__webpack_require__`方法,用于本模块引入其他模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 成功后将缓存状态置`true`
    module.l = true;
    // 返回模块返回值
    return module.exports;
  }
  return __webpack_require__("./src/index.js");
})
  ({

    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        const name = __webpack_require__("./src/a.js");
        const age = __webpack_require__("./src/b/b.js");

        console.log('index.js');
        console.log(name, age);
      }),

    "./src/a.js":
      (function (module, exports, __webpack_require__) {
        module.exports = 'Rskmin';
      }),

    "./src/b/b.js":
      (function (module, exports, __webpack_require__) {
        module.exports = 19;
      }),

  })
  1. 可以看到webpack生成了一个立即执行的表达式,并把打包好的一个个文件模块装入对象,作为参数传入表达式

  2. 在表达式的最底部调用了__webpack_require__方法,执行了主模块的内容,主模块中对其他模块的引入也是调用了这个方法

稍微做个笔记:模块内容即一段程序,程序的目的就是计算并给出结果,缓存就是缓存这个结果

关注点来到打包好的模块,打包的目的就是生成这些模块

  • 每个模块都生成了一个键值对
  • key是文件的相对路径(相对于配置文件)
  • value是将模块的内容装入了一个表达式
  • 模块内对其他模块的引入从require()变成了__webpack_require__,路径也变成了相对于配置文件的路径(这样就可以和key对应)

分析结束,开始处理模块

  1. 把打包功能封装成一个功能对象,该对象用来打包各个模块
/* lib/Complier.js */
class Complier {
    constructor(config) {
    // 想要打包必须知道入口和出口,这个就是webpack的配置文件
    this.config = config
  }
}
/* bin/pack */
const path = require('path')
const Complier = require('../lib/Complier')

// 引入配置文件,创建打包对象传入配置文件
const configPath = path.resolve(process.cwd(), 'webpack.config.js')
const config = require(configPath)
const cp = new Complier(config)
// 调用打包方法开始打包
cp.run()

process.cwd() 传送门

  1. run方法,首先处理模块内容
/* lib/Complier.js */
class Complier {
  constructor(config) {
    this.config = config
    // 保存模块的依赖
    this.modules = {}
  }
  run() {
    // 从入口开始构建模块
    this.buidModule(this.config.entry)
  }
  buidModule(modulePath) {
    // 拿到主模块代码
    let code = this.getSource(modulePath)
    // ***处理当前模块的代码***
    let { resultCode, dependencies } = this.parseModule(code) // <--进入
    // 将主模块的路径和代码保存到modules中(每个模块都生成了一个键值对)
    this.modules[modulePath] = resultCode
    // 处理依赖模块,递归构建
    dependencies.forEach(depPath => {
      this.buidModule(depPath)
    })
  }
  getSource(modulePath) {
    return fs.readFileSync(modulePath, 'utf8')
  }
  parseModule(code) {}
 }

拎出parseModule,读取了入口文件后要做一件事

把模块内的require替换成__webpack_require__,路径替换成相对于配置文件的路径

用正则替换很大概率影响到模块内其他内容,所以用抽象语法树解析,然后替换内容(这里使用babel转化抽象语法树)

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')

parseModule(code) {
    // 将当前模块的代码转换成抽象语法树
    let ast = parser.parse(code)
    // 定义变量保存主模块地址
    let rootPath = path.dirname(this.config.entry)
    // 定义数组保存当前模块所有的依赖,用于继续递归解析模块
    let dependencies = []
    // 遍历抽象语法树修改抽象语法树中的内容
    traverse(ast, {
      CallExpression(nodePath) {
        let node = nodePath.node
        if (node.callee.name === 'require') {
          // 将require修改为__webpack_require__
          node.callee.name = '__webpack_require__'
          // 修改require导入的路径
          let modulePath = node.arguments[0].value
          modulePath = '.\\' + path.join(rootPath, modulePath)
          modulePath = modulePath.replace(/\\/g, '/')
          dependencies.push(modulePath)
          // 创建新的stringLiteral结点替换掉旧的
          node.arguments = [t.stringLiteral(modulePath)]
        }
      }
    })
    // 将修改之后的抽象语法树转换成代码
    let resultCode = generate(ast).code
    // 返回结果
    return { resultCode, dependencies }
  }

如何查看抽象语法树传送门,以及各babel模块功能(请使用搜索框)传送门

总之就干了替换这件事

解析结束后Complier.modules中就保存的你要的keyvalue

把正确的东西置于正确的位置(用模板引擎生成最后的代码)

模板

/* lib/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__("<%-entryId%>");
})
  ({

    <% for(let key in modules) {%>
    "<%-key%>":
      (function (module, exports, __webpack_require__) {
        <%-modules[key]%>
      }),
    <% } %>

  })

最终的Complier类

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

class Complier {
  constructor(config) {
    this.config = config
    this.modules = {}
  }
  run() {
    this.buidModule(this.config.entry)
    // 利用模板引擎生成最终代码
    this.emitFile()
  }
  buidModule(modulePath) {
    let code = this.getSource(modulePath)
    let { resultCode, dependencies } = this.parseModule(code)
    this.modules[modulePath] = resultCode
    dependencies.forEach(depPath => {
      this.buidModule(depPath)
    })
  }
  parseModule(code) {
    let ast = parser.parse(code)
    let rootPath = path.dirname(this.config.entry)
    let dependencies = []
    traverse(ast, {
      CallExpression(nodePath) {
        let node = nodePath.node
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'
          let modulePath = node.arguments[0].value
          modulePath = '.\\' + path.join(rootPath, modulePath)
          modulePath = modulePath.replace(/\\/g, '/')
          dependencies.push(modulePath)
          node.arguments = [t.stringLiteral(modulePath)]
        }
      }
    })
    let resultCode = generate(ast).code
    return { resultCode, dependencies }
  }
  getSource(modulePath) {
    return fs.readFileSync(modulePath, 'utf8')
  }
  emitFile() {
    // 读取EJS模板
    let templatePath = path.resolve(__dirname, 'main.ejs')
    let template = fs.readFileSync(templatePath, 'utf8')
    // 利用变量替换模板中的内容
    let resultCode = ejs.render(template, { 'entryId': this.config.entry, 'modules': this.modules })
    // 将最终的内容写入到文件中
    let outputDir = this.config.output.path
    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir)
    }
    const outputPath = path.resolve(outputDir, this.config.output.filename)
    fs.writeFileSync(outputPath, resultCode)
  }
}

module.exports = Complie

到这里就愉快的结束了