现代前端模块化是ES Modules、动态导入、npm、Vite和Tree Shaking协同工作的系统;ESM强调静态性,import()用于按需加载并触发代码分割,npm包需正确声明入口格式,Vite以ESM为原生前提实现高效开发与构建,Tree Shaking依赖ESM语法、生产模式及无副作用声明。

直接从“为什么需要模块化”切入,而不是先背语法。现代前端项目里,ES Modules 是基础骨架,动态导入是性能开关,npm 是依赖仓库,Vite 是执行引擎,Tree Shaking 是瘦身工具——它们不是孤立知识点,而是一套协同工作的系统。学得快的关键,是抓住每个环节的作用边界和失效条件。
搞懂 ES Modules 的“静态性”本质
ESM 不是“更高级的 require”,它的核心约束是:所有 import/export 必须在顶层、路径必须是字符串字面量。这意味着:
- 不能写
if (env === 'prod') import('./prod.js')—— 这会报语法错误 - 不能写
import(`./${name}.js`)—— 模块路径不能拼接变量 -
import * as utils from './utils.js'会让 Tree Shaking 失效,除非后续明确访问utils.xxx - 浏览器中使用 ESM,HTML 中的 script 标签必须带
type="module",否则当成普通脚本执行,import直接报错
动态导入(import())不是“补救措施”,而是策略选择
它解决的是 ESM 静态限制下的真实需求:按需加载、条件加载、拆包优化。重点理解它返回的是 Promise,且触发自动代码分割:
-
const mod = await import('./HeavyChart.js')—— 加载成功后,mod是一个命名空间对象,含默认导出(mod.default)和命名导出(mod.render) - Vite 默认将每个
import()调用打包成独立 chunk,无需额外配置 - 它不破坏 ESM 静态图谱:构建工具仍能识别依赖关系,只是把“加载时机”推迟到运行时
- 不要滥用:频繁调用
import()可能引发过多网络请求;适合大组件、低频功能、A/B 实验模块
npm 包与模块格式的兼容逻辑
你 import 的不只是文件,而是 npm 包声明的“入口协议”。关键看 package.json 中的字段:
-
"main": "./dist/index.js"→ CommonJS 入口,Node.js 或旧构建工具常用 -
"module": "./dist/index.mjs"→ ESM 入口,Vite/Rollup 优先读取 -
"exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.js" } }→ 现代标准,明确区分不同环境加载方式 - Vite 报
"default" is not exported,大概率是该包只提供 CommonJS 格式,没正确声明 ESM 入口,或缺少@rollup/plugin-commonjs插件转换
Vite 如何串联起整个链条
Vite 不是“另一个打包工具”,它是以 ESM 为原生前提设计的开发服务器 + 构建器:
- 开发时:浏览器直接发起 ESM 请求,Vite 拦截并按需编译、注入 HMR 逻辑,无打包过程
- 生产构建:底层用 Rollup,天然支持 Tree Shaking 和
import()分割 - 它默认开启
treeShaking: true,但前提是你的代码用 ESM 写、第三方包提供 ESM 入口、没有副作用干扰(如全局样式注入未声明"sideEffects": false) - 遇到模块解析失败,先查网络面板是否 404,再看控制台报错是否指向 CommonJS/ESM 混用,最后检查
vite.config.ts是否漏配插件
Tree Shaking 不是自动发生的“魔法”
它只对满足以下全部条件的代码生效:
- 源码使用
export/import(不能是module.exports) - 构建工具启用 production mode(Vite 默认开启)
- 没有副作用:比如某个工具文件只执行
localStorage.setItem('init', '1')却没导出任何东西,若未在package.json中声明"sideEffects": false,它可能被误删 - 按需引入:用
import { debounce } from 'lodash-es',而非import _ from 'lodash'(后者经 Babel 转换后常退化为 CommonJS)


















