NodeJS

模块

安装运行和环境配置就不说了,因为都很简单.

与python类似,也有内置模块和第三方模块.模块主要是一个代码复用方案,而且可以避免函数名和变量名冲突.

比如写这么一个hello.js

1
2
3
4
5
6
7
8
9
'use strict';

const s = 'Hello';

function greet(name) {
console.log(s + ', ' + name + '!');
}

module.exports = greet; // 这一行是模块的关键

module.exports = greet;: 把greet()函数作为模块的输出暴露出去,这样子,其他模块就能使用greet()了.

main.js中调用

1
2
3
4
5
6
7
'use strict';

const greet = require('./hello');// node提供的require函数

let s = 'Michael';

greet(s); // Hello, Michael!

const greet = require('./hello');: 引入的模块作为变量保存在greet中,其实变量greet就是在hello.js中用module.exports = greet;导出的greet函数.

引入模块时候,注意模块的相对路径,如果只写模块名,node会依次在内置模块,全局模块和当前模块下查找.如果找不多会报错

1
2
3
4
5
6
module.js
throw err;
^
Error: Cannot find module 'hello'
at Module._resolveFilename
...

CommonJS规范

上面的这种模块加载机制称为CommonJS规范,在这种规范下,每个.js文件都是一个模块,不同模块内部的变量名和函数名都不冲突.

一个模块想要对外暴露变量(函数也是变量),用module.exports = variable

想要引用其他模块暴露的变量,用var ref = require('module_name')

模块原理

当我们编写JS代码时,可以申明全局变量

1
let s = 'global';

但是,大量定义全局变量肯定不好,容易造成冲突.在ESM之前,a.jsb.js中如果定义了相同的全局变量,是会造成冲突的.

为了避免这个冲突,node.js利用了JS的闭包.如果我们把一段JS代码用一个函数包装起来,这段代码的所有全局变量就会变成函数内部的局部变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 比如我们编写的hello.js是这样的
let s = 'Hello';
let name = 'world';

console.log(s + ' ' + name + '!');

// node.js加载后会把它变成这样
(function () {
// 读取的hello.js代码:
let s = 'Hello';
let name = 'world';

console.log(s + ' ' + name + '!');
// hello.js代码结束
})(); // 立即调用函数表达式(IIFE)"(function() { ... })"定义一个匿名函数,最后的"()"会立即调用这个函数

这样子就实现了变量的隔离.

模块的输出module.exports实现靠的是一个对象module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 准备module对象:
let module = {
id: 'hello',
exports: {}
};
let load = function (module) {
// 读取的hello.js代码:
function greet(name) {
console.log('Hello, ' + name + '!');
}

module.exports = greet;
// hello.js代码结束
return module.exports;
};
let exported = load(module);
// 保存module:
save(module, exported);

变量module是node在加载js文件前准备的一个变量,并将其传入加载函数.所以我们在hello.js中可以直接使用变量module.因为它实际上是函数的一个参数.

load()函数把module变量传递给了node执行环境,node把它保存在某个地方.

由于Node保存了所有导入的module,当我们用require()获取module时,Node找到对应的module,把这个moduleexports变量返回,这样另一个模块就能拿到模块的输出.

不过这个,看看就行了,现代JS开发中都是用ESM.

ESM

Node.js一开始就支持模块,但那并不是JS原生的功能,只能算是一种模拟.

ES6后,原生JS终于有了原生内置的模块支持,称为ECMAScript Modules(ESM),不仅可以直接在浏览器中使用模块,也可以在Node.js中使用ESM模块.

另外ES6后使用模块,会自动使用严格模式,所以并不需要显示写use strict;

普通导入导出

export关键字标识需要导出的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
let s = 'Hello';
// out 是模块内部函数,模块外部不可见
function out(prompt, name) {
console.log(`${prompt}, ${name}`);
}
// greet是导出函数,可被外部调用
export function greet(name) {
out(s, name);
}
// hi也是导出函数,可被外部调用
export function hi(name) {
out('Hi', name);
}

将该文件保存为hello.mjs,注意后缀是.mjs,不过这个不是强制的.

在node.js中有三种方式使用ESM:

  1. 使用.mjs

    1
    2
    3
    4
    5
    // math.mjs
    export const add = (a, b) => a + b;

    // main.mjs
    import { add } from './math.mjs';
  2. package.json中设置"type": "module"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // package.json
    {
    "type": "module"
    }

    // 然后就可以使用 .js 结尾
    // math.js
    export const add = (a, b) => a + b;

    // main.js
    import { add } from './math.js';
  3. 如果在package.json中没有设置type,就要使用.mjs

    1
    2
    3
    4
    // 因为package.json中默认"type": "commonjs"
    // 此时需要使用.mjs
    // math.mjs
    export const add = (a, b) => a + b;

再写一个main.mjs来调用hello模块

1
2
3
4
5
import { greet, hi } from './hello.mjs'; // 导入

let name = 'Bob';
greet(name); // 直接调用
hi(name);

默认导入导出

当一个模块主要提供一个功能时,使用默认导入导出更方便

1
2
// 使用export default关键字
export default function greet(neme) {...}
1
2
// 调用时就不用{}
import greet from './hello.js';

重命名导入

为了避免模块命名冲突,可以用as重命名(别名)

1
2
3
4
// main.js
import { add as sum, subtract as minus } from './math.js';
console.log(sum(5, 3)); // 使用 sum 代替 add
console.log(minus(5, 3)); // 使用 minus 代替 subtract

浏览器加载ESM

浏览器也可以直接使用ESM

1
2
3
4
5
6
7
8
9
<html>
<head>
<script type="module" src="./example.js"></script> /<!-- 加载是需要写上`type="module"` -->
<script type="module"> <!-- 调用 -->
greet('Bob');
</script>
</head>
...
</html>
1
2
3
4
5
6
7
8
9
<html>
<head>
<script type="module">
import { greet } from './example.js'; // 直接import和调用
greet('Bob');
</script>
</head>
...
</html>

基本模块

列一下node.js常用基本模块

  • fs: 文件系统模块,负责文件读写,同时提供了同步和异步的方法
  • stream: 提供流式数据的支持
  • http: 用JS编写web服务器程序
  • crypto: 提供通用的加密和哈希算法.这些功能如果用JS实现会非常慢,所以node.js用c/c++实现后,通过crypto这个模块暴露为JS接口,方便使用.

node.js项目

整理一下node项目常用命令和文件

项目初始化

1
2
3
4
5
6
7
# 初始化项目, 创建package.json
npm init # 交互式创建
npm init -y # 使用默认配置快速创建

# 设置npm配置
npm config set registry https://registry.npmmirror.com # 设置淘宝镜像
npm config get registry # 查看当前镜像

依赖安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 安装依赖
npm install # 安装 package.json 中所有依赖
npm i # npm install 的简写

npm i package-name # 安装指定包
npm i package-name@version # 安装指定版本

# 安装依赖并保存到不同依赖类型
npm i package-name --save # 生产依赖 (-S 简写)
npm i package-name --save-dev # 开发依赖 (-D 简写)

# 全局安装
npm i package-name -g # 全局安装某个包

# 删除依赖
npm uninstall package-name # 删除指定包
npm uninstall -g package-name # 删除全局包

# 更新依赖
npm update package-name # 更新指定包
npm update # 更新所有包

项目运行相关

1
2
3
4
5
6
7
8
9
# 运行 package.json 中定义的脚本
npm run script-name # 运行指定脚本
npm start # 运行 start 脚本的简写
npm test # 运行 test 脚本的简写

# 查看
npm ls # 查看安装的依赖树
npm ls package-name # 查看指定包的依赖关系
npm outdated # 检查过时的包

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "my-project", // 项目名
"version": "1.0.0", // 版本号
"description": "", // 项目描述
"main": "index.js", // 入口文件
"scripts": { // 可运行的脚本
"dev": "vite",
"build": "vite build",
"test": "jest",
"lint": "eslint ."
},
"dependencies": { // 生产依赖
"react": "^18.2.0", // ^ 表示兼容补丁和小版本更新
"react-dom": "~18.2.0", // ~ 表示只兼容补丁更新
"package3": "1.2.3"
},
"devDependencies": { // 开发依赖
"vite": "^4.0.0",
"jest": "^29.0.0"
}
}

重要文件和路径

  1. package-lock.json - 依赖锁定文件
  • 锁定依赖的具体版本
  • 应该提交到代码仓库
  • 保证团队所有成员使用相同版本的依赖
  1. node_modules/ - 依赖包安装目录
  • 不需要提交到代码仓库
  • 可以通过 npm install 重新生成
  1. .npmrc - npm 配置文件

    1
    2
    registry=https://registry.npmmirror.com
    sass_binary_site=https://npmmirror.com/mirrors/node-sass/

常见部署流程

1
2
3
4
5
6
7
8
9
10
11
# 1. 安装依赖
npm install

# 2. 运行测试
npm test

# 3. 构建项目
npm run build

# 4. 运行项目(如果是服务器端渲染)
npm start

一些高级用法

1
2
3
4
5
6
7
8
9
10
11
12
# 清除缓存
npm cache clean --force

# 查看包信息
npm info package-name

# 检查项目依赖问题
npm audit
npm audit fix # 自动修复依赖问题

# 运行脚本时显示详细日志
npm run script-name --verbose

环境变量

1
2
3
4
5
6
7
8
# 开发环境运行
NODE_ENV=development npm run dev

# 生产环境构建
NODE_ENV=production npm run build

# CI流程
CI=true

运维

  1. 缓存node_module目录以加快构建
  2. 使用CI=true环境变量
  3. 保存构建产物
  4. 使用npm ci代替npm install在CI环境中安装依赖

NODE_ENV

development 模式下:

  • 开启详细的调试信息和警告
  • 不会进行代码压缩和优化
  • 启用热重载(Hot Reload)
  • 包含 source map
  • 可能包含测试数据或模拟 API

production 模式下:

  • 关闭调试信息和警告
  • 进行代码压缩和优化
  • 移除开发时才需要的代码
  • 不包含 source map (除非特别配置)
  • 使用真实 API 地址

CI=true

表明在持续集成环境中运行

影响:

  • 禁用交互式输出
  • 遇到错误立即退出
  • 更严格的错误处理
  • 禁用进度条显示
  • npm 会以最大日志级别运行

npm install

  • 根据 package.json 安装依赖

  • 如果有 package-lock.json,会尽量遵循它

  • 可能会更新 package-lock.json

  • 可以安装单个包

  • 会重用现有的 node_modules

npm ci

  • 严格按照 package-lock.json 安装依赖

  • 必须有 package-lock.json,否则报错

  • 不会修改 package.json 和 package-lock.json

  • 不能安装单个包

  • 总是删除现有的 node_modules 后重新安装

  • 通常比 npm install 快(因为跳过了一些计算和验证)


NodeJS
http://example.com/2024/11/21/nodeJS/
作者
Peter Pan
发布于
2024年11月21日
许可协议