3、模块化

什么是模块

解决的问题

CommonJS和ES6Modules

CommonJS和 ES6 模块系统是 JavaScript 中两种主要的模块处理方式。它们在语法、加载方式和使用场景上都有显著的区别。

Node.js 中的每个文件都可以作为一个模块,默认处于独立的作用域内。

Node.js 支持以下几种模块:

package.json中type属性

  1. type字段的产生用于定义package.json文件和该文件所在目录根目录中.js文件和无拓展名文件的处理方式。值为'moduel'则当作es模块处理;值为commonjs则被当作CommonJs模块处理
  2. 目前Node默认的是如果pacakage.json没有定义type字段,则按照CommonJs规范处理
  3. Node官方建议包的开发者明确指定package.jsontype字段的值
  4. 无论package.json中的type字段为何值,.mjs的文件都按照es模块来处理,.cjs的文件都按照CommonJs模块来处理

CommonJS和ES6 模块化的区别

CommonJS 和 ES6 模块系统各有优缺点,选择哪种模块系统取决于你的项目需求和运行环境。对于新的前端项目,推荐使用 ES6 模块,因为它是 JavaScript 的官方标准,并且支持更好的优化。对于现有的 Node.js 项目,CommonJS 仍然是一个可靠的选择。

CommonJS

CommonJs主要为了解决ES6之前无模块概念的问题

引入核心模块不需要写路径,引入文件模块需要写路径(一般是相对路径./../开头)

批量导出与导入

other.js

var msg = 'this is a model';
function myFunc(){
    console.log('this is a function by model');
}

// 批量导出
module.exports = {
    msg,
    myFunc,
    name: 'my name'
}

index.js

// 变量名字可以随便起,一般为文件名
// 引入同目录下的model.js,如果是相对路径,必须以./开头
// 此处的model.js的.js可以省略
var other = require('./other')

other.myFunc();
console.log(other.name);
console.log(other.msg);

逐一导出与选择性导入

other.js

var msg = 'this is a model';
function myFunc(){
    console.log('this is a function by model');
}

// 逐一导出
exports.msg = msg;
exports.myFunc = myFunc;
exports.name = 'my name'

index.js

// 无论是批量导出还是逐一导出都可以如下选择性导入
var {myFunc,msg,name} = require('./other')

myFunc();
console.log(name);
console.log(msg);

区别

ES6Modules

export

other.js

export var  msg = 'this is a model';

export function myFunc(){
    console.log('this is a function by model');
}

index.js

// 导入方式一:导入的多项组成一个对象
import * as other from './other.js'

other.myFunc();
console.log(other.msg);

// 导入方式二:选择性导入
import {myFunc,msg} from './other.js'

myFunc();
console.log(msg);

export default

other.js

var msg = 'this is a model';
function myFunc(){
    console.log('this is a function by model');
}

// 批量导出,一个模块中只能声明一个 export default
export default {
    msg,
    myFunc,
    name: 'my name'
}

index.js

import other from './other.js'

other.myFunc();
console.log(other.msg);
console.log(other.name);

注意:export default导出的不可以使用import {}解构导入!

导入模块

导入内置模块

内置模块是由 Node.js 自带的模块,在安装 Node.js 时就已经包含在环境中,因此无需额外安装。常见的内置模块包括 fs、http、path、os、crypto 等。

导入内置模块只需要使用require()函数并传入模块名称即可。例如,要导入文件系统模块 fspathhttp,可以这样做:

// 导入文件系统模块
const fs = require('fs');

// 导入路径模块
const path = require('path');

// 导入 HTTP 模块
const http = require('http');

导入第三方模块

第三方模块是开发者或开源社区发布的模块,可以通过 npm(Node 包管理器)安装到项目中,常见的第三方模块有 express、lodash、axios 等。

第三方模块安装后,这些模块会被放置在项目的 node_modules 目录下。要导入一个第三方模块,同样使用 require() 函数,但传入的是模块的名称。例如,要导入 express 框架,可以这样做:

# 在导入第三方模块之前,确保已经通过 npm 安装了该模块。
npm install express

然后导入

// 导入第三方模块 express
const express = require('express');

导入自定义模块

按照导出方式的不同(CommonJS或ES6Modules),使用绝对或相对路径进行导入即可。

加载模块的路径解析

**1、核心模块:**如 http 和 fs,在 Node.js 安装时就包含,可以直接加载。

const http = require('http');

**2、本地文件模块:**使用相对路径或绝对路径加载,需指定 .//

const myModule = require('./myModule');

**3、第三方模块:**位于 node_modules 目录中,只需输入模块名称即可加载。

const express = require('express');

模块缓存

**模块缓存机制:**Node.js 会将已加载的模块缓存起来,以提高性能。再次 require() 同一模块时,直接返回缓存中的模块,而不是重新加载。

**刷新缓存:**要重新加载模块,可以删除缓存:

delete require.cache[require.resolve('./myModule')];

循环依赖

当两个或多个模块相互导入时,称为循环依赖。Node.js 能处理简单的循环依赖,但可能导致部分模块导出的对象未完全初始化。

// a.js
const b = require('./b');
console.log('a.js:', b.message);
module.exports = { message: 'Hello from a' };

// b.js
const a = require('./a');
console.log('b.js:', a.message);
module.exports = { message: 'Hello from b' };

// main.js
require('./a');


// b.js: undefined
// a.js: Hello from b
// (node:25426) Warning: Accessing non-existent property 'message' of module exports inside circular dependency
// (Use `node --trace-warnings ...` to show where the warning was created)

b.js 导入 a.js 时,由于 a.js 尚未完全执行,a.messageundefined

模块包装与作用域

Node.js 会将每个模块包装在一个函数中,使得每个模块都有独立的作用域。

// Node.js 内部将模块包装为类似这样的函数:
(function(exports, require, module, __filename, __dirname) {
  // 模块代码在这里
});

这意味着在模块中定义的变量不会污染全局作用域。

模块的加载

img

1、从文件模块缓存中加载

尽管原生模块与文件模块的优先级不同,但是都会优先从文件模块的缓存中加载已经存在的模块。

2、从原生模块加载

原生模块的优先级仅次于文件模块缓存的优先级。require 方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个 httphttp.jshttp.nodehttp.json 文件,require("http") 都不会从这些文件中加载,而是从原生模块中加载。

原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

3、从文件加载

当文件模块缓存中不存在,而且不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件。