js 模块化之 commonjs

时间:2021-01-06 12:19:47   收藏:0   阅读:0

在最初 js 被设计用来做一些表单校验的简单功能,当初的 js 只是用来作为页面展示的一个补充。后来随着 web 的发展,相当一部分业务逻辑前置到了前端进行处理,js 的地位越来越重要,文件也越来越庞大,为了将大的功能模块进行拆分成一个一个小的组成部分,但是拆分成小的 js 文件又带来了新的挑战,由于 js 的加载和执行顺序在引入的时候就已经决定了,这样就需要充分考虑到各变量的作用范围以及各变量之间的依赖关系。

<html>
  <body>
    <script src="a.js"></script>
    <script src="b.js"></script>
    <script src="c.js"></script>
  </body>
</html>

就像上面这样,a.js 会最先被执行,这样如果在 b.js 中存在着与 a.js 同名的变量,就会发生覆盖。同时如果在 c.js 中有使用到 a.js 声明的变量就决定了 a.js 必须在 c.js 上面被引入。这样就存在这一种耦合,为了解决这一类问题 js 的模块化应运而生。

commonjs

commonjs 随着 nodejs 的诞生而面世,主要是用来解决服务端模块化的问题,commonjs 对模块做了如下规定

node 中的 commonjs 模块

node 中一个文件就是一个模块,各模块之间的变量是无法互相访问到的。

// a.js
const a = 1;
// b.js
const b = 2;

console.log(a); // ReferenceError: a is not defined

b.js 中无法访问到变量 a,如果需要使用 a 需要先导入模块

// b.js
const a = require(‘./a.js‘);

console.log(a) // {}

这里还是无法访问到 a 变量是因为模块 a 中没有导出对应的变量

// a.js

const a = 1

exports.a = a

// b.js

const a = require(‘./a.js‘);

console.log(a); // 1

node 模块中的 module 对象

modulenode 中的一个内置对象,是 Module 类的一个实例, module 对象上有几个重要属性

node 模块中的 require 方法

requirenode 模块中的内置方法,该方法用于导入模块,其函数签名如下:

interface require {
  /**
   * id  模块的名称或路径
   */
  (id: string): any
}

require 方法上有几个比较重要的属性和方法

require 本身的用法

require 可以通过传入 string 类型的 id 作为入参,id 可以是一个文件路径或者是一个模块名称,路径可以是一个相对路径(以 ./ 或者 ../ 开头)或者是一个绝对路径(以 / 开头)。相对路径的方式比较简单,会以当前文件的 __dirname 作为基础路径计算出绝对路径,无论是相对路径还是绝对路径都可以是文件或者文件夹。

i. 文件加载规则

LOAD_AS_FILE(X)

LOAD_AS_FILE
1. 是否存在 X 文件,是则优先加载 X
2. 否则会加载 X.js 
3. 否则会加载 X.json
4. 否则会加载 X.node

ii. 文件夹加载规则

LOAD_AS_DIRECTORY(X)

LOAD_AS_DIRECTORY
1. 是否存在 `X/package.json`,是则继续
    a. `package.json` 是否有 `main` 字段,无则执行 2,是则执行 b
    b. 加载 `(X + main)` 文件,规则: `LOAD_AS_FILE(X + main)` ,无则继续执行 c
    c. 加载 `(X + main)/index`,规则: `LOAD_AS_FILE((X + main)/index)`,无则抛出错误
2. 否则会执行去查找 `X/index`,规则: `LOAD_AS_FILE(X/index)`

iii. 模块名称加载规则

id 作为模块名称会遵守如下优先级规则进行模块查找:

  1. 加载内置模块
  2. 加载当前目录下 node_modules 文件夹中的模块
  3. 加载父级目录下 node_modules 文件夹中的模块,一直到最顶层

模块缓存

模块在第一次被加载之后会缓存,多次调用同一个模块只会让模块执行一次。

// a.js
module.exports = {
  name: ‘张三‘
}

// b.js
require(‘./a.js‘) // {name: ‘张三‘}
require(‘./a.js‘).age = 18
require(‘./a.js‘) // {name: ‘张三‘, age: 18}

最后一个 require(‘./a.js‘) 会输出 {name: ‘张三‘, age: 18} 则说明 a.js 模块只执行了一次,返回的还是最早被缓存的对象。如果要强制重新执行被引用的模块代码,可以通过删除缓存的方式

// a.js
module.exports = {
  name: ‘张三‘
}

// b.js
require(‘./a.js‘) // {name: ‘张三‘}
require(‘./a.js‘).age = 18
require(‘./a.js‘) // {name: ‘张三‘, age: 18}
delete require.cache[require.resolve(‘./a‘)]
require(‘./a.js‘) // {name: ‘张三‘}

上面的例子还能说明模块的缓存是基于文件路径进行的,只要在被加载时路径不一致同一个模块也会执行两次

循环依赖

要说弄清楚这个问题需要先了解 node 中模块加载机制,在 commonjs 模块体系中 require 加载的是一个对象的副本,实际也就是 module.exports 所指向的变量,所以除非是存在引用类型的变量否则模块内部的变化是影响不到外部的。举个例子说明这个:

// b.js
let count = 1

let countObj = {
  count: 10
}

module.exports = {
  count,
  countObj,
  setCount(newVal) {
    count = newVal
  },
  setCountObj(newVal) {
    countObj.count = newVal
  }
}

// a.js
const moduleB = require(‘./b.js‘)

console.log(moduleB.count) // 1
moduleB.setCount(2)
console.log(moduleB.count) // 1

console.log(moduleB.countObj.count) // 10
moduleB.setCountObj(20)
console.log(moduleB.countObj.count) // 20

上面的例子说明了 require 的结果实际是 module.exports 的一个副本,按照这样的思路循环加载的情况下,也就会读取已经存在 module.exports 上的属性,如果还存在部分属性未挂在到 module.exports 上则会读取不到。

// a.js
console.log(‘a 开始‘);
exports.done = false;
const b = require(‘./b.js‘);
console.log(‘在 a 中,b.done = %j‘, b.done);
exports.done = true;
console.log(‘a 结束‘);

// b.js
console.log(‘b 开始‘);
exports.done = false;
const a = require(‘./a.js‘);
console.log(‘在 b 中,a.done = %j‘, a.done);
exports.done = true;
console.log(‘b 结束‘);

// main.js
console.log(‘main 开始‘);
const a = require(‘./a.js‘);
const b = require(‘./b.js‘);
console.log(‘在 main 中,a.done=%j,b.done=%j‘, a.done, b.done);

main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

当 main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:

main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true
评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!