深入认识CommonJs

前言

在开始讲解它的原理之前,我想带大家先了解一下什么是 CommonJs?有些同学可能还不知道什么是 CommonJs,有的听说过,但不知道与前端之间的关系?知道的也可能只是一知半解,只停留在知道他是 NodeJs 中的模块化标准,与前端何干?我想告诉大家的是,一位优秀的前端工程师技术栈肯定是包含 NodeJs 的,在大厂,NodeJs 已经变成了前端工程师默认必会的一项技能了。但是此文不讲 Node,讲讲他的模块化标准 —— CommonJs

初探

在讲之前,先来看两个问题。

1.  为什么要出个 CommonJs,他是用来解决什么问题的?
2. 为什么浏览器不使用 CommonJs?

首先要搞懂第一个问题,就要扯到前端的发展史了,我们知道,很长一段时间JavaScript是没有模块化的概念的,直到 Node.Js 的诞生,把 JavaScript 语言带到服务端后,面对文件系统、网络、操作系统等等复杂的业务场景,模块化就变得不可或缺。于是 Node.js 和 CommonJS 规范就相得益彰、相映成辉,共同走入开发者的视线。

为什么浏览器不使用 CommonJs?
在 CommonJs 出来并取得成功后,前端人员就坐不住了,你后端用 js 都能搞出个模块化标准,这不是啪啪打我们的脸吗,也开始了模块化探索的道路,起初大家想的都是能不能使用 CommonJs 也来作为前端的模块化标准,后面发现是行不通的。我们来看看CommonJs的工作原理就知道为什么了。

浏览器模块化的难题?从它的原理入手。

CommonJs的工作原理
image

当使用了require(模块路径)导入一个模块后,node会做以下两件事情(不考虑模块缓存):

  1. 通过模块路径找到本机文件,并读取文件内容。

  2. 将文件的代码放入到一个函数环境中执行,并将执行后module.exports的值作为require的函数的返回结果。

正式这两个步骤,使得CommonJs在node端可以良好的被支持。

可以认为,CommonJs是同步的,必须要等待加载完文件并执行完模块内的代码才能继续向后执行

如果在浏览器使用CommonJs呢?

当想要把CommonJs放到浏览器端时,就遇到了一些挑战。

  1. 浏览器要加载JS文件,需要远程从服务器获取,而网络传输的效率远远低于node环境中读取本地文件的效率。由于CommonJs是同步的,这会极大的降低运行效能。比如模块过大,页面可能会处于“假死”的状态。

  2. 如果需要读取JS文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是CommonJs属于社区标准,并非官方规范,所以浏览器厂商不愿意花时间和精力去实现它。

新的规范

基于以上两点原因,浏览器无法支持模块化。

可这并不代表模块化不能在浏览器中实现。

要在浏览器中实现模块化,只要能解决上面的两个问题就行了。

解决办法其实很简单:

  1. 远程加载JS浪费了时间?做成异步的即可,加载完成后调用一个回调就行了。

  2. 模块中的代码需要放置到函数中执行?编写模块时,直接放入到函数中就行了。

基于这种简单有效的思路,出现了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 对模块做了以下处理。

  1. 为了保证高效的执行,仅加载必要的模块,NodeJs 只有执行到 require 函数时才会加载并执行模块。
  2. 为了隐藏模块中的代码,NodeJs 执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
(function (module) {
  //模块中的代码
})();
复制代码
  1. 为了保证顺利的导出模块内容,NodeJs 做了以下处理
    1. 在模块开始执行前,初始化一个值 module.exports = {}
    2. module.exports 即模块的导出值
    3. 为了方便开发者便捷的导出,NodeJs 在初始化完 module.exports 后,又声明了一个变量 exports = module.exports
    (function (module) {
      module.exports = {};
      var exports = module.exports;
      // 模块化中的代码
      return module.exports;
    })();
    复制代码
  2. 为了避免反复加载同一个模块,NodeJs 默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果。

文章中有错误的地方欢迎大家指正,大家有好的建议可以积极评论留言~,谢谢!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享