JavaScript 模块化详解 - 从传统脚本到现代模块
什么是模块化?
模块化就是将大的程序拆分成小的、独立的、可重用的代码块(模块)。
生活中的类比
想象一下建房子:
传统方式:所有材料堆在一起,现场制作所有东西
模块化方式:预制门窗、标准砖块、成品家具,组装时直接使用
传统JavaScript的问题
传统方式(你熟悉的):
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>传统方式</title>
</head>
<body>
<div id="app">
<button onclick="handleClick()">点击</button>
<div id="result"></div>
</div>
<!-- 所有JS文件按顺序加载 -->
<script src="utils.js"></script> <!-- 工具函数 -->
<script src="api.js"></script> <!-- API调用 -->
<script src="user.js"></script> <!-- 用户相关 -->
<script src="main.js"></script> <!-- 主逻辑 -->
</body>
</html>
// utils.js - 工具函数
function formatDate(date) {
return date.toLocaleDateString()
}
function showMessage(msg) {
alert(msg)
}
// 全局变量
var APP_NAME = "我的应用"
// api.js - API调用
function fetchUserData(userId) {
// 依赖utils.js中的showMessage
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.catch(error => {
showMessage("获取用户数据失败") // 全局函数
throw error
})
}
// user.js - 用户相关逻辑
var currentUser = null // 全局变量
function setCurrentUser(user) {
currentUser = user
document.getElementById('result').innerHTML =
`当前用户: ${user.name} (${formatDate(new Date())})` // 依赖utils.js
}
function getCurrentUser() {
return currentUser
}
// main.js - 主逻辑
function handleClick() {
// 依赖api.js和user.js
fetchUserData(123)
.then(user => {
setCurrentUser(user)
console.log("用户设置成功")
})
}
// 页面加载完成后初始化
window.onload = function() {
console.log("应用启动:", APP_NAME) // 依赖utils.js
}
传统方式的问题:
1. 全局作用域污染
// 所有变量和函数都在全局作用域
var APP_NAME = "我的应用" // 可能被其他脚本覆盖
var currentUser = null // 命名冲突风险
function formatDate() { } // 全局函数,可能重名
function showMessage() { } // 容易被覆盖
2. 依赖关系混乱
<!-- 必须按正确顺序加载,否则报错 -->
<script src="main.js"></script> <!-- 错误!依赖其他文件 -->
<script src="utils.js"></script>
<script src="api.js"></script>
<script src="user.js"></script>
3. 难以维护
不知道哪个函数在哪个文件里
修改一个函数可能影响多个地方
无法轻易重用代码
4. 命名冲突
// utils.js
function log(msg) {
console.log("Utils:", msg)
}
// api.js
function log(msg) {
console.log("API:", msg) // 覆盖了utils.js的log函数!
}
现代模块化解决方案
ES6 模块化语法
1. 导出(export)
// utils.js - ES6模块方式
export function formatDate(date) {
return date.toLocaleDateString()
}
export function showMessage(msg) {
alert(msg)
}
// 导出常量
export const APP_NAME = "我的应用"
// 默认导出
export default function logger(msg) {
console.log(`[${new Date().toISOString()}] ${msg}`)
}
// 也可以统一导出
// export { formatDate, showMessage, APP_NAME }
2. 导入(import)
// main.js - 导入使用
import logger, { formatDate, showMessage, APP_NAME } from './utils.js'
import { fetchUserData } from './api.js'
import { setCurrentUser, getCurrentUser } from './user.js'
// 使用导入的函数
function handleClick() {
logger("按钮被点击了")
fetchUserData(123)
.then(user => {
setCurrentUser(user)
showMessage(`用户加载成功: ${user.name}`)
})
}
console.log("应用启动:", APP_NAME)
与Go语言的对比
Go语言的模块化:
// utils/date.go
package utils
import "time"
func FormatDate(t time.Time) string {
return t.Format("2006-01-02")
}
func ShowMessage(msg string) {
fmt.Println("消息:", msg)
}
// main.go
package main
import (
"fmt"
"./utils" // 导入utils包
)
func main() {
now := time.Now()
formatted := utils.FormatDate(now) // 使用utils包的函数
utils.ShowMessage(formatted)
}
JavaScript的模块化:
// utils.js
export function formatDate(date) {
return date.toLocaleDateString()
}
export function showMessage(msg) {
console.log("消息:", msg)
}
// main.js
import { formatDate, showMessage } from './utils.js'
const now = new Date()
const formatted = formatDate(now) // 使用导入的函数
showMessage(formatted)
相似点:
都有包/模块的概念
都需要显式导入/导出
都解决了命名空间问题
模块化的完整示例
项目结构:
project/
├── index.html
├── js/
│ ├── utils/
│ │ ├── date.js
│ │ ├── dom.js
│ │ └── index.js
│ ├── api/
│ │ ├── user.js
│ │ └── index.js
│ ├── models/
│ │ └── User.js
│ └── main.js
实际代码示例:
工具模块
// js/utils/date.js
export function formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
export function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}
export function formatDateTime(date) {
return `${formatDate(date)} ${formatTime(date)}`
}
// js/utils/dom.js
export function $(selector) {
return document.querySelector(selector)
}
export function $$(selector) {
return document.querySelectorAll(selector)
}
export function createElement(tag, className = '', text = '') {
const element = document.createElement(tag)
if (className) element.className = className
if (text) element.textContent = text
return element
}
export function showMessage(message, type = 'info') {
const messageDiv = createElement('div', `message ${type}`, message)
document.body.appendChild(messageDiv)
setTimeout(() => {
document.body.removeChild(messageDiv)
}, 3000)
}
// js/utils/index.js - 统一导出工具函数
export * from './date.js'
export * from './dom.js'
// 也可以重新组织导出
export { formatDate, formatTime } from './date.js'
export { $, showMessage } from './dom.js'
数据模型
// js/models/User.js
export class User {
constructor(data) {
this.id = data.id
this.name = data.name
this.email = data.email
this.createTime = new Date(data.createTime)
}
getDisplayName() {
return `${this.name} <${this.email}>`
}
isAdmin() {
return this.email.endsWith('@admin.com')
}
getCreateTimeFormatted() {
// 这里我们需要使用utils中的函数
// 但是为了避免循环依赖,我们在使用时再导入
return this.createTime.toLocaleDateString()
}
}
// 默认导出User类
export default User
API模块
// js/api/user.js
import { showMessage } from '../utils/index.js'
import User from '../models/User.js'
// 基础API配置
const API_BASE = '/api'
// 私有函数,不导出
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
// 导出的API函数
export async function fetchUser(userId) {
try {
const response = await fetch(`${API_BASE}/users/${userId}`)
const data = await handleResponse(response)
return new User(data) // 返回User实例
} catch (error) {
showMessage(`获取用户失败: ${error.message}`, 'error')
throw error
}
}
export async function fetchUsers() {
try {
const response = await fetch(`${API_BASE}/users`)
const data = await handleResponse(response)
return data.map(userData => new User(userData))
} catch (error) {
showMessage(`获取用户列表失败: ${error.message}`, 'error')
throw error
}
}
export async function createUser(userData) {
try {
const response = await fetch(`${API_BASE}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
const data = await handleResponse(response)
showMessage('用户创建成功!', 'success')
return new User(data)
} catch (error) {
showMessage(`创建用户失败: ${error.message}`, 'error')
throw error
}
}
// js/api/index.js - 统一API入口
export * from './user.js'
// 可以添加其他API模块
// export * from './product.js'
// export * from './order.js'
主应用逻辑
// js/main.js
import { $, showMessage, formatDateTime } from './utils/index.js'
import { fetchUser, fetchUsers, createUser } from './api/index.js'
// 应用状态
class App {
constructor() {
this.currentUser = null
this.users = []
this.init()
}
init() {
console.log('应用初始化...')
this.bindEvents()
this.loadInitialData()
}
bindEvents() {
// 获取单个用户
$('#btn-get-user').addEventListener('click', async () => {
const userId = $('#input-user-id').value
if (!userId) {
showMessage('请输入用户ID', 'warning')
return
}
try {
const user = await fetchUser(parseInt(userId))
this.displayUser(user)
} catch (error) {
console.error('获取用户失败:', error)
}
})
// 获取用户列表
$('#btn-get-users').addEventListener('click', async () => {
try {
const users = await fetchUsers()
this.displayUsers(users)
} catch (error) {
console.error('获取用户列表失败:', error)
}
})
// 创建用户
$('#btn-create-user').addEventListener('click', async () => {
const name = $('#input-name').value
const email = $('#input-email').value
if (!name || !email) {
showMessage('请填写用户名和邮箱', 'warning')
return
}
try {
const newUser = await createUser({ name, email })
showMessage(`用户创建成功: ${newUser.getDisplayName()}`, 'success')
// 刷新用户列表
$('#btn-get-users').click()
} catch (error) {
console.error('创建用户失败:', error)
}
})
}
async loadInitialData() {
showMessage('加载初始数据...', 'info')
try {
this.users = await fetchUsers()
this.displayUsers(this.users)
} catch (error) {
console.error('加载初始数据失败:', error)
}
}
displayUser(user) {
const userInfo = $('#user-info')
userInfo.innerHTML = `
<h3>${user.getDisplayName()}</h3>
<p>ID: ${user.id}</p>
<p>创建时间: ${formatDateTime(user.createTime)}</p>
<p>管理员: ${user.isAdmin() ? '是' : '否'}</p>
`
}
displayUsers(users) {
const usersList = $('#users-list')
usersList.innerHTML = users.map(user => `
<li>
<strong>${user.name}</strong>
(${user.email})
${user.isAdmin() ? '<span class="admin-badge">管理员</span>' : ''}
</li>
`).join('')
showMessage(`显示了 ${users.length} 个用户`, 'info')
}
}
// 应用启动
const app = new App()
// 导出app实例,供其他模块使用(如果需要)
export default app
HTML文件
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>模块化JavaScript示例</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.controls { margin-bottom: 20px; }
.controls input, .controls button {
margin: 5px; padding: 8px;
}
#user-info {
border: 1px solid #ccc;
padding: 15px;
margin: 20px 0;
}
#users-list {
list-style-type: none;
padding: 0;
}
#users-list li {
padding: 10px;
border-bottom: 1px solid #eee;
}
.message {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
position: fixed;
top: 10px;
right: 10px;
z-index: 1000;
}
.message.info { background: #d1ecf1; color: #0c5460; }
.message.success { background: #d4edda; color: #155724; }
.message.warning { background: #fff3cd; color: #856404; }
.message.error { background: #f8d7da; color: #721c24; }
.admin-badge {
background: #007bff;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>
</head>
<body>
<h1>JavaScript 模块化示例</h1>
<div class="controls">
<h3>获取单个用户</h3>
<input type="number" id="input-user-id" placeholder="输入用户ID">
<button id="btn-get-user">获取用户</button>
</div>
<div class="controls">
<h3>创建新用户</h3>
<input type="text" id="input-name" placeholder="用户名">
<input type="email" id="input-email" placeholder="邮箱">
<button id="btn-create-user">创建用户</button>
</div>
<div class="controls">
<button id="btn-get-users">获取所有用户</button>
</div>
<div id="user-info">
<!-- 单个用户信息显示区域 -->
</div>
<h3>用户列表</h3>
<ul id="users-list">
<!-- 用户列表显示区域 -->
</ul>
<!-- 模块化JavaScript -->
<script type="module" src="js/main.js"></script>
</body>
</html>
不同的模块化方案
1. ES6 Modules(现代标准)
// 导出
export const name = 'link'
export function greet() { }
export default class User { }
// 导入
import User, { name, greet } from './module.js'
2. CommonJS(Node.js)
// 导出
const name = 'link'
function greet() { }
module.exports = { name, greet }
// 导入
const { name, greet } = require('./module')
3. AMD(require.js)
// 定义模块
define(['dependency'], function(dep) {
return {
name: 'link',
greet: function() { }
}
})
// 使用模块
require(['module'], function(mod) {
mod.greet()
})
模块化的优势
1. 命名空间隔离
// 模块A
export function log(msg) {
console.log("模块A:", msg)
}
// 模块B
export function log(msg) {
console.log("模块B:", msg)
}
// 使用时不冲突
import { log as logA } from './moduleA.js'
import { log as logB } from './moduleB.js'
logA("消息1") // "模块A: 消息1"
logB("消息2") // "模块B: 消息2"
2. 依赖关系清晰
// main.js
import { fetchUser } from './api/user.js' // 清晰地知道依赖什么
import { formatDate } from './utils/date.js' // 每个依赖都明确声明
import User from './models/User.js'
// 依赖关系一目了然,不会出现隐式依赖
3. 代码重用
// 工具模块可以在多个项目中重用
import { formatDate, showMessage } from './utils/index.js'
// 可以轻松地在不同项目间复制utils目录
4. 按需加载
// 只导入需要的功能
import { formatDate } from './utils/date.js' // 只要日期格式化
// 而不是导入整个工具库
// 打包工具可以进行Tree Shaking,去掉未使用的代码
在Vue项目中的模块化
Vue组件就是模块
<!-- UserCard.vue -->
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</template>
<script setup>
// Vue组件内部的模块化导入
import { formatDate } from '@/utils/date.js'
import { fetchUser } from '@/api/user.js'
// 组件的props
defineProps({
user: {
type: Object,
required: true
}
})
</script>
// 在其他组件中使用
import UserCard from './components/UserCard.vue'
export default {
components: {
UserCard // 注册组件模块
}
}
常见模块化模式
1. 默认导出 vs 命名导出
// 默认导出 - 每个模块只有一个主要导出
// User.js
export default class User {
constructor(name) {
this.name = name
}
}
// 导入时可以任意命名
import User from './User.js'
import MyUser from './User.js' // 同样有效
// 命名导出 - 每个模块可以有多个导出
// utils.js
export function formatDate() { }
export function formatTime() { }
export const APP_NAME = 'MyApp'
// 导入时必须使用确切的名称
import { formatDate, APP_NAME } from './utils.js'
// 或者导入所有命名导出
import * as utils from './utils.js'
utils.formatDate()
2. 重新导出(Re-export)
// api/index.js - 统一API入口
export { fetchUser, createUser } from './user.js'
export { fetchProducts } from './product.js'
export { fetchOrders } from './order.js'
// 这样其他模块只需要从一个地方导入
import { fetchUser, fetchProducts, fetchOrders } from './api/index.js'
3. 动态导入
// 按需加载模块
async function loadUserModule() {
const userModule = await import('./user.js')
const user = new userModule.User('link')
return user
}
// Vue中的路由懒加载
const routes = [
{
path: '/user',
component: () => import('./views/User.vue') // 懒加载组件
}
]
模块化最佳实践
1. 合理的文件结构
src/
├── components/ # 可重用组件
│ ├── common/ # 通用组件
│ └── business/ # 业务组件
├── utils/ # 工具函数
│ ├── date.js
│ ├── dom.js
│ └── index.js # 统一导出
├── api/ # API接口
│ ├── user.js
│ ├── product.js
│ └── index.js
├── models/ # 数据模型
├── stores/ # 状态管理
└── main.js # 入口文件
2. 清晰的导入导出
// ✅ 好的做法
// 明确的命名
export function formatUserDate(date) { }
export function validateEmail(email) { }
// 统一的导出方式
export {
formatUserDate,
validateEmail,
APP_CONFIG
}
// ❌ 避免的做法
// 默认导出太多东西
export default {
formatUserDate,
validateEmail,
APP_CONFIG,
anotherFunction,
yetAnotherFunction
}
3. 避免循环依赖
// ❌ 循环依赖
// a.js
import { funcB } from './b.js'
export function funcA() { funcB() }
// b.js
import { funcA } from './a.js'
export function funcB() { funcA() } // 循环依赖!
// ✅ 解决方案:提取公共依赖
// common.js
export function sharedFunction() { }
// a.js
import { sharedFunction } from './common.js'
export function funcA() { sharedFunction() }
// b.js
import { sharedFunction } from './common.js'
export function funcB() { sharedFunction() }
总结
从传统到模块化的转变:
方面 | 传统方式 | 模块化方式 |
---|---|---|
作用域 | 全局作用域 | 模块作用域 |
依赖 | 隐式依赖 | 显式声明 |
命名 | 容易冲突 | 命名空间隔离 |
重用 | 复制粘贴 | 导入使用 |
维护 | 难以追踪 | 清晰的关系 |
打包 | 手动管理 | 自动化处理 |
核心概念:
模块 = 独立的代码文件,有明确的输入输出
导出 = 模块向外提供的接口
导入 = 使用其他模块的功能
依赖 = 模块间的使用关系
类比Go语言:
JavaScript模块 ≈ Go包(package)
export/import ≈ 公开函数/import语句
模块作用域 ≈ 包级作用域
模块化让JavaScript从"脚本语言"进化为"工程化语言",这是现代前端开发的基础!
掌握了模块化,你就可以:
编写可维护的大型应用
重用代码组件
与团队协作开发
使用现代构建工具
现在你已经准备好学习Vue 3了,因为Vue项目本质上就是由各种模块组成的!