目录

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() }

总结

从传统到模块化的转变:

方面传统方式模块化方式
作用域全局作用域模块作用域
依赖隐式依赖显式声明
命名容易冲突命名空间隔离
重用复制粘贴导入使用
维护难以追踪清晰的关系
打包手动管理自动化处理

核心概念:

  1. 模块 = 独立的代码文件,有明确的输入输出

  2. 导出 = 模块向外提供的接口

  3. 导入 = 使用其他模块的功能

  4. 依赖 = 模块间的使用关系

类比Go语言:

  • JavaScript模块Go包(package)

  • export/import公开函数/import语句

  • 模块作用域包级作用域

模块化让JavaScript从"脚本语言"进化为"工程化语言",这是现代前端开发的基础!

掌握了模块化,你就可以:

  • 编写可维护的大型应用

  • 重用代码组件

  • 与团队协作开发

  • 使用现代构建工具

现在你已经准备好学习Vue 3了,因为Vue项目本质上就是由各种模块组成的!