什么是模块 #
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
- 在node中,一个js文件就是一个模块,每个模块单独运行在一个函数中,所以一个js文件中的变量和函数的作用域不是全局的,如果需要在其他js中使用,那么需要使用引入
- node中模块分为两大类:核心模块(node提供或npm下载的,例如fs);文件模块(自己写的模块)
解决的问题 #
- 请求过多
- 首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多
- 依赖模糊
- 我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。
- 难以维护
- 以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。
CommonJS和ES6Modules #
CommonJS和 ES6 模块系统是 JavaScript 中两种主要的模块处理方式。它们在语法、加载方式和使用场景上都有显著的区别。
Node.js 中的每个文件都可以作为一个模块,默认处于独立的作用域内。
Node.js 支持以下几种模块:
- 内置模块:Node.js 自带的模块,如
fs、http、path等。 - 用户自定义模块:由开发者创建的模块。
- 第三方模块:通过 npm 安装的模块,如
express、lodash等。
package.json中type属性 #
type字段的产生用于定义package.json文件和该文件所在目录根目录中.js文件和无拓展名文件的处理方式。值为'moduel'则当作es模块处理;值为commonjs则被当作CommonJs模块处理- 目前Node默认的是如果
pacakage.json没有定义type字段,则按照CommonJs规范处理 - Node官方建议包的开发者明确指定
package.json中type字段的值 - 无论
package.json中的type字段为何值,.mjs的文件都按照es模块来处理,.cjs的文件都按照CommonJs模块来处理
CommonJS和ES6 模块化的区别 #
- 语法
- CommonJS 使用
module.exports和require。 - ES6 模块使用
export和import。
- CommonJS 使用
- 加载方式
- CommonJS 是同步加载,适用于服务器端。
- ES6 模块是静态加载,适用于浏览器和服务器端。
- 动态 vs 静态
- CommonJS 支持动态加载,可以在代码运行时决定加载哪些模块。
- ES6 模块是静态的,编译时就确定了模块依赖关系。
- 优化
- ES6 模块支持树摇优化,可以在打包时移除未使用的代码。
- CommonJS 不支持树摇优化。
- 作用域
- CommonJS 模块在加载时执行,模块的顶层作用域是模块自身。
- ES6 模块在导入时不会执行,模块的顶层作用域是模块自身。
- 兼容性
- CommonJS:主要用于 Node.js 环境,广泛支持现有的 Node.js 生态系统。
- ES6 模块:现代浏览器和 Node.js(v12+)都支持 ES6 模块,但在某些旧环境中可能需要使用 Babel 等工具进行转译。
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);
区别 #
- 二者的作用是一样的
- 二者不可以同时存在,如果同时存在,只有
module.exports生效
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()函数并传入模块名称即可。例如,要导入文件系统模块 fs、path、http,可以这样做:
// 导入文件系统模块
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.message 为 undefined
模块包装与作用域 #
Node.js 会将每个模块包装在一个函数中,使得每个模块都有独立的作用域。
// Node.js 内部将模块包装为类似这样的函数:
(function(exports, require, module, __filename, __dirname) {
// 模块代码在这里
});
这意味着在模块中定义的变量不会污染全局作用域。
模块的加载 #
1、从文件模块缓存中加载 #
尽管原生模块与文件模块的优先级不同,但是都会优先从文件模块的缓存中加载已经存在的模块。
2、从原生模块加载 #
原生模块的优先级仅次于文件模块缓存的优先级。require 方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个 http、http.js、http.node、http.json 文件,require("http") 都不会从这些文件中加载,而是从原生模块中加载。
原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。
3、从文件加载 #
当文件模块缓存中不存在,而且不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件。