【ES6】day39-es-module

作者:mario 发布时间: 2026-08-27 阅读量:5 评论数:0

第三十九天:ES Module 模块化 — 从单文件到工程

蜗牛往上爬 · 前端工业可视化学习笔记 第 39 篇
单文件项目写到 300 行就开始失控:全局变量满天飞、依赖靠人肉排序。ES Module 用 import/export 让每个文件成为独立模块——显式声明依赖、私有作用域、按需复用。今天学完,把设备管理系统拆成专业工程。


目录


一、为什么需要模块化

单文件的三大痛点:

<!-- 依赖顺序靠人肉维护,写错就崩 -->
<script src="device.js"></script>
<script src="manager.js"></script>
<script src="app.js"></script>
  1. 全局污染:所有变量共享全局作用域,命名冲突

  2. 顺序脆弱:依赖关系靠加载顺序,改一次崩一次

  3. 无法控制暴露:变量要么全公开要么全不可用

ESM 的解法:文件即模块,内部私有,import/export 显式声明依赖。


二、命名导出与默认导出

// ===== 命名导出:一个文件可导出多个 =====
// utils.js
export const API_URL = "https://api.example.com";
export const MAX_TEMP = 80;

export function formatTemp(temp) {
  return `${temp.toFixed(1)}°C`;
}

// ===== 默认导出:一个文件只能有一个 =====
// Device.js
export default class Device {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}

对比

命名导出

默认导出

数量

多个

一个

导入名

必须一致 + {}

随意命名,不用 {}

场景

工具函数、常量

主类、主组件


三、导入的多种方式

// 命名导入(名字必须一致)
import { API_URL, formatTemp } from "./utils.js";

// 默认导入(名字随便起)
import Device from "./Device.js";

// 混合导入
import Device, { formatTemp, MAX_TEMP } from "./device-module.js";

// 重命名
import { formatTemp as fmt } from "./utils.js";

// 命名空间(一次全拿)
import * as utils from "./utils.js";
utils.formatTemp(65);

// 动态导入(返回 Promise,按需加载)
const { default: Device } = await import("./Device.js");

四、模块的四条规则

// 1. 模块内变量私有(不污染全局)
// counter.js
let count = 0;  // 外部无法访问
export const increment = () => ++count;

// 2. 导入是只读视图
import { config } from "./config.js";
// config = {};        // ❌ 不能重新赋值
config.theme = "dark"; // ✅ 可以改属性

// 3. 模块是单例(导入多次只执行一次)
// 第一次 import 执行代码,之后共享同一份导出

// 4. 自动严格模式
// this 在模块顶层是 undefined

五、浏览器中使用

<!-- ⭐ 必须声明 type="module" -->
<script type="module" src="js/app.js"></script>

注意

  • 路径写清楚(./utils.js,带后缀)

  • 本地开发建议用 Live Server 起服务(file:// 协议会有跨域限制)

  • 模块自动 defer(DOM 加载完后执行)


六、综合实战:模块化设备管理系统

项目结构(分层架构)

device-manager/
├── index.html
└── js/
    ├── app.js          # 入口:事件绑定
    ├── classes/Device.js    # 领域模型
    ├── services/store.js    # 数据与业务
    ├── utils/format.js      # 工具函数
    └── views/render.js      # 渲染层
// js/classes/Device.js
export default class Device {
  constructor(id, name, temp = 0) {
    this.id = id;
    this.name = name;
    this.temp = temp;
  }
  get level() {
    if (this.temp >= 80) return "危险";
    if (this.temp >= 60) return "偏高";
    return "正常";
  }
}
// js/services/store.js —— ⭐ 模块级私有数据
import Device from "../classes/Device.js";

let devices = [];      // 外部无法直接改!
let nextId = 1;

export function addDevice(name, temp) {
  const device = new Device(`DEV-${String(nextId).padStart(3, "0")}`, name, temp);
  devices.push(device);
  nextId++;
  return device;
}

export function removeDevice(id) {
  devices = devices.filter(d => d.id !== id);
}

export function searchDevices(keyword = "") {
  if (!keyword) return [...devices];
  return devices.filter(d => d.name.includes(keyword));
}
// js/views/render.js —— 纯展示
import { formatTemp } from "../utils/format.js";
import { removeDevice } from "../services/store.js";

export function renderDeviceList(devices, container) {
  container.innerHTML = devices
    .map(d => `
      <li class="device-item">
        <strong>${d.id}</strong> ${d.name} <span>[${d.level}]</span>
        <span>${formatTemp(d.temp)}</span>
        <button data-id="${d.id}">删除</button>
      </li>
    `)
    .join("");

  container.querySelectorAll("button[data-id]").forEach(btn => {
    btn.addEventListener("click", () => removeDevice(btn.dataset.id));
  });
}
// js/app.js —— 入口
import { addDevice, searchDevices } from "./services/store.js";
import { renderDeviceList } from "./views/render.js";

const list = document.getElementById("deviceList");

function refresh() {
  renderDeviceList(searchDevices(
    document.getElementById("searchInput").value.trim()
  ), list);
}

document.getElementById("addBtn").addEventListener("click", () => {
  addDevice(
    document.getElementById("nameInput").value.trim(),
    Number(document.getElementById("tempInput").value)
  );
  refresh();
});

document.getElementById("searchInput").addEventListener("input", refresh);

// 预置数据
addDevice("数控机床", 65);
addDevice("搬运机器人", 42);
refresh();

模块化收益对比

维度

单文件

模块化

变量

全局易冲突

模块私有

职责

混在一起

模型/数据/渲染/入口分层

复用

复制粘贴

import 即用

依赖

靠加载顺序

显式声明

分层app.js(事件) → views/(渲染) → services/(业务) → classes/(模型) → utils/(工具)——这个思想就是阶段2 框架工程的前身。


七、总结

语法

用途

export const fn

命名导出(多个)

export default X

默认导出(一个)

import { fn }

命名导入(名字一致)

import X

默认导入(随意命名)

import * as ns

命名空间导入

import("...")

动态导入(异步)

<script type="module">

浏览器启用 ESM

🎯 Class + 模块化打通了工程化地基。下周阶段1 收官:综合实战把 10 周所学融成一个完整项目!

评论