Appearance
npm 与项目结构
npm 是 Node.js 生态的统一入口:管理依赖、定义脚本命令、声明项目元信息。一个 Node.js 项目哪怕只写一个脚本文件,只要引入了第三方包(哪怕只有 commander),就需要 package.json。
初始化:npm init
bash
mkdir service-check && cd service-check
npm init -y # -y 跳过交互问答,直接生成默认 package.json生成的 package.json 最小形态:
json
{
"name": "service-check",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}几个关键字段的含义:
| 字段 | 作用 |
|---|---|
name | 包名,发布到 npm registry 时作为唯一标识 |
version | 语义化版本,1.0.0 |
type | 设为 "module" 时项目用 ESM(import/export) |
main | CommonJS 入口文件 |
exports | ESM 导出映射(比 main 更精确) |
scripts | 项目命令,npm run <name> 执行 |
dependencies | 生产运行时依赖 |
devDependencies | 只在开发和构建时用的依赖 |
engines | 声明要求的 Node.js 版本 |
JSON 不能写注释。字段的含义要么记住,要么查文档——别在 JSON 文件里用 // 或 /**/,它会直接解析失败。
scripts:项目命令
把重复的命令写进 scripts,减少手敲长命令和拼写错误:
json
{
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"check": "node scripts/check.js",
"lint": "eslint ."
}
}执行:
bash
npm start # start/test 是 npm 保留名,可省略 run
npm run dev # 其他脚本必须加 run
npm run checknode --watch 是 Node.js 22+ 内置的文件监视——文件变化自动重启进程,仅用于本地开发。线上进程管理交给 systemd、容器编排或进程管理器,--watch 不参与生产。
依赖管理
bash
# 生产依赖
npm install express
# 开发依赖(-D = --save-dev)
npm install -D nodemon prettier
# 从 package.json 安装所有依赖
npm install
# 生产环境安装——按 lock 文件精确安装,跳过 dev 依赖
npm ci --omit=dev安装后 package.json 会多出:
json
{
"dependencies": {
"express": "^4.21.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}^4.21.0 的含义是"兼容 4.x 的最新版"——npm install 可能拉取 4.21.1 或 4.22.0,但不会跨到 5.0.0。版本号遵循语义化版本(SemVer):主版本.次版本.修订号,^ 允许次版本和修订号升级,~ 只允许修订号升级,不带前缀则为锁定版本。
npm ci 和 npm install 的区别很重要:
| 行为 | npm install | npm ci |
|---|---|---|
| 读什么 | package.json(允许更新 lock) | package-lock.json(严格锁定) |
| 会修改 lock 吗 | 会 | 不会 |
| 删 node_modules 重装 | 不会 | 会 |
| 使用场景 | 本地开发、加依赖 | CI、部署、Dockerfile |
CI 和部署用 npm ci --omit=dev,保证所有环境装的依赖版本完全一致。
lock 文件和 .gitignore
三个文件的职责:
| 文件 | 是否提交 | 作用 |
|---|---|---|
package.json | 是 | 声明直接依赖和 scripts |
package-lock.json | 是 | 锁定整个依赖树的精确版本 |
node_modules/ | 否 | 本地安装的实际代码 |
package-lock.json 必须提交到 Git。它能保证不同机器、不同时间、不同人装出来的依赖完全一样——不提交时,npm install 可能拉取符合 ^ 范围的最新版,导致 CI 上装的版本和本地不同。
.gitignore 至少包含:
gitignore
node_modules/
.env
npm-debug.log*同一个项目只用一种包管理器:package-lock.json、yarn.lock、pnpm-lock.yaml 同时存在时,不同人用不同包管理器的概率很高,最终本地一套、CI 一套、依赖树两套——排查起来很痛苦。
npm / yarn / pnpm
| 工具 | 特点 |
|---|---|
| npm | Node.js 自带,默认选择——运维小工具的首选,零额外依赖 |
| yarn | Facebook 推出的替代品,v1 经典版仍在大量老项目中使用 |
| pnpm | 硬链接共享 node_modules,磁盘占用低,monorepo 场景优势明显 |
运维小工具用 npm 就够了。一个巡检脚本不需要因为"pnpm 更先进"而多装一个工具。
项目目录结构
小项目从最简单的结构开始,文件多了再拆:
text
service-check/
package.json
package-lock.json
index.js # 主入口
src/
config.js # 配置读取和校验
check.js # 核心检查逻辑
scripts/
migrate.js # 一次性辅助脚本命名习惯:
index.js:项目主入口,npm start 默认点src/:核心业务逻辑——函数放在这里,不依赖 CLI 参数scripts/:一次性脚本——数据迁移、批量检查、临时修复
核心逻辑不写 console.log,不读 process.argv。把数据吐给调用方,由 CLI 或 Web 层决定怎么输出。这个拆分的好处不是美观,而是哪天想给它加个 HTTP API 或定时任务入口时,不用重写逻辑。
配置文件读写
配置用 JSON 文件存,脚本里异步读取:
js
// src/config.js
import { readFile } from "node:fs/promises";
export async function loadConfig(file) {
const text = await readFile(file, "utf8"); // 不设 utf8 返回 Buffer
return JSON.parse(text);
}
export function validate(config) {
if (!Array.isArray(config.services)) {
throw new Error("config.services must be an array");
}
}配置里不放密钥。敏感值从环境变量注入,配置文件只放结构、路径和非敏感的默认值。这个习惯对内部工具也一样——某天脚本被复制到另一个仓库或分享给同事时,不会连带发布密文。
engines 字段
json
{
"engines": {
"node": ">=20"
}
}这只是声明意图——npm 默认只在 npm install 时检查(可以关掉),不会在执行 node index.js 时强制校验。真正上线前还是要在部署环节显式检查:
bash
node -v
npm ci --omit=dev
npm start一个完整的最小项目
package.json:
json
{
"name": "service-check",
"version": "0.1.0",
"type": "module",
"scripts": {
"start": "node index.js",
"check": "node index.js nginx sshd"
}
}index.js:
js
// 从 process.argv 取参数——第 0、1 位是 node 路径和脚本路径
const [, , ...services] = process.argv;
if (services.length === 0) {
console.error("usage: npm run check -- <services...>");
process.exit(1);
}
for (const service of services) {
console.log(`checking ${service}`);
}运行:
bash
npm run check -- nginx sshd crond
# -- 之后的参数传给 node index.js,不 -- 时 npm 自己会吞掉这个 -- 分隔符是 npm scripts 的约定,告诉 npm "后面的参数不要自己解析,原样传给底层命令"。忘了加 -- 时参数会被 npm 吞掉,导致 process.argv 里看不到期望的值。