一节课理清楚 JS 的打包模式:IIFE / ESM / UMD 全解
工程化 · JS 打包模式
课程地图
| 模块 | 内容 | 学习产出 |
|---|---|---|
| IIFE 模式 | 匿名自执行函数,项目生产打包默认形态(Webpack / Vite build 都是它) | 看懂项目打包产物 |
| ESM 模式 | import/export 原封不动运行,等于"不打包";Vite 开发模式快的核心 | 理解 Vite 为什么快 |
| UMD 模式 | 库开发主流:挂 window 空对象 + library / libraryTarget 配置,CDN 引入必须 | 能开发自己的"轮子" |
| 冷门方案 | 直接挂 window、var 模式、JSONP | 知道 Webpack 还能这么配 |
| 选型对照 | 项目生产用 IIFE、开发用 ESM、开发库用 UMD | 见码识模式 |
INFO
本课一句话目标:系统地认识 JavaScript 的打包模式——每种模式长什么样、怎么配出来、用在什么场景, 看到任何一个 JS 产物文件,都能一眼判断它是什么模式。
开篇:为什么要系统了解打包模式
这节课非常快,系统地过一遍 JavaScript 的打包模式(打包方案):有哪些打包模式、它们分别适用于什么情况。
为什么值得学?因为前端工程化里,你看到的每一个 dist 目录下的 JS 文件,都是某种打包模式的产物。平时写项目见的是 IIFE,Vite 开发模式跑的是 ESM,开发一个"轮子"(组件库、工具库)要发的是 UMD——搞懂这三兄弟,前端构建的地基就通了。
IIFE 模式:项目生产代码的默认打包
2.1 什么是 IIFE
IIFE = Immediately Invoked Function Expression,匿名自执行函数立即运行代码。看到这个名词要知道它就是这个意思。
它的结构可以概括成一句话:外面一个匿名自执行函数(定义后立刻调用),里面写你的项目代码:
// IIFE 的结构(最简示意)
(function () {
// 你的项目代码
console.log('项目启动')
})()
// 通过 <script> 引入这个文件时:
// 1. 函数被定义
// 2. 函数被立即调用
// 3. 里面的代码一行一行开始执行INFO
注意:这是最简单的结构示意,真实打包产物会包含更多内容(模块封装、运行时等),但核心骨架就是"匿名自执行函数包代码"。
2.2 使用场景:项目本身的代码
IIFE 一般用于项目本身的代码。比如你开发了一个 Vue / React 项目,写完之后 npm run build 打包,然后把打包产物放到服务器上供别人访问——这个时候你项目的 JS 代码基本上都是 IIFE 模式。
它的执行方式:引入即执行。script 标签引入这个 JS 文件,函数立即调用,里面的业务代码立刻从打包入口开始一行一行执行。
2.3 实战:Webpack 默认就是 IIFE 打包
无论是 Webpack 还是 Vite,生产打包默认都是 IIFE。看一个例子:webpack 配置里只写了入口和出口,没有指定打包目标——默认就是符合项目运行方案的 IIFE 打包:
// webpack.config.js —— 只写入口和出口,默认 IIFE 打包
const path = require('path')
module.exports = {
entry: './src/app.js', // 入口:相当于 Vue 项目的 main.js
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}入口 app.js 就相当于平常写项目时的 main.js(Vue 项目的 main.js / React 项目的 index.js)。这里创建了一个 Vue 根实例,挂载到 id 为 app 的 div 上:
// src/app.js —— 相当于 Vue 项目的 main.js
import { createApp } from 'vue'
const app = createApp({
mounted() {
console.log('渲染完成')
},
template: '<div>12312</div>' // 一个最简单的根组件
})
app.mount('#app')执行 npm run build 打包出一个 bundle 文件。文件看起来代码很多,但本质上就是一个匿名自执行函数里面写着你的代码。之后在 HTML 里引入这个 JS 文件:
<!-- index.html:引入打包产物 -->
<div id="app"></div>
<script src="./dist/bundle.js"></script>浏览器加载 bundle.js → 匿名自执行函数立即执行 → 里面的代码一行一行跑 → Vue 挂载到 #app,页面渲染出 12312。
TIP
小结:项目开发、生产上线,用的都是 IIFE——这是大家最常接触的打包模式。
ESM 模式:等同于"不打包"
3.1 概念:原封不动地运行
ESM(ES Module)模式等同于"不打包":所有代码直接原封不动地运行。
你写代码时会用 import 引入、用 export 暴露。注意:正常的打包(IIFE、UMD 等)都会把你写的 import / export 编译掉,编译成浏览器能直接运行的代码;而 ESM 模式不编译,代码写成什么样,浏览器就原样运行什么样。
3.2 这是 Vite 开发模式快的核心
一个老生常谈的话题:Vite 之所以能快速启动、不用花费那么多打包时间,就是因为开发模式下用的是 ESM 方案——代码原封不动运行,根本不需要打包,所以它快。
3.3 实战演示:不加 type="module" 会报错
假设服务器上有一个可以直接在线访问的 util.js:
// util.js —— 放在服务器上,可直接在线访问
export function a() {
console.log('123')
}在 app.js 里直接从这个地址 import 进来并调用:
// app.js —— 直接 import,原封不动运行
import { a } from './util.js'
a() // 调用服务器上 util.js 里的方法HTML 里直接通过 script 引入 app.js(不用打包):
<!-- index.html:直接引入,不打包 -->
<script src="./app.js"></script>WARNING
关键坑:这样直接引入会报错——"cannot use import"(import 是语法错误)。 想用 ESM,必须给 script 标签加上 type="module":
<!-- 修正:必须声明 type="module" -->
<script type="module" src="./app.js"></script>加好之后就没有报错了,控制台打印出 123——这个 123 正是服务器上 util.js 里打印的。整个过程中:
- app.js 根本没有编译,写成什么样就怎么运行;
- 服务器上的 util.js 也是直接用的,export 一个方法出去。
3.4 为什么 ESM 一般不用于生产
生产模式(项目真正上线)基本不会用 ESM,原因有三:
- 所有文件必须保持 ES6 Module 形式(通过 export 暴露、import 引入)——你用到很多第三方库的时候很难保证这一点;
- 所有文件都要能线上访问——都要丢在一个线上可访问的服务器里;
- 有兼容性问题。
TIP
小结:ESM 一般不用于生产,但它是 Vite 开发模式这么快的原因——开发时加加速很好用。
UMD 模式:库开发的王者
4.1 使用场景:开发代码库("轮子")
日常工作除了项目开发,有时需要开发组件库、工具库、请求库——一些 JS 的"轮子"提供给别人使用。当你开发一个库的时候,打包方式往往都是 UMD,这是开发库的主流打包方式。
库的本质是暴露一些方法给别人用。比如 Vue 暴露 createApp 方法给我们创建应用、暴露 h 方法给我们渲染模板。你自己开发工具库也一样:暴露数组排序之类的方法,别人想用哪个就引哪个。
4.2 UMD 打包后长什么样
写库的时候,把方法一个个 export 出去:
// src/index.js —— 假设在开发一个库,暴露 A / B 两个方法
export function A() {
console.log('A 方法:123')
}
export function B() {
console.log('B 方法:456')
}经过 UMD 打包后,结构变成这样(示意):
// UMD 打包后的结构(示意)
(function (global) { // 1. 匿名自执行函数接收一个全局对象(浏览器环境是 window)
const exports = {} // 2. 先往 window 上挂一个"空对象"(库的名字,打包时自定义)
exports.A = function () { // 3. 暴露的 A 方法挂到这个空对象上
console.log('A 方法:123')
}
exports.B = function () { // 4. 暴露的 B 方法也挂上去
console.log('B 方法:456')
}
global.myapp = exports // 5. 整个空对象挂到 window.myapp
})(window)
// 使用者调用:
// window.myapp.A() → 打印 "A 方法:123"
// window.myapp.B() → 打印 "B 方法:456"名字是打包时自定义的——你想叫什么名字就叫什么名字。先挂载为空对象,后续暴露的方法都挂到这个空对象上面。
4.3 实战:UMD 打包配置
打包时说明两个关键配置:库的目标是 UMD(libraryTarget)、库的名字(library)——因为打包后会往 window 上挂这个名字:
// webpack.config.js —— UMD 打包范式
const path = require('path')
module.exports = {
entry: './src/index.js', // 库的入口
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'myapp.umd.js',
library: 'myapp', // 库名字:挂到 window 上的名字
libraryTarget: 'umd' // 打包目标:UMD
}
}打包后在 HTML 里引入,并从 window 上取用:
<!-- index.html:引入 UMD 产物并调用 -->
<script src="./dist/myapp.umd.js"></script>
<script>
window.myapp.A() // 调用成功,打印 "A 方法:123"
window.myapp.B() // 调用成功,打印 "B 方法:456"
</script>4.4 UMD 的两个特点
- 不污染全局变量:所有暴露的东西不是直接挂 window 下的,而是先在 window 下挂一个空对象,再往空对象里挂方法——不会直接污染 window;
- 用于 CDN 引入:如果你想通过 CDN 引入一个库(用 script 标签从线上引),这个库一定得是 UMD 格式。
UMD 格式支持 AMD、UMD、script 标签三种方式引入,但无法用于 import 引入。所以通过 CDN(相当于 script 标签引入)引库时,都是 UMD 格式——它保证暴露的东西挂到 window 上,又不污染全局变量。
4.5 开发库的标准姿势:准备两个产物
开发一个库时,往往准备至少两个格式的产物:
| 产物格式 | 用途 |
|---|---|
| UMD | script 标签直接引入 / CDN 引入时使用 |
| ESM | 平常项目开发时通过 import 引入(UMD 无法 import) |
Vue 就是典型例子。打开 Vue 的 CDN 地址,可以看它的全局结构:它定义了一个全局的 Vue 变量,等于匿名自执行函数的执行结果——函数接收一个空对象,把 computed、createApp 等所有方法挂到这个空对象上,最后把空对象 return 出去,赋给全局 Vue 变量:
// Vue 的 CDN 文件(UMD 格式,结构示意)
var Vue = (function (expose) { // 接收空对象
expose.computed = function (getter) { /* ... */ }
expose.createApp = function (options) { /* ... */ }
// ... 各种方法都挂到 expose 上
return expose // 整个空对象 return 出去
})({}) // 变成全局的 Vue 变量
// 使用:window.Vue.createApp(...)说白了就是:往 window 上先挂一个空对象,所有东西都挂在空对象里面——这就是 UMD 格式。
TIP
小结:开发库 → 至少准备 UMD(给 script / CDN 用)+ ESM(给 import 用)两个产物。 这也解释了第三课里"qiankun 为什么硬性要求子应用 UMD 打包"——主应用要能在运行时从 window 上取到子应用的挂载方法。
冷门方案:挂 window / var / JSONP
除了三大主流,还有几个冷门打包方式,知道一下即可——都是通过修改 libraryTarget 实现的:
5.1 直接挂 window(libraryTarget: 'window')
不想搞那么麻烦,就想直接挂到 window 上:
// webpack.config.js —— 直接挂 window
output: {
library: 'myapp',
libraryTarget: 'window' // 直接往 window 上挂
}打包后,暴露的方法直接挂到 window 上:
window.A() // 直接就能取到 A 方法5.2 var 模式(libraryTarget: 'var')
打包后所有东西都放进一个变量里:
// 打包结果(示意)
var myapp = (function (window) {
// ... 方法都挂到 myapp 上
})(window)通过 myapp.A() 访问,本质还是在全局作用域下可用。
5.3 JSONP(libraryTarget: 'jsonp')
现在用得更加稀少,一般用于解决跨域 JS 才用这个方案。
INFO
这些冷门方案了解即可:Webpack 的 libraryTarget 支持多种取值,想用哪种就配哪种。
三种模式怎么选(对照表)
这节课的核心说白了就三个模式:
| 模式 | 本质 | 主要用途 | 怎么配出来 |
|---|---|---|---|
| IIFE | 匿名自执行函数,引入即执行 | 项目生产代码(npm run build 上线) | Webpack / Vite 默认,无需额外配置 |
| ESM | import/export 原封不动,等于不打包 | 项目开发模式(Vite dev 快的核心) | script 加 type="module" |
| UMD | 挂 window 空对象,不污染全局 | 开发库(CDN 引入必须) | library + libraryTarget: 'umd' |
INFO
一句话记忆:IIFE 用于项目开发(生产),ESM 用于项目开发(开发模式),UMD 用于库开发。 看到产物文件先猜模式:一大团匿名函数自执行 → IIFE;还有 import/export 原样 → ESM;开头往 window 挂名字 → UMD。
与微前端的关联:为什么前几课总在提 UMD
这套打包模式的知识,正好是微前端系列(前四课)里反复出现的概念的地基:
| 微前端里的现象 | 对应的打包模式 |
|---|---|
| 子应用打包后是"一个可执行的 JS 文件" | IIFE——引入即执行,主应用 fetch 到字符串后 eval 就能跑 |
| Vite 开发模式为什么快、为什么子应用 dev 时很难直接接入 | ESM——开发模式不打包、原样运行,无法把 mount/unmount 挂到 window |
| qiankun 为什么硬性要求子应用 UMD 打包 | UMD——打包后挂到 window[名字],主应用才能取到生命周期钩子 |
| 第二课手写框架的 loadJS:IIFE 形参拦截 window | IIFE + 形参——把代理 window 传进自执行函数,代码里对 window 的访问被拦截 |
WARNING
特别提醒:微前端里最常见的"子应用不兼容"问题,根源就在打包模式—— 子应用没按 UMD 打包,主应用从 window 上取不到 mount / unmount,就会"页面出来了但没法接管"。
术语表 & 附录完整代码
术语表
| 术语 | 定义 |
|---|---|
| IIFE | Immediately Invoked Function Expression,匿名自执行函数:定义后立即调用,引入即执行 |
| ESM | ES Module,import/export 原样运行,等同于不打包;需配合 script type="module" |
| UMD | Universal Module Definition,库格式打包:先往 window 挂空对象再挂方法,不污染全局,支持 script / AMD / UMD 引入 |
| library | webpack 配置:库的名字,打包后挂到 window 上的全局名 |
| libraryTarget | webpack 配置:打包目标(umd / window / var / jsonp 等) |
| CDN | 内容分发网络,通过 script 标签从线上引入 JS 文件;CDN 引库要求库是 UMD 格式 |
| 打包 / 编译 | 把 import/export 等源码编译成浏览器可直接运行的代码(IIFE、UMD 都会编译,ESM 不编译) |
附录 A:IIFE 项目打包(webpack.config.js + 入口)
// webpack.config.js —— 默认 IIFE 打包(只写入口出口即可)
const path = require('path')
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
}
// src/app.js —— 项目入口(相当于 Vue 的 main.js)
// import { createApp } from 'vue'
// const app = createApp({ template: '<div>12312</div>' })
// app.mount('#app')
// index.html —— 引入产物,函数立即执行
// <div id="app"></div>
// <script src="./dist/bundle.js"></script>附录 B:ESM 开发模式(不打包直接跑)
// util.js —— 放在服务器上,直接 export
export function a() {
console.log('123')
}
// app.js —— 直接 import,原封不动运行
import { a } from './util.js'
a()
// index.html —— 必须声明 type="module"
// <script type="module" src="./app.js"></script>附录 C:UMD 库打包(完整配置 + 产物结构)
// src/index.js —— 库源码:export 方法
export function A() {
console.log('A 方法:123')
}
export function B() {
console.log('B 方法:456')
}
// webpack.config.js —— UMD 打包范式
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'myapp.umd.js',
library: 'myapp', // 挂到 window 上的名字
libraryTarget: 'umd' // 打包目标:UMD
}
}
// 打包产物结构(示意):
// (function (global) {
// const exports = {}
// exports.A = function () { console.log('A 方法:123') }
// exports.B = function () { console.log('B 方法:456') }
// global.myapp = exports
// })(window)
// index.html —— 引入并调用
// <script src="./dist/myapp.umd.js"></script>
// <script>
// window.myapp.A() // A 方法:123
// window.myapp.B() // B 方法:456
// </script>