关于最近学习webapck打包原理的一些新的分享
我们都知道webpack是静态模块打包器,他能根据我们的项目入口,找到模块与模块之间的依赖,帮我们构建出我们最终想要的代码, 并顺利的在浏览器运行,但是平时工作中可能我们只知道这些东西
1.webpack需要配置很多loader,插件...
2.webpack源码复杂,生命周期特别多...
3.想深入了解无处下手
那现在就来看看webpack是如何进行打包,以及依赖查找的
模块化打包
最终打包结果得到两样东西
1.一个与模块依赖的对象参数
2.自执行函数
(function (modules) {
function exec(id) {
let [fn, mapping] = modules[id];
console.log(fn, mapping)
let module = { exports: {} };
fn && fn(require, module.exports, module);
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 = '我是action模块';
exports.action = action;
},
{}
], 2: [
function (require, exports, module) {
exports.name = '我是name模块';
},
{}
],
}
)
各个模块具体内容
index.js
let action = require('./action.js').action;
let name = require('./name.js').name;
let message = `${name} and ${action}`;
console.log(message);
action.js
let action = '我是action模块';exports.action = action;
name.js
exports.name = '我是name模块';
需要构建的数据结构
我们发现一个模块的定义的数据结构是这样的
- 模块自增id
- 被一个函数包裹的模块内容,为了在浏览器能运行,重写了require方法,requier作为一个模块的参数传递
- 与模块相关的依赖模块
实现这个数据结构
模块定义
-
唯一id
-
文件路径
-
包含依赖文件的数组
-
模块内容
-
id与模块间的引用关系
我们先来尝试处理前面三个
const fs = require('fs');
const path = require('path'); let ID = 0;
/** * 递归获取模块依赖
* @param {*} str 模块内容
* @return 模块依赖的数组集合
*/
function getDependencies(str) {
let reg = /require\(['"](.+?)['"]\)/g;//正则匹配ruquire后面的路径
let result = null; let dependencies = [];
while (result = reg.exec(str)) { dependencies.push(result[1]); }
return dependencies;
}
/** * 获取模块对象
* @param {*} filename 文件路径
* @return 模块对象
*/
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}
}`
}
}
createAsset返回值,但是这个返回值无法帮我们拿到模块之间的引用,也就是这个对象{'./action.js': 1},如果我知道了这个对象引用的id为1,那么我们就能拿到完整的模块集合了
{
id: 0,
filename: './src/index.js',
dependencies: [ './action.js', './name.js' ],
code: 'function(require, exports, module) { \n' +
" let action = require('./action.js').action;\n" +
"let name = require('./name.js').name;\n" +
'\n' +
'let message = `${name} is ${action}`;\n' +
'console.log(message);\n' +
' }'
}
通过id获取模块引用
- id与模块间的引用关系
使用let of 进行遍历,let of会继续遍历新添加的元素
/**通过id获取模块引用
* @param {*} filename 文件路径
* @return 模块对象
*/
function createGraph(filename) {
let asset = createAsset(filename); //模块
let queue = [asset]; // 把模块放入数组,遍历,push,遍历...
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;
queue.push(child);
});
} return queue;
}
最终可以拿到这样的数据
[{
id: 0, filename: './src/index.js',
dependencies: ['./action.js', './name'],
mapping: { './action.js': 1, './name': 2 }, // 模块与id引用关系
code: 'function(require, exports, module) {
let action = require(\'./action.js\').action;
let name = require(\'./name\').name;
let message = `${name} is ${action}`; console.log(message); }'
},
{ 1, // 模块id为1
xxx.js //模块路径
}]
最后我们再通过循环构建出一个开始想要的一个模块集合,加一个处理引用的递归函数,再拼成一个完成的代码,走你!
function createBundle(graph) {
let modules = '';
graph.forEach(mod => {
modules += `${mod.id}: [
${mod.code},
${JSON.stringify(mod.mapping)}
],`;
});
const result = `(function(modules){
function exec(id) {
let [fn, mapping] = modules[id];
console.log(fn, mapping)
let module = { exports: {} };
fn && fn(require, module.exports, module);
function require(path) {
//根据模块路径,返回模块执行的结果
return exec(mapping[path]);
}
return module.exports;
}
exec(0)
})(
{${modules}}
)`
fs.writeFileSync('./dist/bundle.js', result);
}createBundle(createGraph('./src/index.js'))
最后打包生成的文件
(function (modules) {
function exec(id) {
let [fn, mapping] = modules[id];
console.log(fn, mapping)
let module = { exports: {} };
fn && fn(require, module.exports, module);
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 = '我是action模块';
exports.action = action;
},
{}
], 2: [
function (require, exports, module) {
exports.name = '我是name模块';
},
{}
],
}
)
理一下执行顺序
1.exec(0)会拿到模块的入口文件,也就是id为0的这项,并执行fn()
2.exec(mapping[path]), mapping是结构的第二个模块的引用对象
{ "./action.js": 1, "./name.js": 2 }
path能拿到key,mapping[path]得到模块对应的id,继续递归执行下一个模块
最后在浏览器跑一下,完美
最后想说,其实总的代码是挺简单的,稍微花点时间,我觉得每个人都看得懂,但是这么一点代码能带来非常多的收获,我觉得是很值得一看,结束!