誓要拿下 CommonJS

95 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

誓要拿下 CommonJS

start

  • 今天来说说 CommonJS 规范

背景

早期的 JS 是没有模块化规范的。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

常见的使用 CommonJS 规范的主要有:

  • NodeJS
  • webpack
  • 正是由于 NodeJS 使用的是 CommonJS 规范,导致与之相关的 npm 依赖包大多数都采用了 CommonJS 规范。
  • 所以想要和 npm 依赖包 “打好交道”,熟练掌握 CommonJS 规范,是非常有必要的。

开始

1. CommonJS 规范初体验

/* main.js */
var obj = require('./a')
console.log(obj)

/* a.js  */
exports.a = '123'

/* shell   node ./main.js */
// { a: '123' }

2. 模块包装器

提一个问题: 为什么上述示例的代码可以使用 requireexportsmodule.export不会报错?

2.1 模块包装器

在执行模块的代码之前,Node.js 将使用如下所示的函数包装器来包装它:

;(function (exports, require, module, __filename, __dirname) {
  // Module code actually lives in here
  // 模块代码实际上就在这里
})

通过这样做,NodeJs 实现了一些目标:

  • 用 var,const 或 let 声明的顶级变量,作用域在模块内,而不是全局对象的范围内。
  • 它有助于给模块提供特定的全局变量,例如:
    1. 模块和导出对象,实现者可以使用这些对象从模块导出值。
    2. 方便的变量__filename__dirname,包含模块的绝对文件名和目录路径。

官方文档原文: image.png

可以看到上述的模块包装器,传入了五个变量exports, require, module, __filename, __dirname 这就是为什么我们可以直接在 js 文件中使用 exports, require, module 的原因。因为 NodeJs 执行模块代码之前,会在外层包装一层函数。

__filename, __dirname 这里稍微解释一下:

  • __filename 当前模块的文件名(绝对路径), 例如 q:\growth\blog\a.js
  • __dirname 当前模块的目录名(绝对路径) 例如 q:\growth\blog

3. exports or module.export

CommonJS 的导出,可以通过 exports 或者 module.export;

3.1 exportsmodule.export是什么

直接打印一下

/* main.js */
console.log(module)
console.log(exports)
console.log(module.exports === exports)

/* 执行 */
// node main.js

输出

// Module {
//   id: '.',
//   path: 'q:\\growth\\blog',
//   exports: {},
//   parent: null,
//   filename: 'q:\\growth\\blog\\main.js',
//   loaded: false,
//   children: [],
//   paths: [
//     'q:\\growth\\blog\\node_modules',
//     'q:\\growth\\node_modules',
//     'q:\\node_modules'
//   ]
// }

// {}

// true

总结:

由第 2 小节的知识,我们知道:exports 和 module 是 nodejs 执行我们代码的时候,在模块包装器传入的。

为了了解这两者是什么,我特意打印了一下它们。

  1. module 是一个对象,该对象中有id,path,exports等属性;
  2. exports 也是一个对象;
  3. 默认状态下 module.exports === exportstrue

3.2 为什么默认状态下 module.exports === exports

其实本质上 我们导出数据,是利用 module.exports 来实现的,而 exports 存储的是 module.exports 的引用地址

验证第 4 点

/* main.js */
exports.a = '123'
exports.b = '222'
module.exports = {
  name: '新对象',
}

/* a.js  */
var obj = require('./a')
console.log(obj)

/* shell   node ./main.js */
//  { name: '新对象' }

// 这里就解释了为什么,使用exports导出数据的时候,不能这样使用exports:`exports = { name:"新对象"}`

3.3 exports 带 s 结尾

我们看到 exportss结尾。这里需要和 ES6 的 export 做好区分.

说个我自己用来记忆理解:CommonJS 的输出,默认都是放在一个对象中输出, 对象存储的信息比较多,所以需要加 s 后缀。

4. require

4.1 require 是什么

直接打印一下require

/* main.js */
console.log(require)
console.log(typeof require) // function

/* 执行 */
// node main.js

输出

[Function: require] {
  resolve: [Function: resolve] { paths: [Function: paths] },
  main: Module {
    id: '.',
    path: 'q:\\growth\\blog',
    exports: {},
    parent: null,
    filename: 'q:\\growth\\blog\\main.js',
    loaded: false,
    children: [],
    paths: [
      'q:\\growth\\blog\\node_modules',
      'q:\\growth\\node_modules',
      'q:\\node_modules'
    ]
  },
  extensions: [Object: null prototype] {
    '.js': [Function],
    '.json': [Function],
    '.node': [Function]
  },
  cache: [Object: null prototype] {
    'q:\\growth\\blog\\main.js': Module {
      id: '.',
      path: 'q:\\growth\\blog',
      exports: {},
      parent: null,
      filename: 'q:\\growth\\blog\\main.js',
      loaded: false,
      children: [],
      paths: [Array]
    }
  }
}

function

总结

由上述的代码可以了解到,require 本质是一个函数,其次这个函数上还有一些属性:resolve , main , extensions , cache

这里简单说一下这几个属性的作用:

  1. resolve 使用内部 require()机制来查找模块的位置(不是加载模块,只返回解析的文件名)。
  2. main module 表示 Node.js 进程启动时加载的入口脚本的对象。
  3. extensions 指导 require 如何处理某些文件扩展名。
  4. cache 缓存已经导入的内容。

4.2 require 有什么用

作用:用于导入模块、JSON 和本地文件

4.3 require 文件加载流程

主要有三种

  • 核心模块
  • 文件模块
  • 第三方模块

4.3.1 核心模块

// 例如导入 NodeJS内置的核心模块: fs
var fs = require('fs')

4.3.2 文件模块

// 相对路径
var a = require('./a.js')

// 绝对路径
var b = require('q:/growth/blog/.js')

4.3.3 第三方模块

nodejs 是如何寻找我们的第三方模块依赖呢

例如:

var express = require('express')

/* 1. 首先在当前文件目录下寻找node_modules */

/* 2. 如果当前目录没有找到,在父级目录的 node_modules 查找 */

/* 4. 如果父级也没有找到,就寻找父级的父级,沿着路径向上递归,直到根目录下的 node_modules 目录 */

/* 4. 在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有  package.json ,在 node 环境下会以此查找 index.js ,index.json ,index.node  */

4.4 require 加载文件的顺序

main.js

console.log('main开始执行啦')
var say = require('./a.js')
var edit = require('./b.js')
exports.a = 1
console.log('main结束了')

a.js

console.log('a开始执行啦')
module.exports = function say() {
  console.log('开始说话')
}
var edit = require('./b.js')
console.log(edit)
console.log('a结束了')

b.js

console.log('b开始执行啦')
exports.edit = function () {
  console.log('开始编辑')
}
console.log('b结束了')

输出

/* 
main开始执行啦
a开始执行啦
b开始执行啦
b结束了
{ edit: [Function] }
a结束了
main结束了
*/

解释

  1. 从上向下依次加载;
  2. 执行到 require,优先加载引入的文件;

按顺序加载,深度优先加载,重复引入会跳过。

4.5 重复加载

4.4 案例有体现,当重复 require('./b.js'),只会执行一次。

4.6 动态加载

/* main.js */
if (true) {
  var obj = require('./a.js')
  console.log(obj)
}

/* a.js */
module.exports = function say() {
  console.log('开始说话')
}

/* node ./main.js */
// [Function: say]

可以按条件加载

4.7 导入对象和导出对象之间的关系

先来一个案例

/* main.js */
var obj = require('./a.js')

console.log(obj)
/* 
步骤一. 首先打印引入的obj
{
  num: 2,
  outObj: { count: 6 },
  say: [Function: say],
  add: [Function: add]
}
*/

obj.num = 80
obj.outObj.count = 80
console.log(obj)
/* 
步骤二. 手动修改obj的值,再打印一下obj

{
  num: 80,
  outObj: { count: 80 },
  say: [Function: say],
  add: [Function: add]
}
*/

obj.say()
/* 
步骤三. 第二步修改了导入的obj,查看一下原本的obj,发现基础类型的没有更改,复杂类型的发生了改变
2 { count: 80 }

*/
/* a.js */
let num = 2
let outObj = {
  count: 6,
}

function say() {
  console.log(num, outObj)
}

function add() {
  num++
  outObj.count++
}

module.exports = {
  num,
  outObj,
  say,
  add,
}

看上述的案例我们发现,模块导出的对象 A 和模块导入的对象 B,是有一定的关系的。

具体的关系是什么?

对象 A 和对象 B 两者, 简单理解:

1.基础类型的数据是完全复制一份。 2.复杂类型的数据是复制的引用地址。

可以理解为浅拷贝。 这里顺便提一下 ES Module 的导入导出数据之间的关系,ES Module 无论是基础类型还是复杂类型的数据,都是链接引用。也就是说,导出模块的数据发生改变,导入的数据也会发生改变。

end

  • 写到这里基本就写完啦,加油!