文章目录▼CloseOpen
- 先搞懂ESModule的“底层逻辑”:为什么它能解决模块化问题?
- ESModule实战:从“hello world”到复杂项目的3个关键技巧
- ① 为什么导入要写
- ② 路径怎么写才不会错?
- ③ 循环依赖怎么办?
- 用ESModule重构旧代码:从“一团乱”到“模块化”的实操步骤
- 第一步:拆“通用工具”到
- ${article.title}
- ESModule和CommonJS的核心区别是什么?
- 为什么导入ESModule时有时候要用大括号,有时候不用?
- ESModule导入时必须写.js后缀吗?
- ESModule遇到循环依赖(A导入B,B又导入A)怎么办?
- Vue/React组件里的ESModule是怎么用的?
- 具名导出:
export function formatTime() {}
→ 导入:import { formatTime } from './utils.js'
- 默认导出:
export default function formatTime() {}
→ 导入:import formatTime from './utils.js'
- 最常用的两种导出:具名vs默认,别再搞混了
- 如果你写了个工具函数文件
utils.js
,里面有formatTime
(格式化时间)、validateEmail
(验证邮箱)、trimStr
(去除字符串空格)三个功能,就用具名导出——每个功能贴一个标签,这样导入的时候可以按需选,不用把整个文件都引进来:
先搞懂ESModule的“底层逻辑”:为什么它能解决模块化问题?
在ESModule出现之前,前端开发者解决模块化的办法要么是用script
标签串行加载(容易变量冲突),要么是用CommonJS(Node.js的模块化方案,但浏览器不原生支持)。直到ES6推出ESModule,才终于有了浏览器和Node.js都能兼容的原生模块化标准。
我先给你打个比方:你可以把ESModule理解为“快递系统”——每个JS文件是一个“快递盒”,盒子里装着你写的功能(比如工具函数、组件)。要让别人能用这个盒子里的东西,你得给盒子“贴标签”:比如贴“formatTime函数”“PI常量”(这叫导出);别人要用的时候,得按标签“取快递”(这叫导入)。这样一来,就算你改了盒子里的内容,只要标签没变,别人用的时候就不会乱——这就是ESModule解决模块化的核心逻辑。
那ESModule和CommonJS有什么区别?别被专业术语吓到,我用大白话讲:CommonJS是“快递到了再拆箱”(运行时加载),比如require('./utils')
是在代码运行的时候才去读utils.js的内容;而ESModule是“寄快递前先填清单”(编译时静态分析),比如import { formatTime } from './utils.js'
是在代码编译的时候就知道要取什么——这意味着浏览器可以提前加载需要的模块,速度更快,而且能做“按需导入”(只取你需要的功能)。
我之前踩过一个典型的坑:写了个utils.js
,里面用export
导出了formatTime
函数,结果在另一个文件里写成import formatTime from './utils'
——直接报错“没有默认导出”。后来才明白,ESModule的导出分两种:具名导出(named export)和默认导出(default export)。具名导出就像“贴多个小标签”,每个标签对应一个功能,导入的时候得用大括号“按标签取”;默认导出是“贴一个主标签”,导入的时候不用大括号,直接取主功能。比如:
这一步搞懂了,80%的ESModule报错都能解决——我后来帮同事调试过类似的问题,十次有八次都是导出导入的方式弄反了。
ESModule实战:从“hello world”到复杂项目的3个关键技巧
光懂逻辑不够,得落地到实战。我把自己常用的技巧拆成3个部分,每个部分都带真实项目的例子,你跟着做就能上手。
先明确一个 具名导出适合“多功能模块”,默认导出适合“单功能模块”。比如:
// utils.js(具名导出)
export function formatTime(date) {
return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(date);
}
export function validateEmail(email) {
return /^[w-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9]+$/.test(email);
}
export const PI = 3.14159;
导入的时候,你可以只取需要的功能:
javascript
// index.js
import { formatTime, validateEmail } from './utils.js';
console.log(formatTime(new Date())); // 输出“14:30”
console.log(validateEmail('test@example.com')); // 输出true
MyButton.vue
如果你写了个Vue组件 ,核心功能就是一个按钮组件,就用默认导出——贴一个主标签,导入的时候不用记复杂的函数名,直接用组件名:
vue
<!-
MyButton.vue(默认导出) >
export default {
name: 'MyButton',
props: ['text']
}
导入的时候超简单:
vue
<!-
Home.vue >
import MyButton from './MyButton.vue'; // 不用大括号,直接取默认导出
export default {
components: { MyButton }
}
api.js
我之前做电商项目时,把所有接口请求放在
里,用具名导出每个接口函数(比如
getGoodsList、
getGoodsDetail),这样在商品列表页只需要导入
getGoodsList,在商品详情页只导入
getGoodsDetail——既节省了加载时间(不用加载不需要的接口函数),代码也更清晰(一眼就知道这个页面用了哪个接口)。
.js为了让你更清楚,我做了个对比表格:
特点 具名导出 默认导出 导出方式 用export +功能名(如
export function fn())
用export default +功能(如
export default fn)
导入方式 需用大括号(如import { fn } from './file.js' )
不用大括号(如import fn from './file.js' )
适用场景 多功能模块(如工具函数、接口请求) 单功能模块(如组件、类) 必踩的“细节坑”:后缀、路径、循环依赖怎么处理? 我见过很多新手栽在“细节”上——明明逻辑对了,就是报错,其实就是没注意这些小问题:
① 为什么导入要写
后缀?
import from './utils'浏览器原生支持的ESModule必须写完整的文件路径和后缀。比如你写
,浏览器会找不到文件;得写成
import from './utils.js'才行。但为什么用Vite或Webpack的项目不用写?因为脚手架帮你“自动补全后缀”了——但我 你不管用不用脚手架,都养成写后缀的习惯,这样代码在原生环境下也能运行,更规范。
file://我之前用
协议打开本地文件测试时,没写
.js后缀,直接报“模块未找到”——后来改成
./utils.js才解决,这个坑我记到现在。
script② 路径怎么写才不会错?
ESModule的路径规则和
标签的
src一样:
./:当前目录(比如
./utils.js就是和当前文件同目录的utils.js)
../:上一级目录(比如
../components/MyButton.vue就是当前目录的父目录下的components文件夹里的组件)
/:根目录(比如
/src/utils.js就是项目根目录下的src文件夹里的utils.js)
index.js我之前做一个多页面项目,把
放在
pages/home目录下,要导入
src/utils.js,一开始写成
./src/utils.js——报错,后来改成
/src/utils.js才对。记住:路径的起点是“当前文件的位置”,别搞反了层级。
a.js③ 循环依赖怎么办?
循环依赖就是“A导入B,B又导入A”,比如:
:
import { b } from './b.js'b.js
:
import { a } from './a.js'Form.js
我之前做表单组件时就遇到过:
导入了
Input.js,
Input.js又导入了
Form.js里的验证函数,结果页面直接崩了。后来查MDN文档才知道,ESModule会静态处理循环依赖——但得确保导出的内容是“静态的”(比如变量、函数声明,不是运行时生成的内容)。解决办法也简单:把循环依赖的部分拆成独立模块。比如我把验证函数从
Form.js里拆出来,放到
utils/validate.js里,这样
Form.js和
Input.js都导入
validate.js,就不会循环了。
结合框架的实战:Vue/React里的ESModule怎么用? 现在前端项目基本都用框架,ESModule早就和框架深度融合了——你每天写的Vue组件、React组件,其实都是ESModule的默认导出。
比如Vue单文件组件(SFC):
vue
<!-
// 这就是ESModule的默认导出
export default {
name: ‘MyComponent’,
data() {
return { message: ‘Hello ESModule!’ }
}
}
导入的时候直接用默认导出:
vue
<!-
import MyComponent from ‘./MyComponent.vue’; // 不用大括号
export default {
components: { MyComponent }
}
再比如React组件:
jsx
// App.jsx
// 具名导出一个函数组件
export function App() {
return
Hello React!
;
}
// 或者默认导出
// export default function App() { … }
导入的时候:
jsx
// index.jsx
import { App } from ‘./App.jsx’; // 具名导出用大括号
// 或者 import App from ‘./App.jsx’; // 默认导出不用大括号
ReactDOM.render(, document.getElementById(‘root’));
我用Vue做过一个博客项目,把所有组件放在
src/components目录下,每个组件都是默认导出——这样在页面里导入的时候,代码非常简洁,一眼就能看出用了哪个组件。
app.js用ESModule重构旧代码:从“一团乱”到“模块化”的实操步骤
如果你手里有个旧项目,代码都堆在一个
里,怎么用ESModule拆分?我去年帮朋友重构过他的个人博客前端——原来的
app.js有2000多行,混着ajax请求、DOM操作、工具函数,改个功能得翻半小时。我分了三步帮他拆:
utils.js第一步:拆“通用工具”到
formatDate
把所有通用的、不依赖业务的函数拆出来,比如
(格式化日期)、
stripHtml(去除HTML标签)、
debounce(防抖函数)——这些函数在哪里都能用,用具名导出:
javascript
// utils.js
export function formatDate(date) {
return new Intl.DateTimeFormat(‘zh-CN’, { year: ‘numeric’, month: ‘2-digit’, day: ‘2-digit’ }).format(date);
}
export function stripHtml(html) {
return html.replace(/]+>/g, ”);
}
### 第二步:拆“接口请求”到
api.jsgetArticles
把所有和后端交互的ajax请求拆出来,比如
(获取文章列表)、
getComments(获取评论)——这些是业务相关的通用功能,用具名导出:
javascript
// api.js
export async function getArticles() {
const res = await fetch(‘/api/articles’);
return res.json();
}
export async function getComments(articleId) {
const res = await fetch(/api/articles/${articleId}/comments);
return res.json();
}
### 第三步:拆“页面逻辑”到
page.jsutils
剩下的页面逻辑(比如渲染文章列表、绑定点击事件),导入
和
api里的函数来用:
javascript
// page.js
import { formatDate, stripHtml } from ‘./utils.js’;
import { getArticles } from ‘./api.js’;
// 渲染文章列表
async function renderArticles() {
const articles = await getArticles();
const articleList = document.getElementById(‘article-list’);
articles.forEach(article => {
const li = document.createElement(‘li’);
li.innerHTML =
${article.title}
${stripHtml(article.content).slice(0, 100)}…
${formatDate(new Date(article.createdAt))}
;
articleList.appendChild(li);
});
}
// 初始化页面
renderArticles();
拆分之后,朋友的代码量其实没减少,但可维护性提升了10倍——现在他想改日期格式,直接找
utils.js里的
formatDate;想改接口地址,直接找
api.js里的
getArticles;想改页面渲染逻辑,直接找
page.js——再也不用翻2000行代码了。
你是不是已经跃跃欲试了?不如今天就找个小项目试试:比如把你之前写的一个JS文件拆成两个ESModule文件,导出一个函数再导入——如果遇到报错,先检查这三点:后缀写了吗?导出导入方式对吗?路径对吗?要是试了之后有效果,或者遇到了新问题,欢迎回来留个言,我帮你一起看看!
我之前帮一个刚转前端的朋友调代码,他拿着电脑问我:“为什么Node.js里用require引模块没问题,到浏览器里就报错‘模块未找到’?”其实核心就一点——ESModule和CommonJS的“加载逻辑”根本不是一回事儿。
你想啊,ESModule就像你寄快递前先填好“收件清单”:比如你要寄个手机壳,清单上写清楚“手机壳×1”,快递员一看就知道要寄什么,对方收到直接按清单取;可CommonJS是啥?是你把手机壳、充电器、数据线一股脑塞进箱子,没写清单,对方收到箱子得自己拆开翻——这差别大了去了。具体到代码里,ESModule的import语句是“编译时静态分析”,比如你写import { formatTime } from './utils.js'
,浏览器在编译代码的时候就“提前知道”:哦,这个文件要用到utils里的formatTime函数,直接去加载这个函数就行,不用把整个utils.js都读一遍;可CommonJS的require是“运行时加载”,比如require('./utils')
,得等代码运行到这一行,才会去读utils.js的全部内容,再从中找你要的formatTime——这就像你去超市买瓶水,ESModule是直接跟收银员说“拿瓶矿泉水”,收银员直接递过来;CommonJS是你说“拿个饮料”,收银员把整箱饮料搬过来让你自己挑,能不慢吗?
我之前还踩过个坑:用CommonJS写Node.js脚本,require了一个很大的工具库,结果运行的时候卡了两秒——后来换成ESModule,只导入我需要的那个“时间格式化”函数,直接快了一倍不止。还有浏览器里的“tree shaking”(按需摇掉没用的代码),也是靠ESModule的静态分析才能实现:比如你导入了一个有10个函数的库,但只用到其中一个,打包工具会自动把另外9个没用的函数删掉,文件瞬间变小一半,加载速度能不快吗?
再说浏览器的支持——ESModule是浏览器原生就认的,你直接写script type="module"
就能用,不用装什么Babel转译;可CommonJS呢?浏览器根本不认识require,还得用Webpack转一遍才能跑。我那朋友后来把代码改成ESModule,加了个.js后缀,浏览器立马就不报错了——你看,这就是逻辑不一样带来的差别。
还有个细节你可能没注意:ESModule的导入路径必须写全后缀(比如./utils.js),CommonJS可以省略——为啥?因为ESModule是编译时找文件,得精确到“哪个文件”;CommonJS是运行时找,能自动补全。我之前帮朋友调的时候,他把路径写成./utils没加.js,浏览器直接报“模块未找到”,改成./utils.js就好了——这也是静态分析的“严格性”带来的,虽然麻烦点,但胜在准确啊。
不用 你就记着:前端项目优先用ESModule,因为快、省、浏览器原生支持;Node.js里CommonJS还能用,但现在Node.js也支持ESModule了(加个”type”: “module”在package.json里)——反正不管啥场景,ESModule都是 的趋势,早学早省心。
ESModule和CommonJS的核心区别是什么?
ESModule是“编译时静态分析”(寄快递前先填清单),导入语句在代码编译时就明确要取的功能,支持按需导入,速度更快;CommonJS是“运行时加载”(快递到了再拆箱),require语句在代码运行时才读取模块内容,无法提前优化。简单说,ESModule更适合浏览器环境的前端项目,CommonJS更常用在Node.js后端。
为什么导入ESModule时有时候要用大括号,有时候不用?
取决于模块的导出方式:如果是“具名导出”(用export直接导出函数/变量,如export function formatTime()),导入时需要用大括号按名称取(import { formatTime } from ‘./utils.js’);如果是“默认导出”(用export default导出,如export default function formatTime()),导入时不用大括号(import formatTime from ‘./utils.js’)。
ESModule导入时必须写.js后缀吗?
原生浏览器环境(如用file://协议打开本地文件)必须写完整后缀(如./utils.js),否则浏览器找不到模块;但用Vite、Webpack等脚手架时,脚手架会自动补全后缀,所以可以省略。 养成写后缀的习惯,让代码在原生环境也能运行,更规范。
ESModule遇到循环依赖(A导入B,B又导入A)怎么办?
解决核心是“拆分独立模块”:把循环依赖的功能(比如A和B都需要的验证函数)拆到单独的utils文件(如utils/validate.js),让A和B都导入这个utils模块,而非直接互相导入。这样既避免了循环,又保持功能复用。
Vue/React组件里的ESModule是怎么用的?
框架中的组件本质是ESModule的“默认导出”:比如Vue单文件组件(.vue)用export default导出组件选项对象,导入时直接写import MyComponent from ‘./MyComponent.vue’;React函数组件常用export default导出,导入时同样不用大括号。框架通过ESModule实现了组件的模块化复用,这也是前端项目的基础用法。