Node.js 基础(一)

内容纲要

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


进程和环境变量

Node.js 是一个运行时环境,它允许 JavaScript 访问不同于浏览器环境中的全局变量和功能,这是因为 Node.js 运行在操作系统环境中。

进程

操作系统环境提供了许多功能,其中最强大的是进程(process)。通过全局的 process 对象我们可以访问关于操作系统的多种信息。例如可以简单地通过 console.log(process) 来查看这些信息,但这可能会暴露敏感数据,因此实际使用时需要谨慎。

process 对象的一个关键属性是 argv,它允许你获取 node 命令之后传递的所有内容。例如我使用命令运行 node index.js hi javascript

// index.js
console.log(process);
[
  '/home/toor/.nvm/versions/node/v20.12.2/bin/node',
  '/home/toor/Web/index.js',
  'hi',
  'javascript'
]

这将返回一个数组。数组中的前两个元素总是环境和进程本身。数组中的第一个元素总是指向用来执行这个文件的 node 实例的路径。第二个元素总是指向这个 node 执行的文件。之后的所有内容都将按照你传递的其他事物的顺序排列。你可以将这看作是在终端程序中向程序传递参数。

在构建 CLI 时,argv 非常有用,因为它允许我们的 CLI 程序动态地接受不同的参数,并根据这些参数执行不同的操作。

环境变量

另一个重要的概念是环境变量(process.env)。环境变量在部署应用程序时非常有用,尤其是那些不应提交到代码库中的敏感信息,如 API 密钥或秘密。通过创建环境变量并将其注入到部署平台(如 AWS 或 Vercel)中,我们可以安全地使用这些敏感信息,而无需在代码中硬编码它们。

每种操作系统和编程语言都有类似环境变量的概念,因为它们都是为了实现相同的目标:安全地存储和使用敏感信息。

环境变量在部署应用程序时非常重要,因为它们允许开发者在不改变代码的情况下,根据不同的部署环境(开发、测试、生产)调整应用程序的行为。NODE_ENV 是 Node.js 中一个常用的环境变量,用于指定应用程序的运行模式。在代码中,我们可以根据 NODE_ENV 的值来改变程序的行为,例如在开发模式下开启日志记录,在生产模式下关闭日志记录或启用身份验证。

可以使用 process.env 对象访问 NODE_ENV 环境变量的值:

// index.js
console.log(process.env.NODE_ENV);

使用命令 NODE_ENV=production node index.js 将会得到 production

NODE_ENV 也用于性能优化。例如,React 根据 NODE_ENV 的值来改变其警告和错误报告的行为,并在生产模式下进行优化,而在开发模式下则忽略这些优化以加快热重载速度。

创建一个 Node.js 项目

我们可以通过 npm 来创建一个 Node.js 项目,什么是 npmnpm 全称为 Node Package Manager,是 Node.js 的默认包管理工具。使用 npm 创建项目有初始化项目结构、方便管理依赖包以及统一开发环境等好处。

使用 npm init 命令来创建项目,你需要回答如下问题:

package name: (node)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)

或者可以使用 npm init --yes 或者它的简化版 npm init -y 来快速创建一个项目,在运行该命令后会看到目录下有一个 package.json 文件,这个文件用于描述项目的元数据,如项目名称、版本、作者、许可证等。同时它也记录了项目的依赖关系。这个标准化的项目结构有利于代码的可维护性和可分享性。

使用 npm init -y 创建项目的 package.json 文件是这样的:

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

这里的 index.js 文件作为入口文件,入口文件在 Node.js 项目中通常扮演着启动整个应用程序的角色,而名为 index.js 其实是个约定俗成的作法,可以改成其他的或者干脆就使用它。

模块

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

你可能知道「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/

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

npm 替代品

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

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

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

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