目录
什么是原生 ESM
定义
原生 ESM(ES Modules) 是 JavaScript 的官方模块系统,浏览器原生支持,无需构建工具即可在浏览器中直接使用。
核心特点
- ✅ 浏览器原生支持:无需打包工具
- ✅ 按需加载:只加载需要的模块
- ✅ 异步加载:不阻塞页面渲染
- ✅ 严格模式:自动启用严格模式
- ✅ 静态分析:编译时确定依赖关系
与传统方式的对比
<!-- 传统方式:使用全局变量 -->
<script src="jquery.js"></script>
<script src="app.js"></script>
<!-- 问题:全局污染、依赖顺序、难以管理 -->
<!-- 原生 ESM:模块化 -->
<script type="module" src="app.js"></script>
<!-- 优势:模块化、按需加载、无全局污染 -->
浏览器支持情况
支持情况
| 浏览器 | 版本 | 支持情况 |
|---|---|---|
| Chrome | 61+ | ✅ 完全支持 |
| Firefox | 60+ | ✅ 完全支持 |
| Safari | 10.1+ | ✅ 完全支持 |
| Edge | 16+ | ✅ 完全支持 |
| IE 11 | - | ❌ 不支持 |
检查支持
// 检查浏览器是否支持 ESM
if ('noModule' in HTMLScriptElement.prototype) {
console.log('支持原生 ESM');
} else {
console.log('不支持原生 ESM');
}
降级方案
<!-- 为不支持 ESM 的浏览器提供降级 -->
<script type="module" src="app.js"></script>
<script nomodule src="app-legacy.js"></script>
<!-- 支持 ESM 的浏览器:加载 app.js -->
<!-- 不支持 ESM 的浏览器:加载 app-legacy.js -->
基础使用方法
1. 内联脚本
<!DOCTYPE html>
<html>
<head>
<title>原生 ESM 示例</title>
</head>
<body>
<script type="module">
// 导入模块
import { add, subtract } from './math.js';
// 使用模块
console.log(add(1, 2)); // 3
console.log(subtract(5, 2)); // 3
</script>
</body>
</html>
2. 外部脚本
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>原生 ESM 示例</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>
// app.js
import { add } from './math.js';
import { formatDate } from './utils.js';
console.log(add(1, 2));
console.log(formatDate(new Date()));
3. 模块文件结构
project/
├── index.html
├── app.js
├── math.js
└── utils.js
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// utils.js
export function formatDate(date) {
return date.toISOString();
}
// app.js
import { add, subtract } from './math.js';
import { formatDate } from './utils.js';
console.log(add(1, 2));
console.log(formatDate(new Date()));
ESM 语法详解
1. 命名导出(Named Exports)
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// 或者
function multiply(a, b) {
return a * b;
}
export { multiply };
// app.js
import { add, subtract, PI } from './math.js';
console.log(add(1, 2));
console.log(PI);
2. 默认导出(Default Export)
// Calculator.js
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// app.js
import Calculator from './Calculator.js';
const calc = new Calculator();
console.log(calc.add(1, 2));
3. 混合导出
// utils.js
export default function formatDate(date) {
return date.toISOString();
}
export function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
export const VERSION = '1.0.0';
// app.js
import formatDate, { formatCurrency, VERSION } from './utils.js';
console.log(formatDate(new Date()));
console.log(formatCurrency(100));
console.log(VERSION);
4. 全部导入(Namespace Import)
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
// app.js
import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.subtract(5, 2));
console.log(math.multiply(2, 3));
5. 重新导出(Re-export)
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// operations.js
export { add, subtract } from './math.js';
// 或者
export * from './math.js';
// app.js
import { add, subtract } from './operations.js';
6. 重命名导入/导出
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add as sum } from './math.js';
console.log(sum(1, 2));
// math.js
function add(a, b) {
return a + b;
}
export { add as sum };
实际应用场景
场景 1:简单工具函数库
// utils/string.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split('').reverse().join('');
}
// utils/number.js
export function formatNumber(num) {
return num.toLocaleString();
}
export function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// utils/index.js
export * from './string.js';
export * from './number.js';
// app.js
import { capitalize, formatNumber } from './utils/index.js';
console.log(capitalize('hello')); // Hello
console.log(formatNumber(1234567)); // 1,234,567
场景 2:组件化开发
// components/Button.js
export default class Button {
constructor(text) {
this.text = text;
this.element = document.createElement('button');
this.element.textContent = text;
}
render() {
return this.element;
}
}
// components/Card.js
export default class Card {
constructor(title, content) {
this.title = title;
this.content = content;
}
render() {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<h3>${this.title}</h3>
<p>${this.content}</p>
`;
return card;
}
}
// app.js
import Button from './components/Button.js';
import Card from './components/Card.js';
const button = new Button('Click me');
const card = new Card('Title', 'Content');
document.body.appendChild(button.render());
document.body.appendChild(card.render());
场景 3:API 封装
// api/users.js
export async function getUsers() {
const response = await fetch('/api/users');
return response.json();
}
export async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
export async function createUser(user) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
return response.json();
}
// app.js
import { getUsers, getUser, createUser } from './api/users.js';
async function init() {
const users = await getUsers();
console.log(users);
const user = await getUser(1);
console.log(user);
}
init();
场景 4:配置管理
// config.js
export const API_BASE_URL = 'https://api.example.com';
export const API_VERSION = 'v1';
export const TIMEOUT = 5000;
export default {
api: {
baseURL: API_BASE_URL,
version: API_VERSION,
timeout: TIMEOUT
},
features: {
enableCache: true,
enableLogging: false
}
};
// app.js
import config, { API_BASE_URL } from './config.js';
console.log(config.api.baseURL);
console.log(API_BASE_URL);
模块路径和解析
1. 相对路径
// 当前目录
import { func } from './module.js';
// 父目录
import { func } from '../module.js';
// 子目录
import { func } from './utils/module.js';
2. 绝对路径(需要服务器配置)
// 需要配置服务器路径映射
import { func } from '/src/module.js';
3. URL 路径
// 可以从 CDN 导入
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
4. 文件扩展名
// ✅ 推荐:使用扩展名
import { func } from './module.js';
// ⚠️ 可能有问题:无扩展名
import { func } from './module';
5. Import Maps(实验性)
<!-- 使用 Import Maps 映射模块路径 -->
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js",
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js"
}
}
</script>
<script type="module">
import { createApp } from 'vue';
import { debounce } from 'lodash';
</script>
动态导入
1. 基础动态导入
// 动态导入返回 Promise
const module = await import('./module.js');
const { func } = module;
func();
2. 条件导入
// 根据条件动态加载模块
async function loadModule(condition) {
if (condition) {
const module = await import('./moduleA.js');
return module;
} else {
const module = await import('./moduleB.js');
return module;
}
}
3. 懒加载组件
// 路由懒加载
async function loadRoute(route) {
switch (route) {
case '/home':
const Home = await import('./pages/Home.js');
return new Home();
case '/about':
const About = await import('./pages/About.js');
return new About();
default:
const NotFound = await import('./pages/NotFound.js');
return new NotFound();
}
}
4. 按需加载
// 点击按钮时加载模块
document.getElementById('loadBtn').addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.init();
});
5. 错误处理
try {
const module = await import('./module.js');
module.func();
} catch (error) {
console.error('加载模块失败:', error);
// 降级处理
}
与构建工具的区别
原生 ESM vs 构建工具
| 特性 | 原生 ESM | 构建工具(Webpack/Vite) |
|---|---|---|
| 开发速度 | ⚡ 极快(无需构建) | 🐌 需要构建 |
| 浏览器支持 | 现代浏览器 | 所有浏览器 |
| 代码分割 | ✅ 原生支持 | ✅ 需要配置 |
| Tree Shaking | ✅ 原生支持 | ✅ 需要配置 |
| TypeScript | ❌ 不支持 | ✅ 支持 |
| JSX | ❌ 不支持 | ✅ 支持 |
| CSS 模块 | ❌ 不支持 | ✅ 支持 |
| 生产优化 | ❌ 无优化 | ✅ 压缩、优化 |
使用场景
// ✅ 适合原生 ESM:
// - 简单项目
// - 原型开发
// - 学习演示
// - 现代浏览器项目
// ✅ 适合构建工具:
// - 复杂项目
// - 需要 TypeScript/JSX
// - 需要兼容旧浏览器
// - 需要生产优化
最佳实践
1. 使用文件扩展名
// ✅ 推荐
import { func } from './module.js';
// ❌ 不推荐
import { func } from './module';
2. 组织模块结构
project/
├── index.html
├── app.js
├── modules/
│ ├── utils.js
│ ├── api.js
│ └── components.js
└── components/
├── Button.js
└── Card.js
3. 使用默认导出
// 单个主要功能使用默认导出
export default class Calculator {
// ...
}
// 多个相关功能使用命名导出
export function add() {}
export function subtract() {}
4. 错误处理
// 使用 try-catch 处理导入错误
try {
const module = await import('./module.js');
} catch (error) {
console.error('模块加载失败:', error);
}
5. 提供降级方案
<!-- 为不支持 ESM 的浏览器提供降级 -->
<script type="module" src="app.js"></script>
<script nomodule src="app-legacy.js"></script>
6. 使用 Import Maps
<!-- 统一管理依赖路径 -->
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js"
}
}
</script>
7. 代码分割
// 使用动态导入实现代码分割
const module = await import('./heavy-module.js');
8. 性能优化
<!-- 预加载关键模块 -->
<link rel="modulepreload" href="./critical-module.js">
<!-- 预获取模块 -->
<link rel="prefetch" href="./lazy-module.js">
modulepreload 与 prefetch 的区别
rel="modulepreload"(模块预加载):
- ✅ 立即执行:浏览器会立即下载并解析模块及其依赖关系
- ✅ 高优先级:会优先处理,可能阻塞其他资源加载
- ✅ 解析依赖:会递归解析并预加载模块的所有依赖
- 🎯 适用场景:当前页面必须使用的关键模块,需要尽快加载以提升首屏性能
- 📝 示例:主应用入口模块、关键业务逻辑模块
rel="prefetch"(资源预获取):
- ⏱️ 延迟执行:浏览器在空闲时间才下载资源
- ⏱️ 低优先级:不会阻塞当前页面的关键资源加载
- ⚠️ 不解析依赖:只下载文件本身,不会解析模块依赖
- 🎯 适用场景:可能在未来使用的资源(如下一页可能用到的模块、懒加载组件)
- 📝 示例:路由懒加载模块、按需加载的功能模块
使用建议:
- 关键路径上的模块 → 使用
modulepreload - 非关键、可能用到的模块 → 使用
prefetch
常见问题解决
问题 1:CORS 错误
# 错误信息
Access to script at 'file:///...' from origin 'null' has been blocked by CORS policy
# 原因:使用 file:// 协议打开 HTML 文件
# 解决方案:使用 HTTP 服务器
# 方式 1:使用 Python
python -m http.server 8000
# 方式 2:使用 Node.js
npx serve .
# 方式 3:使用 VS Code Live Server 扩展
问题 2:模块路径错误
// 错误:找不到模块
import { func } from './module';
// 解决:使用完整路径和扩展名
import { func } from './module.js';
问题 3:循环依赖
// moduleA.js
import { funcB } from './moduleB.js';
export function funcA() {
funcB();
}
// moduleB.js
import { funcA } from './moduleA.js';
export function funcB() {
funcA();
}
// 解决:重构代码,避免循环依赖
// 或者使用动态导入
问题 4:不支持 ESM 的浏览器
<!-- 解决方案:提供降级版本 -->
<script type="module" src="app.js"></script>
<script nomodule src="app-legacy.js"></script>
文件命名说明:
- ✅ 无特殊要求:文件名可以是任何合法的文件名,
app.js和app-legacy.js只是示例 - ✅ 命名建议:
- 现代版本:
app.js、main.js、index.js等 - 降级版本:
app-legacy.js、app.bundle.js、app.polyfill.js等(通常包含打包后的代码和 polyfill)
- 现代版本:
- 📝 工作原理:
- 支持 ESM 的浏览器:执行
type="module"的脚本,忽略nomodule的脚本 - 不支持 ESM 的浏览器:忽略
type="module"的脚本,执行nomodule的脚本
- 支持 ESM 的浏览器:执行
- ⚠️ 注意事项:
nomodule脚本通常需要是打包后的版本(包含所有依赖)- 可以使用 Babel、Webpack 等工具将 ESM 代码转换为兼容旧浏览器的版本
问题 5:第三方库不支持 ESM
// 问题:某些库只提供 CommonJS 版本
// 解决方案 1:使用 CDN 的 ESM 版本
import Vue from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
// 解决方案 2:使用构建工具转换
// 或者寻找 ESM 替代品
问题 6:相对路径混乱
// 问题:深层嵌套时路径复杂
import { func } from '../../../../utils/module.js';
// 解决:使用 Import Maps
<script type="importmap">
{
"imports": {
"@utils/": "/src/utils/"
}
}
</script>
<script type="module">
import { func } from '@utils/module.js';
</script>
配置多个路径映射:
- ✅ 支持多个路径别名:可以在
imports对象中配置多个路径映射 - 📝 示例:
<script type="importmap">
{
"imports": {
"@utils/": "/src/utils/",
"@components/": "/src/components/",
"@api/": "/src/api/",
"@config/": "/src/config/",
"vue": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
// 使用多个路径别名
import { formatDate } from '@utils/date.js';
import Button from '@components/Button.js';
import { getUser } from '@api/users.js';
import config from '@config/index.js';
import { createApp } from 'vue';
</script>
路径映射规则:
- ✅ 目录映射:使用
@alias/格式,末尾的/表示目录 - ✅ 文件映射:直接使用别名,如
"vue": "https://..."映射到具体文件 - ✅ 相对路径:也可以映射到相对路径,如
"@utils/": "./src/utils/"
实际案例
案例 1:Todo 应用
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Todo App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="app.js"></script>
</body>
</html>
// app.js
import TodoList from './components/TodoList.js';
import TodoItem from './components/TodoItem.js';
const app = document.getElementById('app');
const todoList = new TodoList();
app.appendChild(todoList.render());
// components/TodoList.js
import TodoItem from './TodoItem.js';
export default class TodoList {
constructor() {
this.todos = [];
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
this.render();
}
render() {
const list = document.createElement('ul');
this.todos.forEach(todo => {
const item = new TodoItem(todo);
list.appendChild(item.render());
});
return list;
}
}
// components/TodoItem.js
export default class TodoItem {
constructor(todo) {
this.todo = todo;
}
render() {
const li = document.createElement('li');
li.textContent = this.todo.text;
return li;
}
}
案例 2:使用 CDN 模块
<!DOCTYPE html>
<html>
<head>
<title>Vue App</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app');
</script>
</body>
</html>
案例 3:动态路由
// router.js
export default class Router {
constructor() {
this.routes = {};
}
addRoute(path, modulePath) {
this.routes[path] = modulePath;
}
async navigate(path) {
const modulePath = this.routes[path];
if (modulePath) {
const module = await import(modulePath);
return module.default;
}
return null;
}
}
// app.js
import Router from './router.js';
const router = new Router();
router.addRoute('/', './pages/Home.js');
router.addRoute('/about', './pages/About.js');
async function init() {
const path = window.location.pathname;
const Page = await router.navigate(path);
if (Page) {
const page = new Page();
document.body.appendChild(page.render());
}
}
init();
总结
核心要点
-
基础使用:
<script type="module" src="app.js"></script> -
导出语法:
export function func() {} export default class MyClass {} -
导入语法:
import { func } from './module.js'; import MyClass from './MyClass.js'; -
动态导入:
const module = await import('./module.js'); -
浏览器支持:
- Chrome 61+
- Firefox 60+
- Safari 10.1+
- Edge 16+
优势
- ✅ 无需构建工具
- ✅ 原生支持
- ✅ 按需加载
- ✅ 开发速度快
限制
- ❌ 需要现代浏览器
- ❌ 不支持 TypeScript/JSX
- ❌ 需要 HTTP 服务器(不能 file://)
- ❌ 无生产优化
适用场景
- ✅ 简单项目
- ✅ 原型开发
- ✅ 学习演示
- ✅ 现代浏览器项目