项目结构
├── dist
├── bundle,js //打包生成的文件
└── src
├── index.js
├── action.js
├── family-name.js
└── name,js
└── webpack.js // 打包器
需求分析
// index.js
let action = require('./action.js').action;
let name = require('./name.js').name;
let message = `${name} is ${action}`;
console.log( message );
// action.js
let action = 'making webpack';
exports.action = action;
// name.js
let familyName = require('./family-name.js').name;
exports.name = `${familyName} GongJS`;
// family-name.js
exports.name = 'Alibaba';
在上面的代码中,分别定义了index.js、action.js、name.js、family-name.js四个文件,在node环境下运行node index.js命令时,会打印出:
Alibaba GongJS making webpack
那如果我们想在浏览器环境下,也能达到相同的效果,该怎么做?显然,如果我们直接把index.js文件里的代码直接复制到浏览器的控制台里执行,控制台会报错,因为浏览器并没有帮我们实现node环境里的CommonJS规范,换句话说浏览器识别不了require这个函数,没法去加载其他模块,所以通常这个时候我们需要借助一下第三方的打包工具如webpack,来对我们的代码进行打包处理生成浏览器可以识别的代码。下面让我们来动手写一个简易的webpack,使其也能达到相同的效果。
逆推思路
既然在浏览器里无法像在node里面导入其他模块,那么我们就需要把用到的模块都提前打包放在同一个文件bundle.js里供浏览器加载识别,现在需要考虑的是:当打包完后,bundle.js里的代码要如何组织,才能保证代码能够执行并且执行(调用模块)的顺序也是正确的?下面几点是我们需要考虑的:
- 模块的引用是有顺序的,入口文件
index.js是第一个执行的 - 模块里面的代码要能够执行,我们需要用函数来包裹它
综合这几点,bundle.js文件里的代码可能是这样的:
modules = {
// index.js
0: function() {
let action = require('./action.js').action;
let name = require('./name').name;
let message = `${name} is ${action}`;
console.log( message );
},
// action.js
1: function() {
let action = 'making webpack';
exports.action = action;
},
// name.js
2: function() {
let familyName = require('./family-name.js').name;
exports.name = `${familyName} GongJS`;
},
// family-name.js
3: function() {
exports.name = 'Alibaba';
}
}
在上面的代码中,我们把所有的模块都放到modules对象里,通过对象的key值:0,1,2,3来区分模块的调用顺序,比如modules[0]就表示调用之前的index.js模块,在index.js里又会按顺序先调用action.js modules[1],name.js modules[2];因为浏览器里并没有帮我们定义require和exports这两个变量,所以显然这两个是需要我们自己定义传给模块调用的,并且我们需要一个执行函数exec,用来调用modules[0](index.js),现在的代码就变成了下面这种形式:
modules = {
// index.js
0: function(require,exports) {
let action = require('./action.js').action;
let name = require('./name').name;
let message = `${name} is ${action}`;
console.log( message );
},
// action.js
1: function(require,exports) {
let action = 'making webpack';
exports.action = action;
},
// name.js
2: function(require,exports) {
let familyName = require('./family-name.js').name;
exports.name = `${familyName} GongJS`;
},
// family-name.js
3: function(require, exports) {
exports.name = 'Alibaba';
}
//执行模块,返回结果
function exec(id) {
let fn = modules[id];
let exports = {};
fn(require, exports);
function require(path) {
//todo...
//根据模块路径,返回模块执行的结果
}
}
exec(0) // 首先调用modules[0],即index.js
下面,就是要考虑怎么实现require这个函数。这个函数实现的功能是:当我们给它传入不同路径参数时,它能够去执行相应的模块,并把结果返回。所以,我们需要把路径和模块映射起来,这样index.js里的代码执行到require('./action.js')时候,它知道去执行modules[1](action.js)里的函数,我们再对代码做一下改造:
modules = {
0: [function(require, exports, module) {
let action = require('./action.js').action;
let name = require('./name.js').name;
let message = `${name} is ${action}`;
console.log( message );
},
{ // mapping对象,存的每个模块的依赖
'./action.js': 1,
'./name.js': 2
}
],
1: [function(require, exports, module) {
let action = 'making webpack';
exports.action = action;
},
{
}
],
2: [function(require, exports, module) {
let familyName = require('./family-name.js').name;
exports.name = `${familyName} GongJS`;
},
{
'./family-name.js': 3
}
],
3: [function(require, exports, module) {
exports.name = 'Alibaba';
},
{
}
]
}
//执行模块,返回结果
function exec(id) {
let [fn, mapping] = modules[id]; // 拿到模块的执行函数和依赖对象
let exports = {};
fn && fn(require, exports);
function require(path) {
//根据模块路径,返回模块执行的结果
return exec(mapping[path]);
}
return exports;
}
exec(0) // 从index.js开始执行
我们把每个模块里对其他模块的依赖都单独抽离出来放到一个对象(mapping)里,key就是模块的路径,而value就是该模块在整个modules里的索引,这样再调用require('./action.js')时候,它知道要去调用modules[1]对应的函数,并且所有的模块也是顺序执行的,到这里我们的bundle.js就算改造完了,把他扔到浏览器里,也能够打印出:
Alibaba GongJS making webpack
那我们后面要做的就是实现一个简易的打包器,把index.js、action.js、name.js、family-name.js这四个文件打包成bundle.js里面的代码。
开始实现
解析依赖
const fs = require('fs')
let fileContent = fs.readFileSync('./src/index.js', 'utf-8');
function getDependencies(str) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while(result = reg.exec(str)) {
dependencies.push(result[1]);
}
return dependencies;
}
console.log(getDependencies(fileContent))
这里我们通过正则匹配的方式去匹配类似require(./action.js)的字段,把模块的相关依赖给抽离出来。我们看看该函数的执行效果:
var str = "let action = require('./action.js').action;let name = require('./name.js').name;let message = `${name} is ${action}`;console.log(message);"
var reg = /require\(['"](.+?)['"]\)/g;
reg.exec(file)
// 第一次返回
["require('./action.js')", "./action.js", index: 13, input: "let action = require('./action.js').action;let nam…ge = `${name} is ${action}`;console.log(message);", groups: undefined]
reg.exec(file)
// 第二次返回
["require('./name.js')", "./name.js", index: 54, input: "let action = require('./action.js').action;let nam…ge = `${name} is ${action}`;console.log(message);", groups: undefined]
reg.exec(file)
// 第三次返回
null
这样我们就知道了index.js这个模块依赖了action.js和name.js这两个模块,后面需要通过这两个依赖去构建mapping。
构建模版
参照之前bundle.js代码,我们需要知道模块名(文件名)、模块索引(0,1,2,3)、模块内容(代码块)、模块依赖(mapping),继续改造代码:
const fs = require('fs');
const path = require('path');
let ID = 0;
function getDependencies(str) {
let reg = /require\(['"](.+?)['"]\)/g;
let result = null;
let dependencies = [];
while(result = reg.exec(str)) {
dependencies.push(result[1]);
}
return dependencies;
}
function createAsset(filename) {
let fileContent = fs.readFileSync(filename, 'utf-8');
const id = ID++;
return {
id: id,
filename: filename,
dependencies: getDependencies(fileContent),
code: `function(require, exports, module) {
${fileContent}
}`
}
}
我们把index.js里的内容读取出来,通过之前的getDependencies获取该模块的依赖,并且把读出来内容作为该模块的代码块。后面,我们还需要把其他几个文件(name.js\action.js\family-name.js)里的模版都按照这个方式构建出来。
在这里我们还没有拿到
mapping的相关信息,模版的构造还没有完全结束。
const fs = require('fs');
const path = require('path');
let ID = 0;
....
function createGraph(filename) {
let asset = createAsset(filename);
let queue = [asset];
for(let asset of queue) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath); // 获取文件的全路径
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id; // 获取依赖,拿到mapping的相关信息
queue.push(child);
});
}
return queue;
}
这里我们通过for of的形式来遍历,这样当queue新增了元素,下次遍历就会从新增的元素开始遍历。
拼接字符串
当我们把所有模块的内容都读出来并且配置成相应的模版格式,这时候我们需要把这些内容都拼接起来,并且写入到bundle.js文件里。
const fs = require('fs');
const path = require('path');
let ID = 0;
...
function createBundle(graph) {
let modules = ''; // 定义一个空字段,用来拼接字符串
graph.forEach(mod => {
modules += `${mod.id}: [
${mod.code},
${JSON.stringify(mod.mapping)}
],`;
});
// 拼接模版字符串,这里用了一个立即执行函数来包裹之前的exec函数,并把拼接好的参数当成参数传递进去
const result = `(function(modules){
function exec(id) {
let [fn, mapping] = modules[id];
console.log(fn, mapping)
let module = { exports: {} };
fn && fn(require, module.exports);
function require(path) {
//根据模块路径,返回模块执行的结果
return exec(mapping[path]);
}
return module.exports;
}
exec(0)
})(
{${modules}}
)`
// 生产bundle.js文件
fs.writeFileSync('../dist/bundle.js', result);
}
// 传入入口文件名,开始打包
let graph = createGraph('./src/index.js');
createBundle(graph)
最后,当我们在执行node webpack.js命令时,代码就会开始打包并生成bundle.js文件,内容
如下:
(function (modules) {
function exec(id) {
let [fn, mapping] = modules[id];
console.log(fn, mapping)
let module = {
exports: {}
};
fn && fn(require, module.exports);
function require(path) {
//根据模块路径,返回模块执行的结果
return exec(mapping[path]);
}
return module.exports;
}
exec(0)
})({
0: [
function (require, exports, module) {
let action = require('./action.js').action;
let name = require('./name.js').name;
let message = `${name} is ${action}`;
console.log(message);
},
{
"./action.js": 1,
"./name.js": 2
}
],
1: [
function (require, exports, module) {
let action = 'making webpack';
exports.action = action;
},
{}
],
2: [
function (require, exports, module) {
let familyName = require('./family-name.js').name;
exports.name = `${familyName} GongJS`;
},
{
"./family-name.js": 3
}
],
3: [
function (require, exports, module) {
exports.name = 'Alibaba';
},
{}
],
})
把这段代码放到浏览器控制台也能正确的打印出:
Alibaba GongJS making webpack