一文读懂 ESM:从基础用法到 Vite 的 “快” 之基石
很多开发者第一次接触 Vite 时,都会被 “ESM” 这个概念卡住 —— 它到底是什么?和我们之前用的 CommonJS 有啥区别?为啥 Vite 非要用它才能快?其实 ESM 一点都不复杂,咱们从 “是什么→怎么用→和 Vite 的关系” 三步讲透,用代码例子帮你彻底搞懂。
一、先搞懂:ESM 到底是什么?
ESM 的全称是 ECMAScript Module,也就是 JavaScript 官方的 “模块规范”。简单说,它就是一套 “定义如何导入、导出代码” 的规则 —— 有了这套统一规则,不同文件、不同库之间才能互相引用代码,而不会乱套。
在 ESM 出现之前,前端没有官方模块规范,大家用的是 CommonJS(Node.js 里的require/module.exports)、AMD(RequireJS)等 “民间方案”。但这些方案有个大问题:浏览器不原生支持,必须靠 Webpack、Browserify 等工具把代码 “打包翻译” 后才能在浏览器里跑。
而 ESM 的核心优势是:浏览器原生支持—— 不用任何打包工具,浏览器就能直接识别并加载 ESM 模块,这也是 Vite 能 “秒启动” 的关键!
二、ESM 怎么用?3 分钟学会核心语法
ESM 的用法非常简单,核心就两个关键字:export(导出)和import(导入),咱们用最常见的场景举例子。
2.1 基础用法:导出单个 / 多个内容
假设我们有个工具函数文件utils.js,要导出里面的函数给其他文件用:
// utils.js(导出方)
// 1. 导出单个内容(命名导出)
export function add(a, b) {
return a + b;
}
export const PI = 3.1415;
// 2. 导出多个内容(也可以先定义,再集中导出)
function multiply(a, b) {
return a \* b;
}
const MAX\_NUM = 1000;
export { multiply, MAX\_NUM };
// 3. 导出默认内容(一个文件只能有一个默认导出,导入时可自定义名称)
export default function subtract(a, b) {
return a - b;
}
然后在main.js里导入这些内容:
// main.js(导入方)
// 1. 导入命名导出的内容(必须用大括号,名称要和导出时一致)
import { add, PI, multiply } from './utils.js';
// 2. 导入默认导出的内容(不用大括号,可自定义名称,比如叫minus)
import minus from './utils.js';
// 3. 导入时重命名(避免名称冲突)
import { MAX\_NUM as MAX } from './utils.js';
// 直接用导入的内容
console.log(add(1, 2)); // 3
console.log(PI); // 3.1415
console.log(multiply(3, 4)); // 12
console.log(minus(5, 3)); // 2
console.log(MAX); // 1000
2.2 浏览器里怎么跑 ESM?关键是type="module"
要让浏览器识别 ESM,只需要在 HTML 的<script>标签里加type="module"属性 —— 这一步是 “开关”,没加的话浏览器会把代码当普通 JS 执行,不认识import/export:
\<!-- index.html -->
\<!DOCTYPE html>
\<html>
\<body>
\<!-- 关键:加 type="module" 告诉浏览器这是ESM模块 -->
\<script type="module" src="./main.js">\</script>
\</body>
\</html>
此时直接用浏览器打开index.html(注意:本地测试要开服务器,比如用npx serve,不能直接双击打开文件),浏览器会:
-
加载
main.js,看到里面有import './utils.js'; -
自动请求
utils.js文件; -
加载完
utils.js后,执行main.js里的代码。
整个过程不需要任何打包工具,浏览器自己就能搞定模块加载 —— 这和之前 Webpack 需要 “先把所有模块打包成一个文件” 的逻辑完全不同!
三、ESM 和 CommonJS 的核心区别:为啥 Vite 不用 CommonJS?
很多开发者习惯了 Node.js 里的 CommonJS(require/module.exports),会疑惑 “为啥 Vite 非要用 ESM?”。咱们用表格对比核心差异,你就懂了:
| 对比维度 | ESM(ECMAScript Module) | CommonJS(Node.js 默认) |
|---|---|---|
| 执行时机 | 编译时加载(静态分析) | 运行时加载(动态执行) |
| 浏览器支持 | 原生支持(加type="module") | 不原生支持(需打包工具翻译) |
| 导入导出语法 | import/export | require()/module.exports |
| 加载方式 | 按需加载(浏览器请求哪个加载哪个) | 全量加载(加载模块时执行整个文件) |
| 适用场景 | 浏览器端、现代构建工具(Vite) | Node.js 后端、传统前端打包(Webpack) |
关键差异点:执行时机和加载方式—— 这直接决定了 Vite 能不能 “快”。
举个例子:如果用 CommonJS,Webpack 必须在启动时做两件事:
-
从入口文件开始,递归遍历所有
require的模块,构建完整的 “依赖树”; -
把所有模块翻译成浏览器能懂的代码,打包成一个或多个 bundle 文件。
这个过程就像 “先把所有食材都切好、炒成菜,再端上桌”—— 项目越大,切菜炒菜的时间越长,启动就越慢。
而用 ESM 的 Vite,启动时啥都不用 “炒”:
-
直接启动一个服务器,返回
index.html; -
浏览器加载
main.js,发现import './utils.js',就向服务器请求utils.js; -
服务器收到
utils.js的请求,才临时把它处理成浏览器能懂的代码,返回给浏览器。
这就像 “客人点一道菜,厨房才做一道菜”—— 启动时不用处理所有模块,自然秒启动!
四、Vite 是怎么 “利用” ESM 的?核心逻辑拆解
明白了 ESM 的基础后,咱们再回头看 Vite—— 它对 ESM 的利用,本质是 “借力浏览器原生能力,减少工具链的工作量”,具体分 3 步:
1. 开发时:让浏览器 “主动请求” 模块
Vite 开发服务器启动后,做的第一件事不是打包,而是 “拦截浏览器的模块请求”:
-
当浏览器请求
main.js时,Vite 会检查代码里的import路径,比如把import 'vue'重写成import '/node_modules/.vite/vue.js'(指向预构建后的 Vue); -
如果请求的是
.vue文件(比如import './App.vue'),Vite 会临时把.vue文件编译成 JS 模块(拆分成模板、脚本、样式三部分),再返回给浏览器; -
整个过程中,Vite 只处理 “当前被请求的模块”,不处理没被用到的模块。
2. 依赖预构建:给 ESM “补短板”
ESM 虽然快,但有个小问题:第三方依赖(比如node_modules里的vue、lodash)很多还是 CommonJS 格式,浏览器不认识。而且有些依赖是由上千个小模块组成的,浏览器要发上千次请求,会变慢。
所以 Vite 在第一次启动时,会做一次 “依赖预构建”:
-
把 CommonJS 格式的依赖(如
lodash)转换成 ESM 格式; -
把多个小模块合并成一个大模块(比如把
lodash的上千个小文件合并成一个lodash.js),减少浏览器请求次数; -
预构建的结果存在
.vite/deps目录里,下次启动直接复用,不用重新构建。
这一步相当于 “提前把常用的食材(第三方依赖)处理好,客人点的时候直接加热就行”—— 既保留了 ESM 的快,又解决了第三方依赖的兼容性和请求过多问题。
3. 热更新:ESM 的 “局部替换” 能力
Vite 的热更新(HMR)也离不开 ESM 的支持。当你修改一个文件时:
-
Vite 只重新编译这个修改后的文件(而不是整个项目);
-
通过 WebSocket 告诉浏览器 “哪个模块变了”;
-
浏览器用 ESM 的
import()动态加载新模块,替换掉旧模块,不用刷新整个页面。
这就像 “菜里少放了盐,只需要加一点盐,不用重新炒一盘”—— 热更新自然快到毫秒级!
五、常见问题:ESM 的那些 “坑” 和解决办法
刚开始用 ESM 时,容易遇到几个小问题,咱们提前避坑:
1. 本地打开 HTML 报错:CORS 政策阻止?
问题:直接双击打开index.html(file://协议),浏览器会报错 “Access to script at 'file:///xxx.js' from origin 'null' has been blocked by CORS policy”。
原因:浏览器的 ESM 模块不允许通过file://协议加载(安全限制)。
解决:用本地服务器打开,比如:
-
安装
serve:npm install -g serve; -
进入项目目录,运行
serve; -
用浏览器访问
http://localhost:3000。
2. 导入路径必须写全后缀名?
问题:在 ESM 里,import { add } from './utils'会报错,必须写成import { add } from './utils.js'。
原因:ESM 是浏览器原生支持的,浏览器不知道 “./utils” 到底是utils.js还是utils.html,必须写全后缀。
解决:
-
手动加后缀(
.js/.vue等); -
如果用 Vite,它支持省略后缀(Vite 会自动补全),比如
import { add } from './utils'在 Vite 里是合法的。
3. 能不能在 ESM 里用require?
问题:在加了type="module"的脚本里写const utils = require('./utils.js'),会报错 “require is not defined”。
原因:ESM 和 CommonJS 是两套独立的模块系统,ESM 里不支持require。
解决:统一用import/export,如果依赖是 CommonJS 格式(比如旧的 npm 包),Vite 会自动把它转换成 ESM。
六、总结:ESM 是 Vite “快” 的基石
一句话总结:Vite 的 “快”,本质是 “借了 ESM 的力”—— 利用浏览器原生支持 ESM 的特性,把 “打包” 这个慢过程从 “启动前” 推迟到 “浏览器请求时”,从而实现秒启动、即时热更新。
搞懂 ESM 后,你不仅能理解 Vite 的原理,还能明白现代前端构建工具的发展方向:越来越依赖浏览器原生能力,减少工具链的 “中间环节”,让开发体验更流畅。
最后给你一个小任务:自己新建 3 个文件(index.html、main.js、utils.js),用 ESM 的语法写一个简单的加法功能,然后用serve启动服务器运行 —— 亲手试一次,比看 10 遍文章都管用!总而言之,一键点赞、评论、喜欢加收藏吧!这对我很重要!