node学习笔记(二)

模块化

  • 模块化开发最终的目的是将程序划分成一个个小的结构;
  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;
  • 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
  • 也可以通过某种方式,导入另外结构中的变量、函数、对象等;

历史

js是作为一门简单的脚本语言诞生的,早期的页面很简单,在html页面使用js代码使用script标签即可。

但是随着前端任务量的逐渐增加,js变得越来越复杂,所以js急需一个模块化的方案。

在ESModule方案以及社区的CommonJS未推出之前,开发者使用立即执行函数来使用不同函数作用域下的同名变量。

立即执行函数的弊端

  • 我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
  • 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
  • 在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;

模块化方案

CommonJS(CJS)

CommonJS(原名ServerJS)属于社区提出的一种模块化的方案。

其中,Nodejs是CommonJS在服务端的一个代表性的实现;Browserify(已过时)是CommonJS在浏览器中的一种实现;

webpack打包工具具备对CommonJS的支持和转换;

接下来,基于Node对CommonJS进行简单总结:

在Node中每一个js文件都是一个单独的模块;
文个模块中包括CommonJS规范的核心变量: exports、module.exports、require;

exports module.exports require

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.js
let num = 10;
let num2 = 20;

module.exports = {
num, num2
}
// b.js
const a_data = require("./a");
// a_data指向a.js的exports引用
console.log(a_data);
// { num: 10, num2: 20, fn: [Function: fn] }

// 按需引入,
const { num, num2, fn } = require('./a');
console.log(num, num2, fn);
// 10 20 [Function: fn]

exports和module.exports指向同一个引用:

1
2
console.log(module.exports === exports);
// true

所以,a.js文件的导入也可以是这样:

1
2
3
4
5
6
exports.num = 10;
exports.num2 = 20;
exports.fn = function (params) {
console.log('function fn');
}

但是,如果在其之后加上module.exports语句,则会被覆盖(因为指向同一个引用):

1
2
3
4
5
6
7
8
9
exports.num = 10;
exports.num2 = 20;
exports.fn = function (params) {
console.log('function fn');
}

module.exports = {
num3: 10
}
1
2
3
4
// b.js
const { num, num2, fn, num3 } = require('./a');
console.log(num, num2, fn, num3);
// undefined undefined undefined 10

说明:exports是属于规范中的,module.exports属于Node的实现的。

CommonJS中是没有module.exportst的

但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;

所以在Node中真正用于导出的其实根本不是exports,而是module.exports;

因为module才是导出的真正实现者;

require

本质:引用赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a.js
let num1 = 20;
exports.num1 = num1
setTimeout(() => {
exports.num1 = 30;
}, 1000)
// b.js
const a_data = require("./a");
console.log(a_data);
setTimeout(() => {
console.log(a_data);
}, 2000)
// { num1: 20 }
// two second later
// { num1: 30 }


// a_data和a.js的exports指向同一个引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在b.js引入变量后,修改a_data的值,在a.js中使用
// 再次验证a_data和a.js的exports对象指向同一个引用
// a.js
let num1 = 20;

exports.num1 = num1
setTimeout(() => {
console.log(exports.num1);
}, 2000)
// b.js
const a_data = require("./a");
console.log(a_data);
setTimeout(() => {
a_data.num1 = 100
}, 1000)
// { num1: 20 }
// two second later
// 100

require查找细节

1
const a_data = require('X');

情况一:如果X是node的内置模块,则直接返回;

情况二:如果X是相对或者绝对路径:

第一步:将X当做一个文件在对应的目录下查找;

  1. 如果有后缀名,按照后缀名的格式查找对应的
  2. 如果没有后缀名,会按照如下顺序:
    1. 直接查找文件X
    2. 查找X.js文件
    3. 查找X.json文件
    4. 查找X.node文件

第二步:没有找到对应的文件,将X作为一个目录查找目录下面的index文件

  1. 查找X/index.js文件
  2. 查找X/index.json文件
  3. 查找X/index.node文件

如果没找到,报错

情况三:不是路径,不是核心模块,则从node_modules相关的模块中package.json中定义的入口文件(main属性,如果没有指定,默认是index.js文件)中寻找。

如果没有,去上层目录的node_modules文件夹中寻找,会一直往上,一直到根目录都没有找到,报错。

模块的加载过程

模块在被第一次引入时,模块中的js代码会被运行一次(即导入的文件按遇到require语句,发生阻塞

模块被多次引入时,会缓存,最终只加载(运行)一次

  • 这是因为每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载;

循环引入如何解决?

循环引入图示

Node的依赖引用采用的图遍历的深度优先算法

上图的引入顺序:main–>a–>d–>c–>b

弊端

  • 变量的导入者可以修改导出者导出的变量(大忌);
  • require引入是同步加载,即:在文件A引入文件B,文件B的所有代码(包括require,业务逻辑操作均会被执行一次);若遇到一个文件存在大量的业务逻辑操作,该文件被其他文件引入,会造成阻塞。
    • 这也是浏览器不实现CommonJS的一个原因。
  • 虽然在webpack仍然可以使用CommonJS和ESModule进行开发,这是因为webpack将CommonJS进行了转化,变成浏览器可以执行的代码。

AMD

AMD(Asynchronous Module Definition):采用异步加载模块

AMD规范早于CommonJS,现在已经不常用了。

AMD实现相关的库:require.js和curl.js

CMD

CMD(Common Module Definition)

采用异步加载模块,继承了AMD的优点,也不常用了。

相关的库:SeaJS

ESModule

ES2015官方推出的模块化方案。

导入者不能修改导出者导出的变量

浏览器使用ESModule,需要给script标签加上type属性,同时需要开启本地服务(live server插件)

1
<script src="./b.js" type="module"></script>
1
2
3
4
5
6
7
8
9
10
11
12
// a.js
let a = 10;
let b = 20
// 区分ES6对象的增强语法,此处不是
export {
a,
b
}
// b.js
// 浏览器是同ESModule,引入的文件必须加后缀名
import { a, b } from './a.js'
console.log(a, b);

导出起别名

1
2
3
4
5
6
7
// 导出时使用别名
// 导出
export {
a as $a
}
// 导入
import { $a } from './a.js'

导入起别名

1
2
3
4
5
6
7
8
9
10
11
// 导入时使用别名
// 导出
let name = 'test'
let Person = class { }
export {
name, Person
}
// 导入
import { name as a_name, Person as a_Person } from './a.js'
console.log(a_name, a_Person);

整体导入

1
2
3
4
5
6
7
8
9
10
// 导出
let name = 'test'
let Person = class { }
export {
name, Person
}
// 整体导入
import * as a_data from './a.js'
console.log(a_data.name);
console.log(a_data.Person);

导出导入可以同时起别名

1
2
3
4
5
6
7
8
9
10
11
// 导出
let name = 'test'
let Person = class { }
export {
name as $name, Person as $Person
}
// 导入
import { $name as name, $Person as Person } from './a.js'

console.log(name);
console.log(Person);

定义时导出

1
2
3
4
5
6
7
8
9
10
// 此导出无法定义别名
export const name = 'test'
// 报错
// export const name as $name = 'test'
export class Person { }

import { name, Person } from './a.js';
// 但是可以导入时使用别名
// import { name as a_name, Person as a_Person } from './a.js'
console.log(name, Person);

默认导出(default)

1
2
3
4
5
6
7
8
export default {
name: 'test',
fn: function () { }
}
// 导入
// 导入的名称可自定义,且不带花括号
import a_data from './a.js'
console.log(a_data);

注意:在一个文件(模块)中,只能有一个默认导出。

import和export同时使用

新建统一的出口文件index.js

1
2
3
4
5
6
7
8
9
10
11
12
// b.js
let b = 'test';
export {
b
}
// index.js
export { b } from './b.js'
// export * from './b.js'


// 其他文件从index.js导入使用
import { b } from './index.js'

import函数

import和export只能在文件(模块)的顶层使用

原因:

  • 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
  • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
  • 甚至拼接路径的写法也是错误的:因为我们必须到运行时能确定path的值;

如果想要在逻辑操作满足一定条件时再导入,可使用import函数,这个import函数返回一个promise

1
2
3
4
5
6
7
8
9
10
11
// a.js
let a = 'a_str'
export {
a
}
// b.js
if (true) {
import('./a.js').then((res) => {
console.log(res.a);
})
}

应用场景:VUE组件异步加载

import meta

import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL;
在ES11 (ES2020)中新增的特性;

1
2
3
//b.js
console.log(import.meta);
// {url: 'http://127.0.0.1:5500/test/b.js'}

ESModule的解析流程

  • 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record) ;
  • 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
  • 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;

ESModule的解析流程