Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

导入与导出

ES模块的导入和导出是模块系统的核心功能。本章将深入探讨各种导入导出模式、高级用法以及最佳实践。

导出的详细语法

1. 声明时导出

// declarations.js

// 导出变量声明
export const API_VERSION = 'v1';
export let currentUser = null;
export var debugMode = false;

// 导出函数声明
export function getUserById(id) {
    return fetch(`/api/users/${id}`);
}

// 导出异步函数
export async function fetchUserData(id) {
    const response = await getUserById(id);
    return response.json();
}

// 导出生成器函数
export function* numberGenerator() {
    let i = 0;
    while (true) {
        yield i++;
    }
}

// 导出类声明
export class UserManager {
    constructor() {
        this.users = new Map();
    }
    
    addUser(user) {
        this.users.set(user.id, user);
    }
    
    getUser(id) {
        return this.users.get(id);
    }
}

2. 先声明后导出

// deferred-exports.js

// 先声明
const CONFIG = {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retryAttempts: 3
};

function validateConfig(config) {
    return config.apiUrl && config.timeout > 0;
}

class Logger {
    constructor(level = 'info') {
        this.level = level;
    }
    
    log(message) {
        console.log(`[${this.level.toUpperCase()}] ${message}`);
    }
}

// 后导出
export { CONFIG, validateConfig, Logger };

// 导出时重命名
export { CONFIG as defaultConfig };
export { Logger as EventLogger };

3. 默认导出的多种形式

// default-exports.js

// 方式1:导出类
export default class Calculator {
    add(a, b) { return a + b; }
    subtract(a, b) { return a - b; }
}

// 方式2:导出函数
// export default function calculate(operation, a, b) {
//     switch (operation) {
//         case 'add': return a + b;
//         case 'subtract': return a - b;
//         default: throw new Error('Unknown operation');
//     }
// }

// 方式3:导出箭头函数
// export default (x, y) => x + y;

// 方式4:导出对象
// export default {
//     version: '1.0.0',
//     author: 'John Doe',
//     calculate: (a, b) => a + b
// };

// 方式5:导出表达式
// const multiplier = 2;
// export default multiplier * 10;

// 方式6:先声明后导出
// class AdvancedCalculator {
//     // ... implementation
// }
// export default AdvancedCalculator;

4. 聚合导出(Re-exports)

// utils/index.js - 模块聚合文件

// 重新导出所有命名导出
export * from './string-utils.js';
export * from './number-utils.js';
export * from './date-utils.js';

// 重新导出特定的命名导出
export { debounce, throttle } from './function-utils.js';
export { formatCurrency } from './number-utils.js';

// 重新导出并重命名
export { 
    parseDate as parseDateString,
    formatDate as formatDateString 
} from './date-utils.js';

// 重新导出默认导出
export { default as StringValidator } from './string-validator.js';
export { default as NumberValidator } from './number-validator.js';

// 混合导出:本地定义 + 重新导出
export const UTILS_VERSION = '2.1.0';

export function createUtilsBundle() {
    return {
        strings: './string-utils.js',
        numbers: './number-utils.js',
        dates: './date-utils.js'
    };
}

导入的详细语法

1. 命名导入的各种形式

// named-imports.js

// 基本命名导入
import { API_VERSION, currentUser } from './declarations.js';

// 导入时重命名
import { 
    getUserById as fetchUser,
    UserManager as UserService 
} from './declarations.js';

// 批量导入多个命名导出
import { 
    API_VERSION,
    currentUser,
    getUserById,
    UserManager 
} from './declarations.js';

// 导入所有命名导出到命名空间对象
import * as UserAPI from './declarations.js';
console.log(UserAPI.API_VERSION);
const manager = new UserAPI.UserManager();

// 混合导入:默认导出 + 命名导出
import Calculator, { 
    API_VERSION, 
    getUserById 
} from './mixed-exports.js';

2. 默认导入

// default-imports.js

// 导入默认导出(可以使用任意名称)
import Calculator from './default-exports.js';
import MyCalculator from './default-exports.js'; // 同样有效
import Calc from './default-exports.js'; // 也可以

// 使用导入的默认导出
const calc = new Calculator();
console.log(calc.add(2, 3));

// 如果默认导出是函数
import calculate from './function-export.js';
const result = calculate('add', 5, 3);

// 如果默认导出是对象
import config from './config-export.js';
console.log(config.version);

3. 副作用导入

// side-effect-imports.js

// 仅执行模块,不导入任何绑定
import './polyfills.js';          // 执行polyfill代码
import './global-styles.css';     // 加载CSS(通过构建工具处理)
import './init-app.js';           // 执行初始化代码

// 这些导入会执行目标模块的代码,但不创建任何绑定
console.log('All side effects have been applied');

4. 条件导入

// conditional-imports.js

// 在条件块中的静态导入(会被提升)
if (process.env.NODE_ENV === 'development') {
    // 这个导入会被提升到模块顶部执行
    import './dev-tools.js';
}

// 正确的条件导入方式:使用动态导入
async function loadDevTools() {
    if (process.env.NODE_ENV === 'development') {
        const devTools = await import('./dev-tools.js');
        devTools.setup();
    }
}

// 或者使用顶层await(现代环境)
if (process.env.NODE_ENV === 'development') {
    const { setup } = await import('./dev-tools.js');
    setup();
}

模块导入解析规则

1. 相对路径导入

// 相对路径导入示例
// 文件结构:
// src/
//   ├── components/
//   │   ├── Button.js
//   │   └── Input.js
//   ├── utils/
//   │   └── helpers.js
//   └── app.js

// 在 src/app.js 中
import { Button } from './components/Button.js';    // 相对路径
import { helpers } from './utils/helpers.js';       // 相对路径

// 在 src/components/Button.js 中
import { helpers } from '../utils/helpers.js';      // 上级目录
import { Input } from './Input.js';                 // 同级文件

2. 绝对路径和包导入

// absolute-imports.js

// Node.js 内置模块
import { readFile } from 'fs/promises';
import { join } from 'path';

// npm 包导入
import lodash from 'lodash';
import { debounce } from 'lodash';
import React from 'react';
import { useState } from 'react';

// 作用域包
import { parse } from '@babel/parser';
import { transform } from '@babel/core';

// 子路径导入
import { format } from 'date-fns/format';
import { isValid } from 'date-fns/isValid';

3. 带扩展名和不带扩展名

// extensions.js

// 明确指定扩展名(推荐)
import { utils } from './utils.js';
import config from './config.json';    // JSON文件
import styles from './styles.css';     // CSS文件(需构建工具支持)

// 不指定扩展名(依赖解析器配置)
// import { utils } from './utils';     // 可能解析为utils.js或utils/index.js
// import config from './config';       // 可能解析为config.js或config.json

导入导出的高级模式

1. 条件导出(package.json)

{
  "name": "my-package",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "types": "./dist/types/index.d.ts"
    },
    "./utils": {
      "import": "./dist/esm/utils.js",
      "require": "./dist/cjs/utils.js"
    },
    "./package.json": "./package.json"
  }
}

2. 桶文件模式(Barrel Exports)

// components/index.js - 桶文件
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Modal } from './Modal.js';
export { default as Form } from './Form.js';

// 也可以重新导出类型(TypeScript)
export type { ButtonProps } from './Button.js';
export type { InputProps } from './Input.js';

// 使用桶文件
// 在其他文件中可以从单一入口导入多个组件
import { Button, Input, Modal } from './components';

3. 命名空间模式

// math/index.js - 命名空间模式
import * as BasicMath from './basic.js';
import * as AdvancedMath from './advanced.js';
import * as Statistics from './statistics.js';

export const Math = {
    Basic: BasicMath,
    Advanced: AdvancedMath,
    Statistics
};

// 也导出具体函数以便直接使用
export { add, subtract, multiply, divide } from './basic.js';
export { sin, cos, tan, log } from './advanced.js';
export { mean, median, mode } from './statistics.js';

// 使用方式
// import { Math } from './math';
// Math.Basic.add(1, 2);
// 
// 或者
// import { add, sin, mean } from './math';
// add(1, 2);

4. 插件系统模式

// plugin-system.js

// 插件注册表
const plugins = new Map();

export function registerPlugin(name, plugin) {
    plugins.set(name, plugin);
}

export function getPlugin(name) {
    return plugins.get(name);
}

export function loadPlugin(name) {
    return import(`./plugins/${name}.js`).then(module => {
        registerPlugin(name, module.default);
        return module.default;
    });
}

// 插件接口
export class PluginBase {
    constructor(options = {}) {
        this.options = options;
    }
    
    init() {
        throw new Error('Plugin must implement init method');
    }
    
    destroy() {
        // 默认清理逻辑
    }
}

// 使用示例
// const authPlugin = await loadPlugin('auth');
// authPlugin.init({ apiKey: 'xxx' });

导入导出的最佳实践

1. 导出优先级

// good-exports.js

// 1. 优先使用命名导出
export function createUser(data) { /* ... */ }
export function updateUser(id, data) { /* ... */ }
export function deleteUser(id) { /* ... */ }

// 2. 主要功能使用默认导出
export default class UserService {
    constructor() {
        this.cache = new Map();
    }
    
    async getUser(id) {
        if (this.cache.has(id)) {
            return this.cache.get(id);
        }
        // ... fetch logic
    }
}

// 3. 常量和配置使用命名导出
export const DEFAULT_TIMEOUT = 5000;
export const USER_ROLES = ['admin', 'user', 'guest'];

2. 导入组织

// organized-imports.js

// 1. Node.js 内置模块
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';

// 2. 第三方依赖
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
import axios from 'axios';

// 3. 内部模块(按层级分组)
import { config } from '../config/app.js';
import { UserService } from '../services/user.js';
import { validateInput } from '../utils/validation.js';

// 4. 相对导入
import { Button } from './Button.js';
import { Modal } from './Modal.js';
import styles from './UserForm.module.css';

3. 避免循环依赖

// 反模式:循环依赖
// user.js
import { Order } from './order.js';
export class User {
    getOrders() {
        return Order.findByUserId(this.id);
    }
}

// order.js
import { User } from './user.js';  // 循环依赖!
export class Order {
    getUser() {
        return User.findById(this.userId);
    }
}

// 解决方案1:依赖注入
// user.js
export class User {
    getOrders(orderService) {
        return orderService.findByUserId(this.id);
    }
}

// order.js
export class Order {
    getUser(userService) {
        return userService.findById(this.userId);
    }
}

// 解决方案2:提取到服务层
// user-order-service.js
import { User } from './user.js';
import { Order } from './order.js';

export class UserOrderService {
    getUserOrders(userId) {
        return Order.findByUserId(userId);
    }
    
    getOrderUser(orderId) {
        const order = Order.findById(orderId);
        return User.findById(order.userId);
    }
}

4. 模块重新导出策略

// api/index.js - API 模块聚合

// 按功能模块重新导出
export * as Users from './users.js';
export * as Orders from './orders.js';
export * as Products from './products.js';

// 导出常用的API函数
export { 
    createUser, 
    getUserById 
} from './users.js';

export { 
    createOrder, 
    getOrdersByUser 
} from './orders.js';

// 导出配置和常量
export { API_BASE_URL, DEFAULT_HEADERS } from './config.js';

// 导出默认客户端
export { default as ApiClient } from './client.js';

// 使用示例
// 方式1:使用命名空间
// import { Users, Orders } from './api';
// const user = await Users.getUserById(1);
// const orders = await Orders.getOrdersByUser(1);

// 方式2:直接导入常用函数
// import { createUser, getOrdersByUser } from './api';

常见陷阱和注意事项

1. 导入提升

// import-hoisting.js

console.log('This runs first');

// 这个导入会被提升到模块顶部执行
import { someFunction } from './utils.js';

console.log('This runs second');

// 相当于:
// import { someFunction } from './utils.js';  // <- 实际执行位置
// console.log('This runs first');
// console.log('This runs second');

2. 默认导出的陷阱

// default-export-pitfalls.js

// 陷阱1:默认导出的重命名容易出错
// math.js
export default function add(a, b) { return a + b; }

// main.js
import subtract from './math.js';  // 错误:以为导入的是subtract函数
console.log(subtract(5, 3));       // 实际上是add函数,结果是8而不是2

// 解决方案:使用命名导出
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

// main.js
import { add, subtract } from './math.js';  // 明确的函数名

3. 导入绑定的活性

// live-bindings.js

// counter.js
export let count = 0;

export function increment() {
    count++;
}

export function getCount() {
    return count;
}

// main.js
import { count, increment } from './counter.js';

console.log(count);    // 0
increment();
console.log(count);    // 1 (绑定是活的,会更新)

// 但是不能直接修改导入的绑定
// count = 10;         // TypeError: Assignment to constant variable

总结

ES模块的导入导出系统提供了强大而灵活的代码组织方式:

  • 多样的导出方式: 命名导出、默认导出、重新导出
  • 灵活的导入语法: 支持重命名、命名空间、条件导入
  • 静态结构: 便于工具分析和优化
  • 活绑定: 导入的绑定能反映导出模块的状态变化
  • 良好的重用性: 支持模块聚合和重新导出模式

掌握这些导入导出的技巧和最佳实践,能够帮助你构建更清晰、更可维护的模块化代码。在下一章中,我们将探讨ES模块的动态导入功能。


下一章: 动态导入