一文读懂 ESM:从基础用法到 Vite 的 “快” 之基石

261 阅读8分钟

一文读懂 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,不能直接双击打开文件),浏览器会:

  1. 加载main.js,看到里面有import './utils.js'

  2. 自动请求utils.js文件;

  3. 加载完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/exportrequire()/module.exports
加载方式按需加载(浏览器请求哪个加载哪个)全量加载(加载模块时执行整个文件)
适用场景浏览器端、现代构建工具(Vite)Node.js 后端、传统前端打包(Webpack)

关键差异点:执行时机和加载方式—— 这直接决定了 Vite 能不能 “快”。

举个例子:如果用 CommonJS,Webpack 必须在启动时做两件事:

  1. 从入口文件开始,递归遍历所有require的模块,构建完整的 “依赖树”;

  2. 把所有模块翻译成浏览器能懂的代码,打包成一个或多个 bundle 文件。

这个过程就像 “先把所有食材都切好、炒成菜,再端上桌”—— 项目越大,切菜炒菜的时间越长,启动就越慢。

而用 ESM 的 Vite,启动时啥都不用 “炒”:

  1. 直接启动一个服务器,返回index.html

  2. 浏览器加载main.js,发现import './utils.js',就向服务器请求utils.js

  3. 服务器收到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里的vuelodash)很多还是 CommonJS 格式,浏览器不认识。而且有些依赖是由上千个小模块组成的,浏览器要发上千次请求,会变慢。

所以 Vite 在第一次启动时,会做一次 “依赖预构建”:

  • 把 CommonJS 格式的依赖(如lodash)转换成 ESM 格式;

  • 把多个小模块合并成一个大模块(比如把lodash的上千个小文件合并成一个lodash.js),减少浏览器请求次数;

  • 预构建的结果存在.vite/deps目录里,下次启动直接复用,不用重新构建。

这一步相当于 “提前把常用的食材(第三方依赖)处理好,客人点的时候直接加热就行”—— 既保留了 ESM 的快,又解决了第三方依赖的兼容性和请求过多问题。

3. 热更新:ESM 的 “局部替换” 能力

Vite 的热更新(HMR)也离不开 ESM 的支持。当你修改一个文件时:

  1. Vite 只重新编译这个修改后的文件(而不是整个项目);

  2. 通过 WebSocket 告诉浏览器 “哪个模块变了”;

  3. 浏览器用 ESM 的import()动态加载新模块,替换掉旧模块,不用刷新整个页面。

这就像 “菜里少放了盐,只需要加一点盐,不用重新炒一盘”—— 热更新自然快到毫秒级!

五、常见问题:ESM 的那些 “坑” 和解决办法

刚开始用 ESM 时,容易遇到几个小问题,咱们提前避坑:

1. 本地打开 HTML 报错:CORS 政策阻止?

问题:直接双击打开index.htmlfile://协议),浏览器会报错 “Access to script at 'file:///xxx.js' from origin 'null' has been blocked by CORS policy”。

原因:浏览器的 ESM 模块不允许通过file://协议加载(安全限制)。

解决:用本地服务器打开,比如:

  • 安装servenpm 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.htmlmain.jsutils.js),用 ESM 的语法写一个简单的加法功能,然后用serve启动服务器运行 —— 亲手试一次,比看 10 遍文章都管用!总而言之,一键点赞、评论、喜欢收藏吧!这对我很重要!