web自学4——依赖管理工具 nvm
在前文中,ES Module 解决了 JavaScript 文件之间如何拆分和引用的问题。如果只引用自己的文件,并且路径都是 ./todo.js 这种明确的相对路径,浏览器可以直接处理。
但是模块化还没有解决另一个问题:如果希望使用别人已经写好的库,应该从哪里下载,怎样记录版本,怎样保证不同机器上的依赖尽量一致?
这就是 npm 主要解决的问题。
第三方依赖
对于简单页面,第三方依赖可以直接通过一个 URL 引入。例如在 HTML 中写:
1 | <script src="https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"></script> |
这种写法的意思是:浏览器直接去这个 URL 下载一份已经构建好的第三方 JS 文件,然后把它作为普通脚本执行。对于临时 demo 或很小的页面,这样做没有问题。
但是项目一旦变复杂,直接引用第三方脚本会遇到几个问题:
- 依赖分散在 HTML 或 JS 文件里,不容易集中查看;
- 版本号写在 URL 中,升级和回退都需要手动维护;
- 多人协作或换机器时,很难保证大家使用的是同一组依赖;
- 一个库可能还依赖其它库,手动处理依赖关系会越来越麻烦;
- 如果项目需要开发工具,例如 Vite、ESLint、TypeScript,也需要有统一的安装和调用方式。
所以对于正式一点的 JavaScript 项目,更常见的做法是使用 package manager 管理第三方依赖。Node.js 生态中最标准的包管理器是 npm,当然现在也有 pnpm、yarn 等替代品。
还有一个名称相似的 nvm,但是它是用来管理和切换 Node.js 版本的官方工具,与 npm 完全不同,而且通常搭配使用。
这里需要区分 Node.js 和浏览器:
- Node.js:开发工具的运行环境,例如 npm;
- 浏览器:网页的运行环境,例如 Chrome、Firefox、Edge。
npm 本身运行在 Node.js 环境中,主要用于开发阶段。最终用户访问网页时不需要安装 npm。
基本使用
npm 安装 package 时,首先要区分本地安装和全局安装。
本地安装是最常用的方式,例如:
1 | npm install lodash |
它会把依赖安装到当前项目的:
1 | node_modules/ |
并写入当前项目的 package.json。这种方式适合项目代码实际依赖的库,例如 lodash、axios、React 等。
全局安装使用 -g 选项:
1 | npm install -g typescript |
它会把命令安装到全局环境中,方便在任意目录执行。例如安装 TypeScript 后,可以直接使用:
1 | tsc --version |
不过现在很多工具并不一定需要全局安装。更常见的做法是把工具安装到当前项目中,然后通过 npm run 调用。除此之外,还可以临时使用 npx 或 npm create,例如:
1 | npx eslint . |
这样可以减少不必要的全局安装,也能让不同项目使用各自需要的工具版本。
package.json
package.json 是 Node.js 项目的配置文件,记录了项目的依赖关系和常用脚本,必须纳入 git 版本管理。
一个简化的例子如下:
1 | { |
注意这里把依赖分成两类:
dependencies:应用本身运行时使用的库,例如 React;devDependencies:开发和构建时使用的工具,例如 Vite、ESLint 等。
此外,还需要重点关注的是 scripts 字段。例如执行:
1 | npm run dev |
本质上只是让 npm 执行 package.json 中名为 dev 的脚本。在上面的例子中,它等价于执行:
1 | vite |
node_modules/ 和 package-lock.json
执行 npm install 之后通常会有:
1 | node_modules/ |
其中
node_modules/是实际安装的依赖,不应该提交到 git;package-lock.json记录实际解析得到的精确依赖树,应该提交到 git。
注意:
package.json记录的是依赖的声明和约束,package-lock.json才是精确的依赖信息。- 克隆一个使用 npm 的项目到本地,只需要重新执行
npm install就可以精确恢复记录的依赖。
常用命令
创建或初始化项目:
1 | npm init |
npm init 会交互式生成 package.json,npm init -y 则使用默认值快速生成。
安装和卸载依赖:
1 | npm install |
其中:
npm install根据package.json和package-lock.json安装当前项目全部依赖到本地的node_modules/;- 项目中的安装和卸载命令都会及时更新
package.json和package-lock.json; - 默认是运行时依赖,加
-D选项则是开发依赖; - 默认项目级别的操作,加
-g选项全局操作。
查看依赖:
1 | npm list |
--depth=0 表示只看顶层依赖,否则依赖树可能非常长。
升级依赖:
1 | npm update |
npm update 会在 package.json 允许的版本范围内更新依赖。如果希望直接安装最新版本,可以显式写 @latest。
执行脚本:
1 | npm run dev |
这些命令本身没有固定含义,具体做什么取决于 package.json 中的 scripts。
本地 JS 如何使用安装的库
学完 npm 的基本用法以后,还会遇到一个关键问题:安装完成以后,本地 JS 文件中应该怎样使用这个库?
例如执行:
1 | npm install lodash |
按照 ES Module 的写法,很自然会想写:
1 | import lodash from "lodash"; |
这里需要区分运行环境。
- 如果这段代码是在 Node.js 中运行,Node.js 知道如何按照 package 解析规则去
node_modules/中寻找lodash,因此这种写法是合理的。(npm 最初就是服务于 Node.js 生态的,Node.js 本身理解 package 名和node_modules/) - 但是浏览器原生只认识相对路径或完整 URL,
"lodash"既不是./lodash.js,也不是https://.../lodash.js。npm 只负责把 lodash 下载到node_modules/并记录依赖关系,并不会让浏览器自动知道"lodash"应该对应哪个文件。
对于浏览器运行的情况,如果不使用构建工具,也有一些补丁性质的办法。但是在稍复杂的前端项目中,更推荐的主流做法是引入构建工具例如 vite 去解决这些问题。
