
模块 4.2:Vite, npm 与前端构建
- 零到全栈
- June 15, 2026
Table of Contents
上一节 4.1,我们给项目做了模块化改造,迈出了"现代前端"的第一步。这一节开始接触工具——
npm、Vite,并认识Node.js。其中 npm 的性价比尤其高:这一节学会,以后无论做前端开发,还是使用各种开源工具,都会用到它。
这一节不再提供新代码——你手上 4.1 模块化改完的那份,就是本节的起点。要做的,全是在它身上敲几条命令、改动
package.json和.gitignore,没有需要你编写的业务代码。
模块化,真的什么都好吗?
上一节我们反复强调了模块化的重要性,以及它如何让代码之间的引用关系变得清晰。但若从开发者的体验出发,模块化改造之后,真的处处都好吗?
其实未必。上一节的内容偏"规矩"(import / export 这套机制),学起来可能略显枯燥;即使没有完全消化,也不要紧——那一节真正需要记住的只有两点:
- 模块化势在必行:前端发展到今天,迟早要走这一步;
- 模块化之后,要浏览网页就必须走服务器——不能再双击
index.html直接打开了。
而其中第 2 点——“看一眼效果都要走一趟服务器”——已经相当影响体验。而且不止这一处:到目前为止,前端这一块至少还剩三件让人难受的事。
先盘一盘:4.1 之后,还剩哪些难受
4.1 我们把项目模块化了,也介绍了它的价值——它为第三方库的大规模调用铺平了道路。但跟着做下来,你应该已经亲身体会到这几件别扭事:
难受一:想看一眼效果,太麻烦了。
模块化之后,代码不能再双击 index.html 打开(ES 模块必须走服务器)。所以在 4.1 里,每改一行、想看效果,都得 push、上服务器 pull、再刷新——一整轮折腾下来,黄花菜都凉了。
难受二:打开一个页面,浏览器要拉一大堆文件。 按 F12 看一眼 Network——十几个请求排成一长队。光 css 就有 8 个文件,每个都单独走一趟网络,再加上几个 js 和 anime.js……浏览器要一个个去取,慢。
难受三:改了 css,刷新却没变。 改完某个 css、保存、刷新,页面却没有动静。你以为是自己改错了,排查半天——其实是浏览器把旧文件缓存住了,根本没去取新的。这个坑,在模块 3.5 部署时就隐约碰到过。
这三件事,靠手动去管是治不利索的。好在——有一类工具,专门解决它们。
有一类工具,专门收拾这些:构建工具
先往深一层看:刚才那三件难受,背后其实是一组矛盾——工程治理、开发体验、用户体验,这三方在互相拉扯。
- 工程治理:希望项目规范、安全、好维护、好扩展(模块化、拆文件,都是为它);
- 开发者:希望写得简单、顺手、舒服;
- 浏览器与最终用户:希望加载更快、资源更少、性能更高。
这三方常常彼此冲突:为了规范,牺牲了开发便利;为了开发便利,又影响运行效率……很难同时满意。那么,有没有办法让三方都尽量满意?
有——让一类工具夹在中间做"翻译":工程的规矩照守;开发者按自己舒服的方式写;最后由它把开发者写的源码,转换成浏览器想要的样子。这个转换过程就叫前端构建,这类工具就叫 构建工具(build tool)。
它具体替你做三件事,恰好一一对上那三件难受:
- 在本地起一个服务器——改完一保存,浏览器自动更新(这叫热更新),不必再走 push、pull、刷新那一整轮;→ 治难受一
- 把一堆碎文件合并成一个——8 个 css 合成 1 个,请求数从十几个降到几个;→ 治难受二
- 给文件名加一段"指纹"(hash)——内容一变,名字就变,浏览器立刻知道需要重新取,缓存便失效了。→ 治难受三
这个构建工具,我们选 Vite
构建工具不止一个。早些年最有名的叫 webpack;而最近几年,前端的首选已经换成了 Vite。
Vite 是个法语词,意为"快",读音是 /viːt/(不要按英文习惯读成 “vait”)。它的作者是中国人——尤雨溪。我们选它,不只是因为作者是中国人,更因为它确实是目前最优秀、最流行的那一个。
接下来要做的,就是把 Vite 请进我们的项目。但在此之前,得先铺一点底——因为 Vite 自己,也是需要"被运行"的。
想跑 Vite,先得有"运行 JS 的环境":Node.js
Vite 这个工具,本身是用 JavaScript 写的。
而此前我们接触的 JavaScript 都在浏览器里运行。Vite 不是网页,它是一个命令行工具,需要在浏览器之外、在你的电脑上运行。那么,由谁来运行它?
打一个你可能熟悉的比方:要运行一个
.py文件,得先装 Python;要打开一个.xlsx文件,得先装 Excel。同样的道理——要在浏览器之外运行 JavaScript,就得先装 Node.js。
Node.js 就是"让 JavaScript 跳出浏览器、在你电脑上直接运行"的运行环境。 装了它,Vite 这类用 js 写成的命令行工具才跑得起来。
此外,Node 还附赠了一个非常重要的东西——一个包管理工具,叫 npm。
这个 npm,你先可以理解成一个"JavaScript 的应用商店"——与模块 2、3 里在 Ubuntu 上用过的 apt 是一个意思。apt 帮你装 nginx、装 git;npm 帮你装各种现成的 js 工具和库。待会儿要的 Vite,就用 npm 来装。(之所以说"先",是因为它其实还不止是个商店——这一点等用到时再说。)
先把 Node 装上
按你的系统选择对应方式:
- macOS:去 nodejs.org 下载 LTS 版安装包,双击安装(或
brew install node)。 - Windows:去 nodejs.org 下载 LTS 版
.msi,一路下一步。 - Linux:用 NodeSource 走 apt(与模块 3.5 装 nginx 同一思路):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs
装完后新开一个终端验证:
node -v # 看到 v20.x.x 之类
npm -v # 看到 10.x.x 之类
两条都能输出版本号,就说明安装成功。npm 随 Node 一起安装,不必单独安装;而且这一次装好,以后所有项目都可以使用。
npm 和 apt 有个关键区别:它"只管这一个项目"
虽然 npm 和 apt 都是"应用商店",但有一处重要的不同:
- apt 给整台电脑装东西——装一个 nginx,全系统都能用。
- npm 默认给"当前这一个项目"装东西——这个项目用 anime.js 4.4,那个项目用 anime.js 3.0,两边各装各的、互不干扰。
正因为是"按项目来管",使用前得先明确告诉它:这里有一个项目。这个动作叫初始化。进入上一节做过模块化改造的那份项目的根目录(与 index.html 同级),运行:
npm init -y
运行完,目录里会多出一个文件 package.json,大致如下(你的内容会与此略有出入,下面会解释原因):
{
"name": "zero-to-tech",
"version": "1.0.0",
"description": "> 这是 零到全栈 · 模块 4.1 …… 的配套代码—— > 网站模块化改造之前的起点版本。",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
这个 package.json,就是项目的**“npm 档案”**——npm 以后给它装了什么、它能运行哪些命令,都记在这里。
这个文件的后缀名是
.json,是本课程中第一次见到这个后缀的文件,顺便介绍一下这种格式。JSON 是一种用纯文本承载数据的通用格式——它就是一堆"键": 值,用大括号括起来。人一眼能读懂,机器也便于解析,全世界的程序都用它来存数据、传数据。你不需要学怎么写它,认得出这个"键: 值"结构就够了。
npm init有黑魔法吗?没有。 它做的唯一一件事,就是生成这样一份package.json。你完全可以手动新建一个相同的文件(你在 4.1 手动新建过main.js,是一样的操作)——只要内容是合法 JSON,npm 就认(最小的合法写法就是一对大括号{})。加-y只是图省事,顺手填上几个默认值而已。
字段看着不少,但不必被它唬住——对我们这个项目而言,真正要管的只有两个,其余的全都用不上。
现在真正要管的,就这两个:
name:项目叫什么。默认取你的文件夹名——所以你看到的多半是zero-to-tech。它只是个名字,对我们的项目并不关键。version:项目自己的版本号,默认1.0.0,可改可不改,也不关键。
剩下那一堆——description、main,有的版本还会有 type、keywords、author、license——这里不打算逐个细讲,因为对我们这个网页项目,它们全都用不上:要么是"将来把项目发布成 npm 包"时才用到,要么只是默认占位。
既然用不上,那就直接删掉。package.json 就是个普通文本文件,手动删几行完全没问题(npm 也不要求这些字段必须存在)。把 name、version 之外的都删掉,只留这两行:
{
"name": "zero-to-tech",
"version": "1.0.0"
}
一句话收尾:删完,这份档案清清爽爽,但目前基本还是空的——没记什么有用的东西。不必着急,等下真正用 npm 装了东西,npm 会自动往这个干净的文件里写入有用的内容,那时这本账才算开始有用。
装 Vite
有了 npm、有了 package.json 这本档案,安装 Vite 只需一行命令。在项目根目录(与 index.html、package.json 同级)运行:
npm install -D vite
注意这个
-D(即--save-dev):意思是"把 Vite 装成开发依赖"。为什么强调它?因为 npm 装的东西分两类——一类是在项目里帮你"干活"的工具,例如 Vite。它好比盖房子用的脚手架:帮你把活干完,但本身不会成为房子的一部分——上线的网站里并没有 Vite。这种"脚手架"性质的,安装时加
-D。另一类是会真正成为网站一部分的东西,例如等会儿要装的第三方动画库。它是盖房子的砖,会跟着房子一起上线。这种安装时不加
-D。两类的处理方式不同,等会儿装 anime 时会再说明。
命令运行完,发生了两件事——
第一,目录里多出一个 node_modules 文件夹。 Vite(连同它依赖的一大堆包)都被下载、堆放在这里。这个文件夹通常很大、文件极多——一般不必去翻它,知道"用 npm 装的东西都在这儿"即可。
你可能会疑惑:只装了一个 Vite,为什么这么多文件?因为 Vite 自己也依赖别的包,那些包又依赖别的包……一层层牵连进来一大串,全都会被装进
node_modules。你可以在 npmgraph.js.org 输入vite看一看它的依赖网,就能理解node_modules为何这么大了。
第二,package.json 有了变化,同时还多出一个 package-lock.json。
你的 package.json 现在多了一段 devDependencies:
{
...
"devDependencies": {
"vite": "^7.0.0"
}
}
这一段是 npm 自动加上的——含义是"这个项目用了 vite,版本 ^7.0.0"(^ 表示"7 这个大版本里的最新,但不跳到 8",相当于帮你锁住版本范围)。这就是那本账开始记账了。
不过 devDependencies 只记录我们直接安装的这一个 vite;它所牵连的那些"连环依赖"不记在这本账上——它们各自记在自己的 package.json 里。
与此同时出现的
package-lock.json,把这次实际安装的每个包的精确版本逐一锁死,确保你换一台电脑、或同事拉下来npm install时,装出来的东西完全一致。你不必读懂它,知道它在"锁死版本"、并且需要提交到 git 就够了。
Vite 装哪去了?怎么运行它?
我们说 Vite 装进了 node_modules。但若去翻,你会发现那里有两个与 vite 相关的东西,二者别混淆:
node_modules/vite/—— 一个文件夹,是 vite 的代码本体,即"vite 这个库住在哪儿"。node_modules/.bin/vite—— 一条可执行命令,即"把 vite 运行起来的那个开关"。
为什么有这两个?因为有些 npm 包(如 vite)不只是"供你 import 的代码",它还对外提供一条命令行命令。这类包会在自己的 package.json 里声明一个 bin 字段;npm 见到后,安装时就在 node_modules/.bin/ 下放一个指向命令入口的快捷方式(专业说法叫"符号链接"),并标记为可执行。所以:
想看 vite 的代码 →
node_modules/vite/; 想运行 vite 这条命令 →node_modules/.bin/vite。
跑起来:开发服务器 + 打包
dev:随改随看
Vite 这条命令在 node_modules/.bin/vite,直接用这个路径运行它:
./node_modules/.bin/vite
Vite 当场在本地起了一个服务器,并给出一个地址 http://localhost:5173。
顺带认识一个词:
localhost。它看着像域名,其实代表的就是你自己这台电脑——所以这个地址的意思是"用 http 访问本机的 5173 端口",并未走出本机。
打开它——网站跑起来了(点导航在两个页面之间切换,也都正常)。更重要的是:改一行代码、一保存,浏览器自动就刷新了,不必手动、更不必部署。这就是热更新。
难受一,解决了。 4.1 里看一眼效果要 push、pull、刷新一整轮;现在改完转眼就能看到。开发反馈从"以分钟计"变成"以秒计"。
build:打包成上线版本
“合并文件、加 hash"这两件,则是在上线打包时完成的。先用 Control + C(macOS)停掉开发服务器。
Windows 的 cmd 用 Ctrl + C;Windows 的 PowerShell 用 Ctrl + Shift + C。
./node_modules/.bin/vite build
运行完,多出一个 dist 文件夹:
dist/
├── index.html
└── assets/
├── main-Beya6efK.css ← 你那 8 个 css,合并成了这 1 个
└── main-_go62SDo.js ← 你那几个 js 模块,合并成了这 1 个
- 难受二,解决了。 8 个 css 合并成 1 个、几个 js 合并成 1 个,请求数从十几个降到几个。(你可能注意到:anime.js 此刻还是单独从 CDN 拉取的一个请求——这条尾巴,留到本节最后再收。)
- 难受三,解决了。 注意文件名上挂的那串"乱码”
main-Beya6efK.css——它就是 hash,由文件内容计算而来。内容一改,名字就变,浏览器一看名字不同,立刻知道"需要重新取",不会再拿旧缓存敷衍你。
你可能还会发现:
dist里怎么只有index.html、没有text-lab.html?这是因为 Vite 打包时默认只认根目录那一个index.html作为入口。要让两个页面都打包进来,需要给 Vite 加一个"我有两个入口"的配置——但这一步本节不展开。原因是:下一节我们会把项目改成 React,届时两个页面会合成一个index.html(页面之间靠组件切换),这个"多入口"的麻烦根本就不存在了。所以这个问题先留着,交给 React 去收。
preview:上线前本地验一眼
dist 是打包好的产物,但不要双击 dist/index.html——它里头仍是 <script type="module">,依然不能用 file:// 双击打开(与 4.1 同理,模块必须走服务器)。要在本地查看打包结果是否正确,用:
./node_modules/.bin/vite preview
它会起一个服务器,专门伺服 dist。打开后按 F12 看 Network——干净利落,只有几个请求。
注意:
preview看的是dist里的真实产物。而上面说过,dist里只有index.html、没有text-lab.html——所以在 preview 中点导航,是切不到"文字实验室"那一页的(这正好印证了上面那个"多入口"的坑)。
(补充一句:打开
dist/index.html会看到 Vite 给 script 加了crossorigin标记,那是正经走 http 时的 CORS 处理,此处不必深究。)
一条要记住的规矩:源代码 ≠ 运行的代码
到这一步,你手上其实有了两份东西:
你维护的,是源代码——
js/、css/那些,文件多、好读、用名字 import。 真正上线运行的,是dist——文件少、经过压缩、改了名,是浏览器认得的形态。 这是两份不同的东西,别混为一谈。
由此可得几个直接的结论:
- 改网站永远改源代码,然后重新 build。不要手动改
dist里的文件——下一次 build 就会把它覆盖掉。 dist才是真正要送上线的那一份。dev用于"写代码时"(运行源码 + 热更新,图改得快);build产出的dist才是"给生产服务器运行的版本"。也就是说——下一个模块(4.6 部署)真正推上线的,正是这种dist。preview只是上线前在本地先验一眼。
./node_modules/.bin/vite 太长了:npm run 来帮忙
每次都敲 ./node_modules/.bin/vite 这一长串,实在啰嗦。
还记得 package.json 里那个 scripts 吗?它正是为此而生——给常用命令登记一个简短别名。把它改成这样:
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
以后就不必敲那一长串了,直接:
npm run dev # = ./node_modules/.bin/vite
npm run build # = ./node_modules/.bin/vite build
npm run preview # = ./node_modules/.bin/vite preview
npm run xxx 的意思是"运行 scripts 里那个叫 xxx 的命令"。运行时,它会先去 node_modules/.bin 里查找有没有对应的命令——因为那里正好有 vite,所以这里写 vite 就行,不必写全路径。
这里要解开前面那个扣子。 前面我们说 npm 是个"应用商店",那它怎么还能运行我项目里的命令?这就是前面那个"先"字的伏笔:npm 不止"装、卸"这一手。apt 确实只管装卸软件,但 npm 还兼了一份"项目管家"的职责——因为它认
package.json这本项目档案(前面就说过,它"记着这个项目能运行哪些命令")。 所以npm run dev运行的,并不是 npm 自己的某个功能,而是你写在scripts里的那条命令(vite)——npm 只是翻开package.json、照着上面写的替你执行一遍。一句话:npm run运行的是"你的"命令,npm 不过是个照本宣科的执行者。
那
dev/build/preview这三个名字是固定的吗?不是,是我们自己起的。scripts本质上就是一张"命令快捷方式表"——键(名字)可以随便起,值是任意一条命令(哪怕与 js 毫无关系也行,npm 只是把它交给终端去执行)。例如你完全可以加一条"hello": "echo 你好",然后运行npm run hello。
至此,这一节的三条命令你都见过了,整理如下:
| 命令 | 作用 | 何时使用 |
|---|---|---|
npm run dev | 起本地服务器,随改随看、热更新 | 开发时,写代码全程 |
npm run build | 打包成 dist/ | 上线前,生成给浏览器运行的版本 |
npm run preview | 本地起服务器预览 dist | 上线前自测,确认产物是否正确 |
.gitignore:有些东西,不该提交
到这里,目录里多了两个又大又"沉"的文件夹:node_modules 和 dist。它们要不要提交到 Git?——不要。 它们都是**“随时能重新生成"的产物**:
node_modules:照着package.json,运行一次npm install就能重装出来。dist:照着源代码,运行一次npm run build就能重新打包出来。
既然随时可重建,提交进 Git 就是白占空间、还拖慢同步。我们用 .gitignore(模块 3.3 学过的"忽略名单”)把它们挡在门外——在根目录新建 .gitignore,写上:
node_modules
dist
这与 3.3 是同一个道理:Git 里提交的,永远是"源头"——你的代码,加上
package.json/package-lock.json这两本账;而不提交"产物"(装出来的库、打包出来的 dist)。别人拿到源头,npm install+npm run build,就能原样复现一切。(至于
dist究竟怎么送上线——是推源码让服务器自己 build,还是本地 build 好只推 dist——这是 4.6 部署那一节的内容,到时再细说。)
One more thing:既然有了 npm,让它来管依赖
到这里,构建这套东西你已经全部见过了。但还记得前面留的那条尾巴吗——anime.js 至今仍是从一个 CDN 网址拉取的。
回头看 cards.js、score.js 顶上那一行:
import { animate, stagger } from "https://cdn.jsdelivr.net/npm/animejs@4/+esm";
这是 4.1 留下的写法。它能跑,但有两点并不踏实:
- 每次打开页面,都要联网去 jsdelivr 那台别人的服务器拉取 anime——一旦没网、或那个网址哪天失效,动画就会失灵;
- 网址里的
@4指"最新的 4.x",哪天它小版本一更新,页面可能毫无征兆地改变行为。
现在情况不同了——我们有 npm 了。 既然 npm 能把库装到本地、还能锁版本,那 anime 也没必要再拴在别人的网址上。安装它:
npm install animejs
注意这条命令——它与前面装 Vite 那条几乎一模一样,但角色截然不同。 看一眼
package.json,anime 落进的是dependencies,而非 vite 所在的devDependencies:{ "dependencies": { // ← 库:网站"运行时"真正要用的 "animejs": "^4.4.1" }, "devDependencies": { // ← 工具:只在"开发 / 构建时"帮忙的 "vite": "^7.0.0" } }这两段的区别,正是本节最值得带走的一个直觉——
devDependencies是"脚手架"(Vite 帮你把房子盖好,盖完即撤,上线的网站里没有 Vite);dependencies是"房子的砖"(anime 会被打进最终产物、随之上线,网站运行时确实在用它)。我们特意把这两个安装动作隔了大半节课,就是想说明:它俩看着像,实则一个是工具、一个是库。
装进来之后还差最后一步——仅仅装进来还不算用上。回到 cards.js、score.js,把那行长网址换成一个干净的名字:
import { animate, stagger } from "animejs";
这个
"animejs"是个"光秃秃的名字",浏览器原本并不认——但 Vite 会在背后把它翻译成node_modules里的真实位置。(这一步千万别漏:只npm install而不改 import,代码用的仍是 CDN 那一份,装到本地的等于白装。)
改完,再 npm run build 一次,此时 dist 里那个 js 已经不一样了——anime.js 也被打了进去。于是:
- 那条单独连 CDN 的请求,消失了;
- 你的网站,不再依赖任何别人的服务器,版本也牢牢锁在自己手里。
4.1 留下的那条 CDN 尾巴,到这里就收干净了。
这节课结束时,你至少应该理解什么
- 4.1 之后还剩三件难受:看效果要部署一整轮、打开页面十几个请求、改了 css 被缓存坑——它们都需要一类工具来收拾。
- 构建工具:夹在"工程治理 / 开发体验 / 用户体验"三方之间做翻译——把开发者舒服的写法,转换成浏览器要的样子;它替你"起本地服务器、合并文件、加 hash",最流行的一个叫 Vite(作者尤雨溪)。
- Node.js:让 JavaScript 跳出浏览器、在你电脑上运行的环境(如同跑
.py要装 Python、开.xlsx要装 Excel);它附赠 npm——一个"JS 应用商店",与 apt 同义,但按项目管理。 npm init就是生成一份package.json(手写一个也行);其中name/version保留,main/type/description等用不上、直接删掉,留一个清爽的空账本。npm install把东西下载进node_modules,并自动记进账;你的账只列直接依赖,连环依赖不记在你头上;package-lock.json锁定精确版本、需要提交 git。- 如何运行装好的工具:原始写法是
./node_modules/.bin/vite;给scripts登记别名后,用npm run dev / build / preview(这三个名字是我们自己起的,scripts是一张自由的命令别名表)。 - 源代码 ≠ 运行的代码:你维护源码,上线运行的是
dist;node_modules和dist都是可重建的产物,写进.gitignore、不提交。 dependenciesvsdevDependencies:库(运行时要用、会上线)vs 工具(开发时帮忙、不上线)——这也是为什么 anime 进前者、Vite 进后者。
留个尾巴:复用这件事,还是没解决
回顾这两节:4.1 用模块化治了顺序坑、全局污染;4.2 请来 Vite,把"看一眼太慢、请求太多、缓存坑"全收拾了,最后还顺手把 anime 收进本地、甩掉了 CDN。工程化这套地基,至此铺平了。
但回头看,仍有两笔账模块化和构建都没有碰,且根子在同一处——我们有两个 html 页面:
- 那条导航栏,在
index.html和text-lab.html里一字不差地各写了一遍(模块化能复用 js 逻辑,却复用不了这一整块 HTML 结构); - 刚才打包时也撞见了——两个 html 入口,build 还得专门配置才打得全。
这两笔账,根都在"两个独立的 html"。而下一节那个新角色,恰好把它一并解决——它叫 React:它会把两个页面合成一个 index.html,页面之间靠"组件"切换。于是导航写一次便处处可用,多入口的麻烦也随之消失。
那是下一节的事了。