站点工具

用户工具


手写模块打包器

需求

假设我们在同一个文件夹下有两个源文件

文件 main.js

const { sum } = require('./math.js')
console.log(sum(2, 3))

文件 math.js

function sum(...args) {
  return args.reduce((v1, v2) => v1 + v2)
}
 
exports.sum = sum

之后我们使用Node.js写好一个名叫 webpacker 的打包工具,全局安装。当使用如下命令时,能把 main.js 做为入口文件,自动分析依赖,最终生成可执行的 bundle.js :

webpacker  main.js --output=bundle.js

这个webpacker能做什么,打包之后的文件内容是什么?

如何做

先初略猜测一下bundle.js 里是什么。假设打包过程只是把main.js 里依赖的文件的代码都合并到一起(如下代码所示),以下代码显然无法直接运行,因为代码里出现的 require 和 exports 都不存在,且各个文件中的变量都未隔离开。

const { sum } = require('./sum.js')
console.log(sum(2, 3))
 
function sum(...args) {
  return args.reduce((v1, v2) => v1 + v2)
}
exports.sum = sum

换个思路,每个源文件都内容都被一个函数包裹,如下代码所示,至少代码能勉强跑通。

function fn1(require, exports) {
  const { sum } = require('./sum.js')
  console.log(sum(2, 3))
}
 
function fn2(require, exports) {
  function sum(...args) {
    return args.reduce((v1, v2) => v1 + v2)
  }
  exports.sum = sum
}
 
function require() {
 //todo...
}

不过仍未解决我们都问题:1. 整个代码的入口是什么?如何启动?2. require函数如何定义。3. 如何知道 require('./sum.js')到底加载哪个模块?

更进一步

假设打包后的 bundle.js 如下代码所示,我们把用户的源文件包裹一个函数的壳,放到modules内。其中入口文件对应的模块的id为0。通过exec(0)获取到入口模块并且执行。

const modules = {
  0: function(require, exports) {
    const { sum } = require('./sum.js')
    console.log(sum(2, 3))
  },
  1: function(require, exports) {
    function sum(...args) {
      return args.reduce((v1, v2) => v1 + v2)
    }
    exports.sum = sum
  }
}
//执行模块,返回结果
function exec(id) {
  let module = modules[id]
  let  exports =  {};
  module(require, exports)
  function require(path) {
    //todo...
    //根据模块路径,返回模块执行的结果
  }
  return exports
}
exec(0)

但依旧有个问题尚未解决。当模块内部执行到 require时,到底如何根据requre的路径获取到其他模块?

最终思路

假设打包后的 bundle.js 如下代码所示,我们把用户的源文件包装成一个对象放到modules内,每个模块对象里面包含当前模块代码和依赖映射。通过exec(0)获取到入口模块并且执行。

const modules = {
  0: {
    module(require, exports) {
      const { sum } = require('./sum.js')  
      console.log(sum(2, 3))               
    },
    mapping: {'./sum.js': 1 } 
  },
  1: {
    module(require, exports) {
      function sum(...args) {
        return args.reduce((v1, v2) => v1 + v2)
      }
      exports.sum = sum
    },
    mapping: {}
  }
}
 
function exec(id) {
  const { module, mapping } = modules[id]
  let  exports =  {}
  module(path => exec(mapping[path]), exports)
  return exports
}
exec(0)

代码通过exec(0)获取到入口模块的代码(module) 和依赖映射(mapping);执行module;执行时如果里面遇到了requrie('./sum.js'),require就是箭头函数,执行的结果是exec(mapping['./sum.js']) 也就是 exec(1),最终拿到id为1的模块对象里面的exports。

这个打包后的bundle.js能按照我们的预期正常执行。

总结

我们自己的打包工具 webpacker根据用户配置的入口文件(main.js),读取js源码字符串并分析源码里的依赖(require),再根据依赖的路径层层递进,最终把用户的源码拼接成如上代码所示的bundle,写入 bundle.js。这里就不在详细实现。

Github 源码: https://github.com/jirengu/wheel-webpack

若愚 · 2021/09/27 11:40 · javascript_手写模块打包器.txt