Skip to content

一节课理清楚 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,匿名自执行函数立即运行代码。看到这个名词要知道它就是这个意思。

它的结构可以概括成一句话:外面一个匿名自执行函数(定义后立刻调用),里面写你的项目代码

js
// 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 打包:

js
// 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 上:

js
// 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 文件:

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

js
// util.js —— 放在服务器上,可直接在线访问
export function a() {
  console.log('123')
}

app.js 里直接从这个地址 import 进来并调用:

js
// app.js —— 直接 import,原封不动运行
import { a } from './util.js'

a()   // 调用服务器上 util.js 里的方法

HTML 里直接通过 script 引入 app.js(不用打包):

js
<!-- index.html:直接引入,不打包 -->
<script src="./app.js"></script>

WARNING

关键坑:这样直接引入会报错——"cannot use import"(import 是语法错误)。 想用 ESM,必须给 script 标签加上 type="module":

js
<!-- 修正:必须声明 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 出去:

js
// src/index.js —— 假设在开发一个库,暴露 A / B 两个方法
export function A() {
  console.log('A 方法:123')
}

export function B() {
  console.log('B 方法:456')
}

经过 UMD 打包后,结构变成这样(示意):

js
// 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 上挂这个名字:

js
// 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 上取用:

js
<!-- 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 开发库的标准姿势:准备两个产物

开发一个库时,往往准备至少两个格式的产物:

产物格式用途
UMDscript 标签直接引入 / CDN 引入时使用
ESM平常项目开发时通过 import 引入(UMD 无法 import)

Vue 就是典型例子。打开 Vue 的 CDN 地址,可以看它的全局结构:它定义了一个全局的 Vue 变量,等于匿名自执行函数的执行结果——函数接收一个空对象,把 computedcreateApp 等所有方法挂到这个空对象上,最后把空对象 return 出去,赋给全局 Vue 变量:

js
// 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 上:

js
// webpack.config.js —— 直接挂 window
output: {
  library: 'myapp',
  libraryTarget: 'window'   // 直接往 window 上挂
}

打包后,暴露的方法直接挂到 window 上:

js
window.A()   // 直接就能取到 A 方法

5.2 var 模式(libraryTarget: 'var')

打包后所有东西都放进一个变量里:

js
// 打包结果(示意)
var myapp = (function (window) {
  // ... 方法都挂到 myapp 上
})(window)

通过 myapp.A() 访问,本质还是在全局作用域下可用。

5.3 JSONP(libraryTarget: 'jsonp')

现在用得更加稀少,一般用于解决跨域 JS 才用这个方案。

INFO

这些冷门方案了解即可:Webpack 的 libraryTarget 支持多种取值,想用哪种就配哪种。

三种模式怎么选(对照表)

这节课的核心说白了就三个模式:

模式本质主要用途怎么配出来
IIFE匿名自执行函数,引入即执行项目生产代码(npm run build 上线)Webpack / Vite 默认,无需额外配置
ESMimport/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 形参拦截 windowIIFE + 形参——把代理 window 传进自执行函数,代码里对 window 的访问被拦截

WARNING

特别提醒:微前端里最常见的"子应用不兼容"问题,根源就在打包模式—— 子应用没按 UMD 打包,主应用从 window 上取不到 mount / unmount,就会"页面出来了但没法接管"。

术语表 & 附录完整代码

术语表

术语定义
IIFEImmediately Invoked Function Expression,匿名自执行函数:定义后立即调用,引入即执行
ESMES Module,import/export 原样运行,等同于不打包;需配合 script type="module"
UMDUniversal Module Definition,库格式打包:先往 window 挂空对象再挂方法,不污染全局,支持 script / AMD / UMD 引入
librarywebpack 配置:库的名字,打包后挂到 window 上的全局名
libraryTargetwebpack 配置:打包目标(umd / window / var / jsonp 等)
CDN内容分发网络,通过 script 标签从线上引入 JS 文件;CDN 引库要求库是 UMD 格式
打包 / 编译把 import/export 等源码编译成浏览器可直接运行的代码(IIFE、UMD 都会编译,ESM 不编译)

附录 A:IIFE 项目打包(webpack.config.js + 入口)

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 开发模式(不打包直接跑)

js
// 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 库打包(完整配置 + 产物结构)

js
// 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>

本博客内容基于 MIT 许可证发布,欢迎合理转载。