Skip to content

NPM与发布 npm 包(Day8 2/4) ​

本页对应 Day08_Node,共 6 段视频,约 44 分钟。

学习目标 ​

  • 能够使用 npm install 包名@版本号 安装指定版本的包,并通过 package.json 与运行结果验证版本生效。
  • 能够使用 npm remove 包名 删除本地包、使用 npm remove -g 包名 删除全局包,并验证删除后依赖与命令均不可用。
  • 能够使用 npm outdated 检查可更新包,使用 npm update 包名 更新包,并说明版本范围与锁版本对更新结果的影响。
  • 能够按 npm 与 Git 协作流程完成项目初始化、忽略 node_modules、克隆项目后执行 npm install,并在拉取更新后重新安装依赖以恢复项目运行。
  • 能够在 package.json 的 scripts 中配置命令别名,使用 npm run 别名 执行脚本,并说明 start 别名可简写为 npm start。
  • 能够安装并使用 cnpm 通过淘宝镜像初始化项目、安装依赖,并验证其与 npm 命令的基本对应关系。

知识点 ​

本节回顾清单 ​

js
// 1. CommonJS
// 1.1 模块中暴露数据
module.exports
exports

// 1.2 如何导入模块(引用模块)
require() // 返回值就是模块暴露的数据

// 1.3 模块文件的扩展名
// .js .json .node 其他 目录
// 模块路径省略扩展名

// 2. ESM
// 2.1 暴露数据
// ① 暴露单个数据
export default 数据;

// ② 暴露多个数据
export 变量声明语句
export 变量声明语句
export 变量声明语句
// 或者
export { 变量名, 变量名, 变量名, 变量名 }

// 2.2 导入模块
// ① 如果模块暴露单个数据
import 变量名 from '模块名'

// ② 如果模块暴露多个数据
import { 变量名, 变量名 } from '模块名'
import * as 变量名 from '模块名'

// 2.3 开启 ESM 模块规则
// 方式一:脚本扩展改为 .mjs
// 方式二:package.json 配置 {"type":"module"}

// 3. 模块的路径 文件的路径
// 文件的路径,使用相对路径 参照命令行所在的目录
// 模块的路径,使用相对路径 参照所在文件的目录,跟命令行所在目录无关!

CommonJS 的暴露与导入 ​

为什么需要: Node 早期没有浏览器那套模块语法,代码要拆分、复用,就必须有“导出”和“导入”的约定。CommonJS 就是这套约定。

怎么想: 一个文件就是一个模块。模块内部把要给别人用的数据挂到 module.exports 上;另一个文件用 require() 拿到这个返回值。

怎么做:

js
// a.js
const name = '高小乐'
module.exports = { name }

// b.js
const data = require('./a')
console.log(data.name)

exports 也可以挂属性,例如 exports.name = name。但注意:exports 只是 module.exports 的引用,不能直接写 exports = { name } 来替换整个导出对象。

易错点: require() 的返回值就是对方暴露的数据,不是“模块对象”本身;module.exports 和 exports 混用时,以最终 module.exports 指向为准。

CommonJS 的扩展名与目录 ​

为什么需要: 写 require('./a') 时,我们不想每次都写 .js,Node 需要有一套查找规则。

怎么想: 模块路径可以省略扩展名,Node 会按 .js、.json、.node 等规则尝试;如果给的是目录,还会找目录下的入口文件。

怎么做:

js
const a = require('./a')       // 可能找到 a.js
const data = require('./data') // 可能找到 data.json

易错点: 省略扩展名不是“随便什么文件都能省”,要符合 Node 的解析规则;.json 被 require 时返回的是解析后的对象。

ESM 的暴露 ​

为什么需要: ESM 是 ES6 标准模块语法,浏览器和现代 Node 都支持。它让导出更静态、更清晰。

怎么想: 只给一个主数据,用 export default;要给多个具名数据,用 export 声明或统一 export { ... }。

怎么做:

js
// 单个默认导出
export default function () {}

// 多个具名导出
export const a = 1
export const b = 2
// 或
const c = 3
const d = 4
export { c, d }

易错点: 一个模块只能有一个 export default;export { 变量名 } 不是对象字面量,不能写成 export { a: 1 }。

ESM 的导入与开启规则 ​

为什么需要: 导出后要能按默认导出、具名导出分别导入;同时 Node 默认按 CommonJS 解析 .js,必须显式开启 ESM。

怎么想: 默认导出用任意变量名接;具名导出必须用花括号且名字对应;全部导入用 import * as。开启 ESM 有两条路:.mjs 或 package.json 的 "type":"module"。

怎么做:

js
import fn from './a.js'
import { a, b } from './a.js'
import * as mod from './a.js'
json
{ "type": "module" }

转写稿里有一个调试过程:安装最新 chalk 后写 const chalk = require('chalk'),运行报错,提示不支持 require ES Module。推导结论是:新版 chalk 只支持 ESM,而当前 index.js 是 CommonJS,所以不能直接用 require 导入。解决方向不是硬改 require,而是切换模块规则(.mjs 或 "type":"module"),或者使用仍支持 CommonJS 的旧版本。

易错点: 只改文件扩展名或只加 "type":"module" 后,文件里的 import/export 才会按 ESM 解析;CommonJS 的 require 不能直接导入 ESM 模块。

模块路径与文件路径 ​

为什么需要: 相对路径到底相对谁,是初学者最容易混淆的点。模块路径和文件路径的参照物不同。

怎么想: require('./x') 是模块路径,相对“当前文件所在目录”;fs.readFile('./x') 这类文件路径,相对“命令行所在目录”,也就是进程工作目录。

怎么做:

js
// 模块路径:相对当前文件
const x = require('./utils/x')

// 文件路径:相对命令行目录
fs.readFile('./data.txt', ...)

易错点: 在项目根目录运行和在子目录运行,文件相对路径结果可能不同;模块相对路径不受命令行目录影响。文件路径建议拼成明确的绝对路径,避免“换个目录运行就找不到文件”。

先弄清:为什么要指定版本 ​

为什么需要:npm install 包名 默认装最新版,但最新版不一定兼容你当前项目。转写稿里的例子很典型:安装最新版 chalk 后,用 require('chalk') 运行就报错,因为新版只支持 ES6 模块,而当前项目还在用 CommonJS。

怎么想:先判断这个包是“命令行工具”还是“项目代码依赖”。chalk 是给代码引入的,所以装到本项目,不是全局。若最新版不兼容,就查它的历史版本,找仍支持 CommonJS 的版本。

怎么做:先安装并复现报错:

bash
npm install chalk

代码中这样引入会报错:

js
const chalk = require('chalk')

删除后改装指定版本:

bash
npm remove chalk
npm install [email protected]

安装后 package.json 的依赖里会记录版本,例如 "chalk": "^4.1.0"。再运行:

bash
node index.js

易错点:@ 后写版本号;删除包用 npm remove 包名,全局包要加 -g;^4.1.0 表示允许更新到 4.x,不会随便升到 5.x。

删除包:本地和全局要分清 ​

为什么需要:装错包、包不再使用、或为了换版本,都要删除。删除后应同时从 node_modules 和 package.json 依赖中消失。

怎么想:先看它是项目依赖还是全局命令。项目依赖直接删;全局工具必须加 -g。

怎么做:

bash
npm remove 包名
npm remove 包名 -g

转写稿中还删除了全局安装的 nodemon:

bash
npm remove nodemon -g

易错点:全局包不加 -g,只会去本项目里找,删不掉全局命令;删除后命令报“找不到”是正常现象。

更新包:先看 outdated,再理解锁版本 ​

为什么需要:包会修复 bug、增加功能,需要知道哪些能更新。但不是所有包都能直接升大版本。

怎么想:先用 npm outdated 查看哪些包可更新,再决定是否 npm update。如果 package.json 写的是 ^4.1.0,更新通常只在 4.x 内进行,不会升到 5.x。

怎么做:

bash
npm outdated
npm update 包名
npm update 包名 -g

转写稿中执行更新后,chalk 仍在 4.x,因为 ^4.1.0 起了锁主版本作用。更新后再运行 node index.js 验证项目是否正常。

易错点:把 npm update 当成“必然升到最新大版本”是错的;大版本升级可能破坏兼容性,要结合项目模块规范判断。

npm 与 Git 协作:不要提交 node_modules ​

为什么需要:node_modules 文件多、体积大,提交到 Git 会浪费仓库和网络。协作时只同步源码和 package.json。

怎么想:别人克隆项目后没有 node_modules,直接运行会报“找不到模块”。这时应根据 package.json 重新安装依赖。

怎么做:先初始化并写忽略文件:

bash
git init

.gitignore 中写:

text
node_modules/

提交、推送后,别人克隆下来先执行:

bash
npm install

也可以简写:

bash
npm i

转写稿还演示了:删掉 node_modules 后运行报错,执行 npm install 后依赖恢复;每天拉取新代码后,如果对方新增了包,也要再执行一次 npm install。

易错点:克隆后直接 node index.js,往往因缺依赖失败;忽略目录末尾斜杠表示忽略整个目录。

配置命令别名:把长命令放进 scripts ​

为什么需要:启动命令可能很长,参数很多,每次手写容易错。package.json 的 scripts 可以配置别名。

怎么想:把常用命令写进 scripts,用 npm run 别名 执行。start 是特殊别名,可以省略 run。

怎么做:

json
{
  "scripts": {
    "start": "node index.js",
    "build": "node index.js"
  }
}

运行:

bash
npm run start
npm start
npm run build

易错点:只有 start 通常可直接写 npm start;其他别名一般要写 npm run 别名。拿到别人项目时,先看 scripts 能推断启动方式。

cnpm 淘宝镜像:源不稳定时的替代方案 ​

为什么需要:npm 官方源服务器在国外,网络不稳定时安装可能卡住。国内镜像会同步 npm 包,提高下载稳定性。

怎么想:推荐方式一是额外安装一个 cnpm 命令,不改变原来的 npm;方式三是直接改 npm 的 registry 配置。

怎么做:全局安装 cnpm:

bash
npm install -g cnpm

之后用 cnpm 安装:

bash
cnpm install
cnpm install lodash

查看当前源:

bash
npm config get registry

易错点:cnpm 是全局命令,别人同步你的项目不会自动拥有它,需要自己安装;cnpm 与 npm 用法基本一致,但不要混淆“多一个命令”和“改掉 npm 源”这两种方式。

本节围绕 npm 包的版本管理、删除更新、与 Git 协作、命令别名和国内镜像展开。核心不是“会敲几个命令”,而是理解:package.json 记录依赖,node_modules 只是安装结果;版本号决定代码能不能被当前项目正确加载。

为什么安装包要指定版本:从 chalk 报错说起 ​

为什么需要:npm 默认安装最新版,但最新版不一定兼容你的代码。转写中安装 chalk 后,用 CommonJS 的 require 引入却报错,原因是新版 chalk 只支持 ES6 模块,而当前项目仍用 CommonJS。
怎么想:遇到“新版本不兼容”,有两条路:改项目模块规范,或退回支持 CommonJS 的旧版本。课堂选择后者,因为改动小、能快速验证。
怎么做:先安装最新版观察问题,再卸载并安装指定版本:

bash
npm install chalk
npm remove chalk
npm install [email protected]

代码中仍可这样使用:

js
const chalk = require('chalk')

console.log(chalk.red('高小乐'))
console.log(chalk.bgRed('爱吃鱼'))

运行 node index.js,能看到红字和红底输出。
易错点:@ 后写版本号;卸载后 node_modules 和 package.json 依赖会同步变化;^4.1.0 不是永远锁死 4.1.0,而是允许 4.x 范围内更新,所以更新后仍可能停留在 4.x。全局安装也可指定版本,加 -g 即可。

删除包与更新包:别把本地和全局混了 ​

为什么需要:装错的包、试验用的包要清理;依赖升级也要可控。
怎么想:先判断包装在哪里。项目依赖用本地删除,命令行工具如 nodemon 往往是全局安装,必须全局删。
怎么做:

bash
npm remove 包名
npm remove nodemon -g

更新前先看哪些包可更新:

bash
npm outdated
npm update chalk

易错点:全局包不加 -g,只会去本项目里找,删不到;npm update 受 package.json 版本范围限制,不一定跨大版本升级。删完全局 nodemon 后命令找不到,是正常现象。

npm 与 Git 协作:node_modules 不入库 ​

为什么需要:node_modules 文件多、体积大,提交到 Git 会浪费仓库和流量。真正需要同步的是源码和 package.json。
怎么想:依赖清单在 package.json,别人拿到项目后按清单安装即可。
怎么做:在 .gitignore 写:

text
node_modules/

克隆项目后先执行:

bash
npm install

它会把 package.json 里的依赖全部装上。若队友更新代码并新增了包,你拉取后也要再 npm install。
易错点:克隆后直接 node index.js 常会报“找不到模块”,因为还没有 node_modules;不要手写提交 node_modules;npm install 可简写 npm i。

配置命令别名:把长命令收进 scripts ​

为什么需要:启动命令可能很长,尤其后面接一堆参数,每次手敲容易错。
怎么想:package.json 的 scripts 就是命令别名表。
怎么做:

json
{
  "scripts": {
    "serve": "node index.js",
    "start": "node index.js"
  }
}

运行:

bash
npm run serve
npm start

start 是特殊别名,可省略 run;其他名字一般要 npm run 名字。
易错点:改了别名后旧名字立即失效;看别人项目时,先看 scripts 能推断启动方式;非 start 不要想当然写成 npm serve。

使用 cnpm 淘宝镜像:解决下载慢和卡住 ​

为什么需要:npm 官方源服务器在国外,网络不稳时 npm install 可能很慢甚至卡住。国内镜像会同步包,提升稳定性。
怎么想:方式一安装全局 cnpm,保留原来的 npm 命令,二者并存,最稳妥。
怎么做:按镜像站方式一全局安装 cnpm 后,用 cnpm 执行同类命令:

bash
cnpm init
cnpm install lodash
cnpm install uniq

易错点:cnpm 是全局命令行工具,队友不会因为你提交项目就自动拥有,需要自己安装;镜像同步有短暂延迟;若直接改 npm registry,会影响原 npm 命令,方式一更不容易混淆。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:安装指定版本 + CommonJS 引入 chalk(4.x) ​

bash
# ① 直接装最新版:装到 5.x,只支持 ESM,CommonJS 里 require 会报错
npm install chalk

# ② 卸掉
npm remove chalk

# ③ 改装指定版本:4.x 仍支持 CommonJS
npm i [email protected]

# ④ 全局安装同样可以指定版本
npm i -g [email protected]
js
// index.js —— 项目未配置 {"type":"module"},默认走 CommonJS
const chalk = require('chalk');

console.log(chalk.red('高小乐'));      // 红字
console.log(chalk.bgRed('爱吃鱼'));    // 红底
bash
node index.js
json
// 安装后 package.json 自动记录
{
  "dependencies": {
    "chalk": "^4.1.0"
  }
}

要点解释

  • 包名@版本号 就是安装指定版本,npm i 是 npm install 的简写;全局安装再加 -g。
  • 转写稿里的报错原因:新版 chalk 只提供 ES Module,require() 导入 ESM 会直接抛错;要么改 .mjs / 配 "type":"module",要么退到支持 CommonJS 的 4.x。
  • 易错点:npm remove 会同时清掉 node_modules 里的目录和 package.json 里的依赖记录,所以删完要重新 npm i [email protected]。
  • ^4.1.0 的 ^ 是“锁大版本”,后续 npm update 也只会升到 4.x,不会跳到 5.x——这正是它还能继续跑 CommonJS 的原因。

示例二:package.json 配置命令别名(scripts) ​

json
{
  "name": "product01",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "serve": "node index.js",
    "build": "node index.js",
    "start": "node index.js"
  },
  "dependencies": {
    "chalk": "^4.1.0"
  }
}
bash
npm run serve     # 执行 node index.js
npm run build     # 同上,只是别名不同
npm start         # start 是 npm 特批的,可以省掉 run
npm run start     # 写法等价
npm run test      # 执行默认那条 echo

要点解释

  • scripts 里键名可以随便取(serve、build 都行),值是真正要执行的命令;npm run 别名 就会执行对应命令。
  • 唯一例外是 start:npm start 可以省略 run,其他别名(如 build)写成 npm build 是无效的,必须 npm run build。
  • 为什么要别名:转写稿里说得很清楚,以后接 webpack 之类,命令后面参数一长串,手敲不现实;另外别人拿到你的开源项目,打开 package.json 看 scripts 就知道项目怎么启动。
  • 易错点:别名里的命令是在项目根目录执行的,node index.js 的路径要相对 package.json 所在目录写。

示例三:git + npm 协作流程(忽略 node_modules → install → 更新) ​

bash
# ① 项目初始化并建忽略文件
git init
gitignore
# .gitignore —— 结尾斜杠表示忽略整个目录及其下所有内容
node_modules/
bash
# ② 提交与推送
git add -A
git commit -m "项目初始化"
git remote add origin <远程仓库地址>
git push

# ③ 别人克隆下来后:一定没有 node_modules,第一步必须装依赖
git clone <远程仓库地址>
cd product01
npm install        # 简写 npm i,按 package.json 把依赖全部装回来
node index.js

# ④ 日常:看哪些包能更新、更新、再跑
npm outdated           # 列出可更新的包(全局的加 -g)
npm update chalk       # 更新本项目里的包(受 ^ 约束,不会跨大版本)
npm update -g nodemon  # 全局包更新/删除都要带 -g
npm remove -g nodemon  # 全局包删除,不带 -g 只会在本项目里找

# ⑤ 国内镜像(方式一):全局装一个 cnpm 命令,npm 本身不受影响
npm install -g cnpm --registry=https://registry.npmmirror.com
cnpm -v
cnpm install lodash
cnpm install uniq

要点解释

  • node_modules 绝不进 git(体积大、浪费流量),所以必须有 .gitignore;同步出去的只有源码和 package.json。
  • 克隆后不装依赖直接 node index.js 必然报“找不到模块”,因为 node_modules 压根没同步过来;npm install 就是按 package.json 的 dependencies 把包装回来。
  • 队友新增了包并 push 之后,你 git pull 只会拿到新代码和新 package.json,所以每次拉完代码都要再 npm install 一次,否则新代码里 require 的包本地没有,照样报错。
  • 易错点一:npm remove nodemon(不加 -g)删的是本项目的包,全局包纹丝不动,命令还能用;必须 npm remove -g nodemon。
  • 易错点二:npm outdated 看到有新版本不代表 npm update 会升上去,^ 只允许小版本升级,想跨大版本得重新 npm i 包名@版本号。
  • 易错点三:cnpm 是“多出来的一个命令行工具”,只装在自己机器上,不会写进项目;用方式三(直接改 registry)则会改变 npm 本身的下载源。

常见错误 ​

  1. 现象:npm i chalk 装完最新版,代码里 require('chalk') 直接抛 ERR_REQUIRE_ESM,整个脚本跑不起来。 原因:chalk 5.x 起只发 ESM,CommonJS 的 require 无法加载;而项目本身还是 CJS(没 .mjs、package.json 也没 "type":"module")。 修法:先 npm rm chalk,再 npm i [email protected] 这类仍兼容 CJS 的旧主版本;或者反过来,把项目整体切到 ESM 再装新版。装之前先去包主页看它的版本线和模块格式,不要默认"最新版最好"。

  2. 现象:npm rm nodemon 执行成功,但终端里 nodemon 命令照样能用;或者相反,想删本地依赖却把全局工具删了。 原因:本地依赖和全局包是两套目录,-g 决定删哪一套,很多人凭手感敲。 修法:删本地就 npm rm 包名,删全局工具就 npm rm -g 包名;不确定装在哪,用 npm ls 包名 和 npm ls -g 包名 各查一次再动手。

  3. 现象:npm update chalk 跑完了,npm outdated 再看,当前版本还是 4.1.x,没升到 5.x。 原因:package.json 里记的是 ^4.1.0,update 只在 ^ 允许的范围内升,不会跨主版本。 修法:要跨主版本必须显式指定 npm i [email protected]。反过来说,^ 正是防止依赖被意外大版本升级搞崩的保护,别为了"升级"去手改它。

  4. 现象:从远程仓库 git clone 下来,目录里没有 node_modules,直接 node index.js 报 Cannot find module。 原因:.gitignore 忽略了 node_modules,仓库里只同步源码和 package.json。 修法:进项目第一件事 npm install(简写 npm i),让它按 package.json 把依赖补齐,然后再运行。这不是异常,是所有 Node 项目的标准开场。

  5. 现象:昨天还能跑的项目,今天 git pull 完再跑就报缺包,报的还是自己没写过的包名。 原因:同事新装了依赖并提交了 package.json,但 node_modules 不在版本控制里,你拉下来的只是代码。 修法:养成"拉代码后立刻 npm i"的肌肉记忆,每次同步后都跑一遍,别等报错才想起来。

  6. 现象:scripts 里配了 "build": "node index.js",终端敲 npm build 提示找不到命令。 原因:只有 start 和 test 是 npm 内置可省略 run 的,其他别名必须带 run。 修法:npm run build;想让入口命令短一点,就把启动脚本命名为 start,之后 npm start 即可。

练习 ​

  1. 你的项目是 CommonJS(index.js 用 require),现在要引入 chalk 做彩色输出。写出完整的"装错→发现→回退→验证"四步操作,并说明每一步你在看什么信息来判断下一步。

  2. 描述一个场景:什么时候你会选择 cnpm 而不是 npm?如果你用了方式三(直接改 registry)之后想切回官方源,命令是什么?为什么讲师更推荐方式一?

  3. 你接手一个陌生的开源项目,只有仓库地址。写出从零到"本地跑起来"的完整命令序列,并说明你是从哪里得知"该怎么启动这个项目"的。

  4. 判断对错并说明理由: ① npm update 可以把任意包升到最新版; ② 全局安装的包也会写进 package.json 的 dependencies; ③ npm install 在 node_modules 已存在时执行是浪费时间的多余操作。

  5. 团队里有人把 node_modules 提交进了 Git,仓库体积暴涨。写出修复步骤(含 .gitignore 内容),并说明新同事克隆后要做什么才能跑起来。

原视频对照 ​

#视频时长
1npm-安装指定版本的包07:54
2npm-删除包01:14
3npm-更新包02:24
4npm&git开发流程21:06
5配置命令别名04:54
6cnpm淘宝镜像07:23

小结 ​

  1. "最新版"不是默认正确选项——版本号是指定行为的手段,遇到模块格式不兼容时,降版本比改整个项目的模块体系成本低得多。
  2. 安装、删除、更新三个动作都要先问一句"作用域是本地还是全局",-g 加错比命令敲错更难排查。
  3. ^ 前缀是保护而不是障碍:它让 npm update 只能在小版本内漂移,跨主版本必须由人显式决定。
  4. package.json 是项目的依赖契约,node_modules 是可再生的产物——所以前者进仓库,后者永远不进。
  5. "克隆后先 npm i、拉代码后先 npm i"应当成为条件反射,它解决的是"代码对了但环境没跟上"这一类问题。
  6. 拿到陌生项目时,读 package.json 的 scripts 比读源码更快知道怎么启动;命令别名就是给长参数起的快捷键。
  7. 镜像源解决的是网络稳定性问题,不改变 npm 的语义,可以在 npm / cnpm 之间无缝切换,不必有心理负担。
  8. 判断一个包装对了没有,不看安装时的输出,而看三处:node_modules 里有没有、package.json 里记没记、代码跑起来报不报错。