文章目录▼CloseOpen
- 先搞懂ESModule为啥能治住“代码混乱症”
- ESModule实战里的“避坑小技巧”,我踩过的雷你别再踩
- 按需导入不是“想怎么导就怎么导”,得配合Tree Shaking
- 动态加载模块,解决“首屏加载慢”的大问题
- 别忘加“type="module"”,不然浏览器不认识
- 最后给你个“入门实战清单”,直接照着做就行
- 浏览器中使用ESModule为什么会报错?
- 默认导出和命名导出有什么区别?
- ESModule为什么能让Tree Shaking生效?
- 动态导入(import())适合用在什么场景?
- ESModule和CommonJS(require/module.exports)的核心区别是什么?
- 想导出一个函数,直接写
export function add(a,b) { return a+b }
; - 想导出多个函数,写
export { add, subtract }
; - 想导出模块的“主要内容”(比如一个组件),用默认导出:
export default function multiply(a,b) { return ab }
; - 导入的时候,命名导出用
import { add } from './math.js'
,默认导出用import multiply from './math.js'
。
这篇文章从「模块化解析」到「实战示例」,一步步帮你打通ESModule的入门路径:先讲清它的核心逻辑(静态分析、只读引用),再用真实开发场景演示基础用法(导出单个变量/默认导出)、进阶技巧(按需导入、命名空间导入),甚至工程化中的常见问题(比如Tree Shaking支持)。全程用示例说话,不用死记硬背语法,跟着敲代码就能快速掌握,解决你项目里的模块化痛点,真正做到「一看就会」。
你有没有过写前端项目时,代码越堆越多,最后变量名冲突得让人头大?或者引入文件时,路径绕来绕去像走迷宫?我之前帮朋友改一个小项目,他把所有JS代码都堆在一个文件里,整整800多行,找个函数得翻5分钟,改的时候还不小心把全局变量覆盖了,结果页面直接崩了——这其实就是没做模块化的坑。而ESModule(也就是ES6的模块化规范),刚好就是治这个坑的“药”,我用它解决了无数次代码混乱的问题,今天就把最实用的用法和避坑技巧分享给你,保证你一看就会。
先搞懂ESModule为啥能治住“代码混乱症”
其实模块化的核心需求就三个:不让变量乱飘、要什么拿什么、知道东西从哪来——而ESModule刚好把这三点都解决了。
先说说“不让变量乱飘”。以前用Script标签引入JS文件,不管你写什么变量,都是全局的。比如你在a.js里写var username = '张三'
,在b.js里也写var username = '李四'
,那最后页面上的username
就变成李四了,因为被覆盖了。但ESModule里每个文件都是一个独立的“模块”,变量只在自己的文件里有效,就像每个房间都有自己的抽屉,你在自己房间放的东西,别人拿不到——比如你在math.js
里写var pi = 3.14
,只要没导出,其他文件根本访问不到,彻底解决了全局变量冲突的问题。
再说说“要什么拿什么”。以前要复用代码,只能把整个JS文件引进来,不管你用不用里面的函数,都得加载全量代码。比如你只需要一个计算圆面积的函数,但引进来的文件里还有计算周长、体积的函数,多余的代码就会让页面加载变慢。但ESModule可以“按需导出”和“按需导入”:你可以把math.js
里的圆面积函数单独导出来,其他函数留在里面,然后在需要的地方只导入这个函数——这样既节省加载时间,又让代码更干净。我之前做个电商项目,一开始没拆分模块,购物车和支付的代码混在一起,改购物车逻辑时还得小心翼翼怕碰坏支付功能;后来用ESModule把购物车拆成cart.js
,支付拆成pay.js
,每个模块只导出需要的函数,比如cart.js
导出addToCart
和getCartTotal
,pay.js
导出createOrder
和verifyPayment
,改购物车的时候再也不用怕影响支付了,效率直接提了30%。
还有“知道东西从哪来”。以前用Script标签引入多个文件,得记清楚顺序:比如先引jquery.js
,再引自己的代码,不然会报错。但ESModule里,每个模块的依赖都是显式的——你在app.js
里写import { addToCart } from './cart.js'
,就明确告诉浏览器:我要用到cart.js
里的addToCart
函数,你得先加载cart.js
才能执行我。这样不管文件顺序怎么变,都不会出错,因为依赖关系是“写在代码里”的,不是“靠顺序猜的”。
可能你会问:“那ESModule的语法难吗?”其实特别简单,就两个核心语法:export
(导出)和import
(导入)——就像你给朋友递零食,得说清楚“我给你的是薯片”(export
),朋友要拿的时候得说清楚“我要薯片”(import
),一点都不复杂。比如:
是不是超简单?我第一次学的时候,10分钟就记住了——关键是要多写几次,写两次就熟了。
ESModule实战里的“避坑小技巧”,我踩过的雷你别再踩
学会基础语法后,我猜你肯定想赶紧用在项目里,但这里有几个“坑”我得提前告诉你——都是我踩过的,别再走弯路。
按需导入不是“想怎么导就怎么导”,得配合Tree Shaking
你可能听过“Tree Shaking”这个词,翻译过来就是“摇树”——把没用的代码像摇树上的枯叶一样摇掉,减少打包后的文件体积。但Tree Shaking能生效的前提,是ESModule的“静态分析”特性:ESModule的import
和export
都是“静态”的,也就是说,你在写代码的时候,就得明确告诉打包工具(比如Webpack、Vite)“我要导入什么”,工具才能提前知道哪些代码有用,哪些没用。
举个例子:你在math.js
里导出了三个函数:
export function add(a, b) { return a + b }
export function subtract(a, b) { return a
b }
export function multiply(a, b) { return a b }
然后在app.js
里只导入add
函数:
import { add } from './math.js'
这时候打包工具就会知道:你只需要add
函数,subtract
和multiply
没用,直接删掉——这样打包后的文件里就只有add
的代码,体积小了一大半。我之前做个移动端项目,一开始用lodash的时候,直接导入了整个库:import _ from 'lodash'
,结果打包出来有100多KB;后来改成按需导入:import { debounce } from 'lodash-es'
(注意lodash-es
是ESModule版本的lodash),打包体积直接降到了10KB,页面加载速度快了2秒——你看,就改了个导入方式,效果立竿见影。
但这里要注意:默认导出没法做Tree Shaking。比如你在math.js
里写export default { add, subtract, multiply }
,然后在app.js
里导入import math from './math.js'
,再用math.add
——这时候打包工具没法知道你只用了add
,因为默认导出是一个对象,工具没办法提前分析对象里的属性,所以会把整个对象都打包进去。所以要是想做Tree Shaking,尽量用“命名导出”(就是export function add() {}
这种),别用默认导出导出对象。
动态加载模块,解决“首屏加载慢”的大问题
你有没有遇到过这种情况:项目里有个弹窗组件,只有点击按钮的时候才会出现,但一开始就把弹窗的代码加载了,导致首屏加载变慢?这时候就得用ESModule的“动态导入”功能——它能让你在需要的时候再加载模块,而不是一开始就加载。
动态导入的语法很简单,就是把import
写成函数形式:import('./模块路径')
,它会返回一个Promise,等模块加载完成后,就能拿到里面的导出内容。比如你有个modal.js
,导出了showModal
函数:
// modal.js
export function showModal() {
const modal = document.createElement('div');
modal.innerText = '这是弹窗';
modal.style = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px; background: white; border: 1px solid #ccc;';
document.body.appendChild(modal);
}
然后在app.js
里,点击按钮的时候再加载modal.js
:
// app.js
const btn = document.getElementById('open-modal');
btn.addEventListener('click', async () => {
// 动态加载modal.js
const modalModule = await import('./modal.js');
// 调用showModal函数
modalModule.showModal();
});
这样一来,只有当用户点击按钮的时候,才会加载modal.js
——首屏加载时根本不会碰这个文件,加载速度直接快了一半。我之前做个新闻客户端,详情页的评论组件就是用动态导入的:评论组件有20KB,但只有用户点击“查看评论”的时候才会加载,首屏加载速度从3秒降到了1.2秒,用户留存率涨了15%——你看,小小的一个优化,效果特别明显。
对了,动态导入还有个好处:可以做“代码分割”——把大的代码包分成多个小的代码块,按需加载,这样就算你的项目很大,首屏也能快速加载。比如Vite和Webpack这些打包工具,都会自动把动态导入的模块分成单独的文件,不用你手动处理,特别方便。
别忘加“type=”module””,不然浏览器不认识
最后一个坑:在浏览器里用ESModule的时候,Script标签得加type="module"
。比如你写了个app.js
,用了ESModule语法,那在HTML里得这么引:
要是没加type="module"
,浏览器会把它当成普通的Script文件,然后报错——因为普通Script不认识import
和export
语法。我第一次在浏览器里试ESModule的时候,就忘了加这个属性,结果控制台报了一堆错,查了半小时才发现问题,你可别犯我这个傻。
对了,还有个小知识点:加了type="module"
的Script标签,默认是“延迟执行”的(defer),也就是说,会等HTML文档解析完再执行,不用像普通Script那样加defer
属性——这其实是个优点,因为这样不会阻塞页面渲染。
最后给你个“入门实战清单”,直接照着做就行
说了这么多,我猜你肯定想赶紧动手试试——这里给你列个简单的实战清单,直接照着做,保证你半小时就能学会:
math.js
(放数学函数)、app.js
(入口文件)、index.html
(页面)。math.js
里写函数并导出:比如写add
和multiply
函数,用命名导出和默认导出: javascript
// math.js
// 命名导出add函数
export function add(a, b) { return a + b }
// 默认导出multiply函数
export default function multiply(a, b) { return a * b }
里导入并使用
: javascript
// app.js
// 导入命名导出的add函数
import { add } from ‘./math.js’
// 导入默认导出的multiply函数
import multiply from ‘./math.js’
// 使用函数
console.log(add(1, 2)) // 输出3
console.log(multiply(3, 4)) // 输出12
里引入
app.js
:记得加type=”module”:
html
,按F12打开控制台,就能看到输出的3和12——这就说明你成功用ESModule做了一个模块化的小项目!
要是你想再进阶一点,可以试试动态导入:比如在页面上加个按钮,点击按钮的时候加载math.js并执行函数——这样你就能彻底掌握ESModule的核心用法了。
对了,再给你个小提示:现在所有现代浏览器(Chrome、Firefox、Safari、Edge)都支持ESModule,Node.js从14版本开始也默认支持了,所以不用怕兼容性问题——放心大胆地用就行。
要是你按这些方法试了,欢迎回来告诉我效果!比如你拆了哪个项目,或者遇到了什么问题,我帮你一起看看——毕竟模块化这个东西,越用越顺手,早用早解脱代码混乱的苦。
你是不是也遇到过?写了ESModule的代码,浏览器一打开就报错,提示“Uncaught SyntaxError: Cannot use import statement outside a module”——我第一次试的时候也栽过这坑,根本原因特简单:浏览器默认把标签当成“普通脚本”,而普通脚本压根不认识import、export这些模块化语法。你得给script标签加个type=”module”属性,就像给文件贴了张“模块化专属标签”,浏览器一看这标签,立刻反应过来:“哦,这是ESModule,我得按模块的规则解析”。比如你之前写肯定报错,改成,刷新页面,报错立马消失——就这么个小属性,直接解决80%的浏览器ESModule报错问题。
还有个超容易踩的“隐形雷”:你直接双击打开本地HTML文件,用file://协议访问(比如file:///C:/Users/xxx/index.html),结果控制台又跳出来“Access-Control-Allow-Origin”相关的跨域错误。这是因为ESModule默认遵循CORS(跨域资源共享)规则,而file://协议没有“源”(Origin)的概念,浏览器觉得从本地文件加载模块不安全,直接就拦截了。这时候你得搭个本地服务器,比如用Vite的话,跑个npm run dev,把HTML文件放在项目目录里,用http://localhost:3000访问;或者用Python的小工具,打开命令行输python -m http.server 8000,再在浏览器输http://localhost:8000——这样一来,协议变成http,跨域问题就解决了。我之前帮同事排查问题,他就是直接双击HTML文件,折腾半小时没找到原因,最后开个服务器立马正常,你可别犯这没必要的傻。
浏览器中使用ESModule为什么会报错?
最常见原因是Script标签未添加type=”module”属性——ESModule语法(import/export)需要浏览器以“模块模式”解析脚本, 必须在引入时声明type=”module”,例如。 本地测试需用本地服务器(如Vite的npm run dev),避免直接打开file://协议的HTML文件,否则可能因跨域问题报错。
默认导出和命名导出有什么区别?
默认导出是模块的“核心内容”,一个模块只能有1个默认导出,导入时可自定义名称(如import utils from ‘./math.js’);命名导出是模块的“独立内容”,一个模块可以有多个,导入时必须匹配导出名称(如import { add } from ‘./math.js’)。通常默认导出用于组件、类等“主功能”,命名导出用于工具函数、常量等“子功能”。
ESModule为什么能让Tree Shaking生效?
因为ESModule是“静态分析”的——import/export必须写在模块顶部,不能用变量或条件判断动态改变。打包工具(如Webpack、Vite)能提前识别“哪些导出被使用、哪些没被使用”,从而删除无用代码(Tree Shaking)。而CommonJS的require是动态的(如require(‘./math.js’ + num)),工具无法提前判断, 不支持Tree Shaking。
动态导入(import())适合用在什么场景?
动态导入用于“按需加载”,常见场景包括:
ESModule和CommonJS(require/module.exports)的核心区别是什么?
主要有两点: