前端面试知识库
工程化与场景题

前端模块化

从规范演进、打包机制到 Tree Shaking 实践,整理模块化面试的完整回答路径。

模块化题考的不是规范背诵,而是理解模块系统如何影响打包产物和运行时行为。先讲规范的动机和局限,再讲打包器如何处理模块,最后落到 Tree Shaking 和包设计实践。

高频主线

  • 模块规范如何从全局污染演进到 ESM
  • ESM 与 CJS 的核心差异和工程影响
  • 打包器如何处理模块依赖并生成产物
  • Tree Shaking 的原理和实践条件
  • 如何设计一个可复用的 npm 包

模块规范演进 — 从全局污染到 ESM

阶段规范核心机制解决的问题遗留问题
1IIFE函数作用域隔离全局变量污染依赖顺序需手动管理
2CJSrequire + module.exports模块复用 + 依赖声明同步加载,浏览器不可用
3AMDdefine + require(异步)浏览器端异步加载语法冗余,社区分裂
4ESMimport + export(静态声明)静态分析 + Tree Shaking需打包器转译(兼容性)
IIFE (2005)     → CJS (2009, Node)     → AMD (2011, RequireJS)  → ESM (2015, ES6)
全局污染 → 服务端模块化      → 浏览器端异步加载     → 统一标准 + 静态分析

IIFE 是前端最早的模块化手段(jQuery 时代的立即执行函数)。CJS 和 AMD 是社区分裂期的两个方案(服务端 vs 浏览器)。ESM 是语言级标准,统一了模块系统,并带来了 CJS 无法实现的能力:静态分析和 Tree Shaking。

ESM vs CJS — 核心差异

维度ESMCJS
加载方式编译时静态分析运行时动态加载
导入语法import { foo } from './mod'const { foo } = require('./mod')
导出绑定引用绑定(live binding)值拷贝
this 顶层undefinedmodule 对象
循环依赖引用已声明但未初始化的变量得到已执行部分的 module.exports
Tree Shaking支持(静态分析可确定使用情况)不支持(动态 require 无法静态分析)
异步加载原生支持 import()不支持(require 是同步的)
适用环境浏览器 + Node(需 type: module)Node 原生支持

导出绑定差异 — 最容易踩的坑

// CJS — 值拷贝,后续修改不影响已导入的值
// counter.js
let count = 0
setTimeout(() => { count = 1 }, 100)
module.exports = { count }

// main.js
const { count } = require('./counter')
console.log(count) // 0(拷贝的是导出时的值)
setTimeout(() => console.log(count), 200) // 仍然是 0
// ESM — 引用绑定,修改实时反映
// counter.js
export let count = 0
setTimeout(() => { count = 1 }, 100)

// main.js
import { count } from './counter.js'
console.log(count) // 0
setTimeout(() => console.log(count), 200) // 1(引用绑定,实时更新)

CJS 导出的是值的拷贝,ESM 导出的是引用绑定。这是两者最本质的语义差异,也是 Tree Shaking 只能基于 ESM 的根本原因——静态分析需要确定模块间的精确依赖关系。

模块打包 — Bundle 的本质

打包做了什么

入口文件 → 构建依赖图(Dependency Graph)→ 模块合并 → 产物输出
              ↓                              ↓
         递归解析 import/require        按规则合并、去重、压缩

打包器对比

维度WebpackVite(开发)Vite(构建)Rollupesbuild
开发启动慢(全量打包)极快(原生 ESM,不打包)
HMR增量编译即时(模块级热替换)
构建产物Chunk 分割灵活Rollup 输出Tree Shaking 优秀库打包首选极快但不优化
适用场景复杂应用新项目首选库和组件库极速构建

Vite 开发时不打包——浏览器原生 ESM 按需加载,所以启动极快。构建时用 Rollup 输出优化产物。Webpack 则开发构建都需要全量打包,项目越大启动越慢。

Webpack 核心概念

概念作用
Entry打包入口,构建依赖图的起点
Output产物输出路径和命名规则
Loader转换非 JS 文件(CSS、图片、TS → JS)
Plugin扩展构建流程(压缩、环境变量、HTML 生成)
Module一切文件都是模块(JS、CSS、图片)
Chunk打包过程中的模块集合(入口 Chunk + 异步 Chunk)
Bundle最终输出的产物文件

Tree Shaking — 消除死代码

原理

ESM 静态 import → 构建依赖图 → 标记未使用的导出 → 产物中移除

            需要 ESM 格式 + 无副作用 + 静态可分析

实践条件

条件说明反例
使用 ESM 格式import/export 可静态分析CJS 的 require() 是动态的
标记副作用package.json 设置 "sideEffects": false未标记时打包器保守保留所有代码
避免动态导入干扰运行时条件导入无法静态分析if (cond) import('./mod')
避免全局副作用模块顶层不要有 window.xxx = ...打包器无法安全移除

sideEffects 配置

// package.json — 标记无副作用,允许 Tree Shaking
{
  "sideEffects": false
}

// 有副作用的文件需单独列出
{
  "sideEffects": ["*.css", "./src/polyfill.js"]
}
// utils.ts — ESM 格式,静态导出
export function add(a: number, b: number) { return a + b }
export function multiply(a: number, b: number) { return a * b }

// main.ts — 只导入 add
import { add } from './utils'
console.log(add(1, 2))

// 产物中 multiply 被移除 ✓
// utils.js — CJS 格式,动态导出
module.exports = {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b,
}

// main.js
const { add } = require('./utils')

// 产物中 multiply 仍然存在 ✗
// CJS 的 require 是运行时执行,打包器无法确定哪些导出被使用

Tree Shaking 的核心条件是静态可分析——打包器在编译时就能确定哪些代码不会被使用。ESM 的 import/export 是静态声明,CJS 的 require 是运行时调用。这就是为什么 lodash(CJS)不支持 Tree Shaking,而 lodash-es(ESM)支持。

模块解析 — 路径怎么找到

Node 解析算法

导入 'lodash-es'

1. 是否是内置模块?否
2. 是否是相对/绝对路径?否
3. 当前目录 node_modules/lodash-es/ → 找到
4. 读取 package.json 的 exports/main/module 字段
5. 按条件匹配(import → ESM 入口,require → CJS 入口)

package.json 入口字段优先级

字段优先级说明
exports最高现代标准,精确控制子路径导出和条件
moduleESM 入口(社区约定,非标准)
mainCJS 入口(传统标准)
// 现代 package.json 同时支持 ESM 和 CJS
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  }
}

路径别名配置

// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      '@': '/src',
      '@components': '/src/components',
    }
  }
})

// tsconfig.json 同步配置(TS 也能识别别名)
{
  "compilerOptions": {
    "paths": { "@/*": ["./src/*"] }
  }
}

包设计 — 发布可复用模块

ESM/CJS 双格式发布

格式文件后缀用途
ESM.mjstype: module.js现代打包器、浏览器
CJS.cjs 或默认 .jsNode 直接 require、旧工具链

package.json 关键字段

字段用途示例
name包名,发布到 npm 的唯一标识"@myorg/ui"
version语义化版本"1.2.3"
exports精确控制导出路径和条件见上方示例
typesTypeScript 类型文件路径"./dist/index.d.ts"
files发布时包含的文件白名单["dist"]
sideEffectsTree Shaking 副作用标记false["*.css"]
peerDependencies宿主环境需提供的依赖"vue": "^3.3"

依赖类型选择

类型安装位置适用场景
dependencies用户安装运行时必需(如工具函数库)
peerDependencies用户自行安装与宿主共享(如 Vue 组件库依赖 Vue)
devDependencies不发布构建/测试工具(Vite、Vitest)

Vue/React 组件库的框架依赖必须放 peerDependencies——避免用户项目里安装两份 Vue/React。工具函数库(不依赖框架)放 dependencies

常见误区

误区正解
CJS 和 ESM 可以混用打包器会尽力兼容,但混用可能导致 Tree Shaking 失效或运行时错误
require 可以 Tree Shakingrequire 是运行时调用,无法静态分析,不支持 Tree Shaking
sideEffects: false 随便设必须确认模块真的没有副作用(如 CSS 文件、polyfill)
打包产物越小 Tree Shaking 越好还要看运行时行为,有时保留少量冗余代码比引入复杂副作用管理更可取
module 字段是标准它是社区约定,exports 才是 Node 官方标准
npm 包只能发 JS可以发 CSS、WASM、类型声明等,通过 exports 条件分发
路径别名不需要 TS 配置Vite 配了别名但 TS 没配,编译通过但编辑器报错

本页内容