前言
在开始讲解它的原理之前,我想带大家先了解一下什么是 CommonJs?有些同学可能还不知道什么是 CommonJs,有的听说过,但不知道与前端之间的关系?知道的也可能只是一知半解,只停留在知道他是 NodeJs 中的模块化标准,与前端何干?我想告诉大家的是,一位优秀的前端工程师技术栈肯定是包含 NodeJs 的,在大厂,NodeJs 已经变成了前端工程师默认必会的一项技能了。但是此文不讲 Node,讲讲他的模块化标准 —— CommonJs
初探
在讲之前,先来看两个问题。
1. 为什么要出个 CommonJs,他是用来解决什么问题的?
2. 为什么浏览器不使用 CommonJs?
首先要搞懂第一个问题,就要扯到前端的发展史了,我们知道,很长一段时间JavaScript是没有模块化的概念的,直到 Node.Js 的诞生,把 JavaScript 语言带到服务端后,面对文件系统、网络、操作系统等等复杂的业务场景,模块化就变得不可或缺。于是 Node.js 和 CommonJS 规范就相得益彰、相映成辉,共同走入开发者的视线。
为什么浏览器不使用 CommonJs?
在 CommonJs 出来并取得成功后,前端人员就坐不住了,你后端用 js 都能搞出个模块化标准,这不是啪啪打我们的脸吗,也开始了模块化探索的道路,起初大家想的都是能不能使用 CommonJs 也来作为前端的模块化标准,后面发现是行不通的。我们来看看CommonJs的工作原理就知道为什么了。
浏览器模块化的难题?从它的原理入手。
CommonJs的工作原理
当使用了require(模块路径
)导入一个模块后,node会做以下两件事情(不考虑模块缓存):
-
通过模块路径找到本机文件,并读取文件内容。
-
将文件的代码放入到一个函数环境中执行,并将执行后module.exports的值作为require的函数的返回结果。
正式这两个步骤,使得CommonJs在node端可以良好的被支持。
可以认为,CommonJs是同步的,必须要等待加载完文件并执行完模块内的代码才能继续向后执行
如果在浏览器使用CommonJs呢?
当想要把CommonJs放到浏览器端时,就遇到了一些挑战。
-
浏览器要加载JS文件,需要远程从服务器获取,而网络传输的效率远远低于node环境中读取本地文件的效率。由于CommonJs是同步的,这会极大的降低运行效能。比如模块过大,页面可能会处于“假死”的状态。
-
如果需要读取JS文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是CommonJs属于社区标准,并非官方规范,所以浏览器厂商不愿意花时间和精力去实现它。
新的规范
基于以上两点原因,浏览器无法支持模块化。
可这并不代表模块化不能在浏览器中实现。
要在浏览器中实现模块化,只要能解决上面的两个问题就行了。
解决办法其实很简单:
-
远程加载JS浪费了时间?做成异步的即可,加载完成后调用一个回调就行了。
-
模块中的代码需要放置到函数中执行?编写模块时,直接放入到函数中就行了。
基于这种简单有效的思路,出现了AMD和CMD规范,有效的解决了浏览器模块化的问题。(本文就不介绍AMD和CMD规范了,有兴趣的同学可以自行百度或来问我)。
到最后ECMA组织参考了众多了社区的模块化标准,终于在2015年,随着ES6发布了官方的模块化标准后成为了ES6模块化。一直沿用至今。AMD和CMD也消失匿迹了。
用一句话总结就是:CommonJs —— 不是前端却革命了前端!
正题
在大概了解了 CommonJs 是什么之后,我们就来进入到今天的正题。
现在有两个模块一个为 a.js,一个为 b.js。 a.js 为入口文件。
a.js
const b = require("./b");
b.sayHello();
复制代码
b.js
console.log("b模块被加载了");
exports.sayHello = function () {
console.log("hello, world!");
};
exports.name = "光头强";
复制代码
此时node ./a.js
终端会打印什么结果?
// b模块被加载了
// hello,world!
复制代码
输出结果毋庸置疑。
将 b.js 稍作改动再来看看结果:
b.js
console.log("b模块被加载了");
module.exports = {
sayHello: function () {
console.log("oh,yes,baby!");
},
};
exports.sayHello = function () {
console.log("hello, world!");
};
exports.name = "光头强";
复制代码
现在终端会输出什么呢?答案是:b模块被加载了,on,yes,babay!
有的同学可能就会被蛊惑到了,有些大佬一眼就能看出答案,这就跟他内部如何实现CommonJs规范挂钩了。
别着急,听我给大家伙娓娓道来。
首先CommonJs的规范就是将模块内容放入到一个立即执行函数中执行(防止污染全局变量)。
// 我们写的代码相当于这样执行
(function(module){
// 在模块开始执行前,初始化一个值 module.exports = {}
module.exports = {};
// 其次又生命了一个exports,将其指向module.exports
var exports = module.exports;
// 模块内的代码...
// 最后将module.exports作为返回值
return module.exports;
})()
复制代码
所以以上代码终端输出为什么跟想象中大相径庭就不难理解了。
console.log("b模块被加载了");
module.exports = {
sayHello: function () {
console.log("oh,yes,baby!");
},
};
exports.sayHello = function () {
console.log("hello, world!");
};
exports.name = "光头强";
复制代码
我们直接改变了module.exports的地址,使得exports指向的地址跟module.exports初始化时的地址部位同一个。
所以我们无论再怎么在exports上挂载属性,再怎么操作都跟最后导出的值没有任何关系了。
因为到最后CommonJs规范导出的是module.exports,而并不是exports。
建议:
在使用CommonJs的时候导出最好使用module.exports,因为最终导出的是module.exports而非exports,如果不小心改变了module.exports或者exports的地址就会跟预期结果不同,如果非要写成两者共存的情况可以这么写。
module.exports.sayHello = function () {};
exports.name = "光头强";
// 这样两者操作的都是同一个对象。但是不建议这样写,最好使用module.exports导出。
复制代码
总结
NodeJs 对 CommonJs 的实现
为了实现 CommonJs 规范,NodeJs 对模块做了以下处理。
- 为了保证高效的执行,仅加载必要的模块,NodeJs 只有执行到 require 函数时才会加载并执行模块。
- 为了隐藏模块中的代码,NodeJs 执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
(function (module) {
//模块中的代码
})();
复制代码
- 为了保证顺利的导出模块内容,NodeJs 做了以下处理
- 在模块开始执行前,初始化一个值 module.exports = {}
- module.exports 即模块的导出值
- 为了方便开发者便捷的导出,NodeJs 在初始化完 module.exports 后,又声明了一个变量 exports = module.exports
(function (module) { module.exports = {}; var exports = module.exports; // 模块化中的代码 return module.exports; })(); 复制代码
- 为了避免反复加载同一个模块,NodeJs 默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果。
文章中有错误的地方欢迎大家指正,大家有好的建议可以积极评论留言~,谢谢!