1、Node.js与npm/yarn/pnpm:前端工程化的基石

说实话,前端工程化这件事,绕不开的第一个坎就是 Node.js。我记得刚入行那会儿,还在用 jQuery 写页面,打包全靠手动合并 JS 文件。后来第一次接触 Node.js,感觉就像打开了新世界的大门——原来前端也能有自己的运行时环境。

Node.js 运行时环境安装与版本管理

Node.js 本质上是一个基于 Chrome V8 引擎的 JavaScript 运行时。说白了,就是让你能在服务器端跑 JS 代码。安装方式很简单,去官网下载安装包就行。但这里有个坑——不同项目依赖的 Node 版本可能不一样。

我曾经接手过一个老项目,它只能跑在 Node 10 上,而我本地装的是 Node 18。一运行就报错,折腾了半天才发现是版本问题。从那以后,我就养成了一个习惯:用 nvm 管理 Node 版本。

nvm(Node Version Manager) 是 Mac/Linux 下的版本管理工具。Windows 用户可以用 nvm-windows。

安装 nvm 后,常用的命令就这几个:

# 查看可安装的版本
nvm ls-remote

# 安装指定版本
nvm install 18.16.0

# 切换版本
nvm use 18.16.0

# 设置默认版本
nvm alias default 18.16.0

嗯,这里要注意:nvm 和 nvm-windows 的命令略有不同。Windows 下用 nvm list 查看已安装版本,Mac 下用 nvm ls。别搞混了。

npm/yarn/pnpm 包管理器对比与选型

Node.js 装好之后,npm 就自动带上了。但市面上还有 yarn 和 pnpm,它们到底有什么区别?我个人的经验是这样的:

特性 npm yarn pnpm
安装速度 较慢 最快
磁盘占用 高(每个项目都复制一份) 低(硬链接共享)
依赖隔离 扁平化(容易冲突) 扁平化 严格隔离(更安全)
lock 文件 package-lock.json yarn.lock pnpm-lock.yaml

为什么会这样?pnpm 的核心优势在于它用硬链接的方式存储依赖。你想想看,如果 10 个项目都用 lodash,npm 会复制 10 份,pnpm 只存一份,其他项目通过硬链接引用。这在磁盘空间上省太多了。

我个人建议:新项目直接用 pnpm。老项目如果已经用了 yarn,也没必要强行迁移。npm 嘛...说实话,除了自带的便利性,现在优势不大了。

package.json 核心字段解析

每个 Node.js 项目根目录下都有个 package.json,它就像项目的身份证。我见过不少新手把这里面的字段搞混,尤其是 dependencies 和 devDependencies。

{
  "name": "my-project",        // 项目名
  "version": "1.0.0",          // 版本号
  "description": "一个示例项目", // 描述
  "main": "index.js",          // 入口文件
  "scripts": {                 // 脚本命令
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {            // 运行时依赖
    "vue": "^3.3.0"
  },
  "devDependencies": {         // 开发依赖
    "vite": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

小技巧:dependencies 是生产环境也要用的,比如 Vue、React。devDependencies 是开发时才需要的,比如构建工具、测试框架。别把 vite 装到 dependencies 里,否则部署到服务器上会多出一堆没用的包。

还有一个容易被忽略的字段是 peerDependencies。我曾经写过一个插件,依赖 Vue 2,但用户的项目用的是 Vue 3。如果不声明 peerDependencies,npm 会同时安装两个版本的 Vue,直接导致运行时崩溃。

node_modules 与依赖管理机制

node_modules 这个文件夹,可以说是前端开发者的噩梦。随便一个项目,node_modules 可能就有几百兆。为什么会这么大?

npm 早期用的是嵌套结构。比如 A 依赖 B,B 又依赖 C,那 node_modules 里就是 A/node_modules/B/node_modules/C。这种结构的问题很明显——路径太深,而且同一个包可能被安装多次。

后来 npm 改成了扁平化结构。所有依赖都尽量放在顶层的 node_modules 里。但这样又带来了新问题:依赖冲突。比如项目依赖 A@1.0 和 B@1.0,而 A 和 B 都依赖 C,但版本要求不同。这时候 npm 只能把其中一个 C 放到子目录里。

避坑指南:我曾经遇到过一个问题,项目本地跑得好好的,部署到 CI 上就报错。查了半天,发现是本地 node_modules 里有个幽灵依赖——某个包没有在 package.json 里声明,但因为扁平化结构被提升到了顶层,代码里直接引用了它。CI 上重新安装后,这个包就不在了。所以,永远不要直接引用未声明的依赖

pnpm 的解决方案就优雅多了。它用硬链接把包存在全局 store 里,node_modules 里只放软链接。这样既节省空间,又保证了依赖隔离——每个项目只能访问自己 package.json 里声明的包。

npx 与全局工具调用

npx 是 npm 5.2 之后自带的一个工具。它的作用很简单:执行 node_modules 里的命令,或者临时下载一个包来执行。

举个例子,你想用 create-react-app 创建一个项目,但又不想全局安装它:

npx create-react-app my-app

这条命令会临时下载 create-react-app,执行完后自动删除。既省了全局安装的麻烦,又不用担心版本污染。

我个人习惯用 npx 来运行项目里的脚本。比如团队里有人用 yarn,有人用 npm,那在文档里写 npx eslint 就比 ./node_modules/.bin/eslint 更通用。

总结一下:Node.js 是前端工程化的根基,nvm 帮你管理版本,pnpm 帮你管理依赖,npx 帮你临时调用工具。这四个东西配合好了,开发体验能提升一大截。

嗯,这一章的内容就到这里。下一章我们会聊聊模块化规范——CommonJS 和 ES Module 到底怎么选?为什么说 ESM 是未来?到时候见。