2、工程化基础:Node.js与npm/yarn/pnpm环境搭建、ES6+模块化规范、CommonJS与ES Module的区别
好,咱们正式开始聊工程化。说实话,很多同学一上来就撸代码,结果项目一大了就各种报错、依赖冲突、模块找不到。我当年也踩过这个坑——记得第一次用Node.js写后端,装了一堆包,结果版本对不上,折腾了一整天。所以,这一章咱们把地基打牢。
2.1 Node.js 环境搭建
Node.js 是什么?说白了,它就是让 JavaScript 能跑在服务器上的一个运行时环境。没有它,前端工程化就是空中楼阁。
我个人习惯用 nvm(Node Version Manager)来管理 Node 版本。为什么?因为不同项目可能依赖不同版本的 Node,你总不能每次都卸载重装吧?
安装 nvm(以 macOS/Linux 为例):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
安装完成后,重启终端,然后:
nvm install 18.16.0 # 安装指定版本
nvm use 18.16.0 # 切换版本
nvm alias default 18.16.0 # 设置默认版本
小提示:Windows 用户可以用 nvm-windows,用法基本一致。我个人建议 Node 版本选 LTS(长期支持版),稳定第一。
2.2 npm / yarn / pnpm 包管理器
装好 Node 之后,npm 就自带有了。但说实话,npm 早期版本确实有点慢,而且 node_modules 那个目录结构……嗯,你懂的。
2.2.1 npm
npm 是 Node 官方自带的包管理器。基本命令大家应该都熟:
npm init -y # 初始化项目,生成 package.json
npm install react # 安装依赖
npm install -g vite # 全局安装
不过,npm 有个问题——它的依赖树是嵌套的。同一个包如果被多个依赖引用,可能会被安装多次,导致 node_modules 体积巨大。我曾经在一个老项目里看到 node_modules 占了 1.2GB……
2.2.2 yarn
Facebook 推出的 yarn,主要解决了 npm 的速度和安全性问题。它用了扁平化的 node_modules 结构,还引入了 yarn.lock 锁定版本。
npm install -g yarn # 安装 yarn
yarn init # 初始化
yarn add react # 添加依赖
我个人觉得 yarn 最大的优点是速度快,尤其是第一次安装依赖时,并行下载确实爽。
2.2.3 pnpm
pnpm 是我现在最常用的。它厉害在哪?它用硬链接和符号链接来共享依赖,而不是每个项目都复制一份。你想想看,如果你有 10 个项目都用 React,pnpm 只在磁盘上存一份,其他项目都是链接过去。这能省多少空间?
npm install -g pnpm
pnpm init
pnpm add react
避坑指南:我曾经在一个 CI/CD 流水线里用 npm,结果因为网络波动导致安装失败。后来换成 pnpm,不仅安装快了,还稳定很多。建议新项目直接上 pnpm。
2.3 ES6+ 模块化规范
模块化,说白了就是把代码拆成一个个小文件,各管各的,互不干扰。ES6 之前,JavaScript 没有官方的模块系统,大家都是靠全局变量或者 IIFE 来模拟。嗯,那会儿写代码真的挺痛苦的。
ES6 引入了 import 和 export,这才是亲儿子:
// math.js
export const add = (a, b) => a + b
export const PI = 3.14159
// app.js
import { add, PI } from './math.js'
console.log(add(2, 3)) // 5
还有默认导出:
// utils.js
export default function log(msg) {
console.log(msg)
}
// app.js
import log from './utils.js'
log('Hello')
注意:ES Module 是静态的,也就是说 import 语句必须写在文件顶部,不能写在条件语句里。这是它和 CommonJS 的一个重要区别。
2.4 CommonJS 与 ES Module 的区别
CommonJS 是 Node.js 默认的模块系统,用 require 和 module.exports。ES Module 是 ES6 标准,用 import 和 export。两者到底有啥区别?我列个表,一目了然:
| 对比项 | CommonJS | ES Module |
|---|---|---|
| 语法 | require() / module.exports |
import / export |
| 加载方式 | 动态加载(运行时) | 静态加载(编译时) |
| 同步/异步 | 同步 | 异步(支持 import() 动态导入) |
| 值拷贝 vs 引用 | 输出值的拷贝 | 输出值的引用(只读) |
| Tree Shaking | 不支持 | 天然支持 |
| 使用场景 | Node.js 服务端 | 浏览器、现代前端工程 |
这里有个关键点:CommonJS 是同步加载的,因为 Node.js 读取本地文件很快,同步没问题。但浏览器里如果同步加载 JS,页面会卡死,所以 ES Module 设计成异步的。
还有一个坑——值拷贝 vs 引用。CommonJS 导出的是值的拷贝,也就是说,如果你在模块内部修改了某个变量,外部不会感知到。但 ES Module 导出的是引用,外部模块看到的是同一个值。举个例子:
// CommonJS
let count = 0
module.exports = { count, increment: () => count++ }
// 外部:count 永远是 0,因为拷贝的是初始值
// ES Module
export let count = 0
export const increment = () => count++
// 外部:count 会变化,因为引用的是同一个变量
我的建议:新项目一律用 ES Module。如果你在 Node.js 里用,记得在 package.json 里加 "type": "module"。老项目如果还在用 CommonJS,也别急着改,稳定优先。
2.5 实战:搭建一个最小工程化环境
说了这么多,咱们动手搭一个。假设你要开始一个新项目:
- 安装 Node.js(推荐 18 LTS)
- 安装 pnpm:
npm install -g pnpm - 初始化项目:
pnpm init - 在 package.json 里加
"type": "module" - 创建
src/index.js,用 ES Module 写代码 - 运行:
node src/index.js
就这么简单。你想想看,以前我们还要配置 Webpack、Babel 才能用 ES Module,现在 Node.js 原生支持了,省了多少事。
避坑指南:我曾经在 Node.js 14 上直接跑 ES Module,结果报错说 import 不支持。后来才发现,Node.js 12+ 才开始实验性支持,14 虽然支持但需要加 --experimental-modules 参数。所以,版本一定要看清楚。
好了,这一章的内容就到这里。工程化基础打好了,后面咱们才能聊 Webpack、Vite、CI/CD 这些高级话题。下一章,咱们聊聊模块打包工具——Webpack 的核心原理。