ES Module 实战指南:从基础语法到项目优化技巧

静欣 优化 阅读 1,042
赞 16 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

我最近在重构一个老项目,前端模块管理还是靠全局变量和手动 script 标签引入,简直灾难。改用 ES Module 后,整个结构清爽多了。别被“模块化”吓到,其实上手非常快。下面这段代码,你复制粘贴就能跑:

ES Module 实战指南:从基础语法到项目优化技巧

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ESM Demo</title>
</head>
<body>
  <button id="btn">点我</button>
  <script type="module" src="./main.js"></script>
</body>
</html>
// utils.js
export function formatDate(date) {
  return date.toISOString().split('T')[0];
}

export const API_URL = 'https://jztheme.com/api';
// main.js
import { formatDate, API_URL } from './utils.js';

document.getElementById('btn').addEventListener('click', () => {
  console.log('今天日期:', formatDate(new Date()));
  console.log('API地址:', API_URL);
});

注意那个 type="module",这是关键。没有它,浏览器会当成普通脚本执行,import/export 直接报错。亲测有效,现在所有现代浏览器都支持,除非你还在维护 IE(那另说)。

这个场景最好用

我特别喜欢用 ES Module 处理配置和工具函数。比如项目里有多个页面都要调同一个 API,以前得 copy-paste 配置,现在直接 export 一个常量,import 就行。改一次,全站生效。

更爽的是动态导入(dynamic import)。比如某个功能只在特定条件下才需要,比如用户点了“导出数据”才加载 Excel 生成库:

// 只在需要时才加载
async function exportToExcel() {
  const { generateExcel } = await import('./excel-utils.js');
  generateExcel(data);
}

这样首屏加载更快,包体积也小。我之前在一个报表项目里这么干,首屏 JS 从 1.2MB 降到 600KB,老板直呼内行。

踩坑提醒:这三点一定注意

ES Module 看着简单,但有几个坑我踩过好几次,不吐不快:

  • 路径必须带后缀名:你不能写 import { foo } from './utils',必须写 import { foo } from './utils.js'。这点和 Node.js 不一样,Node 里可以省略 .js,但浏览器不行。我第一次迁移时忘了加,控制台报错 “Failed to resolve module specifier”,折腾了半小时。
  • 本地文件必须用 HTTP 服务:直接双击打开 HTML 文件会报 CORS 错误。因为 ES Module 要求通过 HTTP(S) 加载,file:// 协议不被允许。解决方法很简单:用 npx servepython -m http.server 起个本地服务就行。别傻乎乎地以为是代码问题。
  • 默认是严格模式:ES Module 自动开启严格模式,有些老代码里的隐式全局变量会直接报错。比如 function foo() { x = 1; } 这种,在模块里会抛异常。建议提前用 ESLint 扫一遍,或者把老代码包一层 IIFE 再逐步迁移。

高级技巧:命名导出 vs 默认导出?

很多人纠结该用命名导出(named export)还是默认导出(default export)。我的经验是:优先用命名导出

为什么?因为命名导出更明确,IDE 自动补全更准,重构也更安全。比如:

// 推荐:命名导出
export function calculateTax(amount) { /* ... */ }
export function formatCurrency(amount) { /* ... */ }
// 使用
import { calculateTax, formatCurrency } from './finance.js';

而默认导出容易导致命名混乱:

// utils.js
export default function helper() { /* ... */ }

// main.js
import whateverNameIWant from './utils.js'; // 名字随便起,容易混淆

当然,如果你的模块就导出一个类或一个主函数(比如一个 Vue 组件),默认导出也 OK。但多数情况下,命名导出更清晰。我现在的项目里,默认导出基本只用于 React/Vue 组件文件。

和打包工具怎么配合?

有人问:现在都用 Webpack/Vite 了,还用原生 ES Module 干嘛?

其实两者不冲突。Vite 本身就是基于原生 ES Module 的开发服务器,开发时直接用浏览器原生能力,构建时再打包。而 Webpack 虽然内部用 CommonJS,但最终输出也可以是 ES Module(通过 output.libraryType: 'module')。

更重要的是,写代码时按 ES Module 规范写,未来迁移成本最低。不管底层是 Vite、Webpack 还是纯原生,你的源码结构都不用大改。我之前一个项目从 Webpack 切到 Vite,业务代码一行没动,只改了配置文件,就是因为一直用标准 ES Module 语法。

顺便提一句,如果你用 TypeScript,.ts 文件也支持 ES Module 语法,编译后会自动处理成目标环境兼容的模块格式,完全不用操心。

最后的小遗憾

ES Module 也不是万能的。比如它不支持 JSON 直接导入(虽然 Chrome 已经支持,但其他浏览器还不行),这时候还得用 fetch

// 暂时不能这么写(除非用构建工具)
// import data from './config.json' assert { type: 'json' };

// 得这么干
const res = await fetch('./config.json');
const data = await res.json();

另外,循环依赖在 ES Module 里行为比较诡异,虽然不会直接报错,但可能拿到 undefined。我的建议是:尽量避免循环依赖,实在不行就用函数延迟引用。

总的来说,ES Module 是现代前端的基石。哪怕你用框架,底层也是它。早点掌握,少走弯路。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多(比如用 import map 做微前端),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论