Node.js 基础(二):模块

内容目录

查看「JavaScript 和它的朋友们专题」获取更多相关内容


模块

模块是一种封装代码的方式,它允许将代码组织成独立的单元,每个单元可以独立地被重用和分发。模块化是现代编程中的一个核心概念,它有助于避免命名空间的冲突,并提高代码的可维护性。

你可能知道「IIFE」(立即调用函数表达式):

(function() {
  // 函数体
})();

它是一个匿名函数并用 () 包裹起来将其转为函数表达式,最后再加 () 直接调用它。在早期,人们普遍使用「IIFE」创建私有作用域,避免变量污染全局命名空间。

虽然早期人们使用 IIFE 的特性来实现避免变量污染,但如今而模块则可以更好地解决这个问题,并且提供了更强大的功能。如提供了显式的导入导出机制、作用域隔离、静态解析等功能,使代码更易维护和优化。

Node.js 的模块类型

在 Node.js 中,有几种类型的模块:

  • 内置模块:由 Node.js 提供的如 httpfs
  • 用户创建的模块:开发者自己编写的模块,可以是单个文件或多个文件的集合;
  • 第三方模块:由其他开发者创建并发布的模块,可以通过 npm 安装;

模块格式

有两种模块格式,简称 CJS 的 CommonJS 模块和简称 ESM 的 ES6 模块。

在 ES6(ECMAScript 2015)中引入了新的模块系统,与旧的 CommonJS 模块系统相比,ES6 模块系统提供了更清晰和一致的语法。但由于其标准定型较晚所以 Node.js 早期只能使用 CommonJS 模块标准,且 CommonJS 规范是为浏览器外的环境设计的,主要用于服务器编程,所以更加关注加载文件、同步加载等方式。而 ES6 模块则是为浏览器环境设计的,更加关注网络加载、异步加载等方式。随着时间推移,ES6 模块规范逐渐确定并得到广泛应用 Node.js 也开始支持 ES6 模块,尽管目前两种模块系统依然并存(目前默认使用 CommonJS 模块),但 ES6 模块被认为是未来的发展方向。

此处以在入口文件 index.js 调用位于 app.js 文件里的加法函数为例,简单介绍下两种格式的写法。

CommonJS 模块

// app.js
// 导出加法函数
module.exports = function add(x, y) {
  return x + y;
};
// index.js
// 导入加法函数
const add = require('./app');

// 使用加法函数
const sum = add(5, 3);
console.log(sum); // 输出: 8

在 CommonJS 模块中,使用 module.exports 对象来导出函数,以及使用 require() 函数来导入获取导出的函数。

ES6 模块

Node.js 要求 ES6 模块采用 .mjs 后缀文件名,或者在 package.json 文件中添加一个 type 字段,值为 module

{
  "name": "node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
// app.js
// 导出加法函数
export function add(x, y) {
  return x + y;
}
// index.js
// 导入加法函数
import { add } from './app.js';

// 使用加法函数
const sum = add(5, 3);
console.log(sum); // 输出: 8

在 ES6 模块中使用 export 关键字来导出函数,以及使用 import 关键字和解构赋值来导入获取导出的函数。并且,在导入时需要写完整文件名的后缀。

默认导出

在 ES6 模块里使用 import 关键字需要使用花括号通过解构赋值的方式来导入,除非是使用默认导出。

什么是默认导出?先来看一个例子,这是一个导出多个函数的写法:

// math.js
let add = function(x, y) {
  return x + y;
}

let multiply = function(x, y) {
  return x * y;
}
// 导出多个
export { add, multiply };
// index.js
import { add, multiply } from './math.js';

console.log(add(1, 2)); // 输出 3
console.log(multiply(3, 4)); // 输出 12

但如果我只想导出 add 这个函数,可以写成:

export default add;

如此,在导入时可以不用花括号并且自定义变量名称,如:

import myAdd from './math.js';

两种模块的区别

  • ES 模块是 JavaScript 的标准,而目前在 Node.js 中默认使用 CommonJS 模块;
  • ES6 模块不能使用 require() 加载(require() 是同步加载),另外在加载时需要显示写明文件的后缀名;
  • CommonJS 模块不能使用 import 关键字加载(import 是异步加载);
  • CommonJS 导入在运行时动态解析,而 ES6 模块导入是静态的,在解析时执行;
  • .cjs 文件会以 CommonJS 模块加载,另外你可能会想知道 CommonJS 模块在 package.json 文件中的 type 字段的值为 commonjs
  • .mjs 后缀名的文件总会以 ES6 模块加载,如果不想使用 .mjs 后缀名可以在 package.json 文件中添加一个值为 moduletype 字段:
{
  "name": "node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

使用 index.js 模式聚合模块

例如有一个 module 文件夹里有多个模块文件,可以使用 index.js 模式聚合模块一起导出:

// modules/add.js
export function add(x, y) {
  return x + y;
}

// modules/multiply.js
export function multiply(x, y) {
  return x * y;
}

// modules/index.js
export { add } from './add.js';
export { multiply } from './multiply.js';

然后例如在入口文件 index.js 中导入:

// index.js
import * as modules from './modules/index.js';

console.log(modules.add(2, 3)); // Output: 5
console.log(modules.multiply(2, 3)); // Output: 6

导入内置模块

以内置的用于文件系统操作的 fs 模块为例:

const fs = require('fs');

// ES6 模块语法,从 Node.js v13 开始:
import fs from 'fs';

// 从 Node.js v18 开始:
import fs from 'node:fs'

与导入我们自己编写的模块相比,导入内置模块不需要填写完整路径。

从 Node.js v18 开始使用 node: 前缀明确导入核心内部模块,这不会改变导入的行为,它只是一种明确该模块是核心模块而不是第三方模块的方法。

npm 与第三方模块

现在大致了解了自己编写的模块和内置模块的使用方法,也使用了 npm 创建了一个 Node.js 项目,但还没体验到 npm 作为 Node.js 的官方软件包管理工具的强大之处,其主要用于管理第三方模块及其依赖关系。

第三方模块是由社区开发者编写的可重用代码包。它们可以提供各种功能从而缩短开发时间并提高开发效率。这些模块通常发布到 NPM 仓库 中,上面包含了成千上万个第三方模块,供其他开发者下载和使用。

这里以安装 express为例,在一个 Node.js 项目里使用命令:

# 安装 express
npm install express

安装后可以看到项目里多了一个 node_modules 目录:

.
├── index.js
├── node_modules
├── package.json
└── package-lock.json

第三方模块都下载到 node_modules 目录里。

通过 package.json 安装依赖

除了 node_modules 目录,还多出一个 package-lock.json 文件,以及 package.json 文件也会进行修改:

{
  "name": "node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  }
}

多了一项 dependencies 字段,表示该项目用到依赖极其版本号。

package-lock.json 文件是 NPM 5 引入的一个新特性,它的主要作用是锁定安装的依赖包的版本,以确保在不同环境下重新安装依赖包时,能够获得完全相同的依赖树。这个文件会被自动生成,它会记录 node_modules 中实际安装的依赖包版本信息。

在使用 npm install 而不带包名的时候,就会读取这两个 json 文件来安装依赖,也就说这个项目会用到什么第三方模块只要读取配置文件即可,所以在实际开发中通常将 node_modules 文件夹添加到 .gitignore 中以避免将第三方模块进行提交,且 package-lock.json 文件也保障了在团队成员或不同环境之间获得完全一致的依赖包版本,避免了「它在我的机器上工作正常」的问题,提高了项目的可重复性和可维护性。

导入第三方模块

通过阅读每个模块的仓库页面说明可以得知使用方法,如 express 介绍了导入及使用示例:

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World')
})

app.listen(3000)

阅读项目说明及文档是个好习惯 🙂

npm 命令选项

npm 命令还有一些其他选项:

# npm install express 可以简写成
npm i express

# 如果下载的包不止用于当前项目,可以使用全局安装选项
npm install --global express
npm install -g express # --global 也可以缩写成 -g

# 卸载一个包
npm uninstall express

全局安装不带 -g 选项时 npm install 会将模块安装到当前项目的 node_modules 文件夹中,而使用 -g 选项时 npm 会将模块安装到计算机的全局目录中。全局安装的模块可以在系统的任何位置直接运行,而本地安装的模块只能在当前项目目录下运行。

修改 npm Registry

在如中国大陆地区你很可能会遇上网络问题导致第三方模块难以下载,这时候就可能会想要使用一些 CDN 镜像地址。

有些朋友可能已经知道并使用如命令 npm config set registry.npmrc 文件的方法来使用第三方源(Registry),但这里推荐一个第三方工具如 nnrm,可以快速设置第三方源

它是 nrm 的改良版

# 安装
npm install -g nnrm

# 查看可选第三方源
nnrm ls
We will create '/home/toor/.nnrm/registries.json' to record your custom registries.

 * npm -------- https://registry.npmjs.org/
   yarn ------- https://registry.yarnpkg.com/
   taobao ----- https://registry.npmmirror.com/
   tencent ---- https://mirrors.cloud.tencent.com/npm/
   npmMirror -- https://skimdb.npmjs.com/registry/
   github ----- https://npm.pkg.github.com/

然后使用命令来选择一个第三方源,如淘宝源:nrm use taobao

npm 替代品

npm 本身也是不是尽善尽美,所以也有一些第三方的工具涌现:

  • yarn:由 Meta 公司开发的,主要解决 npm 在安装速度、版本控制等问题上的痛点;
  • pnpm:主要解决 npm 安装速度、磁盘占用(通过内容可寻址存储的方式解决)等问题上的痛点;

cnpm 已经过时淘汰,此处就不多做介绍了

当然也不是说 npm 就是一成不变了,随着发展 npm 也解决了一些第三方工具解决的问题,所以到底要不要使用第三方工具,使用哪个第三方工具取决于你和你的团队

您可能也会喜欢