JavaScript 原生模块通过 type="module" 启用,使用 import 和 export 连接文件;动态 import 返回 Promise,适合按需加载。你把原来一段脚本拆成两个文件,页面却突然出现语法错误或加载失败,往往并不是函数本身写错,而是模块入口、文件路径或服务器响应不符合要求。

本文把商品小计与说明文字拆成两个模块,再由主入口加载,给出完整目录和每个文件的内容。示例使用现代浏览器,不依赖打包工具。你需要一个本地 HTTP 服务;直接双击 HTML 得到的 file 地址,不适合验证浏览器模块加载流程。
一、模块拆分先明确谁依赖谁
模块像一组有明确出口的零件:price.js 只负责金额计算,help.js 只提供说明,main.js 负责页面交互。主入口可以依赖计算模块,但计算模块不需要知道页面上有哪个按钮。
这种边界可以减少互相牵连。更改按钮文案不应迫使你修改计算函数;更改金额规则也不应要求整个页面都认识内部变量。export 指定外部能够导入的名字,不导出的变量留在模块内部。
模块与函数基础可对照 JavaScript 教程。不要把“分成几个文件”直接等同于模块化:如果每个文件仍然依赖同一堆全局变量,依赖关系依旧难以维护。
二、建立一个能够运行的四文件例子
在同一目录创建 index.html、price.js、help.js 和 main.js。下面先给页面入口。
<!doctype html>
<meta charset="utf-8">
<title>编程狮原生模块练习</title>
<p id="total"></p>
<button type="button" id="help">加载说明</button>
<p id="message"></p>
<script type="module" src="./main.js"></script>
price.js 导出一个具名函数。它按整数分计算,输入必须是非负安全整数,结果也不能超出安全整数范围。
export function subtotal(price, quantity) {
if (!Number.isSafeInteger(price) || !Number.isSafeInteger(quantity)
|| price < 0 || quantity < 0) {
throw new TypeError('单价和数量必须是非负安全整数');
}
const result = price * quantity;
if (!Number.isSafeInteger(result)) throw new RangeError('总额超出范围');
return result;
}
help.js 使用默认导出。一个模块可以有多个具名导出,但最多只有一个默认导出。两种形式对应不同的导入语法。
export default '金额以分计算,说明模块仅在点击后导入。';
main.js 在启动时导入小计函数,在用户点击按钮时导入说明模块。动态加载失败会显示错误,不把未加载成功的状态当作成功。
import { subtotal } from './price.js';
const total = document.querySelector('#total');
const button = document.querySelector('#help');
const message = document.querySelector('#message');
total.textContent = `小计:${subtotal(1250, 2)} 分`;
button.addEventListener('click', async () => {
button.disabled = true;
try {
const module = await import('./help.js');
message.textContent = module.default;
} catch (error) {
message.textContent = '说明加载失败,请检查请求和控制台。';
console.error(error);
} finally {
button.disabled = false;
}
});
如果本机已有 Python,可以在这个目录运行 python -m http.server 8000 --bind 127.0.0.1,然后打开本机八千端口。这里的 HTTP 服务只是开发用文件服务,不是上线部署方案。
初次打开时应显示 2500 分,点击按钮才显示说明。浏览器开发者工具的 Network 面板可以观察文件请求,Console 面板查看语法和运行异常。
三、分清具名导入、默认导入和动态导入
具名导入中的 subtotal 必须与导出名匹配,若想换本地名称,可以用 as。默认导入可以选择本地变量名,但对应的模块必须确实提供默认导出。不要仅因为名字相同就随意增删花括号。
动态 import 得到的是模块命名空间对象,默认导出放在 default 属性,具名导出则使用对应属性名。示例中的 await 先等待加载和求值完成,再读取 module.default;如果直接把 Promise 当作文字显示,就无法得到预期内容。
| 场景 | 使用方式 | 需要记住的区别 |
|---|---|---|
| 页面启动就要计算 | 静态具名导入 | 导出名称需要匹配 |
| 模块只有主要说明对象 | 默认导出 | 导入时不加花括号 |
| 点击后才使用功能 | 动态导入 | 结果是 Promise |
语法细节可结合 JavaScript 速查手册 核对。浏览器中的相对导入从当前模块地址解析,不是从你想象的项目根目录解析;main.js 移到子目录后,原来的相对路径也可能需要调整。
同一模块标识通常不会在每次 import 时重新执行一遍顶层代码。模块加载缓存也意味着修改文件后测试应注意刷新与缓存条件,不能只靠重复点击按钮判断是否读到了新版本。
四、从错误信息定位具体层次
看到“不能在模块外使用 import”一类错误,先看 script 是否标注 type="module"。如果是“找不到导出”,检查导出名称和默认/具名方式。遇到四零四或 MIME 类型错误,就去 Network 看文件实际返回内容。
请求地址看起来像 JavaScript 文件,服务器却返回了 HTML 错误页,是很常见的情况。浏览器不会因为文件名以 .js 结尾就把任意响应当成正确模块;应检查状态、响应头和响应体,而不是反复更换导入语法。
浏览器原生模块也不会自动理解所有 npm 包名。直接写一个裸包名,需要 import map 或构建工具提供解析规则。本文全用相对路径,故不需要包管理器。跨域加载时还受跨域规则影响,应由提供模块的服务器正确配置。
测试失败分支时,可以把 help.js 临时改名,再用新的页面加载尝试按钮。页面应显示失败提示,按钮最终恢复可点。因为成功加载过的模块可能已缓存,测试要在新页面上下文中进行,避免误以为失败处理没有效果。

总结
JavaScript 原生模块的关键是依赖方向、导出形式和加载环境。四个文件的小例子已经覆盖启动导入、按需导入和错误反馈,不需要先引入复杂工程配置。
下一步可以增加一个 discounts.js,只导出折扣计算,再让 price.js 组合它。新增依赖时画清“谁引用谁”,保持计算模块不依赖 DOM。遇到问题按入口类型、路径、网络响应、导出名称逐层检查,比无目的地改扩展名或加花括号更有效。
延伸学习
- JavaScript Guide 可补函数、异步和对象相关知识。
- JavaScript 原生模块课程 可继续练习模块组织方式。
- JavaScript 作用域说明 可辅助理解模块内部变量与全局变量的区别。
常见问题
Q:为什么双击 HTML 运行和本地服务结果不同?
浏览器对本地文件与 HTTP 资源的安全处理不同。验证模块加载应使用本地 HTTP 服务,避免 file 地址带来的限制干扰判断。
Q:动态导入一定能让页面更快吗?
不一定。它推迟下载与求值,但用户点击时可能等待加载。适合不常用或体积较大的功能,具体收益仍需要测量。
Q:模块里声明的变量会自动出现在 window 上吗?
不会。模块有自己的作用域,需要通过导出和导入共享。把变量主动挂到 window 是另一种操作,不是模块默认行为。
技术依据:MDN JavaScript 模块指南。

免费 AI IDE



