在开发复杂的 Web 应用程序时,我们通常不会把成千上万行的代码都写在一个单独的文件里。随着项目规模的扩大,将代码分割成多个逻辑清晰、易于管理的文件是必不可少的。这时,一个核心问题就会浮现在我们面前:如何在一个 JavaScript 文件中引入并使用另一个 JavaScript 文件中的代码?
在这篇文章中,我们将深入探讨实现这一目标的各种方法。我们会从现代最主流的 ES6 模块化方案开始,逐步回顾经典的服务端 CommonJS 方案,最后覆盖浏览器端的 标签加载技术。更重要的是,我们将结合 2026 年的开发视角,探讨 AI 辅助开发、模块联邦以及云原生架构下的模块化新趋势。无论你是前端新手还是寻求最佳实践的老手,这篇文章都将为你提供清晰的指引和实用的代码示例。
目录
为什么我们需要模块化?
在正式进入代码之前,让我们先理解一下“文件拆分”为何如此关键。想象一下,如果你正在开发一个大型电商平台,所有的逻辑——用户验证、购物车计算、页面渲染——都塞进一个 main.js 文件中。这个文件将会变得极其庞大,不仅难以维护,还会导致“全局作用域污染”,即变量名相互覆盖的灾难。
通过将功能拆分到不同的文件(模块)中,我们可以获得以下优势:
- 代码复用:定义一次工具函数,可以在项目的任何地方引用。
- 作用域隔离:每个模块都有自己的作用域,不会意外污染全局变量,这是现代前端安全的基础。
- 依赖管理:明确声明依赖关系,使得代码更易于理解和重构。
- 可维护性:代码结构清晰,便于团队协作和后期排查 Bug。
方法一:使用 ES6 模块(现代标准)
目前,最现代、最推荐的在不同 JavaScript 文件之间共享代码的做法是使用 ES6(ECMAScript 2015)引入的模块化系统。这套系统基于 INLINECODE29fa1ede 和 INLINECODEbf122d76 关键字,让我们能够显式地声明哪些代码可以被外部访问。到了 2026 年,这已经不仅是标准,更是所有现代框架和构建工具的通用语。
基础示例:命名导出与导入
让我们从一个实际的例子开始。假设我们正在编写一个处理数学运算的模块。我们创建一个名为 INLINECODE47a91aa0 的文件。注意这里我们使用了 INLINECODE96872d16 扩展名,这明确告诉 Node.js 或构建工具这是一个 ES6 模块。
文件:math.mjs
// math.mjs
// 我们使用 export 关键字将函数和变量暴露给外部
export function add(n1, n2) {
if (typeof n1 !== ‘number‘ || typeof n2 !== ‘number‘) {
throw new Error(‘参数必须是数字‘);
}
return n1 + n2;
}
// 你也可以导出常量
export const pi = 3.14159;
// 甚至可以导出类
export class Calculator {
multiply(a, b) {
return a * b;
}
}
现在,让我们在另一个文件 INLINECODEd1aefdb6 中引入并使用这些功能。我们需要使用 INLINECODEf5be35f0 语句,并通过解构赋值来获取我们需要的具体部分。
文件:main.mjs
// main.mjs
// 使用大括号 {} 进行命名导入,路径必须以 ./ 开头表示当前目录
import { add, pi, Calculator } from ‘./math.mjs‘;
// 让我们尝试调用导入的函数
console.log("计算加法:", add(10, 5)); // 输出: 15
console.log("圆周率常量:", pi); // 输出: 3.14159
// 实例化导入的类
const calc = new Calculator();
console.log("计算乘法:", calc.multiply(3, 7)); // 输出: 21
进阶技巧:默认导出与动态导入
除了命名导出,ES6 还允许每个模块有一个“默认导出”。这通常用于当一个文件只导出一个主要功能(比如一个类或一个主函数)时。
但在 2026 年的复杂应用中,我们更关注性能优化。这里必须提到动态导入(Dynamic Import)。传统的 import 是静态的,必须在文件顶层运行。而动态导入允许我们在代码执行过程中,按需加载模块。
文件:app.mjs
// app.mjs
// 默认导出示例
import sayHello from ‘./utils.mjs‘;
console.log(sayHello("Developer"));
// 动态导入示例:模拟一个图表库,只有用户点击按钮时才加载
const loadChartBtn = document.getElementById(‘loadChart‘);
loadChartBtn.addEventListener(‘click‘, async () => {
try {
// 这里的 import() 返回一个 Promise
const chartModule = await import(‘./chart-lib.mjs‘);
// 假设 chart-lib.mjs 导出了一个 render 函数
chartModule.render();
console.log("图表模块已加载并渲染");
} catch (error) {
console.error("模块加载失败:", error);
// 生产环境中的容灾处理:降级显示静态图片或提示用户
showStaticImageFallback();
}
});
function showStaticImageFallback() {
document.body.innerHTML += "抱歉,图表组件暂时无法加载,请刷新页面。
";
}
这种技术对于构建高性能的单页应用(SPA)至关重要,它能显著减少初始包体积,提升首屏加载速度(FCP)。
方法二:使用 CommonJS(Node.js 经典方案)
在 ES6 模块普及之前,Node.js 社区一直使用 CommonJS 模块系统。如果你查看一些老项目的代码,或者在没有构建工具的 Node.js 后端项目中工作,你会大量接触到 INLINECODE7732a78b 和 INLINECODE193622e1。理解这一点对于维护遗留代码至关重要。
基础用法与互操作
在 CommonJS 中,我们不使用 INLINECODEe024b454,而是通过一个全局变量 INLINECODE3904f8e2 来决定导出什么内容。
文件:math.js
// math.js
function add(n1, n2) {
return n1 + n2;
}
const pi = 3.14159;
// 显式导出
module.exports = {
add,
pi
};
文件:main.js
// main.js
const mathUtils = require(‘./math.js‘);
console.log("CommonJS 加法:", mathUtils.add(5, 3));
2026 视角:CommonJS 的现状与迁移
虽然新的 Node.js 项目已经全面转向 ES Modules ("type": "module"),但 npm 生态中仍有大量遗留包使用 CommonJS。在现代开发中,我们经常面临“模块互操作性”的挑战。
我们如何处理混合依赖?
如果你正在使用 ES6 模块开发,但需要引入一个只有 CommonJS 版本的旧版库,现代打包工具(如 Vite 或 Webpack 5)会自动处理这种兼容性。但在原生 Node.js 环境下,我们需要了解 createRequire。
// 在 ES Module 中引入 CommonJS 包的技巧
import { createRequire } from ‘module‘;
const require = createRequire(import.meta.url);
// 现在你可以使用 require 引入旧的库
const oldLibrary = require(‘old-cjs-library‘);
这是我们在 2026 年维护遗留系统或连接新旧技术栈时经常使用的模式。
方法三:浏览器环境下的 标签
如果你是在没有任何打包工具(如 Webpack、Vite)的情况下,直接在浏览器中编写原生 JavaScript,那么你可能需要依赖传统的 HTML 标签来加载 JavaScript 文件。
顺序加载机制
在使用 标签时,文件的加载顺序至关重要。因为浏览器默认是同步解析并执行这些脚本的,后加载的脚本可以访问先加载的脚本中定义的全局变量。
文件:index.html
JS File Inclusion Example
查看控制台输出
文件:math.js (浏览器全局变量版)
// math.js
// 在浏览器非模块环境下,变量默认挂载到 window 对象上
window.add = function(n1, n2) {
return n1 + n2;
};
使用 type="module" 来原生支持 ES6 模块
现代浏览器其实已经原生支持 ES6 模块了!你不需要任何打包工具,只需在 INLINECODE14de324f 标签上添加 INLINECODEe02a39b6 属性。这样你就可以在浏览器中直接使用 INLINECODEee0430a5 和 INLINECODE1a9bcbfc。
这样做的好处是:
- 支持原生 ES6 模块语法(INLINECODEc367d303/INLINECODE3226cae1)。
- 自动启用“严格模式”。
- 存在顶层作用域,不会污染全局 INLINECODE475ad90c 对象。这意味着你在 INLINECODEafe076ab 脚本中定义的变量,外部无法直接访问,安全性更高。
2026 前沿视角:模块化不仅仅是引入文件
当我们谈论“如何包含 JS 文件”时,我们实际上是在讨论如何构建应用的架构。到了 2026 年,这一话题已经延伸出了新的维度。让我们深入探讨一些我们在高级开发中遇到的真实场景和解决方案。
1. 模块联邦与微前端架构
在现代大型企业应用中,我们不再只是引入本地文件,我们甚至在运行时引入远程服务器上的 JavaScript 模块。这就是 Module Federation(模块联邦)。
场景:想象一下,你正在开发一个主应用,你需要集成由另一个团队开发的“支付中心”模块。以前,你需要将他们的代码打包进你的项目,每次更新都要重新发布整个应用。
现代解决方案:
// 动态引入远程模块
// 这个模块并不存在于你的本地 node_modules,而是在构建时动态获取
async function loadRemotePaymentModule() {
const scope = ‘payment_team‘;
const module = ‘PaymentButton‘;
// 初始化容器
await __webpack_init_sharing__(‘default‘);
const container = window[scope];
// 初始化作用域
await container.init(__webpack_share_scopes__.default);
// 加载模块
const factory = await container.get(module);
const PaymentButton = factory();
// 渲染到页面
const button = new PaymentButton();
document.body.appendChild(button.render());
}
这种技术让我们能够在构建时甚至运行时动态组合应用,彻底改变了我们对“文件包含”的理解——它不再局限于文件系统,而是延伸到了网络。
2. AI 辅助开发与“氛围编程”
现在,当我们编写模块引入代码时,AI 已经成为了我们的结对编程伙伴。在使用 Cursor 或 GitHub Copilot 时,我们不再手动输入长长的路径。
最佳实践:
当你需要引入一个模块时,你可以直接在编辑器中写注释:
// TODO: 引入用户工具类,并使用 formatName 函数
AI 会自动扫描你的项目结构,找到 INLINECODEc4bcd162 文件,并生成正确的 INLINECODE5fa07e6b 语句。更重要的是,如果你的文件重命名了,AI 会智能地检测到所有引用该文件的地方并提示你更新路径。这解决了一个经典的痛点:文件重命名导致的路径引用错误。
3. 故障排查与可观测性
在生产环境中,仅仅知道“如何引入”是不够的,我们还需要知道“引入失败时该怎么办”。
问题:动态导入失败。
当用户网络不稳定,或者 CDN 服务器挂掉时,import() 可能会抛出错误。我们在生产环境中必须实施重试机制和回退策略。
// 带有重试机制的动态导入
async function importWithRetry(url, retries = 3) {
try {
const module = await import(url);
return module;
} catch (error) {
if (retries setTimeout(res, 1000)); // 等待1秒
return importWithRetry(url, retries - 1);
}
}
这种模式体现了现代工程化的严谨性:我们假设一切都会出错,并为此做好准备。
常见错误与解决方案
在文件引入的过程中,新手(甚至是有经验的开发者)经常会遇到一些坑。让我们来看看如何解决它们。
1. CORS 策略错误
如果你直接在浏览器中打开 HTML 文件(即地址栏是 INLINECODE3f7b4e0d),而不是通过本地服务器(如 INLINECODE6e099a03),你会发现 ES6 模块根本无法加载。浏览器会报出 CORS 错误。
解决方案:你必须使用一个本地服务器来运行你的代码。可以使用 VS Code 的 Live Server 插件,或者简单的 Python 命令 (python -m http.server) 来启动服务。这是现代前端开发的基本要求。
2. 文件扩展名陷阱
在 Node.js ES Modules 中,引入文件时必须包含完整的文件扩展名(INLINECODE077b33b6 或 INLINECODE75493924)。这是 CommonJS 开发者最容易犯的错误。
// 错误 (在 ESM 中)
import { add } from ‘./math‘;
// 正确
import { add } from ‘./math.js‘;
性能优化与最佳实践
在大型项目中,如何引入文件也会影响应用的性能和加载速度。
- Tree Shaking(摇树优化):使用 ES6 的命名导出(INLINECODEd309e85c)而不是默认导出(INLINECODE30ef8aee),能帮助打包工具更好地进行 Tree Shaking,移除那些未使用的代码,减小最终包体积。
- 避免循环依赖:文件 A 引入文件 B,文件 B 又引入文件 A。这会导致代码逻辑混乱。如果出现了这种情况,通常意味着你的代码结构需要重构,比如提取共同的逻辑到第三个文件中。
- 预加载:对于极其重要的模块,可以使用
在浏览器加载主脚本之前就开始下载模块,从而优化关键渲染路径。
总结与展望
在这篇文章中,我们全面探讨了如何将一个 JavaScript 文件包含到另一个中。从现代的 ES6 模块(INLINECODE488f2f21/INLINECODE3f569254),到经典的服务端 CommonJS(INLINECODEeb238c16),再到浏览器原生的 INLINECODE0ea057af 标签加载,我们了解了这三种主要机制及其各自的适用场景。
回顾一下我们的建议:
- 首选方案:对于几乎所有的新项目(无论是前端还是 Node.js 后端),请坚持使用 ES6 模块。它是标准,原生支持,并且提供了更好的代码静态分析能力。
- 2026 视角:关注运行时集成(如微前端),利用 AI 工具优化导入效率,并在生产环境中实施动态导入的容灾机制。
通过掌握这些文件引入的技巧,你已经迈出了构建模块化、可维护且高性能 JavaScript 应用的关键一步。下一步,我们建议你尝试在自己的项目中实践 ES6 模块,并尝试配置一个简单的本地服务器来体验浏览器原生模块的加载过程。编码愉快!