【二】CommonJS

364 阅读5分钟

为什么需要模块化

在开始说CommonJS之前,我们先回顾一下为什么需要模块化?

当前端工程到达一定规模后,就会出现下面的问题:

  • 全局变量污染
  • 依赖混乱

上面的问题,共同导致了代码文件难以细分

模块化就是为了解决上面两个问题出现的

模块化出现后,我们就可以把臃肿的代码细分到各个小文件中,便于后期维护管理

前端模块化标准

前端主要有两大模块化标准:

  • CommonJS,简称CMJ,这是一个社区规范,出现的时间较早,目前仅node环境支持
  • ES Module,简称ESM,这是随着ES6发布的官方模块化标准,目前浏览器和新版本node环境均支持

安装Node.js

前面提到CommonJS只支持node环境,所以我们学习CommonJS需要安装Node.js

官网地址:nodejs.org/zh-cn/

Node.js遵循ECMAScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在Node.js中使用ECMAScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在Node.js中使用浏览器的 web api,例如:dom对象、window对象、document对象等

由于大部分开发者是从浏览器端开发转向Node.js开发的,为了降低开发者的学习成本,Node.js中提供了一些和浏览器web api同样的对象或函数,例如:consolesetTimeoutsetInterval

CommonJS

在Node.js中,由于有且仅有一个入口文件(启动文件),在很多服务端语言中,都是这样的模式。在浏览器中我们通常都是在index.html中编写很多script标签来引入js,这是浏览器与Node.js的不同。而开发一个应用肯定会涉及到多个文件配合,因此,Node.js对模块化的需求比浏览器端要大的多

由于nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的CommonJS作为模块化规范

在学习CommonJS之前,首先认识两个重要的概念:模块的导出模块的导入

模块的导出

要理解模块的导出,首先要理解模块的含义

什么是模块?

模块就是一个JS文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用

模块有两个核心要素:隐藏暴露

隐藏的,是自己内部的实现

暴露的,是希望外部使用的接口

任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或api调用来暴露接口

暴露接口的过程即模块的导出

模块的导入

当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。

当通过某种语法或api去使用一个模块时,这个过程叫做模块的导入

CommonJS规范

CommonJS使用exports导出模块,require导入模块

具体规范如下:

  1. 如果一个js文件中存在exportsrequire,那么该js文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局对象造成任何污染
  3. 如果一个模块需要暴露一些api提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
let name = 'CommonJS';  // 隐藏变量
exports.a = 1;  // 暴露变量a
exports.b = 2;  // 暴露变量b
exports.add = function(a, b) {  // 暴露方法add
  return a + b;
}

/*
 * 最终暴露了个对象出去,所以我们在别的模块
 * 导入的时候,接收的是该对象
 * exports = {
 *   a: 1,
 *   b: 2,
 *   add: function(a, b) { ... }
 * }
 */

注意,使用exports语法只能通过添加属性的方式来暴露数据,而不能给exports重新赋值一个对象

exports = {
  a: 1,
  b: 2,
  add: function(a, b) {
    return a + b;
  }
}

这样暴露出来还是一个空对象。原因是CommonJS在实现的时候,最终返回的是module.exports,只是为了方便编写,它做了一个exports = module.exports的操作,如果给exports重新赋值一个对象,而最后导出的却是module.exports,结果当时是一个空对象,大家可以尝试一下。

  1. 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容
const o = require('./1.js')
console.log(o);  // {a: 1, b: 2, add: function(a, b) { ... } }
console.log(o.name);  // undefined 【name为隐藏变量】
console.log(o.a);  // 1
console.log(o.b);  //2
console.log(o.add);  // function add (a, b) { ... }

注意:nodejs中导入模块使用require函数写文件路径时必须以 "./""../" 开头,如果不写会报找不到模块错误

const abc = require("./util.js");

nodejs对CommonJS的实现

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

  2. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。

     (function(){
         //模块中的代码
     })()
    
  3. 为了保证顺利的导出模块内容,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;
     })()
    

    注意:上面的代码只是便于理解,实际CommonJS并不是由立即执行函数实现的,还要注意最后返回的是 module.exports,如果我们对 module.exports 重新赋值一个对象,那么 exportsmodule.exports 就不指向同一个地方了。

  4. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果