1. 模块化开发的前世今生
十年前我刚接触前端开发时,整个行业还在用script标签手动管理依赖。记得有次项目上线后,因为加载顺序问题导致页面白屏,排查到凌晨三点才发现是某个工具库没先加载。这种"刀耕火种"的开发方式,现在想来真是令人唏嘘。
随着ES6模块化标准的落地,现代前端终于有了官方的模块解决方案。export default和具名export作为模块化的核心语法,看似简单却藏着不少门道。上周review团队代码时,我发现不少三年经验的工程师仍然会混淆这两种导出方式的使用场景。
2. 基础概念解析
2.1 具名导出(named exports)
具名导出是模块化系统的基石,它允许一个模块同时暴露多个值。语法形式是在声明前添加export关键字:
javascript复制// utils.js
export const PI = 3.1415926
export function double(x) {
return x * 2
}
export class Logger {
//...
}
导入时需要解构对应名称:
javascript复制import { PI, double } from './utils.js'
console.log(double(PI)) // 6.2831852
关键特性:
- 支持批量导出(export { a, b })
- 导入时必须知道确切导出名
- 支持别名导出(export { foo as bar })
- 适合工具类模块
2.2 默认导出(default export)
每个模块可以有且仅有一个默认导出:
javascript复制// UserModel.js
export default class User {
//...
}
导入时不需要解构:
javascript复制import User from './UserModel.js'
典型场景:
- 模块主要导出一个实体时(如React组件)
- 不希望消费者关心内部命名时
- 需要快速替换实现时
3. 深度对比与实践指南
3.1 技术维度对比
| 特性 | 具名导出 | 默认导出 |
|---|---|---|
| 数量限制 | 不限 | 每个模块仅一个 |
| 导入语法 | 必须使用{} | 直接导入 |
| 重命名 | 导入/导出都可别名 | 只能在导入时别名 |
| Tree Shaking | 友好 | 不友好 |
| 动态导入 | 必须解构 | 直接访问default属性 |
3.2 真实项目中的选择策略
在Vue/React组件开发中,我始终坚持:
- 单个文件只导出一个组件时用default export
- 工具函数库必须用named exports
- 类型定义文件优先用named exports(方便按需导入)
混合导出示例:
javascript复制// auth.js
export const AUTH_TOKEN = 'token'
export const login = () => { /*...*/ }
export default class AuthService {
//...
}
对应的导入方式:
javascript复制import AuthService, { AUTH_TOKEN } from './auth.js'
3.3 鲜为人知的技巧
-
默认导出的本质:实际上只是特殊命名"default"的具名导出
javascript复制// 这两种写法等价 export default function() {} export { function as default } -
重新导出模式:
javascript复制// 合并多个模块的导出 export * from './moduleA.js' export { default } from './moduleB.js' -
动态导入时的处理:
javascript复制const module = await import('./module.js') // 默认导出在module.default // 具名导出直接通过module访问
4. 工程化实践中的陷阱
4.1 循环依赖问题
假设有两个模块互相引用:
javascript复制// a.js
import { b } from './b.js'
export const a = 'A'
// b.js
import { a } from './a.js'
export const b = 'B'
这会形成死循环。解决方案:
- 重构代码结构
- 将相互依赖的部分提取到第三个模块
- 必要时使用动态导入
4.2 Tree Shaking失效场景
Webpack等工具的静态分析会失效于:
javascript复制// 反模式!
export default {
method1: () => {},
method2: () => {}
}
应该改为:
javascript复制export function method1() {}
export function method2() {}
4.3 TypeScript中的特殊处理
在.d.ts类型声明文件中:
typescript复制// 正确的方式
export = MyLib
export as namespace MyLib
// 错误的方式
export default MyLib
5. 性能优化与调试技巧
5.1 打包体积优化
通过以下方式确保Tree Shaking生效:
- 使用具名导出工具函数
- 避免export default包含多个方法
- 在package.json中设置"sideEffects": false
5.2 调试源码映射
当遇到模块导入问题时:
- 在浏览器开发者工具中查看"Sources > Page"下的真实模块代码
- 使用
import.meta.url获取模块绝对路径 - 对于Webpack项目,可在配置中添加
output.chunkFilename: '[name].js'便于调试
5.3 模块热更新策略
在开发环境下:
javascript复制// 对于默认导出组件
if (module.hot) {
module.hot.accept('./Component.js', () => {
// 更新逻辑
})
}
6. 最佳实践总结
经过多个大型项目验证,我总结出这些黄金法则:
-
单一职责原则:每个模块只做一件事,如果发现需要混合导出,考虑拆分模块
-
命名一致性:
- 具名导出使用camelCase
- 默认导出使用PascalCase(类/组件)或camelCase(函数)
-
文档注释规范:
javascript复制/** * 计算商品折扣价格 * @param {number} price - 原始价格 * @param {number} discount - 折扣率(0-1) * @returns {number} 折后价格 */ export function calculateDiscount(price, discount) { //... } -
渐进式迁移策略:
对于老项目迁移,可以:- 先用webpack的
output.libraryTarget: 'umd'兼容旧系统 - 逐步替换require为import
- 最后移除babel的模块转换插件
- 先用webpack的
在最近参与的微前端架构项目中,我们要求所有子应用必须使用具名导出公共API,主框架通过import { exposedAPI } from 'child-app'的方式消费。这种约定显著提升了代码的可维护性和调试效率。