早期JavaScript只需要实现简单的页面交互,几行代码即可搞定。随着浏览器性能的提升以及前端技术的不断发展,JavaScript代码日益膨胀,此时就需要一个完善的模块化机制来解决这个问题。因此诞生了CommonJS(NodeJS), AMD(sea.js), ES6 Module(ES6, Webpack), CMD(require.js)等模块化规范。
什么是模块化?
模块化是一种处理复杂系统分解为更好的可管理模块的方式,用来分割,组织和打包软件。每一个模块完成一个特定的子功能,所有的模块按照某种方式组装起来,成为一个整体,完成整个系统的所有要求功能。
模块化的好处是什么?
模块间解耦,提高模块的复用性。
避免命名冲突。
分离以及按需加载。
提高系统的维护性。
JS模块化的演进
JavaScript模块化的发展进程了。早期JavaScript模块化模式比较简单粗暴,将一个模块定义为一个全局函数
function module1() {
// code
}
function module2() {
// code
}
// 这种方案非常简单,但问题也很明显:污染全局命名空间,引起命名冲突或数据不安全,而且模块间的依赖关系并不明显。
在此基础上,又有了namespace模式,利用一个对象来对模块进行包装
var module1 = {
data: { }, // 数据区域
func1: function() {}
func2: function() {}
}
// 这种方案的问题依然是数据不安全,外面能直接修改module1的data
因此又有了IIFE模式,利用自执行函数(闭包)
!function(window) {
var data = {};
function func1() {
data.hello = "hello";
}
function func2() {
data.world = "world";
}
window.module1 = { func1, func2 };
} (window)
//数据定义为私有,外部只能通过模块暴露的方法来对data进行操作,但这依然没有解决模块依赖的问题
基于IIFE,又提出了一种新的模块化方案,即在IIFE的基础上引入了依赖(现代模块化的基石,Webpack、NodeJS等模块化都是基于此实现的)
!function (window, module2) {
var data = {};
function func1() {
data.world = "world";
module2.hello();
}
window.module1 = { func1 };
} (window, { hello: function() {}, });
// 这样使IIFE模块化的依赖关系变得更明显,又保证了IIFE模块化独有的特性。
什么是 webpack
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
我们知道了模块打包器会将多个文件打包成一个文件,那么打包后的文件到底是什么样的了,我们必须知道这个才能够进行具体实现,因此我们查看以下 webpack 打包后的效果。示例:假设我们在同一个文件夹下有以下几个文件:文件:index.js
let action = require("./action.js").action; // 引入aciton.js
let name = require("./name.js").name; // 引入name.js
let message = `${name} is ${action}`;
// index.js文件中引入了action.js和name.js。
文件:action.js
let action = "making webpack";
exports.action = action;
文件:name.js
let familyName = require("./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
// 文件name.js又引入了family-name.js文件。
文件:family-name.js
exports.name = "haiyingsitan";
接下来我们使用 webpack 进行打包,并去除打包后的注释,得到如下代码:
(() => {
var __webpack_modules__ = ({
"./action.js": ((__unused_webpack_module, exports) => {
let action = "making webpack";
exports.action = action;
}),
"./family-name.js": ((__unused_webpack_module, exports) => {
exports.name = "haiyingsitan";
}),
"./name.js": ((__unused_webpack_module, exports, __webpack_require__) => {
let familyName = __webpack_require__( /*! ./family-name.js */ "./family-name.js").name;
exports.name = `${familyName} 阿尔伯特`;
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
return module.exports;
}
(() => {
let action = __webpack_require__( "./action.js").action;
let name = __webpack_require__( "./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
})();
})();
我们进一步简化它
(() => {
// 获取所有的依赖
var modules = {
"./action.js": (module, exports) => {
let action = "making webpack";
exports.action = action;
},
// ... 其他代码
};
// require对应的模块函数执行
function __webpack_require__(moduleId) {
// 其他实现
return module.exports;
}
// 入口函数立即执行
let entryFn = () => {
let action = __webpack_require__("./action.js").action;
let name = __webpack_require__("./name.js").name;
let message = `${name} is ${action}`;
console.log(message);
};
entryFn();
})();
我们可以发现,文件最终打包后就是一个立即执行函数。这个函数由三部分组成:
1、模块集合 这个模块集合是所有模块的集合,以路径作为key值,模块内容作为value值。当我们需要使用某个模块时,直接从这个模块集合中进行获取即可。为什么需要这个模块集合了?试想一下,如果我们遇到require("./action.js"),那么这个action.js到底对应的是哪个模块了?因此,我们必须能够获取到所有的模块,并对他们进行区分(使用模块id或者模块名称),到时候直接从这个模块集合中通过模块id或者模块名进行获取即可。
2、模块函数执行 每一个模块对应于一个函数,当遇到require(xxx)的时候实际上就是去执行引入的这个模块函数。
3、入口文件立即执行(执行模块的函数) 我们都知道一个模块的打包,必须有一个入口文件,而且这个文件必须立即执行,才能获取到所有的依赖。其实入口文件,也是一个模块,立即执行这个模块对应的函数即可。
我们可以发现每个模块实际上就是在外层套上了一个函数的外壳。为什么要把文件内容放入到一个函数中了,这是因为我们都知道模块化最重要的一个特点就是环境隔离,各个模块之间互不影响。
如果我们想要实现同样的功能,只需要同时实现:模块集合,模块执行和入口函数立即执行即可。
也就是说,我们最终要实现的就是这样的一个集合。到目前为止,我们要实现的功能是:
1、给每个文件内容加壳
2、每个模块以路径作为模块 id
3、将所有的模块合在一起形成一个集合
我们看下具体的实现如下:
const fs = require("fs");
let modules = {};
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path, // 这里以路径作为模块id
code: `function(require,exports){ // 这里加壳了
${fileContent.toString()};
}`,
};
};
let result = fileToModule("./index.js");
modules[result["id"]] = result.code;
console.log("modules=",modules);
输出的结果为:
modules= {
'./index.js':'function(require,exports){\n let action = require("./action.js").action;\r\nlet name = require("./name.js").name;\r\nlet message = `${name} is ${action}`;\r\nconsole.log(message);\r\n;\n }'
从上面我们可以看出,我们成功地将入口文件加壳转化成一个模块,并且给其命名,然后添加到模块对象中去了。但是我们发现我们的文件中其实还依赖了./action.js和./name.js,然而我们无法获取到他们的模块内容。因此,我们需要处理require引入的模块。也就是说要找到当前模块中的所有依赖,然后解析这些依赖将其放入模块集合中。
获取当前模块的所有依赖
// const action = require("./action.js")
function getDependencies(fileContent) {
let reg = /require\(['"](.+? "'"")['"]\)/g;
let result = null;
let dependencies = [];
while ((result = reg.exec(fileContent))) {
dependencies.push(result[1]);
}
return dependencies;
}
这里我们使用了正则判断,只要是require("")或者require('')这种格式的都当作模块引入进行处理(这种处理有点问题,我们暂时先不管,等到下面进行优化)。然后把所有的引入都放到一个数组中,从而获取到当前模块所有的依赖。我们使用这个函数查看下入口文件的依赖:
const fileContent = fs.readFileSync(path).toString();
let result = getDependencies(fileContent);
console.log(result) // ["./action.js","./name.js"]
将所有模块组成一个集合
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
const child = fileToModule(absolutePath);
queue.push(child);
});
}
// 上面得到的是一个数组。转化成对象
let modules = {}
queue.forEach((item) => {
modules[item.id] = item.code;
})
return modules;
}
console.log(createGraph("./index.js"));
执行模块的函数
我们在上面的模块对象中获得了所有模块信息,接下来我们执行入口文件对应的函数exec。
从上图中我们可以看出:当我们执行入口文件对应的函数时exec(index.js),它发现:
存在依赖./action.js,于是调用exec("./action.js")。这时候不存在其他依赖了,那么直接返回值。这条线结束。
存在依赖./name.js,于是调用exec("./name.js")。又发现依赖./family-name.js,于是调用exec("./family-name.js")。这时候不存在其他依赖了,返回值。这条线结束。
我们可以发现其实这就是一个递归的过程,不断查找依赖,然后执行对应的函数。因此,我们可以大致写出以下这个函数:
const exec = function(moduleId){
const fn = modules[moduleId]; // 获取到每个id对应的函数
let exports = {};
const require = function(filename){
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
}
fn(require, exports);
return exports
}
最终完整的代码如下:
const fs = require("fs");
const path = require("path");
const {parse} = require("@babel/parser");
const traverse = require("@babel/traverse").default;
// 1.加壳
const fileToModule = function (path) {
const fileContent = fs.readFileSync(path).toString();
return {
id: path,
dependencies: getDependencies(path),
code: `function (require, exports) {
${fileContent};
}`,
};
};
// 2.获取依赖
function getDependencies(filePath) {
let result = null;
let dependencies = [];
const fileContent = fs.readFileSync(filePath).toString();
// parse
const ast = parse(fileContent, { sourceType: "CommonJs" });
// transform
traverse(ast, {
enter: (item) => {
if (
item.node.type === "CallExpression" &&
item.node.callee.name === "require"
) {
const dirname = path.dirname(filePath);
dependencies.push(path.join(dirname, item.node.arguments[0].value));
console.log("dependencies", dependencies);
}
},
});
return dependencies;
}
// 3. 将所有依赖形成一个集合
function createGraph(filename) {
let module = fileToModule(filename);
let queue = [module];
for (let module of queue) {
const dirname = path.dirname(module.id);
module.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
console.log("queue:",queue);
console.log("absolutePath:",absolutePath);
const result = queue.every((item) => {
return item.id !== absolutePath;
});
if (result) {
const child = fileToModule(absolutePath);
queue.push(child);
} else {
return false;
}
});
}
let modules = {};
queue.forEach((item) => {
modules[item.id] = item.code;
});
return modules;
}
let modules = createGraph("./index.js");
// 4. 执行模块
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
// exec("./index.js");
// 5. 写入文件
function createBundle(modules){
let __modules = "";
for (let attr in modules) {
__modules += `"${attr}":${modules[attr]},`;
}
const result = `(function(){
const modules = {${__modules}};
const exec = function (moduleId) {
const fn = modules[moduleId];
let exports = {};
const require = function (filename) {
const dirname = path.dirname(module.id);
const absolutePath = path.join(dirname, filename);
return exec(absolutePath);
};
fn(require, exports);
return exports;
};
exec("./index.js");
})()`;
fs.writeFileSync("./dist/bundle3.js", result);
}
createBundle(modules)