Skip to content

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
mainCommonJS 入口文件
exportsESM 导出映射(比 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 check

node --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.14.22.0,但不会跨到 5.0.0。版本号遵循语义化版本(SemVer):主版本.次版本.修订号^ 允许次版本和修订号升级,~ 只允许修订号升级,不带前缀则为锁定版本。

npm cinpm install 的区别很重要:

行为npm installnpm 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.jsonyarn.lockpnpm-lock.yaml 同时存在时,不同人用不同包管理器的概率很高,最终本地一套、CI 一套、依赖树两套——排查起来很痛苦。

npm / yarn / pnpm

工具特点
npmNode.js 自带,默认选择——运维小工具的首选,零额外依赖
yarnFacebook 推出的替代品,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 里看不到期望的值。