假设我们在同一个文件夹下有两个源文件
文件 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