一篇面向实战的 Node.js 后端入门博客:从 CommonJS / ESM 模块化、NPM 生态与包发布,到 HTTP 协议 与 原生
http服务 的完整知识脉络。文中所有示例均可独立运行,不依赖外部讲义或资源路径。
目录
- 导读:知识架构与权威参考
- 1. Node.js 模块系统回顾
- 2. NPM 包管理工具
- 3. 国内镜像工具
- 4. 发布 NPM 包
- 5. HTTP 协议详解
- 6. 创建 Node.js HTTP 服务
- 7. 实战案例与最佳实践
- 8. 核心概念总结与术语解析
- 9. 知识点归纳速查与行业应用场景
导读:知识架构与权威参考
本文解决什么问题
| 阶段 | 你会学到 | 典型产出 |
|---|---|---|
| 模块化 | require / import、路径规则、查找机制 | 可拆分的工具库、业务模块 |
| 包管理 | npm / yarn / npx、锁文件、镜像 | 可协作的前端/全栈工程 |
| 发布 | npm publish、bin 全局命令 | 团队内部 CLI、开源工具包 |
| HTTP | 报文结构、状态码、http 模块 | 静态服务、简易 API、调试代理 |
知识脉络(Mermaid)
权威文档(建议对照阅读)
| 主题 | 官方/权威来源 | 说明 |
|---|---|---|
Node 包与 package.json | Node.js — Modules: Packages | "type"、exports、imports 等 |
| ESM 在 Node 中 | Node.js — ECMAScript modules | .mjs、import 互操作 |
| CommonJS | Node.js — Modules: CommonJS | require 查找算法 |
| NPM CLI | npm Docs | install、publish、workspaces |
| 语义化版本 | SemVer 规范 | ^ / ~ 含义 |
| HTTP 方法 | MDN — HTTP 方法 | GET/POST/PUT/DELETE |
| HTTP 首部 | MDN — HTTP 标头 | 请求头/响应头字段 |
| HTTP 状态码 | MDN — HTTP 状态码 | 2xx/4xx/5xx 分类 |
行业中的典型落点(只谈技术)
- Express / Koa / Fastify:依赖 NPM 安装,通过
require或import挂载路由与中间件。 - Vite / Webpack / Rollup:
devDependencies管理构建链;npm run build产出静态资源。 - Next.js / Nuxt:
package.json的scripts统一dev/build/start工作流。 - 企业私有源:Verdaccio、cnpm 镜像、
.npmrc的registry,解决内网依赖分发。 - 运维与网关:Nginx 终止 TLS 后转发到 Node
http或集群进程(PM2、Docker)。
1. Node.js 模块系统回顾
Node.js 采用模块化架构,每个文件都被视为一个独立的模块。Node.js 支持两种模块系统:CommonJS 和 ES Modules。
1.1 CommonJS 模块规范
CommonJS 是 Node.js 默认的模块系统,采用同步加载方式,主要适用于服务端开发。
名词解释:CommonJS
CommonJS 是一个模块化规范,定义了模块的格式和加载机制。Node.js 采用了这个规范,使得 JavaScript 可以像其他编程语言一样进行模块化开发。
1.1.1 模块暴露数据
CommonJS 提供了两种暴露数据的方式:
方式一:使用 module.exports
module.exports = function(x, y) {
return x + y;
};
module.exports = {
add: function(x, y) { return x + y; },
subtract: function(x, y) { return x - y; },
PI: 3.14159
};
【代码注释】
module.exports = fn时,require('./add')得到的就是该函数,可直接add(1, 2)(课堂01-mod~03-mod同理)。- 赋值为对象时,
require得到整个对象,用const { add } = require(...)解构。 - 同一文件只能有一个最终
module.exports;后赋值会覆盖先前的导出。
方式二:使用 exports
exports.add = function(x, y) {
return x + y;
};
exports.subtract = function(x, y) {
return x - y;
};
// exports = function() { ... }; // ❌ 错误:断开与 module.exports 的引用
【代码注释】
exports是module.exports的引用,只能exports.xxx =追加属性,不能exports = 新对象。- 错误赋值后
require可能拿到初始空对象{},是课堂高频考点。 - 最终以
module.exports指向的值为准;exports只是写法糖。
重要区别:
module.exports是真正的暴露对象,可以直接赋值exports是module.exports的引用,只能添加属性- 最终以
module.exports的值为准
1.1.2 导入模块
使用 require() 函数导入模块:
const fs = require('fs');
const path = require('path');
const add = require('./add.js');
const converter = require('./converbyte');
const express = require('express');
console.log(add(100, 200));
【代码注释】
require('fs')/require('path'):内置模块,按模块名加载,不走相对路径。require('./add.js'):自定义模块,必须以./或../开头,相对当前文件目录解析(与fs.readFile('./data.txt')相对 cwd 不同,见 §1.3)。require('express'):第三方模块,从当前目录向上查找node_modules/express,需先npm install express。require是同步的:返回module.exports的最终值;同一文件多次require同一路径只执行一次顶层代码(缓存)。
1.1.3 模块文件扩展名规则
Node.js 支持多种模块文件扩展名,查找顺序如下:
支持扩展名说明:
.js- JavaScript 源代码文件(最常用).json- JSON 数据文件.node- C/C++ 扩展模块- 目录 - 自动查找目录下的
index.js
省略扩展名:
const utils = require('./utils');
const config = require('./config.json');
const pkg = require('./package');
【代码注释】
- 省略
.js时 Node 按顺序尝试:module.js→module.json→module.node→module/index.js等(见上文 Mermaid)。 require('./config.json')直接解析为 JS 对象,无需fs.readFile+JSON.parse。require('./package')会加载同目录package.json;注意变量名勿用保留字package,示例改为pkg。- 目录作为模块时,优先读该目录下
package.json的main字段,否则index.js。
1.2 ES Modules (ESM) 模块规范
ES Modules 是 ECMAScript 标准的模块系统,采用异步加载方式,同时适用于浏览器和服务端。
名词解释:ESM (ECMAScript Modules)
ESM 是 JavaScript 官方标准的模块系统,通过 import 和 export 关键字实现模块的导入和导出。它是现代 JavaScript 开发推荐的模块化方式。
1.2.1 暴露数据
方式一:暴露单个数据(默认导出)
// 【代码注释】使用 export default 暴露单个数据
export default function(x, y) {
return x + y;
}
// 【代码注释】也可以导出对象、类、常量等
export default {
name: 'calculator',
version: '1.0.0'
};
方式二:暴露多个数据(命名导出)
// 【代码注释】直接在声明时导出
export const add = (x, y) => x + y;
export const subtract = (x, y) => x - y;
export const PI = 3.14159;
// 【代码注释】或者集中导出
const add = (x, y) => x + y;
const subtract = (x, y) => x - y;
const multiply = (x, y) => x * y;
export { add, subtract, multiply };
// 【代码注释】导出时重命名
export { add as addition, subtract as subtraction };
1.2.2 导入模块
导入默认导出:
// 【代码注释】导入默认导出的模块
import calculator from './calculator.js';
import MathUtils from './math-utils.js';
导入命名导出:
// 【代码注释】导入指定的导出
import { add, subtract } from './math.js';
// 【代码注释】导入时重命名
import { add as addition, subtract as subtraction } from './math.js';
// 【代码注释】导入所有导出作为对象
import * as MathUtils from './math.js';
MathUtils.add(1, 2);
MathUtils.subtract(5, 3);
混合导入:
// 【代码注释】同时导入默认导出和命名导出
import Calculator, { add, subtract } from './calculator.js';
【代码注释】
- 默认导出:
import 任意名 from './x.js',一个模块仅一个export default。 - 命名导出:
import { add }名称须与导出一致,或用as重命名;import * as M得到命名空间对象M.add()。 - 混合导入:默认 + 命名可写在一行
import Def, { a } from '...'。 - 浏览器端
<script type="module">与 Node ESM 语法一致,但 Node 需.mjs或"type":"module"。
1.2.3 开启 ESM 模块规则
Node.js 默认使用 CommonJS,需要特殊配置才能使用 ESM:
方式一:使用 .mjs 扩展名
// 【代码注释】将文件扩展名改为 .mjs 自动启用 ESM
// calculator.mjs
export default (x, y) => x + y;
// main.mjs
import calc from './calculator.mjs';
方式二:在 package.json 中配置
{
"type": "module"
}
// 【代码注释】配置 "type": "module" 后,.js 文件也使用 ESM
// calculator.js
export default (x, y) => x + y;
// main.js
import calc from './calculator.js';
重要提示:
// 【代码注释】在 ESM 中不能使用 require()
// require('./module.js'); // 错误:require is not defined
// 【代码注释】在 CommonJS 中不能使用 import
// import calc from './calc.js'; // 错误:Unexpected token 'import'
【代码注释】
- 默认导出
export default:一个模块只能有一个;导入时可随意命名import calc from './x.js'。 - 命名导出
export const add:可有多个;导入必须同名或用as重命名,import * as M得到命名空间对象。 - 启用 ESM:
.mjs文件或根目录package.json的"type": "module"(此时.js按 ESM 解析,.cjs仍为 CommonJS)。 - ESM 中无
require/module.exports;CJS 中不能写顶层import(除非用动态import()或构建工具转译)。 - 浏览器与 Node 现代项目均推荐 ESM;维护老 Express 项目时仍常见 CJS。
1.3 模块路径与文件路径的区别
理解模块路径和文件路径的区别对于正确使用 Node.js 模块系统至关重要。
核心区别
实践案例
// 【代码注释】项目结构:
// project/
// ├── src/
// │ ├── main.js
// │ └── utils/
// │ └── helper.js
// └── data.txt
// 【代码注释】在 src/main.js 中:
// 文件路径(相对命令行目录)
fs.readFile('./data.txt', 'utf-8', (err, data) => {
// 【代码注释】这里 ./data.txt 相对于命令行所在目录
// 如果在 project/ 目录运行 node src/main.js
// 那么实际路径是 project/data.txt
});
// 模块路径(相对于当前文件目录)
const helper = require('./utils/helper.js');
// 【代码注释】这里 ./utils/helper.js 相对于 src/main.js
// 【代码注释】实际路径是 src/utils/helper.js
常见错误示例
// 【代码注释】错误示例:混淆文件路径和模块路径
// 假设在 src/modules/main.js 中
// 项目结构:
// project/
// ├── data/
// │ └── config.json
// └── src/
// └── modules/
// └── main.js
// 【代码注释】从 project/ 目录运行:node src/modules/main.js
// ❌ 错误:这会查找 project/data/config.json
fs.readFile('./data/config.json', 'utf-8', callback);
// ✅ 正确:使用 __dirname 获取当前文件目录
const path = require('path');
fs.readFile(
path.join(__dirname, '../../data/config.json'),
'utf-8',
callback
);
// 【代码注释】而对于 require(),始终相对于当前文件
const config = require('../../data/config.json'); // ✅ 正确
核心案例:模块路径 vs 文件路径(完整可运行)
下面三个文件放在同一目录 demo-module-path/ 下,可直接复制运行。
add.js — CommonJS 导出加法函数:
module.exports = (x, y) => x + y;
【代码注释】
module.exports = (x, y) => x + y导出单个函数;require('./add.js')返回值可直接add(100, 200)。- 文件名可写
./add或./add.js,Node 按扩展名查找顺序解析。 - 与
exports.add = fn区别:后者require得到{ add: fn },需解构或点号调用。
01-read-and-add.js — 演示「文件路径相对命令行、模块路径相对当前文件」:
const fs = require('fs');
const path = require('path');
const add = require('./add.js');
// 【代码注释】文件路径:相对「执行 node 时所在目录」
fs.readFile('./data.txt', 'utf-8', (err, data) => {
if (err) {
console.log('文件读取失败:', err.message);
return;
}
console.log('data.txt 内容:', data);
});
console.log('100 + 200 =', add(100, 200));
【代码注释】
- 必须在
demo-module-path/目录执行node 01-read-and-add.js,否则./data.txt相对 cwd 会找不到(ENOENT)。 require('./add.js')始终相对本 .js 文件所在目录,与 cwd 无关——这是 §1.3「模块路径 vs 文件路径」的核心对比。fs.readFile('./data.txt')读的是「你站在哪执行 node」下的 data.txt;require读的是「脚本旁边」的模块。- 先打印文件内容再
add(100,200):readFile异步,加法结果往往先于文件内容输出。
02-stat-with-converter.js — 结合自定义模块做容量换算:
const fs = require('fs');
const converByte = require('./converbyte/index.js');
const filename = './sample.bin'; // 【代码注释】换成你本地的任意文件即可
fs.stat(filename, (err, stat) => {
if (err) {
console.log('文件读取失败:', err.message);
return;
}
console.log('B:', stat.size);
console.log('KB:', converByte(stat.size, 1));
console.log('MB:', converByte(stat.size, 2));
console.log('GB:', converByte(stat.size, 3));
console.log('TB:', converByte(stat.size, 4));
});
converbyte/index.js:
const coverByte = (bytes, type = 0) => bytes / 1024 ** type;
module.exports = coverByte;
【代码注释】
fs.stat的stat.size单位为字节(B);converByte(size, type)用bytes / 1024 ** type换算。type=0原样字节;1KB;2MB;3GB;4TB(基于 1024 进制,与 Windows 资源管理器一致)。require('./converbyte/index.js')可省略index,Node 会解析目录下的index.js。./sample.bin换成任意存在文件即可;无文件时err.message提示路径错误。
03-use-esm.mjs — ESM 写法(需 Node 14+):
import * as fs from 'node:fs';
import converByte from './converbyte/index.mjs';
const filename = './sample.bin';
fs.stat(filename, (err, stat) => {
if (err) {
console.log('文件读取失败:', err.message);
return;
}
console.log('B:', stat.size);
console.log('KB:', converByte(stat.size, 1));
});
converbyte/index.mjs:
const coverByte = (bytes, type = 0) => bytes / 1024 ** type;
export default coverByte;
【代码注释】
import converByte from './converbyte/index.mjs'为 ESM 默认导入,对应export default coverByte。import * as fs from 'node:fs'使用node:前缀显式加载内置模块(Node 14.18+ 推荐写法)。.mjs扩展名强制按 ESM 解析;或在根package.json写"type": "module"使.js也按 ESM。- 同一文件内不要混写顶层
require与import;converbyte需同时提供.js(CJS)与.mjs(ESM)两套入口时,注意发布字段exports。
node 01-read-and-add.js
node 02-stat-with-converter.js
node 03-use-esm.mjs
2. NPM 包管理工具
NPM (Node Package Manager) 是 Node.js 的默认包管理工具,也是全球最大的开源库生态系统。
2.1 NPM 的作用与核心功能
名词解释:NPM (Node Package Manager)
NPM 是 Node.js 的包管理工具,它不仅是命令行工具,也是一个在线的包仓库。开发者可以通过 NPM 发现、共享、使用和构建代码包。
核心功能
三大使用场景
-
使用第三方包
# 【命令注释】安装项目依赖包 npm install express npm install lodash npm install axios -
安装命令行工具
# 【命令注释】全局安装命令行工具 npm install -g nodemon npm install -g create-react-app npm install -g typescript -
发布自己的包
# 【命令注释】发布包到 NPM 仓库 npm publish
2.2 NPM 常用命令详解
2.2.1 版本查看与初始化
# 【命令注释】查看 npm 版本
npm -v
# 输出示例:8.19.2
# 【命令注释】查看 Node.js 和 npm 版本信息
node -v
npm -v
# 【命令注释】初始化项目(交互式)
npm init
# 【命令注释】会依次询问:
# - package name: (项目名称)
# - version: (1.0.0)
# - description: (项目描述)
# - entry point: (index.js)
# - test command:
# - git repository:
# - keywords:
# - author:
# - license: (ISC)
# 【命令注释】快速初始化(使用默认配置)
npm init -y
# 或
npm init --yes
# 【命令注释】快速初始化生成的 package.json:
# {
# "name": "project-name",
# "version": "1.0.0",
# "description": "",
# "main": "index.js",
# "scripts": {
# "test": "echo \"Error: no test specified\" && exit 1"
# },
# "keywords": [],
# "author": "",
# "license": "ISC"
# }
【代码注释】
npm init交互式生成package.json;-y/--yes全部用默认值,适合课堂快速搭架子。- 生成后应检查
name(发布时全局唯一)、main(require('你的包名')的入口)、license。 npm init不会安装依赖;装包用npm install <pkg>,会同时更新package.json与package-lock.json。- 已有
package.json时勿重复init,直接npm install即可。
2.2.2 包的安装
# 【命令注释】安装包(添加到 dependencies)
npm install express
# 或简写
npm i express
# 【命令注释】安装多个包
npm install express lodash axios
# 【命令注释】安装指定版本
npm install [email protected]
npm install [email protected] # 安装 4.x.x 最新版本
npm install express@^4.18.0 # 兼容版本
# 【命令注释】安装到开发依赖(添加到 devDependencies)
npm install --save-dev jest
npm install -D typescript
# 【代码注释】开发依赖:只在开发过程中需要的包
# 例如:测试框架、构建工具、类型检查工具等
# 【命令注释】全局安装(主要用于命令行工具)
npm install -g nodemon
npm install --global create-react-app
npm install -g @vue/cli
# 【命令注释】查看全局安装位置
npm root -g
# 输出示例:/usr/local/lib/node_modules
# 【命令注释】查看全局安装的包
npm list -g --depth=0
【代码注释】
npm install express:写入dependencies并安装到./node_modules/express,同时更新 lock 文件。-D/--save-dev:写入devDependencies,生产部署时npm ci --omit=dev可省略(仅装运行依赖)。-g:装到全局node_modules(npm root -g可查路径),用于 CLI 工具(nodemon、typescript),不会进入当前项目的package.json(除非加-g且未-D的误用)。express@^4.18.0:^允许次版本、补丁升级,安装时由 npm 解析为 lock 中的精确版本。npm outdated对比 Current / Wanted / Latest,升级前建议先看 CHANGELOG 破坏性变更。
2.2.3 包的删除与更新
# 【命令注释】卸载已安装的包
npm uninstall express
# 或
npm remove express
# 或
npm r express
# 【命令注释】卸载全局包
npm uninstall -g nodemon
# 【命令注释】更新包到最新版本
npm update express
npm update -g npm
# 【命令注释】检查哪些包可以更新
npm outdated
# 【命令注释】输出示例:
# Package Current Wanted Latest Location
# express 4.17.1 4.18.2 4.18.2 project
# lodash 4.17.15 4.17.21 4.17.21 project
# 【命令注释】更新 npm 自身到最新版本
npm install -g npm@latest
2.2.4 依赖安装与缓存管理
# 【命令注释】根据 package.json 安装所有依赖
npm install
# 或
npm i
# 【命令注释】精确安装 package-lock.json 中的版本
npm ci
# 【命令注释】清除 npm 缓存
npm cache clean --force
# 【代码注释】强制清除缓存,解决安装问题时的常用方法
# 【命令注释】验证缓存
npm cache verify
# 【命令注释】查看缓存内容
npm cache ls
【代码注释】
- 克隆项目后先
npm install:按package-lock.json(或npm-shrinkwrap)装与作者一致的树。 npm ci:删node_modules后按 lock 精确重装,CI/CD 常用;要求 lock 与package.json一致,否则失败。npm cache clean --force:清缓存排错「装了一半、checksum 不对」;之后重装会重新拉 tarball。- 不要手改
node_modules后指望install自动修复;应改package.json再install或ci。
2.2.5 信息查询命令
# 【命令注释】查看包信息
npm view express
npm info express
# 【命令注释】查看包的特定版本信息
npm view [email protected]
# 【命令注释】查看包的依赖关系
npm view express dependencies
# 【命令注释】查看已安装的包
npm list
npm list --depth=0 # 只显示顶层依赖
# 【命令注释】查看全局安装的包
npm list -g --depth=0
# 【命令注释】查看包的文档
npm home express
npm repo express
npm bugs express
2.3 package.json 配置文件详解
名词解释:package.json
package.json 是 Node.js 项目的配置文件,包含了项目的元数据、依赖信息、脚本命令等关键信息。它是 Node.js 项目的身份证。
完整配置示例
{
"name": "my-awesome-project", // 【代码注释】包名(必须唯一)
"version": "1.0.0", // 【代码注释】版本号(遵循语义化版本)
"description": "An awesome Node.js project", // 【代码注释】项目描述
"main": "src/index.js", // 【代码注释】入口文件
"type": "module", // 【代码注释】模块类型(commonjs 或 module)
"scripts": { // 【代码注释】可执行脚本
"start": "node src/index.js", // 【代码注释】启动命令
"dev": "nodemon src/index.js", // 【代码注释】开发模式
"test": "jest", // 【代码注释】运行测试
"build": "webpack --mode production",// 【代码注释】构建命令
"lint": "eslint src/**/*.js" // 【代码注释】代码检查
},
"keywords": [ // 【代码注释】关键词(便于搜索)
"nodejs",
"web-framework",
"api"
],
"author": "Your Name <[email protected]>", // 【代码注释】作者信息
"license": "MIT", // 【代码注释】开源协议
"dependencies": { // 【代码注释】生产环境依赖
"express": "^4.18.2", // 【代码注释】Web 框架
"mongoose": "^7.0.0", // 【代码注释】MongoDB ODM
"axios": "^1.4.0", // 【代码注释】HTTP 客户端
"dotenv": "^16.0.3" // 【代码注释】环境变量管理
},
"devDependencies": { // 【代码注释】开发环境依赖
"nodemon": "^2.0.22", // 【代码注释】自动重启工具
"jest": "^29.5.0", // 【代码注释】测试框架
"eslint": "^8.40.0", // 【代码注释】代码检查工具
"webpack": "^5.85.0", // 【代码注释】打包工具
"typescript": "^5.0.4" // 【代码注释】TypeScript 编译器
},
"engines": { // 【代码注释】运行环境要求
"node": ">=14.0.0",
"npm": ">=6.0.0"
},
"config": { // 【代码注释】自定义配置
"port": 3000
},
"repository": { // 【代码注释】代码仓库信息
"type": "git",
"url": "https://github.com/username/my-awesome-project.git"
},
"bugs": { // 【代码注释】问题反馈地址
"url": "https://github.com/username/my-awesome-project/issues"
},
"homepage": "https://github.com/username/my-awesome-project#readme",
"bin": { // 【代码注释】命令行工具配置
"my-cli": "./bin/cli.js"
},
"files": [ // 【代码注释】发布包含的文件
"src",
"README.md",
"LICENSE"
]
}
【代码注释】
name:发布到 npm 时全局唯一;作用域包写@scope/pkg。main:别人require('你的包名')时加载的文件;与exports字段(Node 12+)可并存,现代包优先配exports。type: "module":根目录.js按 ESM 解析;CommonJS 工具链项目勿随意开启。dependencies:运行时需要(express);devDependencies:仅开发/构建需要(jest、webpack),生产npm ci --omit=dev可不装。scripts:通过npm run执行;bin注册全局命令,见 §4.2 / §7.3。files/.npmignore:控制npm publish上传哪些文件;未列出的可能被忽略(也受.gitignore影响,npm 7+ 行为以文档为准)。engines:提示 Node/npm 最低版本,不强制;可用engines.node配合only-allow锁包管理器。
版本号规则详解
版本号示例说明:
{
"dependencies": {
"express": "^4.18.2", // 【代码注释】^ 锁定大版本,>=4.18.2 <5.0.0
"lodash": "~4.17.21", // 【代码注释】~ 锁定小版本,>=4.17.21 <4.18.0
"axios": "1.4.0", // 【代码注释】精确版本,必须 1.4.0
"react": "*", // 【代码注释】任意版本(不推荐)
"vue": "latest", // 【代码注释】最新版本(不推荐)
"moment": "file:./local" // 【代码注释】本地文件
}
}
版本选择逻辑:
| 版本范围 | 匹配版本示例 | 说明 |
|---|---|---|
^1.2.3 | 1.2.3, 1.3.0, 1.9.9 | 不更新最左边的非零版本号 |
~1.2.3 | 1.2.3, 1.2.9 | 只更新补丁版本 |
* | 2.0.0, 3.0.0 | 任意版本 |
1.x | 1.0.0, 1.9.9 | 任意 1.x.x 版本 |
>=1.2.3 | 1.2.3, 2.0.0 | 大于等于指定版本 |
1.2.3 - 2.0.0 | 1.2.3, 1.9.9, 2.0.0 | 版本范围 |
package-lock.json 文件
名词解释:package-lock.json
package-lock.json 是 npm 自动生成的文件,用于锁定项目依赖的精确版本号,确保团队成员安装相同版本的依赖包。
作用:
- 锁定依赖包的确切版本
- 提高安装速度(跳过版本解析)
- 确保团队成员使用相同的依赖版本
- 记录依赖包的完整依赖树
示例结构:
{
"name": "project",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-...",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1"
}
}
}
}
【代码注释】
package-lock.json记录精确版本与下载地址(resolved、integrity),提交到 Git 后队友npm ci装出相同树。lockfileVersion随 npm 大版本变化;勿手改 lock,用npm install让工具更新。- 与
package.json中^范围的关系:install在范围内解析一次并锁定;后续ci不再重新解析范围。 - 删 lock 再
install可能升级到依赖的最新次版本,导致「我这能跑你那不行」——团队应统一保留 lock。
2.4 模块查找机制
Node.js 的模块查找机制遵循特定的优先级顺序,理解这个机制对于解决模块加载问题非常重要。
查找流程图
查找顺序详解
1. 相对路径模块(./ 或 ../ 开头)
// 【代码注释】查找当前目录下的 utils.js
const utils = require('./utils');
// 【代码注释】查找上级目录的 helper.js
const helper = require('../helper');
// 【代码注释】查找子目录下的 config.json
const config = require('./config/config.json');
2. 绝对路径模块
// 【代码注释】使用绝对路径加载模块
const module = require('/absolute/path/to/module.js');
3. 核心模块
// 【代码注释】Node.js 内置模块,优先级最高
const fs = require('fs'); // 文件系统模块
const http = require('http'); // HTTP 服务器模块
const path = require('path'); // 路径处理模块
// 【代码注释】核心模块不需要路径,直接加载
4. 第三方模块查找
// 【代码注释】从 node_modules 目录查找
const express = require('express');
// 【代码注释】查找顺序示例:
// 假设在 /home/user/project/app.js 中 require('express')
// 1. /home/user/project/node_modules/express
// 2. /home/user/node_modules/express
// 3. /home/node_modules/express
// 4. /node_modules/express
5. 目录模块处理
// 【代码注释】require('./utils') 会查找:
// 1. utils.js
// 2. utils.json
// 3. utils.node
// 4. utils/package.json (查找 main 字段)
// 5. utils/index.js
实践示例:
// 【代码注释】项目结构
// project/
// ├── node_modules/
// │ └── lodash/
// ├── src/
// │ ├── node_modules/
// │ │ └── axios/
// │ └── app.js
// └── package.json
// 【代码注释】在 src/app.js 中:
// 【代码注释】1. 核心模块
const fs = require('fs'); // 直接加载核心模块
// 【代码注释】2. 项目级第三方模块
const _ = require('lodash'); // 查找 project/node_modules/lodash
// 【代码注释】3. 目录级第三方模块
const axios = require('axios'); // 查找 src/node_modules/axios
// 【代码注释】4. 相对路径模块
const utils = require('./utils'); // 查找 src/utils.js
【代码注释】
require('lodash')从当前文件所在目录向上逐层找node_modules,故src/node_modules/axios可覆盖/补充项目根依赖(npm 依赖提升后常见扁平结构在根node_modules)。require('fs')不走node_modules,由 Node 内置加载器直接提供。require('./utils')绝不查找node_modules,只解析相对路径文件/目录。- 报错
Cannot find module 'xxx':先区分是第三方包(未 install)还是相对路径写错。
2.5 远程仓库协作工作流程
在团队协作开发中,正确处理 node_modules 和依赖管理是确保开发环境一致性的关键。
实际工作流程
新员工入职第一天:
# 【命令注释】1. 克隆项目代码
git clone https://github.com/company/project.git
cd project
# 【命令注释】2. 安装项目依赖
npm install
# 【代码注释】根据 package.json 安装所有依赖
# 【代码注释】生成 node_modules 目录
# 【命令注释】3. 启动开发环境
npm run dev
# 或
npm start
日常开发流程:
# 【命令注释】1. 拉取最新代码
git pull origin main
# 【命令注释】2. 检查是否有新的依赖
git diff package.json
git diff package-lock.json
# 【命令注释】3. 安装新依赖(如有)
npm install
# 【命令注释】4. 继续开发
npm run dev
# 【命令注释】5. 提交代码前安装新依赖(如添加了新包)
git add package.json package-lock.json
git commit -m "Add new feature"
git push origin feature-branch
.gitignore 配置:
# 【代码注释】.gitignore 文件内容
# 依赖目录(不要提交 node_modules)
node_modules/
# 【代码注释】package-lock.json 应提交到 Git,保证团队依赖版本一致
# 日志文件
*.log
npm-debug.log*
# 环境变量文件
.env
.env.local
# 操作系统文件
.DS_Store
Thumbs.db
# IDE 配置文件
.vscode/
.idea/
*.swp
*.swo
2.6 配置命令别名
通过 package.json 的 scripts 字段配置命令别名,可以简化常用命令的执行。
基本配置
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest",
"build": "webpack --mode production",
"lint": "eslint src/**/*.js",
"format": "prettier --write \"src/**/*.js\""
}
}
使用示例:
# 【命令注释】使用配置的别名
npm start # 等同于 node src/index.js
npm run dev # 等同于 nodemon src/index.js
npm test # 等同于 jest(可省略 run)
npm run build # 等同于 webpack --mode production
npm run lint # 等同于 eslint src/**/*.js
【代码注释】
scripts里的命令在项目根目录的 shell 环境中执行;路径、环境变量与你在终端cd的位置有关(npm 会先定位到含package.json的目录)。npm start、npm test、npm stop等是 npm 内置生命周期,可省略run;自定义脚本如dev、build必须npm run dev。prebuild/postbuild等钩子:执行npm run build时会自动先跑prebuild、后跑postbuild(若已定义)。- Windows 下设置环境变量建议用
cross-env,避免NODE_ENV=production语法不兼容。
npm run 的向上查找特性:
与 require() 类似,若在子目录执行 npm run xxx 且当前目录没有 package.json,npm 会向上级目录查找最近的 package.json 再执行对应脚本。适合 monorepo 或在 src/ 子目录临时启动根项目脚本的场景。
# 【命令注释】在 monorepo 子包目录,仍可能触发根目录 scripts(取决于 npm 版本与 cwd)
cd packages/ui
npm run build # 若本目录无 package.json,可能使用上级仓库的配置
【代码注释】
- 克隆仓库后第一步:
npm install(或pnpm i/yarn),再打开package.json→scripts查看dev/start/build含义。 - monorepo 子目录执行
npm run build时,npm 可能向上找到根package.json的脚本(与版本、cwd 有关),避免在错误目录误启服务。 - 常见约定:
start生产启动、dev热更新开发、test单测、lint静态检查;无start时看 README 说明。
高级配置技巧
1. 传递参数:
{
"scripts": {
"start": "node src/index.js",
"start:prod": "NODE_ENV=production npm start",
"start:dev": "NODE_ENV=development npm start",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
2. 组合命令:
{
"scripts": {
"clean": "rm -rf dist",
"prebuild": "npm run clean",
"build": "webpack --mode production",
"postbuild": "npm run test",
"rebuild": "npm run clean && npm run build"
}
}
3. 环境变量:
{
"scripts": {
"start": "cross-env NODE_ENV=production node src/index.js",
"dev": "cross-env NODE_ENV=development nodemon src/index.js",
"test": "cross-env NODE_ENV=test jest"
}
}
4. 平台兼容:
{
"scripts": {
"clean": "rimraf dist",
"preinstall": "npx only-allow pnpm"
}
}
生命周期钩子
npm scripts 支持生命周期钩子,某些操作会自动触发相关钩子:
实际应用:
{
"scripts": {
"preinstall": "node scripts/check-node-version.js",
"postinstall": "node scripts/post-install.js",
"prestart": "npm run build",
"start": "node dist/index.js",
"prepublishOnly": "npm run build && npm test",
"prepare": "husky install"
}
}
3. 国内镜像工具
由于网络原因,在中国大陆地区使用官方 npm 仓库可能较慢,因此需要使用国内镜像工具。
3.1 cnpm 淘宝镜像
名词解释:cnpm
cnpm 是淘宝团队提供的 npm 镜像服务,将 npm 官方仓库的包同步到国内服务器,大幅提升下载速度。
安装与配置方式
方式一:安装 cnpm 命令行工具
# 【命令注释】全局安装 cnpm
npm install -g cnpm --registry=https://registry.npmmirror.com
# 【命令注释】使用 cnpm 代替 npm
cnpm install express
cnpm install -g nodemon
cnpm update
方式二:Shell 别名(不改全局 registry,临时走镜像)
# 【命令注释】在 ~/.zshrc 或 ~/.bashrc 中定义(路径按本机 HOME 调整)
alias cnpm="npm --registry=https://registry.npmmirror.com \
--cache=$HOME/.npm/.cache/cnpm \
--disturl=https://npmmirror.com/dist \
--userconfig=$HOME/.cnpmrc"
cnpm install express
【代码注释】
alias cnpm="npm --registry=..."不改全局registry,仅在该命令走 npmmirror.com;适合公司内网与官方源混用。--cache/--userconfig分离 cnpm 缓存与配置,避免与官方 npm 缓存冲突。npm publish必须对官方源:发布前执行npm config get registry,应为https://registry.npmjs.org/。- 全局安装
cnpm后命令与npm并列;团队更常用nrm或.npmrc项目级registry统一源。
方式三:配置 npm 使用淘宝镜像
# 【命令注释】查看当前镜像源
npm config get registry
# 默认: https://registry.npmjs.org/
# 【命令注释】设置为淘宝镜像
npm config set registry https://registry.npmmirror.com
# 【命令注释】验证配置
npm config get registry
# 输出: https://registry.npmmirror.com
# 【命令注释】恢复官方镜像
npm config set registry https://registry.npmjs.org/
方式四:使用 nrm 管理镜像源
# 【命令注释】安装 nrm(镜像源管理工具)
npm install -g nrm
# 【命令注释】查看可用镜像源
nrm ls
# * npm -------- https://registry.npmjs.org/
# yarn ------- https://registry.yarnpkg.com/
# tencent ---- https://mirrors.cloud.tencent.com/npm/
# cnpm ------- https://registry.npmmirror.com/
# taobao ----- https://registry.npmmirror.com/
# npmMirror -- https://skimdb.npmjs.com/registry/
# 【命令注释】切换镜像源
nrm use taobao
# 【命令注释】输出: The registry has been changed to 'taobao'
# 【命令注释】测试镜像源速度
nrm test
镜像源对比:
| 镜像源 | URL | 特点 |
|---|---|---|
| npm 官方 | https://registry.npmjs.org/ | 最全最新,但速度较慢 |
| 淘宝镜像 | https://registry.npmmirror.com/ | 速度快,更新频繁 |
| 腾讯云 | https://mirrors.cloud.tencent.com/npm/ | 速度快,稳定性好 |
| 华为云 | https://repo.huaweicloud.com/repository/npm/ | 速度快,企业级 |
3.2 yarn 包管理工具
名词解释:Yarn
Yarn 是 Facebook 开发的 JavaScript 包管理工具,旨在解决 npm 在性能和一致性方面的问题。它提供了更快的安装速度和更可靠的依赖管理。
Yarn vs npm 对比
性能对比:
- 安装速度:Yarn 并行安装包,通常比 npm 快 2-3 倍
- 缓存机制:Yarn 缓存已下载的包,避免重复下载
- 版本一致性:Yarn 使用 yarn.lock 确保版本一致性
Yarn 常用命令
# 【命令注释】安装 yarn
npm install -g yarn
# 或使用 npm 安装(不推荐全局)
corepack enable # Node.js 16.10+ 内置 yarn 支持
# 【命令注释】初始化项目
yarn init
yarn init -y # 快速初始化
# 【命令注释】安装依赖
yarn add express # 添加到 dependencies
yarn add jest --dev # 添加到 devDependencies
yarn global add nodemon # 全局安装
# 【命令注释】安装项目依赖
yarn install
# 或
yarn
# 【命令注释】更新依赖
yarn upgrade
yarn upgrade express # 更新特定包
# 【命令注释】删除依赖
yarn remove express
yarn global remove nodemon
# 【命令注释】运行脚本
yarn start
yarn build
yarn test
# 【命令注释】查看信息
yarn list
yarn info express
npm vs yarn 命令对照表:
| 操作 | npm | Yarn |
|---|---|---|
| 安装依赖 | npm install | yarn / yarn install |
| 添加包 | npm install package | yarn add package |
| 开发依赖 | npm install -D package | yarn add package --dev |
| 全局安装 | npm install -g package | yarn global add package |
| 更新包 | npm update package | yarn upgrade package |
| 删除包 | npm uninstall package | yarn remove package |
| 运行脚本 | npm run command | yarn command |
| 全局运行 | npx command | yarn dlx package |
cyarn(淘宝镜像版 yarn)
# 【命令注释】安装 cyarn(使用淘宝镜像的 yarn)
npm install -g cyarn --registry "https://registry.npmmirror.com"
# 【命令注释】使用方式与 yarn 相同
cyarn install
cyarn add express
cyarn build
3.3 npx 命令工具
名词解释:npx
npx 是 npm 5.2.0+ 自带的命令执行工具,可以直接执行 npm 包中的命令行工具,无需全局安装。
npx 的优势
npx 使用示例
1. 执行一次性命令:
# 【命令注释】创建 React 应用(无需安装 create-react-app)
npx create-react-app my-app
# 【命令注释】运行 TypeScript 编译器(无需全局安装)
npx typescript --version
# 【命令注释】使用 prettier 格式化代码
npx prettier --write src/**/*.js
2. 指定版本执行:
# 【命令注释】使用特定版本的包
npx [email protected] my-app
npx [email protected] --version
# 【命令注释】使用不同版本的 Node.js
npx -p node@16 npm start
npx -p node@18 npm test
3. 从 Git 仓库执行:
# 【命令注释】直接从 GitHub 执行包
npx github:user/repo
npx bitnamicharts/repo-name
4. 执行本地包:
# 【命令注释】执行 node_modules/.bin 中的命令
npx jest
npx eslint src/**/*.js
npx webpack --config webpack.config.js
5. 创建别名:
# 【命令注释】创建常用命令的别名
npx --yes npm-check-updates
npx -y create-next-app@latest
# 【代码注释】-y 或 --yes 跳过确认提示
实际应用场景:
# 【命令注释】1. 项目脚手架
npx create-vue@latest my-project
npx create-react-app my-app
npx @angular/cli new my-project
# 【命令注释】2. 代码质量工具
npx prettier --check "src/**/*.js"
npx eslint --fix src/
npx stylelint "css/**/*.css"
# 【命令注释】3. 构建工具
npx webpack --mode production
npx rollup -c
npx parcel build src/index.html
# 【命令注释】4. 测试工具
npx vitest
npx cypress open
npx jest --coverage
# 【命令注释】5. 文档生成
npx typedoc src/
npx jsdoc -c jsdoc.conf.json
【代码注释】
npx create-react-app会临时下载包到缓存并执行其bin,不污染全局node_modules,用完可删缓存。- 执行本地项目脚本:
npx jest等价于./node_modules/.bin/jest,保证用的是本项目锁定版本的 jest。 -p node@18可指定用某版本 Node 跑后续命令,多版本共存时有用。-y/--yes跳过「是否安装」确认,适合 CI 非交互环境。
3.4 pnpm 高性能包管理工具
名词解释:pnpm
pnpm(Performant npm)是一个高效的 JavaScript 包管理工具,通过**内容寻址存储(Content-Addressable Storage)**技术在全局存储中只保存每个版本的依赖一份,再用符号链接(symlink)引用到各项目的 node_modules,从而大幅节省磁盘空间并加快安装速度。Vite、Vue、Turborepo 等主流项目均采用 pnpm。
为什么选择 pnpm?
幽灵依赖(Phantom Dependency):npm/yarn 将所有依赖扁平化到根
node_modules,导致项目能访问未在package.json中声明的包。一旦上游包升级或删除,代码就会崩溃。pnpm 的符号链接结构从根本上杜绝了这个问题。
pnpm vs npm vs yarn 对比:
| 特性 | npm | yarn | pnpm |
|---|---|---|---|
| 磁盘占用 | 高(每个项目独立) | 高(本地缓存) | 低(全局共享,可节省 50%+) |
| 安装速度 | 一般 | 快 | 最快 |
| 幽灵依赖 | 存在 | 存在 | 无(严格隔离) |
| Monorepo | 需要工具链辅助 | workspace | 原生支持 |
| 锁文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
| 主流框架使用 | 广泛 | React 生态 | Vite、Vue、Turborepo |
安装与初始化
# 【命令注释】方式一:通过 Node.js 内置的 corepack 启用(Node 16.10+ 推荐)
corepack enable
corepack prepare pnpm@latest --activate
# 【命令注释】方式二:全局安装
npm install -g pnpm
# 【命令注释】验证安装
pnpm --version
# 输出: 9.x.x
# 【命令注释】初始化项目
pnpm init
# 与 npm init -y 效果相同,生成 package.json
pnpm 常用命令
# 【命令注释】安装项目全部依赖
pnpm install # 等同于 npm install
pnpm i # 简写
# 【命令注释】添加依赖
pnpm add express # 生产依赖
pnpm add -D jest typescript # 开发依赖
pnpm add -g nodemon # 全局安装
# 【命令注释】删除依赖
pnpm remove express
pnpm remove -g nodemon
# 【命令注释】运行脚本(无需写 run)
pnpm dev # 等同于 npm run dev
pnpm build
pnpm test
# 【命令注释】一次性执行(等同于 npx)
pnpm dlx create-vite my-app # 不安装到全局,执行完即删
pnpm create vite my-app # 简写
# 【命令注释】查看依赖树
pnpm list
pnpm list --depth=0 # 只显示顶层
# 【命令注释】检查过期包
pnpm outdated
# 【命令注释】更新依赖
pnpm update
pnpm update express # 更新特定包
pnpm vs npm 命令对照表:
| 操作 | npm | pnpm |
|---|---|---|
| 安装所有依赖 | npm install | pnpm install |
| 添加生产包 | npm install pkg | pnpm add pkg |
| 添加开发包 | npm install -D pkg | pnpm add -D pkg |
| 全局安装 | npm install -g pkg | pnpm add -g pkg |
| 删除包 | npm uninstall pkg | pnpm remove pkg |
| 运行脚本 | npm run dev | pnpm dev |
| 一次性执行 | npx create-vite | pnpm dlx create-vite |
| 按 lock 精确安装 | npm ci | pnpm install --frozen-lockfile |
pnpm Workspace(Monorepo 支持)
Monorepo 是将多个相关项目(如「组件库 + 管理后台 + 文档站」)放在同一 Git 仓库的架构,pnpm workspace 是其最流行的实现方式之一。
目录结构示例:
my-monorepo/
├── pnpm-workspace.yaml # workspace 配置
├── package.json # 根包(定义共享脚本)
├── packages/
│ ├── ui/ # 组件库
│ │ └── package.json # { "name": "@myapp/ui" }
│ └── utils/ # 工具函数库
│ └── package.json # { "name": "@myapp/utils" }
└── apps/
├── web/ # 前端应用
│ └── package.json
└── admin/ # 管理后台
└── package.json
# 【代码注释】pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
# 【命令注释】在特定子包中运行命令
pnpm --filter @myapp/ui build
pnpm --filter @myapp/api dev
# 【命令注释】在所有子包中递归运行
pnpm -r build
pnpm -r test
# 【命令注释】子包之间互相引用(workspace 协议)
# apps/web/package.json:
# "dependencies": { "@myapp/utils": "workspace:*" }
# 安装时 pnpm 会建立符号链接,无需发布即可本地引用
【代码注释】
- pnpm 全局存储默认在
~/.local/share/pnpm/store(Linux/macOS);100 个项目引用同版本 lodash 只占一份磁盘。 pnpm dlx等价于npx,临时下载执行后清理,不污染全局;Vite 生态推荐pnpm create vite。- Monorepo 中
workspace:*协议让子包可以互相依赖本地最新代码,发布时 pnpm 自动将其替换为实际版本号。 - 切换到 pnpm 的第一步:删除
node_modules和原有 lock 文件,执行pnpm install生成pnpm-lock.yaml。
4. 发布 NPM 包
发布自己的 npm 包可以让其他开发者使用你的代码,这是开源社区贡献的重要方式。
4.1 发布普通包的步骤
名词解释:npm 包发布
npm 包发布是指将开发的模块上传到 npm 仓库,使其他开发者可以通过 npm install 命令安装使用。
详细发布步骤
第一步:本地开发包内容
# 【命令注释】1. 创建项目目录
mkdir my-awesome-package
cd my-awesome-package
# 【命令注释】2. 初始化项目
npm init -y
# 【命令注释】3. 编辑 package.json
{
"name": "my-awesome-package", // 【代码注释】确保包名唯一
"version": "1.0.0", // 【代码注释】版本号
"description": "An awesome utility package", // 【代码注释】包描述
"main": "index.js", // 【代码注释】入口文件
"keywords": ["utility", "helper", "tools"], // 【代码注释】关键词
"author": "Your Name <[email protected]>",
"license": "MIT", // 【代码注释】开源协议
"repository": { // 【代码注释】仓库地址
"type": "git",
"url": "https://github.com/your-username/my-awesome-package.git"
},
"files": [ // 【代码注释】包含的文件
"index.js",
"lib/",
"README.md"
]
}
// 【代码注释】4. 创建包的主要功能(index.js)
/**
* 【代码注释】字符串处理工具函数
* @param {string} str - 要处理的字符串
* @returns {string} 处理后的字符串
*/
function capitalize(str) {
if (!str || typeof str !== 'string') {
return '';
}
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* 【代码注释】反转字符串
* @param {string} str - 要反转的字符串
* @returns {string} 反转后的字符串
*/
function reverse(str) {
if (!str || typeof str !== 'string') {
return '';
}
return str.split('').reverse().join('');
}
/**
* 【代码注释】生成随机字符串
* @param {number} length - 字符串长度
* @returns {string} 随机字符串
*/
function randomString(length = 10) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// 【代码注释】导出函数
module.exports = {
capitalize,
reverse,
randomString
};
// 【代码注释】5. 创建测试文件(test.js)
const MyPackage = require('./index.js');
console.log('=== 测试字符串工具包 ===');
// 测试 capitalize
console.log('测试 capitalize:');
console.log(MyPackage.capitalize('hello')); // 输出: Hello
console.log(MyPackage.capitalize('')); // 输出: (空字符串)
console.log(MyPackage.capitalize(null)); // 输出: (空字符串)
// 测试 reverse
console.log('\n测试 reverse:');
console.log(MyPackage.reverse('hello')); // 输出: olleh
// 测试 randomString
console.log('\n测试 randomString:');
console.log(MyPackage.randomString(10)); // 输出: 10位随机字符串
console.log(MyPackage.randomString(20)); // 输出: 20位随机字符串
第二步:注册账号并登录
# 【命令注释】1. 访问 https://www.npmjs.com/ 注册账号
# 【命令注释】2. 命令行登录
npm login
# 【代码注释】依次输入:
# - Username: (用户名)
# - Password: (密码)
# - Email: (邮箱地址)
# 【代码注释】登录成功会显示:Logged in as username on https://registry.npmjs.org/.
# 【命令注释】3. 验证登录状态
npm whoami
# 输出: username
# 【命令注释】4. 如果使用了镜像源,需要切换回官方源
npm config set registry https://registry.npmjs.org/
第三步:发布包
# 【命令注释】1. 检查包名是否已被占用
npm view my-awesome-package
# 【代码注释】如果显示 404,说明包名可用
# 【命令注释】2. 发布包
npm publish
# 【代码注释】发布成功会显示:
# + [email protected]
# 【代码注释】包已成功发布到 npm 仓库
# 【命令注释】3. 验证发布
npm view my-awesome-package
# 【代码注释】会显示包的详细信息
第四步:更新包
# 【命令注释】1. 修改代码后,更新版本号
# 方式一:手动修改 package.json 中的 version
# 方式二:使用 npm version 命令
npm version patch # 1.0.0 -> 1.0.1 (修复 bug)
npm version minor # 1.0.1 -> 1.1.0 (新增功能)
npm version major # 1.1.0 -> 2.0.0 (破坏性更新)
# 【命令注释】2. 重新发布
npm publish
# 【代码注释】3. 查看已发布的版本
npm view my-awesome-package versions
发布注意事项:
// 【代码注释】1. package.json 必填字段
{
"name": "unique-package-name", // 【代码注释】包名必须唯一
"version": "1.0.0", // 【代码注释】版本号必须符合语义化版本
"description": "Package description", // 【代码注释】包描述
"main": "index.js" // 【代码注释】入口文件
}
// 【代码注释】2. 避免 .npmignore 忽略重要文件
// .npmignore 示例:
node_modules/
test/
*.test.js
.git/
.DS_Store
// 【代码注释】3. 确保 README.md 完整
// README.md 应包含:
// - 项目介绍
// - 安装方法
// - 使用示例
// - API 文档
// - 许可证信息
【代码注释】
- 发布流程:
npm init→ 写main/files→ 本地node test.js验证 →npm login→registry切官方源 →npm publish。 npm view 包名返回 404 表示包名可用;已占用需改名或申请作用域@你的组织/包名。npm version patch|minor|major会改package.json并打 Git tag(若在 Git 仓库),再npm publish发新版本。- 勿把
.env、密钥、node_modules打进包;用files白名单或.npmignore控制上传内容。
4.2 发布全局命令工具
发布全局命令工具可以让用户直接在命令行中使用你的工具。
创建可执行命令
第一步:创建命令行脚本
// 【代码注释】bin/cli.js
#!/usr/bin/env node
// 【代码注释】上面这行是 shebang,告诉系统用 node 执行
const fs = require('fs');
const path = require('path');
// 【代码注释】命令行参数处理
const args = process.argv.slice(2);
const command = args[0];
// 【代码注释】显示帮助信息
function showHelp() {
console.log(`
文件大小转换工具
使用方法:
fileconv <文件路径> 显示文件大小
fileconv -h, --help 显示帮助信息
fileconv -v, --version 显示版本信息
示例:
fileconv ./large-file.txt
fileconv /path/to/document.pdf
`);
}
// 【代码注释】显示版本信息
function showVersion() {
const packageJson = require('../package.json');
console.log(`fileconv v${packageJson.version}`);
}
// 【代码注释】转换文件大小
function convertFileSize(filePath) {
try {
// 【代码注释】获取文件信息
const stats = fs.statSync(filePath);
const bytes = stats.size;
// 【代码注释】转换单位
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
// 【代码注释】输出结果
console.log(`文件: ${path.basename(filePath)}`);
console.log(`大小: ${size.toFixed(2)} ${units[unitIndex]}`);
console.log(`字节: ${bytes} bytes`);
} catch (error) {
console.error(`错误: ${error.message}`);
process.exit(1);
}
}
// 【代码注释】命令处理
switch (command) {
case '-h':
case '--help':
showHelp();
break;
case '-v':
case '--version':
showVersion();
break;
default:
if (!command) {
showHelp();
} else {
convertFileSize(command);
}
break;
}
第二步:配置 package.json
{
"name": "file-size-converter",
"version": "1.0.0",
"description": "A command-line tool to convert file sizes",
"main": "index.js",
"bin": {
"fileconv": "./bin/cli.js"
},
"keywords": [
"file",
"size",
"converter",
"cli",
"command-line"
],
"author": "Your Name",
"license": "MIT",
"preferGlobal": true,
"engines": {
"node": ">=14.0.0"
}
}
第三步:测试和发布
# 【命令注释】1. 本地测试(在项目目录下)
npm link
# 【代码注释】这会在全局创建一个符号链接
# 【命令注释】2. 测试命令
fileconv --help
fileconv --version
fileconv ./large-file.txt
# 【命令注释】3. 取消链接
npm unlink -g file-size-converter
# 【命令注释】4. 发布到 npm
npm publish
用户安装和使用:
# 【命令注释】用户全局安装
npm install -g file-size-converter
# 【命令注释】使用命令
fileconv --help
fileconv ./my-document.pdf
fileconv /path/to/large/video.mp4
# 【命令注释】卸载命令
npm uninstall -g file-size-converter
高级命令行工具开发
使用 Commander.js:
// 【代码注释】使用 commander.js 创建更强大的命令行工具
const { Command } = require('commander');
const fs = require('fs');
const path = require('path');
const program = new Command();
program
.name('fileconv')
.description('File size converter tool')
.version('1.0.0');
// 【代码注释】转换命令
program
.command('convert <file>')
.description('Convert file size to human-readable format')
.option('-u, --unit <unit>', 'Specify output unit (B, KB, MB, GB, TB)')
.action((file, options) => {
try {
const stats = fs.statSync(file);
let bytes = stats.size;
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let targetUnit = options.unit ? options.unit.toUpperCase() : null;
let targetIndex = targetUnit ? units.indexOf(targetUnit) : -1;
// 【代码注释】如果没有指定单位或单位无效,自动选择
if (targetIndex === -1) {
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
console.log(`${path.basename(file)}: ${size.toFixed(2)} ${units[unitIndex]}`);
} else {
// 【代码注释】转换为指定单位
let size = bytes;
for (let i = 0; i < targetIndex; i++) {
size /= 1024;
}
console.log(`${path.basename(file)}: ${size.toFixed(2)} ${units[targetIndex]}`);
}
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
// 【代码注释】批量转换命令
program
.command('batch <pattern>')
.description('Convert multiple files matching a pattern')
.action((pattern) => {
const glob = require('glob');
const files = glob.sync(pattern);
console.log(`Found ${files.length} files:\n`);
files.forEach(file => {
try {
const stats = fs.statSync(file);
let bytes = stats.size;
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
console.log(`${file}: ${size.toFixed(2)} ${units[unitIndex]}`);
} catch (error) {
console.error(`${file}: Error - ${error.message}`);
}
});
});
program.parse();
package.json 配置:
{
"name": "fileconv-pro",
"version": "1.0.0",
"description": "Advanced file size converter tool",
"main": "index.js",
"bin": {
"fileconv": "./bin/cli.js"
},
"dependencies": {
"commander": "^11.0.0",
"glob": "^10.3.0"
},
"keywords": [
"file",
"size",
"converter",
"cli",
"batch"
],
"author": "Your Name",
"license": "MIT"
}
使用示例:
# 【命令注释】安装后使用
npm install -g fileconv-pro
# 【命令注释】单个文件转换
fileconv convert document.pdf
fileconv convert video.mp4 --unit MB
# 【命令注释】批量转换
fileconv batch "./src/**/*.js"
fileconv batch "images/*.png"
# 【命令注释】查看版本
fileconv --version
5. HTTP 协议详解
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的协议,理解 HTTP 协议对于 Web 开发至关重要。
5.1 HTTP 协议概述
名词解释:HTTP 协议
HTTP 是基于 TCP/IP 协议的应用层通信协议,规定了客户端和服务器之间如何通信、传输数据。它是万维网(WWW)数据通信的基础。
HTTP 协议特点:
- 简单快速:请求方法简单,程序开发方便
- 灵活:可以传输任意类型的数据
- 无连接:每次连接只处理一个请求
- 无状态:不记录客户端的状态信息
HTTP 版本演进:
| 版本 | 发布时间 | 主要特点 |
|---|---|---|
| HTTP/0.9 | 1991年 | 简单的 GET 请求,只支持 HTML |
| HTTP/1.0 | 1996年 | 增加 POST、HEAD 等方法,支持多种数据格式 |
| HTTP/1.1 | 1997年 | 持久连接、管道化、分块传输 |
| HTTP/2.0 | 2015年 | 多路复用、头部压缩、服务器推送 |
| HTTP/3.0 | 2022年 | 基于 QUIC 协议,解决队头阻塞 |
5.2 请求报文详解
HTTP 请求报文由客户端发送给服务器,包含请求的所有信息。
请求报文结构
实际请求示例
POST /api/v1/products/comments HTTP/1.1
Host: comment.api.163.com
Connection: keep-alive
Content-Length: 342
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json, text/plain, */*
Origin: https://comment.tie.163.com
Referer: https://comment.tie.163.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: session_id=abc123; user_token=xyz789
content=This+is+a+comment&userId=123×tamp=1685348594866
① 请求行
组成部分:
// 【代码注释】请求行格式:方法 + URL + 协议版本
POST /api/v1/products/comments HTTP/1.1
// ↑ ↑ ↑
// 方法 URL 版本
常见请求方法:
| 方法 | 描述 | 幂等性 | 可缓存 | 使用场景 |
|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | 查询数据、页面跳转 |
| POST | 创建资源 | ❌ | ❌ | 提交表单、上传文件 |
| PUT | 更新资源 | ✅ | ❌ | 完整更新资源 |
| PATCH | 部分更新 | ❌ | ❌ | 部分更新资源 |
| DELETE | 删除资源 | ✅ | ❌ | 删除资源 |
| HEAD | 获取头部 | ✅ | ✅ | 检查资源是否存在 |
| OPTIONS | 查询选项 | ✅ | ❌ | 跨域预检请求 |
| CONNECT | 建立隧道 | ❌ | ❌ | HTTPS 代理 |
| TRACE | 回显请求 | ✅ | ❌ | 诊断测试 |
GET vs POST 对比:
// 【代码注释】GET 请求示例
// 【代码注释】特点:数据在 URL 中,有长度限制,可被缓存
fetch('https://api.example.com/users?page=1&limit=10', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
// 【代码注释】POST 请求示例
// 【代码注释】特点:数据在请求体中,无长度限制,更安全
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John Doe',
email: '[email protected]'
})
});
② 请求头
请求头包含客户端的环境信息和请求的元数据。
常见请求头详解:
# 【代码注释】基础信息头
Host: api.example.com # 【代码注释】服务器域名和端口
Connection: keep-alive # 【代码注释】连接方式(keep-alive/close)
# 【代码注释】内容协商头
Accept: application/json # 【代码注释】客户端接受的数据类型
Accept-Encoding: gzip, deflate # 【代码注释】客户端支持的压缩格式
Accept-Language: zh-CN,zh;q=0.9 # 【代码注释】客户端接受的语言
Accept-Charset: utf-8 # 【代码注释】客户端接受的字符集
# 【代码注释】客户端信息头
User-Agent: Mozilla/5.0... # 【代码注释】客户端浏览器信息
Referer: https://example.com # 【代码注释】请求来源页面
Origin: https://example.com # 【代码注释】请求源(CORS 相关)
# 【代码注释】安全相关头
Cookie: sessionId=abc123; # 【代码注释】客户端存储的 Cookie
Authorization: Bearer token123 # 【代码注释】身份认证信息
# 【代码注释】内容类型头
Content-Type: application/json # 【代码注释】请求体的数据类型
Content-Length: 1024 # 【代码注释】请求体的字节长度
Content-Encoding: gzip # 【代码注释】请求体的压缩方式
# 【代码注释】缓存控制头
Cache-Control: no-cache # 【代码注释】缓存控制指令
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT # 【代码注释】缓存验证
If-None-Match: "33a64df551425fcc" # 【代码注释】ETag 验证
# 【代码注释】条件请求头
If-Match: "33a64df551425fcc" # 【代码注释】匹配 ETag 才执行
If-None-Match: "33a64df551425fcc" # 【代码注释】不匹配 ETag 才执行
If-Range: "33a64df551425fcc" # 【代码注释】范围请求条件
# 【代码注释】特殊用途头
Upgrade: websocket # 【代码注释】协议升级
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 【代码注释】WebSocket 握手
③ 空行
空行用于分隔请求头和请求体,是 HTTP 协议格式的要求。
Host: example.com
Content-Type: application/json
Content-Length: 25
# 【代码注释】这个空行是必须的,用于分隔请求头和请求体
{"name":"John","age":30}
④ 请求体
请求体包含要发送给服务器的数据,GET 请求通常没有请求体。
不同数据类型的请求体示例:
# 【代码注释】1. 表单数据 (application/x-www-form-urlencoded)
Content-Type: application/x-www-form-urlencoded
username=john&password=123&[email protected]
# 【代码注释】2. JSON 数据 (application/json)
Content-Type: application/json
{
"username": "john",
"password": "123",
"email": "[email protected]"
}
# 【代码注释】3. 文件上传 (multipart/form-data)
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
john
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
This is the file content.
------WebKitFormBoundary7MA4YWxkTrZu0gW--
# 【代码注释】4. XML 数据 (application/xml)
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<user>
<username>john</username>
<password>123</password>
</user>
# 【代码注释】5. 纯文本 (text/plain)
Content-Type: text/plain
This is a plain text message.
5.3 响应报文详解
HTTP 响应报文由服务器返回给客户端,包含响应的所有信息。
响应报文结构
实际响应示例
HTTP/1.1 200 OK
Server: nginx/1.18.0
Content-Type: application/json; charset=utf-8
Content-Length: 1024
Connection: keep-alive
Date: Wed, 24 May 2023 10:30:45 GMT
Cache-Control: max-age=3600
ETag: "33a64df551425fcc55"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
Set-Cookie: sessionId=abc123; HttpOnly; Secure
Access-Control-Allow-Origin: *
{
"status": "success",
"data": {
"users": [
{"id": 1, "name": "John", "email": "[email protected]"},
{"id": 2, "name": "Jane", "email": "[email protected]"}
]
},
"pagination": {
"page": 1,
"limit": 10,
"total": 100
}
}
① 响应行
组成部分:
// 【代码注释】响应行格式:协议版本 + 状态码 + 状态描述
HTTP/1.1 200 OK
// ↑ ↑ ↑
// 版本 状态码 状态描述
状态码分类:
| 类别 | 状态码 | 描述 |
|---|---|---|
| 1xx | 100-199 | 信息性响应,请求已接收 |
| 2xx | 200-299 | 成功响应,请求已处理 |
| 3xx | 300-399 | 重定向,需要进一步操作 |
| 4xx | 400-499 | 客户端错误,请求有误 |
| 5xx | 500-599 | 服务器错误,服务器故障 |
② 响应头
响应头包含服务器的环境信息和响应的元数据。
常见响应头详解:
# 【代码注释】服务器信息头
Server: nginx/1.18.0 # 【代码注释】服务器软件信息
Date: Wed, 24 May 2023 10:30:45 GMT # 【代码注释】响应时间
X-Powered-By: Express # 【代码注释】应用框架信息
# 【代码注释】内容协商头
Content-Type: application/json # 【代码注释】响应体的数据类型
Content-Type: text/html; charset=utf-8 # 【代码注释】类型和字符集
Content-Encoding: gzip # 【代码注释】响应体的压缩方式
Content-Length: 1024 # 【代码注释】响应体的字节长度
Content-Language: zh-CN # 【代码注释】响应体的语言
Content-Disposition: attachment; filename="file.txt" # 【代码注释】下载文件名
# 【代码注释】缓存控制头
Cache-Control: max-age=3600 # 【代码注释】缓存策略
Cache-Control: no-cache # 【代码注释】不使用缓存
Cache-Control: no-store # 【代码注释】不存储缓存
Expires: Wed, 24 May 2023 11:30:45 GMT # 【代码注释】缓存过期时间
ETag: "33a64df551425fcc" # 【代码注释】资源版本标识
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT # 【代码注释】最后修改时间
Age: 600 # 【代码注释】缓存已存在时间
# 【代码注释】安全相关头
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict # 【代码注释】设置 Cookie
Strict-Transport-Security: max-age=31536000; includeSubDomains # 【代码注释】强制 HTTPS
X-Frame-Options: DENY # 【代码注释】防止点击劫持
X-Content-Type-Options: nosniff # 【代码注释】防止 MIME 类型嗅探
X-XSS-Protection: 1; mode=block # 【代码注释】XSS 保护
Content-Security-Policy: default-src 'self' # 【代码注释】内容安全策略
# 【代码注释】跨域相关头
Access-Control-Allow-Origin: * # 【代码注释】允许的源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 【代码注释】允许的方法
Access-Control-Allow-Headers: Content-Type, Authorization # 【代码注释】允许的头
Access-Control-Max-Age: 3600 # 【代码注释】预检请求缓存时间
# 【代码注释】位置头
Location: https://example.com/new-location # 【代码注释】重定向地址
Content-Location: /api/v1/users/123 # 【代码注释】实际资源位置
# 【代码注释】认证头
WWW-Authenticate: Basic realm="Access to staging site" # 【代码注释】认证要求
Proxy-Authenticate: Basic realm="Proxy authentication" # 【代码注释】代理认证
# 【代码注释】范围请求头
Accept-Ranges: bytes # 【代码注释】支持范围请求
Content-Range: bytes 0-1023/2048 # 【代码注释】当前返回的范围
③ 空行
空行用于分隔响应头和响应体,是 HTTP 协议格式的要求。
④ 响应体
响应体包含服务器返回给客户端的实际数据。
不同类型的响应体示例:
# 【代码注释】1. HTML 页面
Content-Type: text/html; charset=utf-8
<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
# 【代码注释】2. JSON 数据
Content-Type: application/json
{
"status": "success",
"data": {
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
}
# 【代码注释】3. 图片文件
Content-Type: image/jpeg
Content-Length: 24568
[二进制图片数据]
# 【代码注释】4. 文件下载
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="document.pdf"
Content-Length: 102400
[文件二进制数据]
# 【代码注释】5. 错误信息
Content-Type: application/json
{
"error": {
"code": 400,
"message": "Invalid request parameters",
"details": "The 'email' field is required"
}
}
5.4 URL 统一资源定位符
名词解释:URL
URL(Uniform Resource Locator,统一资源定位符)是互联网上标准资源的地址,用于定位和访问网络上的资源。
URL 结构详解
URL 示例解析:
https://www.example.com:8080/api/v1/users?page=1&limit=10#results
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ 锚点 fragment
│ │ │ │ │ └─ 查询参数 query
│ │ │ │ └─ 路径 path
│ │ │ └─ 端口 port
│ │ └─ 主机 host
└───────┴─ 协议 scheme
各组成部分详解:
// 【代码注释】1. 协议(scheme)
const schemes = {
'http': '超文本传输协议(默认端口 80)',
'https': 'HTTP 安全版(默认端口 443)',
'ftp': '文件传输协议',
'mailto': '电子邮件地址',
'file': '本地文件',
'ws': 'WebSocket 协议',
'wss': 'WebSocket 安全版'
};
// 【代码注释】2. 主机(host)
const hosts = {
'域名': 'www.example.com',
'IP地址': '192.168.1.1',
'本地': 'localhost',
'本机IP': '127.0.0.1'
};
// 【代码注释】3. 端口(port)
const ports = {
'HTTP': '80',
'HTTPS': '443',
'FTP': '21',
'SSH': '22',
'MySQL': '3306',
'MongoDB': '27017',
'Redis': '6379'
};
// 【代码注释】4. 路径(path)
const paths = {
'绝对路径': '/api/v1/users',
'相对路径': 'users/profile',
'根路径': '/',
'文件路径': '/images/logo.png'
};
// 【代码注释】5. 查询参数(query)
const queryStrings = {
'单一参数': '?page=1',
'多个参数': '?page=1&limit=10&sort=desc',
'编码参数': '?name=John%20Doe&email=john%40example.com',
'数组参数': '?tags[]=javascript&tags[]=nodejs',
'对象参数': '?user[name]=John&user[age]=30'
};
// 【代码注释】6. 锚点(fragment)
const fragments = {
'章节定位': '#chapter1',
'评论定位': '#comments',
'结果区域': '#results'
};
URL 编码与解码:
// 【代码注释】URL 编码处理
const url = require('url');
// 原始字符串
const originalString = 'Hello World! 你好世界!@#$%^&*()';
// URL 编码
const encoded = encodeURIComponent(originalString);
console.log(encoded);
// 输出: Hello%20World!%20%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C%EF%BC%81%40%23%24%25%5E%26*()
// URL 解码
const decoded = decodeURIComponent(encoded);
console.log(decoded);
// 输出: Hello World! 你好世界!@#$%^&*()
// 【代码注释】完整的 URL 解析示例
const urlString = 'https://www.example.com:8080/api/v1/users?page=1&limit=10#results';
const parsedUrl = new URL(urlString);
console.log(parsedUrl.protocol); // https:
console.log(parsedUrl.hostname); // www.example.com
console.log(parsedUrl.port); // 8080
console.log(parsedUrl.pathname); // /api/v1/users
console.log(parsedUrl.search); // ?page=1&limit=10
console.log(parsedUrl.hash); // #results
// 【代码注释】解析查询参数
console.log(parsedUrl.searchParams.get('page')); // 1
console.log(parsedUrl.searchParams.get('limit')); // 10
5.5 HTTP 状态码详解
名词解释:HTTP 状态码
HTTP 状态码是服务器返回的3位数字代码,用于表示服务器对请求的处理结果。状态码分为5大类,每类表示不同类型的响应。
状态码分类图
常用状态码详解
1xx 信息性响应(100-199)
| 状态码 | 状态描述 | 说明 | 使用场景 |
|---|---|---|---|
| 100 Continue | 继续 | 客户端应继续请求 | 大文件上传 |
| 101 Switching Protocols | 切换协议 | 服务器同意切换协议 | HTTP 升级到 WebSocket |
| 102 Processing | 处理中 | 服务器正在处理请求 | 长时间处理任务 |
2xx 成功响应(200-299)
| 状态码 | 状态描述 | 说明 | 使用场景 |
|---|---|---|---|
| 200 OK | 成功 | 请求成功 | GET、POST 请求成功 |
| 201 Created | 已创建 | 资源创建成功 | POST 创建新资源 |
| 202 Accepted | 已接受 | 请求已接受,正在处理 | 异步任务处理 |
| 204 No Content | 无内容 | 请求成功,无返回内容 | DELETE 请求成功 |
| 206 Partial Content | 部分内容 | 返回部分内容 | 断点续传、视频分段 |
3xx 重定向(300-399)
| 状态码 | 状态描述 | 说明 | 使用场景 |
|---|---|---|---|
| 301 Moved Permanently | 永久移动 | 资源永久重定向 | 网站域名变更 |
| 302 Found | 临时重定向 | 资源临时重定向 | 临时维护页面 |
| 304 Not Modified | 未修改 | 资源未修改,可使用缓存 | 缓存验证 |
| 307 Temporary Redirect | 临时重定向 | 保持请求方法的重定向 | HTTP 到 HTTPS 重定向 |
| 308 Permanent Redirect | 永久重定向 | 保持请求方法的永久重定向 | RESTful API 重构 |
4xx 客户端错误(400-499)
| 状态码 | 状态描述 | 说明 | 使用场景 |
|---|---|---|---|
| 400 Bad Request | 错误请求 | 请求格式错误 | 参数验证失败 |
| 401 Unauthorized | 未授权 | 缺少身份认证 | 需要登录访问 |
| 403 Forbidden | 禁止访问 | 服务器拒绝请求 | 权限不足 |
| 404 Not Found | 未找到 | 资源不存在 | 访问不存在的页面 |
| 405 Method Not Allowed | 方法不允许 | 请求方法不支持 | GET 访问只支持 POST 的接口 |
| 408 Request Timeout | 请求超时 | 客户端请求超时 | 网络连接问题 |
| 409 Conflict | 冲突 | 请求与服务器状态冲突 | 资源已被修改 |
| 410 Gone | 已删除 | 资源已被永久删除 | API 已废弃 |
| 413 Payload Too Large | 请求体过大 | 请求体超过服务器限制 | 文件上传过大 |
| 415 Unsupported Media Type | 不支持的媒体类型 | Content-Type 不支持 | 发送 JSON 到只接受 XML 的接口 |
| 422 Unprocessable Entity | 无法处理的实体 | 语义错误 | 业务逻辑验证失败 |
| 429 Too Many Requests | 请求过多 | 超过速率限制 | API 限流 |
5xx 服务器错误(500-599)
| 状态码 | 状态描述 | 说明 | 使用场景 |
|---|---|---|---|
| 500 Internal Server Error | 内部服务器错误 | 服务器遇到意外情况 | 程序异常 |
| 501 Not Implemented | 未实现 | 服务器不支持该功能 | 使用了不支持的 HTTP 方法 |
| 502 Bad Gateway | 网关错误 | 上游服务器返回无效响应 | 反向代理配置错误 |
| 503 Service Unavailable | 服务不可用 | 服务器暂时无法处理 | 服务器维护 |
| 504 Gateway Timeout | 网关超时 | 上游服务器响应超时 | 后端服务响应慢 |
| 505 HTTP Version Not Supported | 不支持的 HTTP 版本 | 服务器不支持请求的 HTTP 版本 | 使用了 HTTP/2.0 而服务器只支持 HTTP/1.1 |
实际应用示例:
// 【代码注释】使用 Fetch API 处理不同状态码
async function fetchData(url) {
try {
const response = await fetch(url);
switch (response.status) {
case 200:
console.log('请求成功');
return await response.json();
case 201:
console.log('资源创建成功');
return await response.json();
case 204:
console.log('删除成功,无返回内容');
return null;
case 301:
case 302:
console.log('重定向到:', response.headers.get('Location'));
return await fetchData(response.headers.get('Location'));
case 400:
console.error('请求参数错误');
throw new Error('Bad Request');
case 401:
console.error('未授权,需要登录');
throw new Error('Unauthorized');
case 403:
console.error('权限不足');
throw new Error('Forbidden');
case 404:
console.error('资源不存在');
throw new Error('Not Found');
case 500:
console.error('服务器内部错误');
throw new Error('Internal Server Error');
case 503:
console.error('服务暂时不可用');
throw new Error('Service Unavailable');
default:
console.warn('未知状态码:', response.status);
throw new Error(`Unexpected status: ${response.status}`);
}
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}
6. 创建 Node.js HTTP 服务
Node.js 内置的 http 模块可以轻松创建 HTTP 服务器,无需额外依赖。
6.1 创建基础 HTTP 服务
核心对象介绍
创建简单服务器
// 【代码注释】导入 http 模块
const http = require('http');
// 【代码注释】创建服务器
const server = http.createServer((request, response) => {
// 【代码注释】request 是 http.IncomingMessage 实例(客户端请求对象)
// 【代码注释】response 是 http.ServerResponse 实例(服务器响应对象)
// 【代码注释】设置响应头
response.setHeader('Content-Type', 'text/html; charset=utf-8');
// 【代码注释】设置响应体并结束响应
response.end('<h1>Hello, Node.js HTTP Server!</h1>');
});
// 【代码注释】监听端口
const PORT = 3000;
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}/`);
});
【代码注释】
http.createServer(callback)为每个 HTTP 请求调用一次回调;request(IncomingMessage)只读请求,response(ServerResponse)写响应。- 必须先
setHeader再end;end(body)发送正文并结束响应,同一请求多次end会报错。 listen(PORT)绑定端口;默认监听0.0.0.0,本机访问http://localhost:3000。- 这是 Express/Koa 的底层:框架封装了路由、中间件,本质仍是
createServer。
完整功能的服务器:
const http = require('http');
const url = require('url');
const path = require('path');
// 【代码注释】创建服务器
const server = http.createServer((req, res) => {
// 【代码注释】获取请求信息
const method = req.method;
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
console.log(`${method} ${pathname}`);
console.log('查询参数:', query);
// 【代码注释】设置响应头
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
// 【代码注释】根据路径返回不同内容
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>Node.js HTTP Server</title>
<meta charset="utf-8">
</head>
<body>
<h1>欢迎使用 Node.js HTTP 服务器</h1>
<p>这是一个简单的 HTTP 服务器示例</p>
<ul>
<li><a href="/api">API 接口</a></li>
<li><a href="/time">当前时间</a></li>
<li><a href="/user?id=1">用户信息</a></li>
</ul>
</body>
</html>
`);
} else if (pathname === '/api') {
res.writeHead(200);
res.end(JSON.stringify({
status: 'success',
message: 'API 接口正常工作',
timestamp: Date.now()
}));
} else if (pathname === '/time') {
res.writeHead(200);
res.end(JSON.stringify({
datetime: new Date().toISOString(),
timestamp: Date.now(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}));
} else if (pathname === '/user') {
const userId = query.id;
if (userId) {
res.writeHead(200);
res.end(JSON.stringify({
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`
}));
} else {
res.writeHead(400);
res.end(JSON.stringify({
error: '缺少用户ID参数'
}));
}
} else {
res.writeHead(404);
res.end(JSON.stringify({
error: '页面不存在',
path: pathname
}));
}
});
// 【代码注释】启动服务器
const PORT = 3000;
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}/`);
console.log(`访问 http://localhost:${PORT}/api 查看 API 接口`);
console.log(`访问 http://localhost:${PORT}/time 查看当前时间`);
});
【代码注释】
url.parse(req.url, true)第二参数true把query解析为对象(如?id=1→{ id: '1' }),与 §2.3URL/ §2.4querystring对应。writeHead(200)可一次写入状态码与头;若已setHeader,注意勿重复冲突。- 路由用
pathname分支,是 Expressapp.get('/api')的雏形;/user缺id时返回 400 演示客户端错误。 Access-Control-Allow-Origin: *便于 §7.4 的fetch联调;生产应限制来源。
端口占用处理
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello, World!');
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}/`);
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`端口 ${PORT} 已被占用`);
console.error('解决方案:');
console.error('1. 更换端口:修改 PORT 变量的值');
console.error('2. 关闭占用端口的程序');
console.error('3. 查找占用端口的进程:');
console.error(` - macOS/Linux: lsof -i :${PORT}`);
console.error(` - Windows: netstat -ano | findstr :${PORT}`);
} else {
console.error('服务器错误:', err);
}
});
6.2 获取请求报文信息
获取请求行信息
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
// 【代码注释】获取 HTTP 版本
const httpVersion = req.httpVersion;
console.log('HTTP 版本:', httpVersion); // 1.1
// 【代码注释】获取请求 URL
const requestUrl = req.url;
console.log('请求 URL:', requestUrl); // /path?query=value
// 【代码注释】获取请求方法
const method = req.method;
console.log('请求方法:', method); // GET, POST, PUT, DELETE 等
// 【代码注释】解析 URL
const parsedUrl = url.parse(req.url, true);
console.log('路径:', parsedUrl.pathname); // /path
console.log('查询参数:', parsedUrl.query); // { query: 'value' }
res.end('请求信息已记录到控制台');
});
server.listen(3000);
获取请求头信息
const http = require('http');
const server = http.createServer((req, res) => {
// 【代码注释】获取所有请求头
const headers = req.headers;
console.log('所有请求头:', headers);
// 【代码注释】获取特定的请求头
const userAgent = req.headers['user-agent'];
const contentType = req.headers['content-type'];
const authorization = req.headers['authorization'];
const accept = req.headers['accept'];
const cookie = req.headers['cookie'];
console.log('User-Agent:', userAgent);
console.log('Content-Type:', contentType);
console.log('Authorization:', authorization);
console.log('Accept:', accept);
console.log('Cookie:', cookie);
// 【代码注释】处理不同类型的请求
if (contentType && contentType.includes('application/json')) {
console.log('客户端发送的是 JSON 数据');
} else if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
console.log('客户端发送的是表单数据');
}
res.end('请求头信息已记录到控制台');
});
server.listen(3000);
获取客户端 IP 地址
const http = require('http');
const server = http.createServer((req, res) => {
// 【代码注释】获取客户端 IP 地址
let clientIp = req.socket.remoteAddress;
// 【代码注释】处理 IPv4 映射的 IPv6 地址
if (clientIp.startsWith('::ffff:')) {
clientIp = clientIp.substring(7);
}
// 【代码注释】如果使用了代理,从请求头获取真实 IP
const forwardedFor = req.headers['x-forwarded-for'];
if (forwardedFor) {
clientIp = forwardedFor.split(',')[0].trim();
}
console.log('客户端 IP:', clientIp);
// 【代码注释】获取其他连接信息
const remotePort = req.socket.remotePort;
const localAddress = req.socket.localAddress;
const localPort = req.socket.localPort;
console.log('客户端端口:', remotePort);
console.log('服务器地址:', localAddress);
console.log('服务器端口:', localPort);
res.end(`您的 IP 地址是: ${clientIp}`);
});
server.listen(3000);
获取 URL 查询字符串
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
// 【代码注释】解析 URL
const parsedUrl = url.parse(req.url, true);
// 【代码注释】获取路径名
const pathname = parsedUrl.pathname;
console.log('路径名:', pathname);
// 【代码注释】获取查询参数对象
const query = parsedUrl.query;
console.log('查询参数:', query);
// 【代码注释】访问特定查询参数
const page = parseInt(query.page) || 1;
const limit = parseInt(query.limit) || 10;
const keyword = query.keyword || '';
console.log('页码:', page);
console.log('每页数量:', limit);
console.log('关键词:', keyword);
// 【代码注释】构建响应
const response = {
pathname: pathname,
params: {
page: page,
limit: limit,
keyword: keyword
},
data: [`Item 1`, `Item 2`, `Item ${page}`]
};
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(response, null, 2));
});
server.listen(3000);
// 【代码注释】测试 URL:http://localhost:3000/search?page=2&limit=5&keyword=test
获取请求体信息
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
// 【代码注释】处理 POST 请求
if (req.method === 'POST') {
let body = '';
// 【代码注释】监听 data 事件,接收数据块
req.on('data', (chunk) => {
body += chunk.toString();
console.log('接收数据块:', chunk.length, '字节');
});
// 【代码注释】监听 end 事件,数据接收完成
req.on('end', () => {
console.log('完整请求体:', body);
console.log('请求体长度:', body.length);
// 【代码注释】根据 Content-Type 解析数据
const contentType = req.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
// 【代码注释】解析 JSON 数据
try {
const jsonData = JSON.parse(body);
console.log('JSON 数据:', jsonData);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
status: 'success',
received: jsonData
}));
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
} else if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
// 【代码注释】解析表单数据
const formData = querystring.parse(body);
console.log('表单数据:', formData);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
status: 'success',
received: formData
}));
} else {
// 【代码注释】原始文本数据
console.log('原始数据:', body);
res.setHeader('Content-Type', 'text/plain');
res.end(`接收到的数据: ${body}`);
}
});
} else {
// 【代码注释】处理 GET 请求
res.setHeader('Content-Type', 'text/html');
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>POST 测试</title>
<meta charset="utf-8">
</head>
<body>
<h1>POST 请求测试</h1>
<form method="POST" action="/">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>邮箱:</label>
<input type="email" name="email" required>
</div>
<div>
<label>消息:</label>
<textarea name="message" rows="4" required></textarea>
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
`);
}
});
server.listen(3000);
6.3 设置响应报文
设置响应行
const http = require('http');
const server = http.createServer((req, res) => {
// 【代码注释】设置响应状态码
res.statusCode = 200;
// 【代码注释】设置响应状态描述
res.statusMessage = 'OK';
// 【代码注释】使用 writeHead 同时设置状态码、状态描述和响应头
res.writeHead(200, 'OK', {
'Content-Type': 'text/plain',
'Custom-Header': 'Custom-Value'
});
res.end('响应设置完成');
});
server.listen(3000);
设置响应头
const http = require('http');
const server = http.createServer((req, res) => {
// 【代码注释】设置单个响应头
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*');
// 【代码注释】设置多个相同名称的响应头
res.setHeader('Set-Cookie', [
'sessionId=abc123; HttpOnly; Secure',
'theme=dark; Max-Age=3600'
]);
// 【代码注释】获取已设置的响应头
const contentType = res.getHeader('Content-Type');
console.log('Content-Type:', contentType);
// 【代码注释】删除响应头
res.removeHeader('Cache-Control');
// 【代码注释】检查是否已发送响应头
if (!res.headersSent) {
console.log('响应头尚未发送');
res.setHeader('X-Custom-Header', 'Custom-Value');
}
// 【代码注释】发送响应
res.end(JSON.stringify({ message: '响应头设置完成' }));
});
server.listen(3000);
设置响应体
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
// 【代码注释】设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// 【代码注释】使用 write() 方法多次写入响应体
res.write('<!DOCTYPE html>\n');
res.write('<html>\n');
res.write('<head>\n');
res.write(' <title>Node.js HTTP Server</title>\n');
res.write(' <meta charset="utf-8">\n');
res.write('</head>\n');
res.write('<body>\n');
res.write(' <h1>欢迎使用 Node.js HTTP 服务器</h1>\n');
res.write(' <p>这是一个使用 write() 方法构建的页面</p>\n');
res.write('</body>\n');
res.write('</html>\n');
// 【代码注释】结束响应
res.end();
});
// 【代码注释】流式传输大文件
const fileServer = http.createServer((req, res) => {
const filePath = './large-file.html';
// 【代码注释】设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// 【代码注释】创建文件读取流
const readStream = fs.createReadStream(filePath);
// 【代码注释】将文件流通过响应流发送
readStream.pipe(res);
// 【代码注释】处理错误
readStream.on('error', (err) => {
res.statusCode = 500;
res.end('文件读取失败');
});
});
// 【代码注释】下载文件
const downloadServer = http.createServer((req, res) => {
const filePath = './example.pdf';
const fileName = 'downloaded-file.pdf';
// 【代码注释】设置文件下载响应头
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
// 【代码注释】创建文件读取流
const readStream = fs.createReadStream(filePath);
// 【代码注释】流式传输文件
readStream.pipe(res);
readStream.on('error', (err) => {
res.statusCode = 500;
res.end('文件下载失败');
});
});
// 【代码注释】JSON API 响应
const apiServer = http.createServer((req, res) => {
// 【代码注释】设置 JSON 响应头
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
// 【代码注释】根据请求路径返回不同数据
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
if (pathname === '/api/users') {
// 【代码注释】返回用户列表
const users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
{ id: 3, name: 'Charlie', email: '[email protected]' }
];
res.statusCode = 200;
res.end(JSON.stringify({
status: 'success',
data: users,
total: users.length
}));
} else if (pathname === '/api/error') {
// 【代码注释】返回错误响应
res.statusCode = 400;
res.end(JSON.stringify({
status: 'error',
message: '请求参数错误',
code: 400
}));
} else {
// 【代码注释】404 响应
res.statusCode = 404;
res.end(JSON.stringify({
status: 'error',
message: '接口不存在',
path: pathname
}));
}
});
// 【代码注释】启动不同的服务器
const PORT1 = 3001;
const PORT2 = 3002;
const PORT3 = 3003;
server.listen(PORT1, () => {
console.log(`基础服务器运行在 http://localhost:${PORT1}/`);
});
fileServer.listen(PORT2, () => {
console.log(`文件服务器运行在 http://localhost:${PORT2}/`);
});
apiServer.listen(PORT3, () => {
console.log(`API 服务器运行在 http://localhost:${PORT3}/`);
});
结束响应
const http = require('http');
const server = http.createServer((req, res) => {
// 【代码注释】方式一:只结束响应
res.write('Hello, ');
res.write('World!');
res.end(); // 【代码注释】结束响应
// 【代码注释】方式二:设置响应体并结束响应
res.end('Hello, World!'); // 【代码注释】直接结束响应
// 【代码注释】方式三:发送 JSON 数据并结束响应
const data = {
message: 'Hello, World!',
timestamp: Date.now()
};
res.end(JSON.stringify(data));
// 【代码注释】检查响应是否已结束
if (res.writableEnded) {
console.log('响应已结束');
}
});
server.listen(3000);
7. 实战案例与最佳实践
7.1 文件大小转换工具
功能介绍
文件大小转换工具可以将字节数转换为人类可读的格式(B、KB、MB、GB、TB),这在文件管理、存储分析等场景中非常实用。
实现代码
// 【代码注释】文件大小转换工具
// 【代码注释】文件名: convertbyte.js
/**
* 【代码注释】计算机容量单位转换函数
* @param {number} bytes - 字节数
* @param {number} type - 转换类型:0:不转换 1:KB 2:MB 3:GB 4:TB,默认值是0
* @returns {number} 转换之后的结果
*/
const convertByte = (bytes, type = 0) => {
// 【代码注释】验证输入参数
if (typeof bytes !== 'number' || bytes < 0) {
throw new Error('字节数必须是非负数');
}
if (type < 0 || type > 4) {
throw new Error('转换类型必须在 0-4 之间');
}
// 【代码注释】执行转换计算
return bytes / Math.pow(1024, type);
};
/**
* 【代码注释】自动选择最佳单位的转换函数
* @param {number} bytes - 字节数
* @returns {object} 包含转换结果的详细对象
*/
const autoConvertByte = (bytes) => {
if (typeof bytes !== 'number' || bytes < 0) {
throw new Error('字节数必须是非负数');
}
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
let size = bytes;
let unitIndex = 0;
// 【代码注释】自动选择合适的单位
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return {
original: bytes,
converted: size,
unit: units[unitIndex],
formatted: `${size.toFixed(2)} ${units[unitIndex]}`,
unitIndex: unitIndex
};
};
/**
* 【代码注释】格式化文件大小显示
* @param {number} bytes - 字节数
* @param {number} decimals - 小数位数,默认2位
* @returns {string} 格式化后的字符串
*/
const formatFileSize = (bytes, decimals = 2) => {
if (typeof bytes !== 'number' || bytes < 0) {
throw new Error('字节数必须是非负数');
}
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
// 【代码注释】导出函数
module.exports = {
convertByte,
autoConvertByte,
formatFileSize
};
测试代码
// 【代码注释】测试文件
// 【代码注释】文件名: test.js
const fs = require('fs');
const { convertByte, autoConvertByte, formatFileSize } = require('./convertbyte');
console.log('=== 文件大小转换工具测试 ===\n');
// 【代码注释】测试 convertByte 函数
console.log('1. 测试指定单位转换:');
console.log('1024 字节 =', convertByte(1024, 1), 'KB');
console.log('1048576 字节 =', convertByte(1048576, 2), 'MB');
console.log('1073741824 字节 =', convertByte(1073741824, 3), 'GB');
// 【代码注释】测试 autoConvertByte 函数
console.log('\n2. 测试自动单位转换:');
const sizes = [500, 1024, 1048576, 1073741824, 1099511627776];
sizes.forEach(size => {
const result = autoConvertByte(size);
console.log(`${size} 字节 = ${result.formatted}`);
});
// 【代码注释】测试 formatFileSize 函数
console.log('\n3. 测试格式化输出:');
console.log(formatFileSize(500));
console.log(formatFileSize(1024));
console.log(formatFileSize(1048576));
console.log(formatFileSize(1073741824));
// 【代码注释】测试实际文件大小
console.log('\n4. 测试实际文件大小:');
const filename = './large-file.txt';
const stats = fs.statSync(filename);
console.log(`文件大小: ${formatFileSize(stats.size)}`);
// 【代码注释】测试错误处理
console.log('\n5. 测试错误处理:');
try {
console.log(convertByte(-100));
} catch (error) {
console.log('捕获错误:', error.message);
}
try {
console.log(convertByte(100, 10));
} catch (error) {
console.log('捕获错误:', error.message);
}
使用示例
// 【代码注释】在实际项目中的使用示例
const http = require('http');
const fs = require('fs');
const { formatFileSize } = require('./convertbyte');
const server = http.createServer((req, res) => {
if (req.url === '/file-info') {
// 【代码注释】获取文件信息
const filePath = './example.pdf';
const stats = fs.statSync(filePath);
// 【代码注释】格式化文件大小
const formattedSize = formatFileSize(stats.size);
// 【代码注释】返回文件信息
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
filename: 'example.pdf',
size: stats.size,
formattedSize: formattedSize,
created: stats.birthtime,
modified: stats.mtime
}, null, 2));
} else {
res.setHeader('Content-Type', 'text/html');
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>文件信息</title>
<meta charset="utf-8">
</head>
<body>
<h1>文件信息查询</h1>
<p>访问 <a href="/file-info">/file-info</a> 查看文件信息</p>
</body>
</html>
`);
}
});
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000/');
});
7.2 模块路径实践案例
项目结构示例
project/
├── data/
│ └── data.txt
├── lib/
│ ├── fileHandler.js
│ └── utils/
│ └── stringUtils.js
├── modules/
│ └── converter/
│ ├── index.js
│ └── helpers.js
└── app.js
代码实现
lib/utils/stringUtils.js
// 【代码注释】字符串工具模块
const stringUtils = {
// 【代码注释】首字母大写
capitalize(str) {
if (!str || typeof str !== 'string') return '';
return str.charAt(0).toUpperCase() + str.slice(1);
},
// 【代码注释】反转字符串
reverse(str) {
if (!str || typeof str !== 'string') return '';
return str.split('').reverse().join('');
},
// 【代码注释】截断字符串
truncate(str, length) {
if (!str || typeof str !== 'string') return '';
return str.length > length ? str.substring(0, length) + '...' : str;
}
};
module.exports = stringUtils;
lib/fileHandler.js
// 【代码注释】文件处理模块
const fs = require('fs');
const path = require('path');
const stringUtils = require('./utils/stringUtils');
const fileHandler = {
// 【代码注释】读取文件内容
readFile(filePath) {
// 【代码注释】使用模块路径(相对于当前文件)
const fullPath = path.join(__dirname, filePath);
try {
const content = fs.readFileSync(fullPath, 'utf-8');
return content;
} catch (error) {
throw new Error(`文件读取失败: ${error.message}`);
}
},
// 【代码注释】处理文件内容
processContent(filePath) {
const content = this.readFile(filePath);
const lines = content.split('\n');
return lines.map(line => ({
original: line,
capitalized: stringUtils.capitalize(line),
reversed: stringUtils.reverse(line),
truncated: stringUtils.truncate(line, 10)
}));
}
};
module.exports = fileHandler;
modules/converter/helpers.js
// 【代码注释】转换器辅助函数
const helpers = {
// 【代码注释】字节转换为 KB
toKB(bytes) {
return bytes / 1024;
},
// 【代码注释】字节转换为 MB
toMB(bytes) {
return bytes / (1024 * 1024);
},
// 【代码注释】字节转换为 GB
toGB(bytes) {
return bytes / (1024 * 1024 * 1024);
},
// 【代码注释】自动选择单位
autoConvert(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return {
size: size,
unit: units[unitIndex],
string: `${size.toFixed(2)} ${units[unitIndex]}`
};
}
};
module.exports = helpers;
modules/converter/index.js
// 【代码注释】转换器主模块
const helpers = require('./helpers');
const fs = require('fs');
const converter = {
// 【代码注释】文件大小转换
fileSize(filePath) {
const stats = fs.statSync(filePath);
return helpers.autoConvert(stats.size);
},
// 【代码注释】批量文件大小转换
batchFileSize(filePaths) {
return filePaths.map(path => ({
file: path,
info: this.fileSize(path)
}));
}
};
module.exports = converter;
app.js
// 【代码注释】主应用程序
const fs = require('fs');
const fileHandler = require('./lib/fileHandler');
const converter = require('./modules/converter');
// 【代码注释】使用文件处理模块
console.log('=== 文件处理示例 ===');
try {
// 【代码注释】注意:这里使用的是文件路径,相对于命令行目录
const content = fs.readFile('./data/data.txt', 'utf-8');
console.log('文件内容:', content);
// 【代码注释】使用 fileHandler 模块
const processed = fileHandler.processContent('../data/data.txt');
console.log('处理后的内容:', processed);
} catch (error) {
console.log('错误:', error.message);
}
// 【代码注释】使用转换器模块
console.log('\n=== 文件大小转换示例 ===');
const files = [
'./data/data.txt',
'./lib/fileHandler.js',
'./app.js'
];
files.forEach(file => {
try {
const result = converter.fileSize(file);
console.log(`${file}: ${result.string}`);
} catch (error) {
console.log(`${file}: 文件不存在`);
}
});
// 【代码注释】批量转换
console.log('\n=== 批量转换示例 ===');
const batchResults = converter.batchFileSize(files);
batchResults.forEach(result => {
console.log(`${result.file}: ${result.info.string}`);
});
7.3 核心案例合集(模块 + 发布 CLI)
将 容量换算 发布为可安装的 NPM 包,并注册全局命令 converbyte,是「库 + CLI」的经典组合。
package.json(节选)
{
"name": "converbyte-tool",
"version": "1.0.0",
"description": "字节到 KB/MB/GB/TB 的换算工具",
"main": "index.js",
"bin": {
"converbyte": "./exec.js"
},
"type": "commonjs",
"keywords": ["bytes", "converter", "cli"],
"license": "MIT"
}
【代码注释】
bin对象:键为安装后全局命令名(如converbyte),值为项目内可执行文件相对路径(如./exec.js)。npm install -g .或npm link会在全局bin目录创建符号链接,无需先publish即可本地调试 CLI。main供require('converbyte-tool')引用库;CLI 与库可共用index.js逻辑。- 发布前在
package.json填好files或.npmignore,避免把测试、源码地图上传到 npm。
index.js
const coverByte = (bytes, type = 0) => bytes / 1024 ** type;
module.exports = coverByte;
exec.js
#!/usr/bin/env node
const converbyte = require('./index.js');
const bytes = Number(process.argv[2]);
const type = Number(process.argv[3] ?? 0);
if (Number.isNaN(bytes)) {
console.error('用法: converbyte <字节数> [类型0-4]');
process.exit(1);
}
console.log(converbyte(bytes, type));
【代码注释】
- Shebang
#!/usr/bin/env node:在 Unix/macOS 上让系统用 PATH 里的node执行本文件;Windows 靠 npm 的.cmd包装。 process.argv[0]为 node 路径,[1]为脚本路径,从[2]起才是用户参数(converbyte 1048576 2→ bytes=1048576, type=2)。process.exit(1)表示异常退出,CI/脚本可据此判断失败。Number(process.argv[3] ?? 0):??在未传第三参数时用默认type=0(字节)。
npm link
converbyte 1048576 2
# 输出约 1(MB)
经典场景:运维脚本统计日志目录体积、上传组件显示「已选文件 xx MB」、CI 产物体积门禁。
7.4 浏览器端 HTTP 演示(可运行 HTML)
下面是一个完整 HTML 页面:先启动上一节的 Node 服务(例如 http://localhost:3000),再在浏览器打开本文件,用 fetch 观察请求/响应与状态码(跨域时服务端需设置 Access-Control-Allow-Origin)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>HTTP 客户端演示</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
button { margin: 0.25rem; padding: 0.5rem 1rem; cursor: pointer; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; overflow: auto; border-radius: 8px; }
.ok { color: #4ec9b0; } .err { color: #f48771; }
</style>
</head>
<body>
<h1>HTTP 请求演示</h1>
<p>请先在本机启动 API:<code>node server.js</code>(监听 3000 端口)</p>
<button type="button" id="btn-get">GET /api</button>
<button type="button" id="btn-time">GET /time</button>
<button type="button" id="btn-404">GET /not-exist</button>
<pre id="out">点击按钮发送请求…</pre>
<script>
const out = document.getElementById('out');
const base = 'http://localhost:3000';
async function call(path) {
out.textContent = '请求中…';
try {
const res = await fetch(base + path);
const text = await res.text();
out.innerHTML =
'<span class="' + (res.ok ? 'ok' : 'err') + '">' +
'状态: ' + res.status + ' ' + res.statusText + '</span>\n' +
'URL: ' + base + path + '\n\n' + text;
} catch (e) {
out.textContent = '请求失败(服务未启动或 CORS): ' + e.message;
}
}
document.getElementById('btn-get').onclick = () => call('/api');
document.getElementById('btn-time').onclick = () => call('/time');
document.getElementById('btn-404').onclick = () => call('/not-exist');
</script>
</body>
</html>
【代码注释】
fetch只有网络失败才catch;HTTP 404/500 不会抛错,需看res.ok(true 当且仅当状态码 200–299)。res.text()/res.json()解析响应体;先读 body 再判断业务逻辑是常见模式。- 跨域时浏览器先发 OPTIONS 预检;服务端未返回 CORS 头会表现为
catch里「CORS」错误。 - 按钮
/not-exist演示 404,对应 §5.5 状态码表与 Node 里res.statusCode = 404。
配套最小 server.js(与上文 HTML 联调)
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (req.url === '/api') {
res.end(JSON.stringify({ ok: true, msg: 'API 正常' }));
} else if (req.url === '/time') {
res.end(JSON.stringify({ time: new Date().toISOString() }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found' }));
}
});
server.listen(3000, () => console.log('http://localhost:3000'));
【代码注释】
Access-Control-Allow-Origin: *允许任意网页用fetch访问本 API,仅限本地演示。- 生产应改为具体源,如
https://app.example.com,并配合Access-Control-Allow-Methods、凭证时不能用*。 req.url含路径与查询串;示例只判断路径,未解析?id=1(可用url.parse或new URL)。res.statusCode = 404须在end前设置;JSON 体与Content-Type: application/json成对出现。
7.5 完整 RESTful API 案例(纯 Node.js)
不依赖任何第三方框架,用原生 http 模块实现完整的用户 CRUD 接口。这是理解 Express/Koa 路由机制的必备基础——框架帮你做的,底层都在这里。
API 设计
| 方法 | 路径 | 描述 | 成功状态码 |
|---|---|---|---|
| GET | /api/users | 获取所有用户(支持 ?role= 过滤) | 200 |
| GET | /api/users/:id | 获取单个用户 | 200 / 404 |
| POST | /api/users | 创建用户 | 201 |
| PUT | /api/users/:id | 更新用户信息 | 200 / 404 |
| DELETE | /api/users/:id | 删除用户 | 204 / 404 |
完整实现代码
// 【代码注释】文件:api-server.js
// 纯 Node.js RESTful API,无任何第三方依赖
// 运行:node api-server.js
const http = require('http');
const url = require('url');
// ──────────────────────────────────────
// 内存数据库(重启后清空,仅用于演示)
// 实际项目应替换为 MySQL / MongoDB 等持久化存储
// ──────────────────────────────────────
let users = [
{ id: 1, name: '张三', email: '[email protected]', role: 'admin' },
{ id: 2, name: '李四', email: '[email protected]', role: 'user' },
];
let nextId = 3; // 【代码注释】模拟自增主键
// ──────────────────────────────────────
// 工具函数
// ──────────────────────────────────────
// 【代码注释】统一 JSON 响应封装:设置状态码 + Content-Type + 序列化
function sendJSON(res, status, data) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(data, null, 2));
}
// 【代码注释】读取请求体(HTTP body 是流,需分块拼接后解析)
// Express 的 express.json() 中间件做的正是这件事
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch {
reject(new Error('Invalid JSON'));
}
});
req.on('error', reject);
});
}
// 【代码注释】从路径中提取动态参数 :id
// /api/users/3 → 3
// /api/users → null
function extractId(pathname) {
const match = pathname.match(/^\/api\/users\/(\d+)$/);
return match ? parseInt(match[1]) : null;
}
// ──────────────────────────────────────
// HTTP 服务器 & 路由分发
// ──────────────────────────────────────
const server = http.createServer(async (req, res) => {
// 【代码注释】允许所有源跨域访问(生产环境应限制为指定域名)
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 【代码注释】浏览器发 PUT/DELETE 前会先发 OPTIONS 预检请求
// 服务器必须回 204 才能放行后续正式请求(CORS 协议)
if (req.method === 'OPTIONS') {
res.writeHead(204);
return res.end();
}
const { pathname } = url.parse(req.url);
const method = req.method;
const id = extractId(pathname);
try {
// ── GET /api/users ──────────────────────────
if (method === 'GET' && pathname === '/api/users') {
const { query } = url.parse(req.url, true);
// 【代码注释】支持 ?role=admin 过滤
let result = query.role
? users.filter(u => u.role === query.role)
: users;
return sendJSON(res, 200, { total: result.length, data: result });
}
// ── GET /api/users/:id ──────────────────────
if (method === 'GET' && id !== null) {
const user = users.find(u => u.id === id);
if (!user) return sendJSON(res, 404, { error: `用户 ${id} 不存在` });
return sendJSON(res, 200, user);
}
// ── POST /api/users ─────────────────────────
if (method === 'POST' && pathname === '/api/users') {
const body = await parseBody(req);
// 【代码注释】字段校验:name 和 email 必填
if (!body.name || !body.email) {
return sendJSON(res, 400, { error: 'name 和 email 为必填字段' });
}
// 【代码注释】业务规则:邮箱不允许重复(返回 409 Conflict)
if (users.find(u => u.email === body.email)) {
return sendJSON(res, 409, { error: '邮箱已被注册' });
}
const newUser = {
id: nextId++,
name: body.name,
email: body.email,
role: body.role || 'user',
};
users.push(newUser);
// 【代码注释】创建成功返回 201 Created,响应体含新资源
return sendJSON(res, 201, newUser);
}
// ── PUT /api/users/:id ──────────────────────
if (method === 'PUT' && id !== null) {
const index = users.findIndex(u => u.id === id);
if (index === -1) return sendJSON(res, 404, { error: `用户 ${id} 不存在` });
const body = await parseBody(req);
// 【代码注释】展开合并,但强制保留原始 id,防止客户端篡改主键
users[index] = { ...users[index], ...body, id };
return sendJSON(res, 200, users[index]);
}
// ── DELETE /api/users/:id ───────────────────
if (method === 'DELETE' && id !== null) {
const index = users.findIndex(u => u.id === id);
if (index === -1) return sendJSON(res, 404, { error: `用户 ${id} 不存在` });
users.splice(index, 1);
// 【代码注释】删除成功返回 204 No Content(无响应体,符合 REST 语义)
res.writeHead(204);
return res.end();
}
// ── 未匹配路由 ──────────────────────────────
sendJSON(res, 404, { error: '接口不存在', path: pathname });
} catch (err) {
if (err.message === 'Invalid JSON') {
return sendJSON(res, 400, { error: '请求体 JSON 格式错误' });
}
console.error('[Server Error]', err);
sendJSON(res, 500, { error: '服务器内部错误' });
}
});
server.listen(3000, () => {
console.log('RESTful API 服务器启动:http://localhost:3000');
console.log('\n测试命令(curl):');
console.log(' # 获取全部用户');
console.log(' curl http://localhost:3000/api/users');
console.log(' # 按角色过滤');
console.log(' curl "http://localhost:3000/api/users?role=admin"');
console.log(' # 获取单个用户');
console.log(' curl http://localhost:3000/api/users/1');
console.log(' # 创建用户');
console.log(' curl -X POST http://localhost:3000/api/users \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"name":"王五","email":"[email protected]"}\'');
console.log(' # 更新用户');
console.log(' curl -X PUT http://localhost:3000/api/users/1 \\');
console.log(' -H "Content-Type: application/json" \\');
console.log(' -d \'{"name":"张三(已更新)"}\'');
console.log(' # 删除用户');
console.log(' curl -X DELETE http://localhost:3000/api/users/2');
});
【代码注释】
parseBody手动处理 HTTP 请求流:data事件分块传入,end事件表示完整接收——这就是 Expressexpress.json()中间件的底层逻辑。- 状态码语义严格对应 REST 规范:201 = 新资源已创建;204 = 成功无内容;409 = 业务冲突;400 = 客户端参数错误。
{ ...users[index], ...body, id }合并更新时把id写在最后,防止客户端通过 body 修改主键(安全实践)。- 这个服务器与 Express
app.get/post/put/delete的区别仅在于:Express 帮你封装了路由匹配、body 解析、错误处理中间件;底层 HTTP 语义完全一致。
7.6 静态文件服务器(可直接用于前端开发)
将指定目录作为 Web 根目录,对外提供 HTML、CSS、JS、图片等静态资源访问。这是 vite preview / npx serve dist 命令的简化版原理,也是前端工程师最常用的 Node.js 应用场景之一。
// 【代码注释】文件:static-server.js
// 用法:node static-server.js [端口] [目录]
// 示例:node static-server.js 8080 ./dist
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// 【代码注释】MIME 类型映射表:文件扩展名 → Content-Type
// 浏览器依赖此字段决定如何解析/渲染文件
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.mp4': 'video/mp4',
'.pdf': 'application/pdf',
'.webp': 'image/webp',
};
const PORT = parseInt(process.argv[2]) || 3000;
// 【代码注释】将根目录解析为绝对路径,防止目录遍历攻击时比较出错
const ROOT = path.resolve(process.argv[3] || '.');
const server = http.createServer((req, res) => {
// 【代码注释】decodeURIComponent 解码中文路径,如 /图片/logo.png
const pathname = decodeURIComponent(url.parse(req.url).pathname);
// ── 安全检查:防止目录遍历攻击 ──────────────────
// 攻击示例:GET /../../../etc/passwd
// path.join 会规范化路径,startsWith(ROOT) 确保不逃出根目录
const filePath = path.join(ROOT, pathname);
if (!filePath.startsWith(ROOT)) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
return res.end('403 Forbidden');
}
// 【代码注释】如果请求的是目录,自动寻找 index.html(SPA 常见模式)
let resolvedPath = filePath;
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
resolvedPath = path.join(filePath, 'index.html');
}
// ── 404 处理 ─────────────────────────────────────
if (!fs.existsSync(resolvedPath)) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(`
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>404</title></head>
<body>
<h1>404 - 文件不存在</h1>
<p>路径:${pathname}</p>
<p><a href="/">返回首页</a></p>
</body>
</html>
`);
}
const ext = path.extname(resolvedPath).toLowerCase();
// 【代码注释】未知扩展名回退到 octet-stream,浏览器会触发下载而非直接渲染
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// 【代码注释】设置缓存头:开发时可改为 no-cache;生产静态资源打 hash 后可设更长时间
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Content-Type', contentType);
// 【代码注释】流式传输:边读边写,内存占用与文件大小无关
// 对比 fs.readFileSync() 会把整个文件载入内存,大视频/PDF 会 OOM
const stream = fs.createReadStream(resolvedPath);
stream.on('error', () => {
res.writeHead(500);
res.end('500 Internal Server Error');
});
stream.pipe(res);
// 【代码注释】简单访问日志:时间 + 方法 + 路径 + 类型
console.log(`[${new Date().toLocaleTimeString()}] ${req.method} ${pathname} → ${contentType}`);
});
server.listen(PORT, () => {
console.log(`静态文件服务器启动`);
console.log(` 根目录: ${ROOT}`);
console.log(` 地址: http://localhost:${PORT}`);
console.log(`\n按 Ctrl+C 停止服务器`);
});
经典使用场景:
# 【命令注释】为 Vite/Webpack/Rollup 构建产物提供本地预览
node static-server.js 8080 ./dist
# 【命令注释】为课堂练习 HTML 文件提供本地 HTTP 服务
# (直接用 file:// 打开时 fetch/模块化功能受限)
node static-server.js 3000 ./src
# 【命令注释】等效的一行命令(更便捷)
npx serve dist # 使用 serve 包
npx http-server dist -p 8080 # 使用 http-server 包
【代码注释】
- 目录遍历攻击:
path.join(ROOT, '/../../../etc/passwd')经path.join规范化后为/etc/passwd,不以ROOT开头,被拒绝(403)——这是服务静态文件的必须防御措施。 - MIME 类型:
Content-Type: application/javascript缺失时,现代浏览器会拒绝执行<script type="module">的 JS 文件(MIME 嗅探保护)。 - SPA 路由:React Router / Vue Router 的 history 模式需要服务器将所有路径返回
index.html,可在 404 处理中改为resolvedPath = path.join(ROOT, 'index.html')。 - 流式传输:
createReadStream().pipe(res)是 Node.js 背压(backpressure)机制的实际应用,客户端读慢时自动暂停读文件,不会撑爆内存。
8. 核心概念总结与术语解析
核心概念总结
重要术语解析
模块相关术语
-
CommonJS
- 定义:Node.js 默认的模块系统规范
- 特点:同步加载,适合服务端开发
- 关键字:
require(),module.exports,exports
-
ES Modules (ESM)
- 定义:ECMAScript 标准的模块系统
- 特点:异步加载,同时支持浏览器和 Node.js
- 关键字:
import,export,default
-
模块路径
- 定义:在模块系统中引用其他模块的路径
- 特点:相对于当前文件所在目录,与命令行目录无关
- 示例:
require('./utils/helper')
-
文件路径
- 定义:操作系统中文件的路径
- 特点:相对于命令行当前目录
- 示例:
fs.readFile('./data.txt')
包管理术语
-
NPM (Node Package Manager)
- 定义:Node.js 的包管理工具和在线仓库
- 功能:包的搜索、安装、发布、管理
- 官网:https://www.npmjs.com/
-
package.json
- 定义:Node.js 项目的配置文件
- 作用:定义项目元数据、依赖关系、脚本命令
- 位置:项目根目录
-
dependencies
- 定义:项目生产环境依赖的包
- 安装命令:
npm install package-name - 使用场景:项目运行时必需的包
-
devDependencies
- 定义:项目开发环境依赖的包
- 安装命令:
npm install package-name -D - 使用场景:开发工具、测试框架等
-
语义化版本 (Semantic Versioning)
- 格式:MAJOR.MINOR.PATCH (如 1.2.3)
- 规则:破坏性更新.功能新增.Bug修复
- 符号:
^兼容更新,~补丁更新
HTTP 协议术语
-
HTTP (HyperText Transfer Protocol)
- 定义:超文本传输协议
- 作用:定义客户端与服务器之间的通信规则
- 特点:无状态、基于请求-响应模型
-
请求报文
- 定义:客户端发送给服务器的数据
- 组成:请求行、请求头、空行、请求体
- 方法:GET、POST、PUT、DELETE 等
-
响应报文
- 定义:服务器返回给客户端的数据
- 组成:响应行、响应头、空行、响应体
- 状态码:200、404、500 等
-
URL (Uniform Resource Locator)
- 定义:统一资源定位符
- 组成:协议://主机:端口/路径?查询参数#锚点
- 示例:https://example.com:8080/path?key=value
-
状态码
- 定义:服务器返回的3位数字代码
- 分类:1xx(信息) 2xx(成功) 3xx(重定向) 4xx(客户端错误) 5xx(服务器错误)
- 常见:200 OK、404 Not Found、500 Internal Server Error
Node.js HTTP 服务术语
-
http 模块
- 定义:Node.js 内置的 HTTP 服务器模块
- 作用:创建 HTTP 服务器和客户端
- 导入:
const http = require('http');
-
http.IncomingMessage
- 定义:HTTP 请求对象
- 作用:获取客户端请求信息
- 属性:method、url、headers、httpVersion
-
http.ServerResponse
- 定义:HTTP 响应对象
- 作用:设置服务器响应信息
- 方法:writeHead()、setHeader()、write()、end()
-
端口 (Port)
- 定义:服务器监听的数字标识
- 范围:0-65535
- 常用:80(HTTP)、443(HTTPS)、3000(开发服务器)
最佳实践建议
模块开发
-
使用明确的文件扩展名
// ✅ 推荐 const utils = require('./utils.js'); // ❌ 避免(虽然可以省略) const utils = require('./utils');【代码注释】显式
.js便于工具链与新人阅读;省略扩展名虽合法,但多扩展名并存时易歧义。 -
优先使用 ES Modules
// ✅ 推荐(现代 JavaScript) import { express } from 'express'; // ❌ 传统(CommonJS) const express = require('express');【代码注释】新项目优先 ESM;维护老库时可保留 CJS,或通过
"exports"双格式发布。 -
模块路径使用相对路径
// ✅ 推荐 const helper = require('./utils/helper'); // ❌ 避免(绝对路径) const helper = require('/absolute/path/to/helper');【代码注释】相对路径保证项目可移植;绝对路径换机器即失效。
包管理
-
明确区分依赖类型
# ✅ 生产依赖 npm install express # ✅ 开发依赖 npm install -D jest # ❌ 避免混淆 npm install jest --save【代码注释】测试/构建工具应进
devDependencies,避免生产镜像体积膨胀。 -
使用 package-lock.json
# ✅ 提交 package-lock.json 到版本控制 git add package-lock.json # ❌ 不要忽略 lock 文件 echo "package-lock.json" >> .gitignore【代码注释】锁文件与
npm ci配合,保证 CI 与本地依赖树一致。 -
定期更新依赖
# ✅ 检查可更新的包 npm outdated # ✅ 更新到最新版本 npm update【代码注释】大版本升级前阅读 CHANGELOG,避免破坏性 API 变更。
HTTP 服务开发
-
正确处理错误
// ✅ 推荐:完整的错误处理 const server = http.createServer((req, res) => { try { // 处理请求 res.end('Success'); } catch (error) { res.statusCode = 500; res.end('Internal Server Error'); } }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error('端口已被占用'); } });【代码注释】
createServer内 try/catch 处理同步逻辑;server.on('error')捕获监听失败(如端口占用)。 -
设置合适的响应头
// ✅ 推荐:设置必要的响应头 res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cache-Control', 'no-cache');【代码注释】
Content-Type决定客户端如何解析 body;CORS 头仅在与浏览器跨域联调时需要。 -
使用流处理大数据
// ✅ 推荐:使用流处理文件 const stream = fs.createReadStream('large-file.txt'); stream.pipe(res); // ❌ 避免:一次性读取大文件 const content = fs.readFileSync('large-file.txt'); res.end(content);【代码注释】
pipe边读边写,内存占用稳定;readFileSync会把整个文件载入内存,大文件易 OOM。
总结
通过本指南,我们系统地学习了:
- Node.js 模块系统:CommonJS 和 ES Modules 的使用方法和区别
- NPM 包管理:包的安装、管理、发布等完整流程
- HTTP 协议:请求和响应报文的详细结构和工作原理
- HTTP 服务开发:使用 Node.js 创建和配置 HTTP 服务器
- 实战案例:文件大小转换工具和模块路径的实际应用
- 最佳实践:开发规范和常见问题的解决方案
这些知识点为 Node.js 后端开发打下了坚实的基础,掌握了这些内容,你就可以开始构建实际的 Web 应用和服务了。
9. 知识点归纳速查与行业应用场景
9.1 一图总览(复习用)
9.2 模块系统对照表
| 维度 | CommonJS | ES Modules |
|---|---|---|
| 关键字 | require / module.exports | import / export |
| 加载 | 同步(适合服务端) | 静态分析、可 tree-shake |
| Node 默认 | .js 且无 "type":"module" | .mjs 或 "type":"module" |
| 动态导入 | require() 任意位置 | import() 返回 Promise |
| 经典场景 | Express 传统项目、工具脚本 | 前端构建、新一代全栈框架 |
9.3 NPM 命令速查
| 目的 | 命令 | 备注 |
|---|---|---|
| 装生产依赖 | npm i express | 写入 dependencies |
| 装开发依赖 | npm i -D jest | 写入 devDependencies |
| 按锁文件装 | npm ci | CI 环境推荐,更快更严 |
| 查过期包 | npm outdated | 配合升级策略 |
| 跑脚本 | npm run dev | start 可省略 run |
| 一次性 CLI | npx create-vite@latest | 无需全局安装 |
| 发布 | npm publish | 需官方 registry + 登录 |
9.4 HTTP 与 Node 服务速查
| 概念 | 记忆要点 | Node API |
|---|---|---|
| 请求行 | 方法 + URL + 版本 | req.method, req.url |
| 请求头 | 键值对元数据 | req.headers |
| 请求体 | POST/PUT 常有 | req.on('data') |
| 响应行 | 版本 + 状态码 + 描述 | res.statusCode |
| 响应头 | Content-Type 等 | res.setHeader / writeHead |
| 响应体 | HTML/JSON/流 | res.end() / pipe |
9.5 行业应用场景(技术向)
| 场景 | 用到的本章知识 | 说明 |
|---|---|---|
| 前端工程初始化 | npx、package.json scripts | 一条命令生成 Vite/React 模板 |
| 接口联调 | HTTP 报文、http 或 Express | 理解 401/403/404 与 CORS 头 |
| 私有 NPM 源 | registry 配置、cnpm/nrm | 内网包与公网包隔离 |
| 发布内部 CLI | bin + shebang | 统一团队脚手架、代码生成器 |
| 微服务健康检查 | GET + 200 + JSON | {"status":"ok"} 供网关探测 |
| 静态资源服务 | 文件路径 + createReadStream | 大文件用流,避免内存暴涨 |
9.6 常见坑与对策
| 现象 | 原因 | 对策 |
|---|---|---|
Cannot find module | 路径或 node_modules 缺失 | 检查相对路径;根目录 npm install |
require is not defined | 在 ESM 里用了 CJS | 改用 import 或 .cjs |
读不到 data.txt | 混淆文件路径与模块路径 | 文件操作用 path.join(__dirname, ...) |
| 端口 EADDRINUSE | 3000 已被占用 | 换端口或结束占用进程 |
npm publish 失败 | 用了镜像源 | npm config set registry https://registry.npmjs.org/ |
| 浏览器 fetch 失败 | 未开服务或未配 CORS | 启动 server 并设置 Access-Control-Allow-Origin |
9.7 学习路径建议
- 手写 CommonJS 小模块(工具函数 +
require)。 - 用
npm init -y建工程,区分dependencies/devDependencies。 - 读一个真实项目的
package.jsonscripts,本地npm run跑通。 - 用原生
http写一个返回 JSON 的接口,再用 HTMLfetch调用。 - 尝试
npm link+ bin 发布本地 CLI,理解包与命令的关系。
【代码注释】
- 建议巩固顺序:①
require/module.exports②package.json+npm install③ 原生http.createServer④npm publish或npm linkCLI。 - 已掌握「请求/响应、状态码、CORS、模块路径」后,学习 Express 只是在同一模型上加路由表与中间件链,不会重新学 HTTP。
- 遇
MODULE_NOT_FOUND:查相对路径、node_modules是否安装、是否拼错包名;遇EADDRINUSE:换端口或关掉占用 3000 的进程。 - 与 Day07 的
fs/path、Day09~11 的 Express 形成连续链路,本文是包管理与 HTTP 的枢纽章节。
9.8 安全与质量工具
npm audit — 依赖安全漏洞扫描
npm audit 将项目依赖树与 npm 安全公告数据库(npm Advisory Database)比对,输出已知漏洞报告。这是生产项目必须集成的安全卡口。
# 【命令注释】扫描全部依赖漏洞
npm audit
# 【命令注释】典型输出示例:
# ┌─────────────┬──────────────────────────────────────────┐
# │ moderate │ ReDoS 漏洞(正则表达式拒绝服务) │
# ├─────────────┼──────────────────────────────────────────┤
# │ Package │ [email protected] │
# │ Patched in │ >=1.2.4 │
# │ Dependency │ your-project > some-package │
# └─────────────┴──────────────────────────────────────────┘
# 1 moderate severity vulnerability found
# 【命令注释】自动修复(只升级不引入破坏性变更的包)
npm audit fix
# 【命令注释】强制修复(可能涉及 MAJOR 版本升级,需人工验证)
npm audit fix --force
# 【命令注释】只报告高危及以上漏洞(CI 中常用作合并门禁)
npm audit --audit-level=high
# 【命令注释】仅扫描生产依赖(忽略 devDependencies)
npm audit --omit=dev
# 【命令注释】输出为 JSON(便于 CI 系统解析)
npm audit --json
漏洞级别说明:
| 级别 | 中文 | 建议操作 |
|---|---|---|
| critical | 严重 | 立即修复,考虑临时下线 |
| high | 高危 | 48 小时内升级依赖 |
| moderate | 中危 | 纳入下次迭代修复 |
| low | 低危 | 择机修复 |
| info | 信息 | 关注即可 |
在 CI/CD 中集成:
{
"scripts": {
"pretest": "npm audit --audit-level=high",
"test": "jest"
}
}
【代码注释】:pretest 钩子在 npm test 前自动执行。--audit-level=high 发现高危漏洞时 npm audit 返回非 0 exit code,CI pipeline 自动中断,强制在合并前修复。
.npmrc 配置文件详解
.npmrc 是 npm 的持久化配置文件,支持三级作用域(优先级从高到低):
项目级:<项目根目录>/.npmrc # 只影响本项目
用户级:~/.npmrc # 影响当前系统用户
全局级:$(npm prefix -g)/.npmrc # 影响所有用户(需管理员)
# 【代码注释】.npmrc 完整配置示例
# ── 镜像源 ──────────────────────────────────────────────
# 项目级覆盖,不影响全局 npm 配置
registry=https://registry.npmmirror.com/
# 【代码注释】作用域包单独指向私有源
# @mycompany 开头的包走内网源,其他包走淘宝镜像
@mycompany:registry=https://npm.mycompany.internal/
# ── 认证 ─────────────────────────────────────────────────
# 【代码注释】私有源 Token(CI 环境通过环境变量注入,不硬编码)
//npm.mycompany.internal/:_authToken=${NPM_TOKEN}
# ── 安装行为 ─────────────────────────────────────────────
# 【代码注释】save-exact=true:npm install 时自动锁定精确版本(不加 ^)
save-exact=true
# 【代码注释】engine-strict=true:node/npm 版本不满足 engines 字段时报错
engine-strict=true
常用场景:
# 【命令注释】临时切换源(不修改配置文件)
npm install express --registry=https://registry.npmmirror.com
# 【命令注释】查看当前所有生效配置(含来源文件)
npm config list
# 【命令注释】查看某项配置的值
npm config get registry
【代码注释】
${NPM_TOKEN}语法让 CI 从环境变量读取 Token,避免将密钥提交到 Git——这是私有包认证的最佳实践。- 项目根
.npmrc提交到 Git 可统一团队镜像源,无需每人手动配置;但 Token 行绝不能提交。 save-exact=true配合package-lock.json双保险,彻底锁定版本,适合对稳定性要求极高的项目。
package.json 高级字段
peerDependencies(对等依赖)—— 插件库必备:
{
"name": "my-react-plugin",
"version": "1.0.0",
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
},
"peerDependenciesMeta": {
"react-dom": { "optional": true }
}
}
【代码注释】:peerDependencies 声明「我需要 React,但不自己安装,由使用我的应用提供」。插件类库(eslint-plugin-*、babel-plugin-*、React 组件库)必须使用此字段,否则同一项目中可能出现多个 React 实例,导致 Hook 内部状态错乱(经典 Bug:Hooks can only be called inside of the body of a function component)。
exports 字段(现代包的条件导出):
{
"name": "my-lib",
"version": "1.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}
【代码注释】:
exports是 Node.js 12+ 支持的现代字段,优先级高于main。import条件走 ESM(Vite / 现代打包器使用),require条件走 CJS(Node.js 原生使用),实现双格式发布。- 未在
exports中列出的子路径会返回ERR_PACKAGE_PATH_NOT_EXPORTED,增强封装性(防止用户直接访问内部模块)。 types条件让 TypeScript 自动找到类型声明文件,无需额外配置typings字段。
9.9 常见面试题解析
Q1:CommonJS 和 ES Modules 的核心区别?
| 维度 | CommonJS | ES Modules |
|---|---|---|
| 关键字 | require() / module.exports | import / export |
| 加载时机 | 运行时(动态、按需) | 编译时(静态分析) |
| 值的语义 | 导出值的副本 | 导出值的实时绑定 |
| Tree-shaking | 困难 | 支持(打包器可静态分析未用代码) |
| 动态导入 | require() 任意位置均可 | import() 返回 Promise |
| Node 默认 | .js(无 "type":"module") | .mjs 或 "type":"module" |
// 【代码注释】ESM 实时绑定 vs CJS 值拷贝(经典面试题)
// ── ESM:导出的是变量的"引用",修改可见 ──
// counter.mjs
export let count = 0;
export const increment = () => count++;
// main.mjs
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 ← 实时绑定,能看到变化
// ── CJS:require() 得到的是导出时的值快照 ──
// counter.js
let count = 0;
module.exports = { count, increment: () => count++ };
// main.js
const { count, increment } = require('./counter.js');
increment();
console.log(count); // 0 ← 值拷贝,看不到变化!
Q2:require() 有缓存机制吗?如何清除?
有。Node.js 第一次 require 某个模块时,执行该模块并将 module.exports 缓存到 require.cache 对象中;后续再 require 同路径时直接返回缓存,不重新执行模块代码。
// 【代码注释】require 缓存演示
// counter.js:每次 require 不会重置 count
let count = 0;
module.exports = { get: () => count, inc: () => count++ };
// app.js
const a = require('./counter');
const b = require('./counter'); // 同一对象引用(缓存命中)
a.inc();
console.log(b.get()); // 1 ← a 和 b 指向同一个模块导出对象
// 【代码注释】手动清除缓存(极少使用,常见于热更新实现或测试隔离)
delete require.cache[require.resolve('./counter')];
const c = require('./counter'); // 重新执行,count 归零
console.log(c.get()); // 0
【代码注释】:缓存机制是 Node.js 实现单例模式的天然基础——数据库连接、配置对象等全局共享资源可直接利用此特性。
Q3:npm install 和 npm ci 有什么区别?
| 特性 | npm install | npm ci |
|---|---|---|
| 使用场景 | 日常开发 | CI/CD 流水线、Docker 构建 |
| lock 文件 | 可以创建/更新 | 必须存在且与 package.json 一致 |
node_modules | 增量安装(不删除已有) | 先完全删除再重新安装 |
| 安装速度 | 稍慢 | 更快(跳过版本范围解析) |
| 一致性保证 | 较弱 | 强(100% 按 lock 文件安装) |
| lock 不一致时 | 更新 lock 文件 | 直接报错退出 |
# 【命令注释】CI 环境标准实践
npm ci # 严格按 lock 安装(保证可复现)
npm ci --omit=dev # 跳过 devDependencies(生产镜像)
npm test # 运行测试套件
# 【命令注释】Docker 中的最佳实践
# COPY package.json package-lock.json ./
# RUN npm ci --omit=dev # 每次构建依赖树一致
Q4:什么是语义化版本?^ 和 ~ 的区别?
版本格式:MAJOR.MINOR.PATCH
MAJOR:不兼容的 API 变更 1.x.x → 2.0.0
MINOR:向下兼容的新功能新增 1.0.x → 1.1.0
PATCH:向下兼容的 Bug 修复 1.0.0 → 1.0.1
{
"express": "^4.18.2", // >=4.18.2 <5.0.0 允许 MINOR + PATCH 更新
"lodash": "~4.17.21", // >=4.17.21 <4.18.0 只允许 PATCH 更新
"react": "18.2.0" // 精确版本,不允许任何自动更新
}
【代码注释】:package-lock.json 会将范围锁定到首次安装时的精确版本;npm ci 使用 lock 中的精确版本,因此实际安装结果不受 ^/~ 影响——两者配合才是完整的版本管理方案。
Q5:HTTP 304 状态码的工作原理?
304 Not Modified 是浏览器协商缓存的核心机制,服务器返回 304 时响应体为空,浏览器直接使用本地缓存,节省带宽。
// 【代码注释】Node.js 实现 ETag 协商缓存
const http = require('http');
const fs = require('fs');
const crypto = require('crypto');
http.createServer((req, res) => {
const content = fs.readFileSync('./style.css');
// 【代码注释】对文件内容求 MD5 哈希,内容变化则 ETag 变化
const etag = crypto.createHash('md5').update(content).digest('hex');
// 【代码注释】对比客户端缓存的 ETag 版本
if (req.headers['if-none-match'] === etag) {
res.writeHead(304); // 文件未变化,让浏览器用缓存
return res.end();
}
res.setHeader('ETag', etag);
res.setHeader('Content-Type', 'text/css');
res.end(content); // 200 + 完整内容
}).listen(3000);
Q6:CORS 跨域的本质是什么?如何在 Node.js 中处理?
跨域的本质:浏览器的同源策略(Same-Origin Policy)限制了从 A 域向 B 域发起的 fetch/XMLHttpRequest 请求。CORS(Cross-Origin Resource Sharing)是服务器通过响应头告知浏览器「允许哪些源访问」的机制。
// 【代码注释】Node.js 完整 CORS 处理
const http = require('http');
http.createServer((req, res) => {
// 【代码注释】允许指定域名(生产环境不要用 *,尤其是带凭证的请求)
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
// 【代码注释】OPTIONS 预检请求:浏览器在发 PUT/DELETE/自定义 Header 前自动发出
// 服务器必须回 204 并带上允许信息,浏览器才会发出正式请求
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // 预检结果缓存 24 小时
res.writeHead(204);
return res.end();
}
res.end(JSON.stringify({ data: 'Hello CORS!' }));
}).listen(3000);
CORS 头对照:
| 请求方向 | 请求头 | 响应头 | 作用 |
|---|---|---|---|
| 浏览器 → 服务器 | Origin | Access-Control-Allow-Origin | 来源与允许来源匹配 |
| 预检 → 服务器 | Access-Control-Request-Method | Access-Control-Allow-Methods | 允许的 HTTP 方法 |
| 预检 → 服务器 | Access-Control-Request-Headers | Access-Control-Allow-Headers | 允许的请求头 |
| - | - | Access-Control-Max-Age | 预检结果缓存时间(秒) |
| - | - | Access-Control-Allow-Credentials | 是否允许携带 Cookie |
【代码注释】:CORS 是浏览器行为——Postman / curl 不受同源策略限制,服务器端 Node.js 代码调接口也不受限;只有浏览器中的前端代码才需要 CORS 头配合。
Q7:module.exports 和 exports 的区别?
// 【代码注释】Node.js 源码中,每个模块初始化时执行了:
// const exports = module.exports = {};
// 因此初始状态下两者指向同一对象
// ✅ 正确:通过 exports 追加属性(两者仍指向同一对象)
exports.add = (a, b) => a + b;
exports.sub = (a, b) => a - b;
// require('./math') → { add: fn, sub: fn }
// ✅ 正确:替换整个 module.exports(exports 变量被遗弃,无副作用)
module.exports = function add(a, b) { return a + b; };
// require('./add') → function add
// ❌ 错误:直接替换 exports 变量(断开了与 module.exports 的引用)
exports = { add: (a, b) => a + b };
// require('./broken') → {} ← 拿到空对象!因为 module.exports 没变
// 【代码注释】记忆口诀:
// exports 是 module.exports 的快捷方式,只能"追加"不能"替换"
// 最终 require() 返回的永远是 module.exports 的值
Q8:Node.js 单线程如何处理高并发?
Node.js 使用事件循环(Event Loop)+ 非阻塞 I/O 处理高并发,而不是多线程。
┌───────────────────────────────────────────────────────────────┐
│ Event Loop │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ timers │→│ poll │→│ check │→│ close │→ ... │
│ │setTimeout│ │I/O事件 │ │setImmed │ │callbacks │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────────────┘
↕ (非阻塞)
┌───────────────────────────────────────────────────────────────┐
│ libuv 线程池(默认 4 线程) │
│ 文件 I/O │ DNS 解析 │ 加密运算 │ 压缩解压 │
└───────────────────────────────────────────────────────────────┘
// 【代码注释】1000 个并发请求的处理方式
// ❌ 阻塞模式:每个请求都等待磁盘读取,后面的请求无法响应
const data = fs.readFileSync('./big-file.txt'); // 阻塞事件循环!
// ✅ 非阻塞模式:注册回调后立即返回,事件循环继续处理其他请求
// 磁盘读取完成后,libuv 通知事件循环,回调入队执行
fs.readFile('./big-file.txt', (err, data) => {
res.end(data); // 读完再响应
});
// 这行代码注册回调后立即继续 ↓
// 事件循环继续处理下一个请求(高并发的秘诀)
【代码注释】:Node.js 的并发模型与 Java/PHP 的「一请求一线程」完全不同。JavaScript 业务代码是单线程执行的(避免锁和竞争条件),但 I/O 操作委托给 libuv 线程池异步处理。因此,CPU 密集型任务(大量计算)会阻塞事件循环,应使用 worker_threads 或子进程处理。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/quyixiao/article/details/161323914



