使用 ES Modules(import/export)重构前端代码:从 CommonJS 迁移到原生模块规范
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
本指南以 Front-End-Checklist 仓库中的
es-modules规则(高优先级、入门难度、约 15 分钟)为核心,系统讲解 JavaScript 原生 ES Modules 语法(import/export)如何取代 CommonJS 的require()/module.exports,涵盖静态分析与 tree-shaking 原理、命名导出与默认导出的取舍、浏览器原生type="module"加载、动态导入代码分割、Barrel 再导出文件,以及 TypeScript 与构建管线下的落地验证。读完你将掌握一套可立即套用的迁移与验收流程。
规则速览
在 es-modules 技能定义 中,这条规则的定位是:
- 优先级:high(高)——影响打包体积、加载性能与工具链质量;
- 难度:beginner(入门);
- 预估耗时:15 分钟;
- 分类:
javascript; - 核心主张:使用原生 ES module 语法(
import/export)替代 CommonJS 的require(),以启用静态分析、tree-shaking 与更完善的工具支持。
规则给出的快速参考要点如下:
- 用
import/export语法替代require()/module.exports; - 命名导出(named exports)改善 IDE 自动补全并支持 tree-shaking;
- 在浏览器中要么给
<script>加type="module",要么使用打包器加载 ES modules; - 默认导出(default exports)可用,但大型代码库中命名导出扩展性更好。
为什么 ES Modules 更重要:静态可分析与 tree-shaking
ES modules 的核心优势在于静态可分析性(statically analyzable):import/export语句的依赖关系在源码层面是显式、确定、可枚举的,打包器(bundler)可以在构建期确定哪些代码真正被使用,并把其余部分消除掉——这正是 tree-shaking 的前提。相比之下,CommonJS 的require()是动态的——它可以在任意作用域、任意条件下、用任意表达式调用,模块依赖图在构建期无法完整推断,从而阻止了这项优化。
另一个关键事实是:ES modules 已是浏览器与 Node.js 的原生标准,原生支持意味着无需再依赖构建期转换即可在现代环境直接运行,同时天然受益于引擎级的模块缓存与严格模式语义。
从仓库的规则体系看,这条规则与其他 JavaScript 规则紧密关联:
- 代码分割规则 在
relatedRules中明确指出:"Code splitting requires ES module syntax — dynamic import() is an ES feature"(代码分割需要 ES module 语法——动态import()是 ES 特性); - type-only-imports 规则 也把
import type定义为 "builds on ES module syntax and interacts with bundler tree-shaking behaviour"(建立在 ES module 语法之上,并与打包器的 tree-shaking 行为交互)。
也就是说,ES Modules 不是孤立的一种写法偏好,而是代码分割、按需加载、类型安全导入等一系列现代前端优化的语法地基。
代码对照:从 CommonJS 迁移到 ES Modules
原规则给出的迁移对照如下(ES Modules 一侧的完整代码已补全):
// ❌ CommonJS(新代码应避免) const utils = require('./utils') const { formatDate } = require('./utils') module.exports = { myFunction } module.exports = myFunction // ✅ ES Modules import utils from './utils.js' import { formatDate } from './utils.js' export { myFunction } export default myFunction迁移时的规则很简单:把所有require()和module.exports语句转换为 ES module 的import/export语法。这也是技能中Check/Fix两个动作的定义——先识别 JavaScript 文件中需要转换的require()、module.exports、exports用法,再逐一转换。
本仓库自身的配置文件就是 ES module 语法的活例:apps/web/next.config.js 通篇使用import { withContentCollections } from '@content-collections/next'、import { withSentryConfig } from '@sentry/nextjs'以及export default withContentCollections(sentryWrappedConfig),而@type {import('next').NextConfig}这类 JSDoc 类型导入同样依赖 ESM 语法。
命名导出与默认导出:如何选择
规则强调命名导出与默认导出的适用场景差异:
// utils.js — 命名导出(库与工具函数首选) export function formatDate(date) { return date.toISOString().slice(0, 10) } export function formatCurrency(amount) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount) } // consumer.js import { formatDate, formatCurrency } from './utils.js' // 只有被导入的函数会进入产物包(tree-shaking 生效) console.log(formatDate(new Date()))// UserCard.js — 默认导出(适用于单一概念的模块) export default function UserCard({ user }) { return `<article>${user.name}</article>` } // consumer.js import UserCard from './UserCard.js'选择建议:
- 命名导出(named exports)是库与工具模块的首选:IDE 可以精确补全导入名、静态检查可以校验拼写、打包器可以精确 tree-shaking——只有真正被导入的绑定才会被打进产物;
- 默认导出(default exports)适合"一个模块表达一个概念"的场景(如一个组件文件对应一个组件),但命名不统一时容易造成引用名漂移(
import X fromvsimport Y from); - 在大型代码库中,命名导出扩展性更好,这也是技能 Quick Reference 的明确结论。
在浏览器中直接使用:type="module"
不需要打包器时,可以直接在 HTML 中使用原生模块脚本:
<!-- 添加 type="module" 即可在浏览器直接使用 import/export --> <script type="module"> import { formatDate } from './utils.js' console.log(formatDate(new Date())) </script>模块脚本具有三个区别于普通脚本的语义:
- 默认延迟执行(deferred):模块脚本不会阻塞 HTML 解析,行为等价于
defer; - 自动严格模式:模块代码以严格模式运行,无需显式书写
'use strict'; - 独立作用域:模块顶层声明的变量不会泄漏到全局(
window),避免全局污染。
动态导入与代码分割
import()表达式允许在运行时按需加载模块,是代码分割(code splitting)的语法入口:
// 仅在需要时加载重型模块 async function loadChart() { const { Chart } = await import('./chart.js') return new Chart(document.getElementById('canvas')) } button.addEventListener('click', loadChart)仓库中的代码分割规则给出了更完整的工程化示例:静态导入会把整棵依赖图拖入初始包,而动态导入可以让 PDF 生成、图表库、富文本编辑器等重模块仅在用户交互时下载并执行:
// ❌ 静态导入:即使从未使用也会加载 import { generatePDF } from './pdf-generator.js' // ✅ 动态导入:仅在需要时加载 async function handleExportClick() { const { generatePDF } = await import('./pdf-generator.js') generatePDF(document) }在 React 应用中,动态导入常与lazy/Suspense配合实现路由级切包——每个路由一个 chunk,用户只下载当前页面所需代码(详见 code-splitting.mdx 的 Route-Based Splitting 示例)。这就是es-modules规则被code-splitting规则列为前置条件的原因:没有 ES module 语法,动态import()便无从谈起。
再导出与 Barrel 文件
Barrel 文件(桶文件)通过再导出把多个子模块收敛到一个中心入口,简化调用方的导入路径:
// index.js — 从中心入口再导出 export { formatDate, formatCurrency } from './utils.js' export { default as UserCard } from './UserCard.js' export * from './validators.js'三种再导出形式的语义区别:
export { a } from './mod.js':显式再导出命名绑定;export { default as X } from './mod.js':给默认导出重命名后再导出;export * from './mod.js':批量再导出全部命名导出(不含默认导出)。
注意:Barrel 文件本身也参与 tree-shaking,但过深的 barrel 链会引入循环引用风险,因此在大型代码库中应控制 barrel 的使用范围——仅对公共 API 面做收敛,内部模块保持直接导入。
与 TypeScript 的协作:import type与模块解析配置
ES module 语法与 TypeScript 的结合值得单独强调。仓库中的 type-only-imports 规则 指出:即使只使用某个模块的类型(interface/type),普通import也会让运行时去求值该模块——增加包体积、引入副作用并可能引发循环依赖。正确做法是使用import type:
// ✅ 仅类型导入:编译期完全擦除,零运行时开销 import type { RuleMeta } from './types.js' // ❌ 若 RuleMeta 仅用于类型注解,普通 import 仍会保留运行时模块求值 import { RuleMeta } from './types.js'仓库的 TypeScript 基础配置 configs/config-typescript/base.json 展示了与 ES module 语义对齐的编译器设置:
{ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "isolatedModules": true, "esModuleInterop": true, "moduleDetection": "force", "target": "ES2022" } }这些选项的实战意义:
module: "NodeNext"+moduleResolution: "NodeNext":按 Node.js 的 ESM/CJS 双模式语义解析模块,强制开发者明确写全文件扩展名;isolatedModules: true:要求每个文件可被独立转译,间接强制了import type等安全写法;moduleDetection: "force":把所有文件当作模块处理,避免"全局脚本"陷阱;esModuleInterop: true:允许在 ESM 中便捷地默认导入 CommonJS 模块(import fs from 'node:fs')。
工作区根配置 tsconfig.base.json 通过extends继承该基础配置,并补充@repo/*路径映射——整个 monorepo 的 TypeScript 编译均在同一套 ESM 语义下进行。
支持说明:迁移时要注意的三个坑
原规则的 Support Notes 给出了三条实战警示:
- 验证最终产物而非源码语法:模块加载行为取决于目标浏览器矩阵和构建管线(
type="module"的原生支持、打包器的输出格式、module/moduleResolution配置等都会改变运行时行为),所以必须检查最终 shipped 输出,不能只看源码写法; - 遗留目标需要显式回退方案:如果兼容范围仍包含不支持原生 ESM 的老浏览器(如 IE),需要把降级(fallback)或转译路径(如打包器输出 IIFE/UMD、
nomodule回退脚本)明确文档化; - 严格模式并非处处自动生效:ES modules 与 class 代码体自动处于严格模式,但如果代码库仍有传统的非模块
<script>,'use strict'指令在那里依然有意义——混合脚本环境下不要假设所有代码都处于严格模式。
验证清单:如何确认改动正确
规则要求"在浏览器中验证行为,而不仅是静态分析",并给出了两层检查手段。
自动化检查
- 代码变更后在浏览器中验证实际行为,而非只看静态分析结果;
- 当规则影响加载或执行顺序时,检查 DevTools 的 Network / Performance 面板(确认模块脚本是否按预期 defer、chunk 是否按预期拆分与加载);
- 测试主用户流 + 一条由变更脚本路径触发的边缘用例。
对应到本仓库的验证体系,还可参考代码分割规则 的自动化检查建议:用 bundle visualizer 对比前后产物、确认初始 chunk 显著缩小且import()真正把代码移出初始依赖图、用 Lighthouse 或性能预算复测初始 JS 成本(常见起始阈值为主路由包 gzip 后 ≤ 150 KB)。
手动检查
- 确认功能被延迟、懒加载或失败时(比如网络中断导致动态 chunk 加载失败),代码仍能正确降级——加载状态、
Suspensefallback 或错误提示是否到位。
小结
ES Modules 不是一种可选的风格偏好,而是现代 Web 的性能与工具链基石:静态可分析性带来 tree-shaking,原生浏览器支持降低转换负担,动态import()打开代码分割的大门,import type让类型导入零运行时成本。迁移时记住三条主线:新代码一律import/export、浏览器侧用type="module"或打包器、TypeScript 侧用verbatimModuleSyntax/isolatedModules等配置把 ESM 语义固化到编译层。最后,永远以最终浏览器产物作为验收标准。
如需进一步深入,可在本仓库中继续阅读:es-modules 技能定义(含 Agent 使用的 Check/Fix/Explain/Code Review 提示词)、代码分割规则、type-only-imports 规则、Next.js 配置示例 与 TypeScript 基础配置。
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考