1. 模块化开发的核心价值
前端开发从最初的脚本堆砌到如今的工程化体系,模块化是其中最关键的技术演进之一。想象一下,如果所有JavaScript代码都写在同一个文件里,随着项目规模扩大,这个文件会变成难以维护的"意大利面条式代码"。模块化就像把图书馆的藏书分类整理到不同书架,既避免了命名冲突,又提高了代码复用率。
在ES6标准之前,社区已经出现了多种模块化方案:
- CommonJS(Node.js默认规范)
- AMD(RequireJS为代表)
- UMD(通用模块定义)
但这些方案都需要依赖第三方库实现。ES6模块化(ES Modules)的诞生,终于让JavaScript在语言层面拥有了原生模块系统。现在所有现代浏览器和Node.js都已支持ES Modules,这成为我们日常开发的首选方案。
2. export default 详解
2.1 基本语法与特点
javascript复制// moduleA.js
const message = 'Hello World'
export default message
// 等效写法
export default 'Hello World'
export default 是模块的默认导出,每个模块只能有一个默认导出。它的特点包括:
- 导入时可以自定义名称
javascript复制import customName from './moduleA.js' console.log(customName) // 'Hello World' - 导出内容可以是任意类型(变量、函数、类、对象等)
- 通常用于导出模块的主要功能
2.2 实际应用场景
- 单功能模块:当模块主要提供一个核心功能时
javascript复制// logger.js export default function log(message) { console.log(`[${new Date().toISOString()}] ${message}`) } - 类定义:导出整个类定义时
javascript复制// User.js export default class User { constructor(name) { this.name = name } } - 配置对象:导出默认配置时
javascript复制// config.js export default { apiUrl: 'https://api.example.com', timeout: 5000 }
注意:虽然技术上可以在一个模块中同时使用默认导出和具名导出,但这会造成使用时的混乱,建议一个模块只选择一种导出方式。
3. 具名export深度解析
3.1 基本语法形式
javascript复制// mathUtils.js
export const PI = 3.14159
export function sum(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
具名导出的特点:
- 一个模块可以有多项具名导出
- 导入时必须使用相同名称(或通过as重命名)
- 更适合导出多个相关的工具函数或常量
3.2 导入方式对比
javascript复制// 方式1:逐个导入
import { PI, sum } from './mathUtils.js'
// 方式2:整体导入
import * as math from './mathUtils.js'
math.PI // 3.14159
// 方式3:重命名导入
import { sum as add } from './mathUtils.js'
3.3 最佳实践建议
- 相关功能分组导出:
javascript复制// stringUtils.js export function capitalize(str) { /*...*/ } export function truncate(str, length) { /*...*/ } - 使用统一前缀保持命名清晰:
javascript复制// api.js export const API_TIMEOUT = 5000 export const API_BASE_URL = 'https://api.example.com' - 导出列表统一管理:
javascript复制// 先定义后统一导出 const VERSION = '1.0.0' const DEBUG_MODE = true export { VERSION, DEBUG_MODE }
4. 两种导出方式的对比与选择
4.1 技术差异对比表
| 特性 | export default | 具名export |
|---|---|---|
| 每个模块导出数量 | 仅1个 | 多个 |
| 导入时名称 | 可自定义 | 必须匹配或使用as |
| 适用场景 | 模块主要功能 | 工具函数/常量集合 |
| 静态分析友好度 | 较低 | 较高 |
| 代码自动补全 | 较差 | 优秀 |
4.2 选择依据与决策树
-
单一功能原则:如果模块只提供一个主要功能,使用默认导出
javascript复制// axiosInstance.js export default axios.create({ /* 配置 */ }) -
工具集合原则:如果是工具函数集合,使用具名导出
javascript复制// domUtils.js export function getElement(selector) { /*...*/ } export function addClass(el, className) { /*...*/ } -
框架适配原则:
- React组件通常使用默认导出
- Vue 3的组合式API函数通常使用具名导出
4.3 混合使用场景(谨慎)
虽然可以混合使用,但通常不建议:
javascript复制// 不推荐写法
export default function main() { /*...*/ }
export const helper = () => { /*...*/ }
如果必须混合使用,导入时需要:
javascript复制import mainFunc, { helper } from './module.js'
5. 高级技巧与性能优化
5.1 动态导入与懒加载
javascript复制// 按需加载模块
button.addEventListener('click', async () => {
const module = await import('./dialogBox.js')
module.open()
})
5.2 导出转发技巧
- 统一入口文件:
javascript复制// utils/index.js export { default as StringUtils } from './stringUtils.js' export { default as ArrayUtils } from './arrayUtils.js' - 聚合多个模块的导出:
javascript复制// constants/index.js export * from './apiConstants.js' export * from './appConstants.js'
5.3 Tree Shaking优化
- 确保使用具名导出:
javascript复制// 优化前(无法tree shaking) export default { func1, func2 } // 优化后 export function func1() { /*...*/ } export function func2() { /*...*/ } - 避免导出时的副作用:
javascript复制// 避免这种写法 export const data = computeExpensiveValue()
6. 常见问题与解决方案
6.1 循环依赖问题
症状:
- 模块A导入模块B,模块B又导入模块A
- 导致某些导出值为undefined
解决方案:
- 重构代码,提取公共部分到第三个模块
- 使用函数延迟调用:
javascript复制// moduleA.js import { funcB } from './moduleB.js' export function funcA() { return funcB() } // moduleB.js import { funcA } from './moduleA.js' export function funcB() { // 通过函数延迟访问 return () => funcA() }
6.2 类型提示增强
对于TypeScript项目,可以通过JSDoc增强类型提示:
javascript复制/**
* 计算两个数的和
* @param {number} a - 第一个数
* @param {number} b - 第二个数
* @returns {number} 两数之和
*/
export function sum(a, b) {
return a + b
}
6.3 Node.js中的特殊处理
在Node.js中使用ES模块:
- 文件扩展名必须为
.mjs,或 - 在
package.json中设置"type": "module"
javascript复制// package.json
{
"type": "module"
}
7. 现代前端框架中的实践差异
7.1 React中的惯例
- 组件通常默认导出:
javascript复制// Button.jsx export default function Button() { return <button>Click</button> } - 工具函数具名导出:
javascript复制// hooks/useFetch.js export function useFetch(url) { /*...*/ }
7.2 Vue 3的组合式API
- 组合函数通常具名导出:
javascript复制// composables/useCounter.js export function useCounter() { const count = ref(0) return { count } } - 组件可以两种方式:
javascript复制// 选项1:默认导出 export default { setup() { /*...*/ } } // 选项2:组合式API风格 export const defineComponent = { /*...*/ }
7.3 Svelte的独特约定
Svelte组件必须默认导出:
svelte复制<script>
export let name // 这是组件prop,不是模块导出
</script>
8. 构建工具配置要点
8.1 webpack配置
javascript复制// webpack.config.js
module.exports = {
output: {
module: true, // 输出ES模块
},
experiments: {
outputModule: true,
},
}
8.2 Rollup优化
javascript复制// rollup.config.js
export default {
input: 'src/index.js',
output: {
format: 'esm', // ES模块格式
dir: 'dist',
},
}
8.3 Babel转译配置
javascript复制// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { modules: false }] // 保留ES模块语法
]
}
9. 测试策略与Mock技巧
9.1 单元测试中的模块处理
javascript复制// 使用jest测试具名导出
import { sum } from './math'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
9.2 Mock默认导出模块
javascript复制// __mocks__/logger.js
export default {
log: jest.fn()
}
// 测试文件
jest.mock('./logger')
import logger from './logger'
test('should call logger', () => {
logger.log('test')
expect(logger.log).toHaveBeenCalled()
})
10. 未来演进与提案观察
10.1 JSON模块导入
javascript复制import config from './config.json' assert { type: 'json' }
10.2 Top-level await
javascript复制// 模块顶层直接使用await
const data = await fetchData()
export { data }
10.3 Import Maps提案
html复制<script type="importmap">
{
"imports": {
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
在实际项目中,选择导出方式时应该考虑团队约定、框架惯例和长期维护成本。具名导出通常更适合工具库和工具函数,而默认导出更适合单一功能的组件或服务。无论选择哪种方式,保持项目内部的一致性往往比技术细节本身更重要。