学 JavaScript 的过程中,很多人一直停留在最舒服的姿势:写一个 HTML,script 标签里塞满代码,双击 index.html 就能看效果。这个阶段确实不需要任何工程化工具,直接双击浏览器就能跑,可是当你想引入别人的库、想把代码拆成几个文件、想把自己的练习项目真正整理成能上线的东西时,原本的“写死”做法就会冒出一堆问题。这一节要聊的就是 JavaScript 基础课程里的“工程化分水岭”——npm 和 Vite。
我在带新人时最常看到的情况是:JavaScript 语法学得还不错,但一进到真实项目,看到 npm install、package.json、node_modules、npm run dev、npm run build 这些词就发懵。其实你不用把它们想得多玄,npm 是管理 JavaScript 项目依赖的工具,Vite 是帮你启动开发环境和打包项目的工具。这篇文章不追求讲完所有高级配置,只希望带你跑通一条主链路:从创建一个项目、安装依赖、启动开发服务器、拆分模块,最后得到一份可以部署的打包产物。
1. 从“双击 HTML 文件”到工程化:先搞清楚 npm 和 Vite 解决什么问题
1.1 只靠浏览器打开 HTML 时,你迟早会遇到三个坎
第一阶段用 <script> 标签写 JavaScript 很顺手,但代码量到了两三百行之后,第一个坎就来了:所有函数和变量都会挂在同一个全局作用域里。你引用的两个 JS 文件里如果有同名变量,后加载的那个会覆盖前面那个,而且报错位置往往不是真正出错的位置。为了理清依赖顺序,你只能手工调整 script 标签的前后顺序,这种维护方式在项目稍微大一点后非常脆弱。
第二个坎出现在你开始用 ES Modules,也就是 import 和 export 语法之后。现代浏览器原生支持 type="module",但这里有个让很多初学者卡住的细节:直接双击本地 HTML 文件时,很多浏览器会因为 file:// 协议下的跨域限制,拒绝加载本地模块。你明明写了一手正确的语法,浏览器却告诉你 CORS 错误。想要避开这个限制,就得在本地起一个开发服务器,从 http://localhost 去访问页面。
第三个坎是你想使用第三方的 JavaScript 库。过去最常见的做法是去 CDN 网站复制一段 script 标签进来,这当然能用,但版本怎么锁定、下载失败怎么办、代码在本地怎么调试、后续怎么升级,都成了令人头疼的问题。而 npm 和 Vite,恰恰是分别解决“依赖管理”和“本地开发与构建”这两件事的。
1.2 npm:JavaScript 世界的“应用商店”
npm 的全称是 Node Package Manager,它做的事情可以理解成给 JavaScript 项目装第三方“插件”。你只需要执行 npm install dayjs,它就会把 dayjs 这个库下载到项目里的 node_modules 目录,同时把记录写到 package.json 文件里。
这解决了什么问题?第一,依赖来源清晰,package.json 加上 package-lock.json 就能准确描述项目依赖了哪些包以及什么版本;以后团队里别人把代码拉下来,一条 npm install 就能还原整个依赖环境。第二,更新版本时不用再去 CDN 网站手动替换链接,在 package.json 里改版本号,或者执行 npm install dayjs@最新版本号 就完成了。
可能有人会问:“JavaScript 基础阶段有必要学这个吗?”我的看法是,越早学越值得。因为后面的 Vue、React、Node.js 后端开发,几乎全部建立在 npm 的依赖管理方式之上。如果每次都靠浏览器里粘贴 CDN 链接,那等于一直没离开温室玩法。
1.3 Vite:既是开发服务器,也是打包器
Vite 这个名字来源于法语“快”的意思。初学阶段你需要知道它承担两个角色。
开发时,npm run dev 启动的是一个开发服务器,它利用浏览器原生 ES Module 的特性,只加载当前页面真正用到的文件,而不是把你整个项目一次性翻译编译完,所以启动速度非常快;你修改代码后,它又能通过热更新机制让浏览器立刻反应,不需要手动刷新页面。
发布时,npm run build 会执行构建过程,把源码压缩、兼容、合并成适合部署到服务器的静态文件。有人会疑惑:“浏览器不是已经支持模块了吗?为什么还要打包?”因为并不是所有浏览器都支持源码里那种 import dayjs from 'dayjs' 的写法,更不可能让浏览器直接去 node_modules 里找依赖。Vite 在构建阶段会把模块依赖关系梳理好,最后生成普通的 JS/CSS/HTML 静态资源,让任意静态服务器都能承载。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:Node 装好后,为什么总是弹出各种 npm 报错
2.1 先确认 Node 版本,别急着开跑
使用 npm 和 Vite 前,先要安装 Node.js。Node.js 是 JavaScript 的运行环境,npm 会随 Node 一起装到电脑里,所以打开命令行后,可以先用两条命令确认环境到底行不行。
bash复制node -v
npm -v
如果你准备使用新版 Vite,注意 Node 版本不能太老。Vite 5 要求 Node.js 18 以上,Vite 6 建议使用 18+ 或 20+。建议到 Node.js 官网直接下载 LTS(长期支持版本),不要长期停留在 14、16 这些已经相对落后的版本上。
另外,如果工作中需要在不同项目间切换 Node 版本,可以考虑使用 nvm-windows 或 nvm 这类版本管理工具。初学阶段不必强求,但知道有这么个工具存在,遇到“这个项目必须用 Node 16”的情况时不会慌张。
2.2 Windows 上最常见的“禁止运行脚本”问题
如果你在 Windows 的 PowerShell 里执行 npm 命令,可能看到一整段红色报错:
text复制npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
很多新手第一次看到这个会以为 npm 装坏了,其实不是。npm 是通过 npm.ps1 这个 PowerShell 脚本来执行的,而 Windows PowerShell 默认的执行策略是 Restricted,也就是不允许运行本地脚本。这本来是防止恶意脚本执行的一种安全措施,但它把正常的 npm 脚本也一起拦下来了。
解决办法通常有两种。一种是用管理员身份打开 PowerShell,执行下面的命令,把执行策略改成 RemoteSigned:
powershell复制Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
RemoteSigned 的含义是:本地创建的脚本可以运行,从互联网下载的脚本必须有可信签名。这比直接把策略改成 Unrestricted 要安全得多,也是很多前端开发者在 Windows 上推荐的配置。
另一种办法是换一个终端,比如在 VSCode 里把终端从 PowerShell 切换成 Command Prompt(cmd),或者使用 Git Bash。cmd 下执行的不是 .ps1 脚本,所以不会触发这个策略限制。我个人的经验是,Windows 环境下开发 Node.js 项目,不止一次遇到过 PowerShell 执行策略相关的怪问题,与其每次折腾,不如直接把代码编辑器里的默认终端调整为 cmd 或 Git Bash,至少在入门阶段会省心很多。
2.3 “npm 不是内部或外部命令”是什么原因
还有一类高频报错是:
text复制npm 不是内部或外部命令,也不是可运行的程序或批处理文件。
或者 PowerShell 下提示:
text复制npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
这基本说明系统在 PATH 环境变量里找不到 npm。最常见的原因是安装 Node.js 时没有勾选“Add to PATH”选项,或者你明明装了 NVM 但还没有主动切换到某个 Node 版本。处理思路分情况:
- 如果没装 Node,重新安装并确保把 Node.js 加入 PATH;
- 如果装了多版本管理工具,先执行
nvm list看看有哪些版本,再nvm use 版本号切换一遍; - 如果确实需要手动改环境变量,在系统环境变量的 Path 中加入 Node.js 安装目录,例如
C:\Program Files\nodejs\。
另外,Windows 项目路径里尽量避免中文、空格和特殊符号。这个建议在入门阶段听起来很啰嗦,但实际遇到原生模块编译失败、路径解析异常时才会意识到它有多值钱。比如路径里带空格可能导致某些工具链在拼接命令时出错,这类问题排查起来会比业务代码 bug 更让人头疼。
2.4 看到 warn、deprecated、cert_has_expired 先别慌
跑 npm install 时,控制台常会出现一些以 npm warn 开头的提示,比如:
text复制npm warn deprecated node-domexception@1.0.0: use your platform's native DOMException instead
npm warn using --force recommended protections disabled.
先说结论:npm warn 不是 npm error,关键看后面有没有出现 npm error。deprecated 一般表示某个依赖包被作者声明为“不再推荐使用”,但它不一定会立刻让安装失败,更多时候是某个老库间接依赖了它,暂时不影响运行。以后你有精力时再去更新依赖即可。
至于 npm ERR! code cert_has_expired,这类问题多半和网络环境有关。网上讨论区里有人会直接建议关闭 npm 的证书校验,比如把 strict-ssl 设为 false,但我不建议把这个当长期方案,因为关闭证书校验后,你下载依赖时无法确认来源身份,传输内容的安全性也没有保障。比较稳的顺序是:先检查系统时间是不是正常,再看看是不是公司内网 npm 镜像的证书过期了。如果是公司源,应该找管理员处理;如果是个人电脑下载速度太慢,可以换公共镜像源:
bash复制npm config get registry
npm config set registry https://registry.npmmirror.com/
镜像源只影响下载依赖时的服务器地址,不会改变 npm 本身的使用方式。如果后面觉得不需要镜像源了,执行 npm config delete registry 就能回到默认官方源。
3. package.json、依赖安装和 scripts:npm 的日常三件事
3.1 用 npm init 生成 package.json
依赖管理不是凭空产生的,每个项目都需要一个 package.json 来记录元信息和依赖。在项目目录里执行:
bash复制npm init -y
就会自动生成一个最基础的 package.json。里面 name、version、description 这些字段很好理解,初学阶段你只要知道它们描述的是当前这个项目的基本信息。以后如果想把自己的代码打包成 npm 包发布给别人用,这些字段就会变得重要。
大多数教材会让你手动编辑 package.json,这也没错。但我的建议是先理解基本字段的作用,不要一上来就背配置。实际用多了自然会记住,忘了就用 npm init 生成后再随手改。
3.2 dependencies 和 devDependencies 的区别
安装依赖时,最常用的是两条命令:
bash复制npm install dayjs
npm install -D vite
不带 -D 的依赖会写入 dependencies,表示项目真正运行起来需要它,比如 Vue、React、dayjs;带 -D 的依赖写入 devDependencies,表示只在开发阶段使用,构建和部署时不会被打包进去,比如 Vite、ESLint 这类工具。
这个区分刚学时容易忽略,但理解后对项目结构会清晰很多。举个具体例子:Vite 是启动开发服务器和打包用的工具,最终线上运行的是 dist 目录里的静态文件,不再需要 Vite 参与,所以它属于 devDependencies;dayjs 是业务代码里频繁使用的日期工具库,最终构建产物里要有它的代码,所以它是 dependencies。
卸载依赖时,如果忘了当初是 dependencies 还是 devDependencies,直接执行 npm uninstall 包名 就行,npm 自己会从对应的字段里移除它。
3.3 package-lock.json 是项目的一把“锁”
每次安装依赖后,项目里除了 node_modules,还会出现一个 package-lock.json。很多初学者不明白它的作用,会随手删掉。这里必须提醒一句:package-lock.json 不要手动删除,而且应该提交到代码仓库。
package.json 里记录依赖版本时通常带 ^ 符号,比如 "dayjs": "^1.11.10",意思是允许安装 1.x.x 范围内的最新版本。这就带来了不确定性:今天别人拉代码执行 npm install,可能安装到了 1.11.13;明天你拉,又装到 1.11.15。虽然小版本升级一般不会破坏代码,但为了可复现,npm 会用 package-lock.json 把某一次安装的完整依赖树锁下来。只要 lock 文件还在,新同事执行 npm install 就能得到和你几乎一致的依赖环境。
说到报错,有新人看到“found 0 vulnerabilities”会问是不是系统有问题,其实这是 npm 在告诉你安全检查结果,0 表示没有已知漏洞,属于正常输出。另外,如果哪天你在网上搜索某个安装报错,看到有人建议加 --force,不要盲目执行。--force 会导致 npm 忽略依赖冲突等保护机制,应该先弄清楚冲突来源,再决定是否使用,而不是把无脑规避当作常规操作。
3.4 为什么要用 npm run dev 而不是直接敲 vite
package.json 里可以自定义 scripts,例如:
json复制{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
然后你执行 npm run dev 时,npm 会去项目的 node_modules 里找 vite 并执行。它之所以不推荐全局安装 Vite,是因为项目依赖应该跟随项目本身走。你在 A 项目装 Vite 5,在 B 项目装 Vite 6,彼此不会互相影响。如果全部全局安装成同一个版本,换项目时很容易出现版本不匹配的奇怪问题。
npm run 还有一个隐藏机制:执行时会把 node_modules/.bin 临时放入 PATH,所以你在 scripts 里写 vite,能直接解析到本地依赖中的 vite。这也是为什么很多项目里不装全局工具也能跑起来的原因。
4. 用 Vite 创建一个项目并让它跑起来
4.1 create-vite:通过模板而不是手工配置创建项目
Vite 提供了官方脚手架 create-vite,它可以帮你生成一个可直接运行的项目目录。基础 JavaScript 阶段选择 vanilla 模板最合适,后面学 Vue 或 React 时也可以选择对应的框架模板。
bash复制npm create vite@latest my-vite-demo
执行后,工具会询问使用什么框架。如果当前目录下已经存在同名文件夹,它也会提醒你。选择 vanilla,再选择 JavaScript 变体,项目就创建好了。想跳过交互直接指定模板,也可以使用:
bash复制npm create vite@latest my-vite-demo -- --template vanilla
这里注意 -- 的作用,它告诉 npm 把后面的参数传给 create-vite。如果漏写,在部分终端下可能会出现参数被 npm 自己解析的情况,导致进入交互页面后又重来一遍。第一遍操作时不用太纠结,实际体验过交互模式后自然会熟悉。
4.2 目录结构:为什么 index.html 在最外层
创建出来的 Vite 项目初始结构大概是这样的:
text复制my-vite-demo/
├─ node_modules/ # 项目依赖目录,由 npm install 生成
├─ public/ # 公共静态资源,会被原样复制到 dist
├─ src/ # 源码目录
│ ├─ main.js # 入口文件,通常在这里引入其他 JS/CSS
│ ├─ style.css
│ └─ ...
├─ index.html # Vite 的入口页面
├─ package.json
├─ vite.config.js
└─ .gitignore
和传统后端项目不一样的地方在于,index.html 并不是放在 src 里,而是直接放在项目根目录。因为 Vite 在设计上以 index.html 为入口,开发模式下,浏览器访问 http://localhost:5173 时先拿到这个 HTML,然后 HTML 里通过 <script type="module" src="/src/main.js"> 加载源码。Vite 再顺着这个入口去解析依赖。
你可以把初始代码里的计数器 demo 删掉,让 main.js 变成一个简单的入口:
js复制import './style.css';
const app = document.querySelector('#app');
app.innerHTML = '<h1>Hello Vite</h1>';
这样它就不是 Vite 给你预设的示例,而是你自己能看懂的代码,出现问题时更好排查。
4.3 安装依赖并启动 dev server
进入项目文件夹,执行依赖安装:
bash复制cd my-vite-demo
npm install
npm run dev
终端会输出一个本地访问地址,通常是 http://localhost:5173/。打开浏览器,你就能看到刚才写的内容。端口被占用时 Vite 会尝试向后顺延,比如 5174、5175,这种自动切换是正常现象,别因为看到端口变了就以为自己操作错了。
dev server 启动阶段,浏览器里加载的是未压缩的源码,这样能保留清晰的错误堆栈,读起来比较直观。
4.4 初识热更新:页面为什么自己变了
保持 dev server 运行,修改 main.js 里的文案,保存后切回浏览器观察,几乎不需要手动刷新页面,内容就变了。这个体验来自 Vite 的热更新机制。
简单理解,Vite 在浏览器和你本地开发服务器之间维护了一条通信通道。你保存文件后,Vite 检测到变化,会分析哪些模块被影响,然后通知浏览器拉取新的模块并执行替换。它和“整页刷新”不同,整页刷新会丢失当前页面 JavaScript 运行产生的一些状态,比如你正在填写的表单;热更新则尽量只替换改变的模块,把影响控制到最小。
初学阶段体验到这个“快”就行。真正需要深挖 HMR 的边界和坑,我们放到第 6 节细说。
5. 把旧的 JavaScript 代码搬进 Vite 项目:模块化和第三方包的真实落地
5.1 拆分文件,用 import 和 export 组织代码
很多人在原生 JavaScript 阶段积累的小练习,都是下面这个样子:一个 HTML 文件,script 里既有工具函数,又有事件监听,还有一些按钮逻辑。这种代码拆进 Vite 项目正是最自然的进阶练习。
假设你有一个计算函数:
js复制// src/utils.js
export function formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
然后在入口文件里引入:
js复制// src/main.js
import './style.css';
import { formatDate } from './utils.js';
document.querySelector('#app').innerHTML = `
<h1>今天是 ${formatDate(new Date())}</h1>
`;
这就是 ES Modules 最基础的用法。学过 import/export 语法的人看到这里不会陌生,区别只在于:以前可能是在浏览器直接开一个 module 文件,现在是通过 Vite 的开发服务器来加载和解析,文件之间形成了一个明确的依赖图。把这个依赖图理清,工程化的基石就站稳了。
5.2 用 npm 安装一个第三方库,比起 CDN 引入有什么好处
为了体会 npm 的价值,我们装一个日期处理库 dayjs,因为自己处理日期边界问题往往容易出错,比如月份从 0 开始计、闰年判断、时区等等。
bash复制npm install dayjs
然后在 main.js 中导入它:
js复制import dayjs from 'dayjs';
console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'));
有人会问,以前不也能直接复制 CDN 的 script 链接吗?为什么一定要用 npm 装?关键在于可维护性。CDN 方式需要你去记住引用的是哪个版本,项目文档不会记录这个依赖;某一天 CDN 域名访问异常或者版本下线了,你的页面内容可能就崩了。npm 则会把 dayjs 的版本记录在 package.json 里,构建阶段它会被打进本地 dist 产物,线上访问不再依赖第三方 CDN 的可用性。
至于卸载包,也很简单:
bash复制npm uninstall dayjs
我会建议新手不要随意手动删除 node_modules 目录。node_modules 看起来又大又乱,但那是 npm 维护的产物,手动删除了反而可能导致依赖状态不一致。真需要重置环境时,先删除整个 node_modules,再执行 npm install,而不是只删掉某一个文件夹。
5.3 为什么很多教程里的命令一照抄就报错
搜索前端问题时,经常能看到一些以 $ 开头的命令,比如:
text复制$ NODE_OPTIONS=--max-old-space-size=4096 vite
这种写法在 macOS 或 Linux 的 bash 环境下是有效的,但如果有人直接复制到 Windows 的 cmd 或 PowerShell 里运行,就可能看到这样一条报错:
text复制'node_options' 不是内部或外部命令,也不是可运行的程序或批处理文件。
原因很简单:Windows 的 cmd 不认 $ 开头的环境变量赋值语法。如果你确实遇到了构建大项目时的内存不足问题,在 Windows cmd 下应该使用:
cmd复制set NODE_OPTIONS=--max-old-space-size=4096
npm run build
在 PowerShell 下则是:
powershell复制$env:NODE_OPTIONS="--max-old-space-size=4096"; npm run build
不过说实话,基础阶段的练习项目几乎不会遇到这种内存问题。这个现象真正想提醒你的是:网上教程的命令样例常常带有作者操作系统的痕迹。看到不认识的前缀,先分辨一下它是不是环境变量赋值,再结合自己的终端类型执行,能避免很多“照着抄都报错”的挫败感。
6. 文件改了但页面不热更新时,一套实用的排查路径
6.1 HMR 不是“任何文件变化都整页刷新”
有些同学会描述这样的现象:Vite 开发服务器跑得好好的,改 JS 文件就更新,但改了 Vue 单文件组件里的内容,页面却不热更新了。听起来像 HMR 坏掉,但很多情况下只是你改动了不属于当前页面依赖链的文件,或者某个文件存在语法错误导致更新过程被覆盖了。
HMR 的触发前提是:被修改的模块必须处于当前页面的 import 依赖链中。比如你新建了一个 tools.js,但暂时没有在任何文件里 import 它,那么修改 tools.js 不会触发页面更新,因为 Vite 根本不知道它和当前页面有什么关系。这很容易被新手误以为热更新失效。
另外,不同文件的更新表现不一样。修改 main.js、style.css、组件文件时,HMR 倾向于局部热替换;修改 index.html,Vite 一般会触发整页刷新;修改 vite.config.js,通常会导致 dev server 自动重启。所以“改了文件没有反应”之前,先判断自己改的是哪类文件,再看终端输出有没有 hmr update 之类的日志。
