JavaScript 浏览器如何使用第三方模块
简介
在浏览器(V8 引擎)中使用第三方模块,有多种方式。从传统的 <script> 标签引入 CDN,到现代的 ES Modules(import/export),再到借助打包工具(Webpack、Vite 等)构建项目。
参考文档:
js-runtime.md— 浏览器和 Node.js 运行时对比js-browser-api.md— 浏览器特有 API(DOM、BOM 等)js-nodejs-api.md— Node.js 特有 API(含 CommonJS 和 ES Modules)
传统方式:script 标签引入
基本用法
直接使用 <script> 标签引入第三方库的 CDN 链接,库会作为全局变量可用。
<!-- 引入 jQuery -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script>
// $ 现在作为全局变量可用
$('#app').text('Hello jQuery!');
</script>
使用 CDN 服务
| CDN 服务 | 地址 | 特点 |
|---|---|---|
| unpkg | https://unpkg.com/ |
基于 npm 包,自动解析最新版本 |
| jsDelivr | https://cdn.jsdelivr.net/ |
支持 npm、GitHub、WordPress 等资源 |
| cdnjs | https://cdnjs.cloudflare.com/ |
老牌 CDN,库较全 |
| BootCDN | https://www.bootcdn.cn/ |
国内访问快 |
<!-- unpkg:自动解析 npm 包 -->
<script src="https://unpkg.com/[email protected]/lodash.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- jsDelivr:指定版本 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
全局变量污染问题
<script src="https://unpkg.com/[email protected]/dist/jquery.min.js"></script>
<script src="https://unpkg.com/[email protected]/lodash.min.js"></script>
<script>
// 所有库都成为全局变量
console.log(typeof $); // "function"(jQuery)
console.log(typeof _); // "function"(Lodash)
console.log(typeof axios); // "function"(Axios)
// 问题:全局命名空间被污染,可能冲突
// 如果另一个库也用了 $ 或 _ ,就会冲突
</script>
依赖管理问题
<!-- 必须手动保证加载顺序 -->
<script src="https://unpkg.com/[email protected]/dist/jquery.min.js"></script>
<!-- 某个插件依赖 jQuery,必须放在 jQuery 之后 -->
<script src="https://unpkg.com/[email protected]/dist/jquery-ui.min.js"></script>
<script>
// 如果顺序反了,插件会报错:$ is not defined
</script>
常见库的 CDN 引入示例
<!-- jQuery -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<!-- Lodash -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<!-- day.js(Moment.js 的轻量替代品)-->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js"></script>
<script>
// 使用
axios.get('https://api.example.com/data')
.then(res => console.log(res.data));
let arr = _.debounce(() => console.log('debounced'), 500);
</script>
ES Modules(现代方式)
启用 ES Modules
在 <script> 标签中添加 type="module" 属性,即可使用 import/export 语法。
<!-- 启用 ES Modules -->
<script type="module">
import { debounce } from 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.js';
let debounced = debounce(() => {
console.log('debounced!');
}, 500);
</script>
import 和 export 语法
<!-- module.js — 导出模块 -->
<script type="module">
// 命名导出
export const PI = 3.14159;
export function add(a, b) { return a + b; }
// 默认导出
export default function multiply(a, b) { return a * b; }
</script>
<!-- main.js — 导入模块 -->
<script type="module">
import { PI, add } from './module.js';
import multiply from './module.js'; // 默认导入
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6
</script>
路径规则
<script type="module">
// 必须使用完整路径(包括 .js 扩展名)
import { something } from './utils.js'; // ✅ 正确
import { something } from './utils'; // ❌ 错误:省略扩展名
// 可以使用绝对 URL
import _ from 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.js';
// 可以使用 / 开头的路径(相对于站点根目录)
// import { foo } from '/js/module.js';
// 不支持 bare specifier(裸模块说明符)
// import _ from 'lodash'; // ❌ 错误:浏览器不知道去哪里找
</script>
defer 和 async 属性
| 属性 | 说明 | 适用场景 |
|---|---|---|
| 无 | 同步加载,阻塞 HTML 解析 | 不推荐用于模块 |
defer |
异步加载,HTML 解析完成后执行(按顺序) | 普通脚本,默认行为 |
async |
异步加载,下载完成后立即执行(顺序不确定) | 独立脚本(如分析代码) |
type="module" |
默认具有 defer 行为 |
ES Modules,推荐 |
<!-- 传统脚本:defer vs async -->
<script src="script1.js" defer></script> <!-- 解析完后按顺序执行 -->
<script src="script2.js" async></script> <!-- 下载完立即执行,顺序不定 -->
<!-- ES Module:默认具有 defer 行为 -->
<script type="module" src="module1.js"></script> <!-- 按顺序执行 -->
<script type="module" src="module2.js"></script> <!-- 按在文档中的顺序 -->
CORS 限制
<script type="module">
// 跨域加载模块时,服务器必须返回正确的 CORS 头
import _ from 'https://unpkg.com/[email protected]/lodash.js';
// unpkg 等服务会返回 Access-Control-Allow-Origin: * 头
// 如果服务器没有 CORS 头,则会报错
// import xxx from 'https://some-server.com/module.js'; // 可能 CORS 错误
</script>
crossorigin 属性
<!-- 匿名跨域请求(不发送凭据) -->
<script type="module" crossorigin src="https://unpkg.com/lodash.js"></script>
<!-- 携带凭据的跨域请求(cookies 等) -->
<script type="module" crossorigin="use-credentials" src="https://unpkg.com/lodash.js"></script>
动态导入(Dynamic Import)
基本语法
import() 是 ES2020 引入的动态导入语法,可以在需要时加载模块。
<script type="module">
// 点击按钮时才加载模块
document.getElementById('load-btn').addEventListener('click', async () => {
let module = await import('https://cdn.jsdelivr.net/npm/[email protected]/lodash.js');
console.log(module.debounce); // Lodash 的 debounce 函数
let result = module.default(2, 3); // 如果有默认导出
});
</script>
按需加载提升性能
<script type="module">
// 路由级别的代码分割
async function loadPage(page) {
switch (page) {
case 'home':
let { renderHome } = await import('./pages/home.js');
renderHome();
break;
case 'about':
let { renderAbout } = await import('./pages/about.js');
renderAbout();
break;
}
}
document.getElementById('home-link').addEventListener('click', () => loadPage('home'));
document.getElementById('about-link').addEventListener('click', () => loadPage('about'));
</script>
与静态导入的区别
| 特性 | 静态导入(import ... from) |
动态导入(import()) |
|---|---|---|
| 位置 | 只能在模块顶层 | 可以在任何地方(函数内、条件内) |
| 加载时机 | 模块加载时就加载 | 执行到时才加载 |
| 是否返回 Promise | 否 | 是 |
| 是否支持 bare specifier | 否(浏览器中) | 否(浏览器中) |
| 适用场景 | 初始化依赖 | 懒加载、条件加载 |
使用打包工具(Bundle)
为什么需要打包工具
浏览器原生 ES Modules 有限制:
- 不支持 bare specifier(
import 'lodash'不行) - 每个
import都是一个 HTTP 请求,大量模块时性能差 - 无法直接处理非 JS 资源(CSS、图片等)
- 无法使用高级语法(JSX、TypeScript 等)
打包工具解决了这些问题:
- 将 npm 包转换为浏览器可用的格式
- 将所有模块打包成一个或少数几个文件
- 支持转换 TypeScript、JSX 等语法
- 支持代码分割、懒加载等优化
Webpack(经典工具)
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: require('path').resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
<!-- 打包后,只需引入一个文件 -->
<script src="dist/bundle.js"></script>
Vite(现代推荐)
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
outDir: 'dist'
}
});
# 开发模式(原生 ES Modules,快)
npx vite
# 构建生产版本(打包)
npx vite build
<!-- 构建后 -->
<script src="/assets/index.xxxxx.js"></script>
Parcel(零配置)
# 无需配置文件,自动识别
npx parcel index.html
常见第三方库使用示例
jQuery(传统,但仍在维护)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script>
// DOM 操作
$('#app').text('Hello jQuery!');
$('.item').hide();
// 事件监听
$('#btn').on('click', () => {
alert('Clicked!');
});
// AJAX
$.ajax({
url: 'https://api.example.com/data',
method: 'GET',
success: (data) => console.log(data),
error: (err) => console.error(err)
});
// 或者使用更简洁的 $.get / $.post
$.get('https://api.example.com/data', (data) => console.log(data));
</script>
Lodash(实用工具库)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script>
// 防抖(debounce):连续触发时,只执行最后一次
let debouncedSearch = _.debounce((query) => {
console.log('搜索:', query);
}, 500);
document.getElementById('search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 节流(throttle):连续触发时,每隔一段时间执行一次
let throttledScroll = _.throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 200);
window.addEventListener('scroll', throttledScroll);
// 深拷贝
let original = { a: { b: 1 } };
let cloned = _.cloneDeep(original);
cloned.a.b = 2;
console.log(original.a.b); // 1(未被影响)
// 数组操作
let arr = [1, 2, 3, 4, 5];
console.log(_.chunk(arr, 2)); // [[1,2], [3,4], [5]]
console.log(_.uniq([1, 2, 2, 3])); // [1, 2, 3]
</script>
Axios(HTTP 客户端)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
// GET 请求
axios.get('https://api.example.com/users')
.then(res => console.log(res.data))
.catch(err => console.error(err));
// POST 请求
axios.post('https://api.example.com/users', {
name: 'Alice',
age: 25
})
.then(res => console.log('创建成功:', res.data))
.catch(err => console.error(err));
// 使用 async/await
async function fetchData() {
try {
let res = await axios.get('https://api.example.com/data');
console.log(res.data);
} catch (err) {
console.error('请求失败:', err);
}
}
// 设置默认配置
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = 'Bearer token';
</script>
day.js(轻量级日期库)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js"></script>
<script>
// 解析和格式化
let now = dayjs();
console.log(now.format('YYYY-MM-DD HH:mm:ss')); // "2024-05-15 10:30:45"
// 日期运算
let tomorrow = dayjs().add(1, 'day');
let lastWeek = dayjs().subtract(1, 'week');
console.log(tomorrow.format('YYYY-MM-DD'));
// 日期比较
let date1 = dayjs('2024-01-01');
let date2 = dayjs('2024-12-31');
console.log(date1.isBefore(date2)); // true
console.log(date1.isAfter(date2)); // false
console.log(date1.isSame(date2, 'year')); // true
// 相对时间
console.log(dayjs('2024-05-10').fromNow()); // "5 天前"(需要 relativeTime 插件)
</script>
Chart.js(图表库)
<canvas id="myChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script>
let ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['红', '蓝', '黄', '绿', '紫'],
datasets: [{
label: '票数',
data: [12, 19, 3, 5, 2],
backgroundColor: ['red', 'blue', 'yellow', 'green', 'purple']
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '投票结果' }
}
}
});
</script>
Vue 3(通过 CDN)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js"></script>
<div id="app">
<h1>{{ message }}</h1>
<button @click="changeMessage">点击我</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return { message: 'Hello Vue!' };
},
methods: {
changeMessage() {
this.message = 'Message Changed!';
}
}
}).mount('#app');
</script>
CDN 服务速查
| CDN 服务 | 地址示例 | 特点 |
|---|---|---|
| unpkg | https://unpkg.com/:package@:version/:file |
自动解析 npm 包,支持 URL 重定向 |
| jsDelivr | https://cdn.jsdelivr.net/:type/:name@:version/:file |
支持 npm、GitHub、WordPress,国内访问较快 |
| cdnjs | https://cdnjs.cloudflare.com/ajax/libs/:lib/:version/:file |
Cloudflare 提供,稳定 |
| BootCDN | https://www.bootcdn.cn/:lib/:version/:file |
国内服务,访问快 |
| skypack | https://cdn.skypack.dev/:package |
专为 ES Modules 设计,返回 ES Module 格式 |
| esm.sh | https://esm.sh/:package |
专为 ES Modules 设计,支持 Deno、Bun 等 |
使用示例
<!-- unpkg:自动获取最新版本 -->
<script src="https://unpkg.com/lodash"></script>
<!-- unpkg:指定版本 -->
<script src="https://unpkg.com/[email protected]"></script>
<!-- unpkg:指定具体文件 -->
<script src="https://unpkg.com/[email protected]/lodash.min.js"></script>
<!-- jsDelivr:指定版本和文件 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<!-- skypack:返回 ES Module 格式(可用于 import) -->
<script type="module">
import _ from 'https://cdn.skypack.dev/lodash';
console.log(_.debounce);
</script>
<!-- esm.sh:返回 ES Module 格式 -->
<script type="module">
import axios from 'https://esm.sh/[email protected]';
console.log(axios.get);
</script>
模块属性:defer vs async
<!-- 普通脚本的行为 -->
<script src="script.js"></script>
<!-- 立即加载并执行,阻塞 HTML 解析 -->
<script src="script.js" defer></script>
<!-- 异步加载,HTML 解析完成后按顺序执行 -->
<script src="script.js" async></script>
<!-- 异步加载,下载完成后立即执行(顺序不确定) -->
<!-- ES Module 的行为 -->
<script type="module" src="module.js"></script>
<!-- 默认具有 defer 行为,按顺序执行 -->
<script type="module" async src="module.js"></script>
<!-- 下载完成后立即执行(类似 async 脚本) -->
| 属性 | HTML 解析 | 脚本下载 | 脚本执行时机 | 执行顺序 |
|---|---|---|---|---|
| 无 | 阻塞 | 立即 | 立即(阻塞解析) | 按顺序 |
defer |
继续 | 异步 | HTML 解析完成后 | 按顺序 |
async |
继续 | 异步 | 下载完成后立即 | 不确定 |
type="module" |
继续 | 异步 | HTML 解析完成后 | 按顺序(默认 defer) |
type="module" async |
继续 | 异步 | 下载完成后立即 | 不确定 |
浏览器模块的限制和注意事项
1. 需要 HTTP 服务器
// ❌ 错误:使用 file:// 协议打开 HTML 文件
// 浏览器会阻止 ES Modules 使用 file:// 协议
// Access to script at 'file:///C:/.../module.js' from origin 'null' has been blocked by CORS policy
// ✅ 正确:使用 HTTP 服务器
// 使用 VS Code 的 Live Server 插件
// 使用 Python:python -m http.server 8000
// 使用 Node.js:npx serve
2. 路径必须完整
<script type="module">
// ❌ 错误:省略扩展名
import { foo } from './utils'; // 浏览器不会自动尝试 .js
// ✅ 正确:包含扩展名
import { foo } from './utils.js';
// ✅ 正确:使用 / 结尾(导入目录下的 index.js)
import { bar } from './components/'; // 导入 ./components/index.js
</script>
3. 不支持 bare specifier
<script type="module">
// ❌ 错误:浏览器不知道 'lodash' 在哪里
import _ from 'lodash';
// ✅ 正确:使用完整 URL
import _ from 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.js';
// ✅ 正确:使用相对路径
import _ from './node_modules/lodash/lodash.js'; // 如果本地有
// ✅ 解决方法:使用打包工具(Webpack/Vite)处理 bare specifier
</script>
4. CORS 限制
<script type="module">
// 跨域加载时,服务器必须返回 CORS 头
// Access-Control-Allow-Origin: *
// ✅ unpkg、jsdelivr 等 CDN 都支持 CORS
import axios from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.esm.js';
// ❌ 如果服务器没有 CORS 头,会报错
</script>
5. 模块默认使用严格模式
<script type="module">
// 模块内部自动启用严格模式
// 以下代码会报错(严格模式下不允许)
// undeclaredVar = 10; // ReferenceError
// 也不能使用 with 语句
// with (Math) { console.log(random()); } // SyntaxError
</script>
综合示例
示例 1:使用 CDN 引入多个库构建应用
<!DOCTYPE html>
<html>
<head>
<title>CDN 示例</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<h1>用户列表</h1>
<button id="load-btn">加载数据</button>
<ul id="user-list"></ul>
<p id="time"></p>
</div>
<script>
// 显示当前时间
function updateTime() {
document.getElementById('time').textContent = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
updateTime();
setInterval(updateTime, 1000);
// 防抖搜索
let searchInput = document.createElement('input');
searchInput.placeholder = '搜索用户...';
document.getElementById('app').prepend(searchInput);
searchInput.addEventListener('input', _.debounce((e) => {
console.log('搜索:', e.target.value);
}, 500));
// 加载数据
document.getElementById('load-btn').addEventListener('click', () => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(res => {
let list = document.getElementById('user-list');
list.innerHTML = '';
res.data.forEach(user => {
let li = document.createElement('li');
li.textContent = user.name;
list.appendChild(li);
});
})
.catch(err => console.error(err));
});
</script>
</body>
</html>
示例 2:ES Modules 项目示例
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script type="module" src="main.js"></script>
</body>
</html>
// utils.js
export function formatDate(date) {
return new Date(date).toLocaleDateString('zh-CN');
}
export const PI = 3.14159;
// api.js
export async function fetchUsers() {
let res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error('请求失败');
return res.json();
}
// main.js
import { formatDate } from './utils.js';
import { fetchUsers } from './api.js';
async function init() {
try {
let users = await fetchUsers();
let app = document.getElementById('app');
users.forEach(user => {
let div = document.createElement('div');
div.textContent = `${user.name} — 注册于 ${formatDate(user.registered)}`;
app.appendChild(div);
});
} catch (err) {
console.error('初始化失败:', err);
}
}
init();
示例 3:动态导入实现懒加载
<script type="module">
// 路由配置
const routes = {
'/': () => import('./pages/home.js'),
'/about': () => import('./pages/about.js'),
'/contact': () => import('./pages/contact.js')
};
// 简单的路由处理
async function router() {
let path = window.location.pathname;
let loader = routes[path] || routes['/'];
let { default: render } = await loader();
render();
}
// 拦截链接点击,实现 SPA 效果
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.preventDefault();
history.pushState(null, '', e.target.href);
router();
}
});
window.addEventListener('popstate', router);
router(); // 初始加载
</script>
总结:浏览器使用第三方模块方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| script + CDN | 简单直接,无需构建 | 全局污染,依赖管理困难,无 Tree-shaking | 简单页面、演示、旧项目 |
| ES Modules | 标准语法,无全局污染,静态分析 | 浏览器限制多(路径、CORS、bare specifier) | 现代浏览器、轻量项目 |
| 动态导入 | 懒加载,提升首屏性能 | 需要 ES Modules 环境 | 路由级代码分割、按需加载 |
| 打包工具 | 解决所有限制,支持高级特性 | 需要构建步骤,配置复杂 | 现代前端项目(推荐) |
推荐工作流:
- 学习阶段:使用
script + CDN快速上手- 简单项目:使用
ES Modules+ CDN(如 skypack、esm.sh)- 生产项目:使用
Vite或Webpack打包工具