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 环境 路由级代码分割、按需加载
打包工具 解决所有限制,支持高级特性 需要构建步骤,配置复杂 现代前端项目(推荐)

推荐工作流

  1. 学习阶段:使用 script + CDN 快速上手
  2. 简单项目:使用 ES Modules + CDN(如 skypack、esm.sh
  3. 生产项目:使用 ViteWebpack 打包工具