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 引入了 importexport,这才是亲儿子:

// 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 默认的模块系统,用 requiremodule.exports。ES Module 是 ES6 标准,用 importexport。两者到底有啥区别?我列个表,一目了然:

对比项 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 实战:搭建一个最小工程化环境

说了这么多,咱们动手搭一个。假设你要开始一个新项目:

  1. 安装 Node.js(推荐 18 LTS)
  2. 安装 pnpm:npm install -g pnpm
  3. 初始化项目:pnpm init
  4. 在 package.json 里加 "type": "module"
  5. 创建 src/index.js,用 ES Module 写代码
  6. 运行: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 的核心原理。