D10 学习笔记:模式 1 的精髓——3 个页面共用 1 个 JS 文件
系列:海口三港 AI 全栈实战 · 从参赛大屏到 AI 平台
仓库:https://github.com/2003Tim/haikou-ai-port
前言:D10 的核心洞察
D9 我让 index.html 动态化了。但还有 3 个港口详情页(xiuying / xinhai / tielunan)完全没动,里面还是硬编码。
D10 我意识到一个关键事实:这 3 个页面 95% 相同!它们只是展示"当前港口"的数据,其他结构一模一样。
如果用 D9 的"模式 0"(JS 内联),我得复制 3 遍 JS 代码。但用"模式 1"(JS 分离),3 个页面共用 1 个 JS 文件,改一次全生效。
这就是 D10 的精髓:共享代码,DRY 原则。
一、模式 1 的核心:JS 分离到独立文件
D9 的模式 0
<!-- index.html -->
<script>
async function loadPorts() { ... }
loadPorts()
</script>
JS 代码写在 HTML 里,跟着 HTML 一起加载。
D10 的模式 1
<!-- xiuying.html / xinhai.html / tielunan.html -->
<script src="js/port-detail.js"></script>
HTML 只引一行,JS 在独立文件里。
模式 1 的 3 大好处
| 好处 | 说明 |
|---|---|
| 浏览器缓存 | JS 文件缓存,二次访问只下载 HTML,更快 |
| 多页面共享 | 3 个 HTML 引同一 JS,改一次全生效 |
| 职责清晰 | HTML 管结构,JS 管行为,各管一摊 |
二、3 个页面的"重复"问题
xiuying / xinhai / tielunan 的结构对比
我对比了 3 个页面,发现:
- 头部 logo、showTime:完全相同
- 港口名(秀英港 / 新海港 / 铁路南港):只有 1 个字不同
- 监控视频:几乎相同(只是文件名不同)
- 班次表格:数据不同,结构相同
唯一真正不同的是"当前显示的港口"。
错误做法(模式 0)
每个 HTML 里复制一份 JS:
<!-- xiuying.html -->
<script>
async function loadPortDetail() {
const portId = 1
const response = await fetch(`http://127.0.0.1:8000/ports/${portId}`)
const port = await response.json()
// ... 渲染
}
loadPortDetail()
</script>
<!-- xinhai.html -->
<script>
async function loadPortDetail() {
const portId = 2
// ... 完全一样的代码
}
loadPortDetail()
</script>
<!-- tielunan.html -->
<script>
async function loadPortDetail() {
const portId = 3
// ... 完全一样的代码
}
loadPortDetail()
</script>
问题:如果 JS 要改,3 个文件都改——容易漏、容易出错。
正确做法(模式 1)
1 个 JS 文件,自动判断是哪个港口:
// js/port-detail.js
async function loadPortDetail() {
// 1. 从 URL 拿 portId
const urlParams = new URLSearchParams(location.search)
const portId = parseInt(urlParams.get('id')) || 1
// 2. 调 API
const response = await fetch(`http://127.0.0.1:8000/ports/${portId}`)
const port = await response.json()
// 3. 渲染
renderPortDetail(port)
}
3 个 HTML 都加同一行:
<script src="js/port-detail.js"></script>
三、URL 参数:让 1 个 JS 知道"当前是哪个港口"
核心问题
xiuying.html 和 xinhai.html 文件名不同,但 port-detail.js 是同一个文件。
JS 怎么知道当前应该加载哪个港口?
解决方案:URL 参数
<!-- 不同的 URL -->
<a href="xiuying.html?id=1">秀英港</a>
<a href="xinhai.html?id=2">新海港</a>
<a href="tielunan.html?id=3">铁路南港</a>
?id=1 就是 URL 参数,JS 用 URLSearchParams 读取。
URLSearchParams API
// 假设当前 URL: xiuying.html?id=1
const urlParams = new URLSearchParams(location.search)
// ↑
// window.location.search
// 返回 "?id=1" 字符串
const portId = urlParams.get('id') // "1"(字符串)
const portIdNum = parseInt(urlParams.get('id')) || 1 // 1(数字,|| 1 是 fallback)
API 链:
location.search:返回?id=1(字符串)new URLSearchParams(...):解析成对象.get('id'):拿id的值,返回字符串parseInt(...):转数字,失败返回NaN|| 1:NaN 时用 1 兜底
index.html 的关键改动
D10 我让 index.html 的港口链接加 ?id=N:
const PORT_PAGE_MAP = {
1: 'xiuying.html?id=1',
2: 'xinhai.html?id=2',
3: 'tielunan.html?id=3',
}
这样点"秀英港" → 跳转到 xiuying.html?id=1 → JS 拿到 id=1 → 加载秀英港数据。
四、D10 实战改造
第 1 步:创建共享 JS 文件
新建 js/port-detail.js:
/**
* js/port-detail.js
* 3 个港口详情页共用(xiuying.html / xinhai.html / tielunan.html)
*/
const API_BASE = 'http://127.0.0.1:8000'
async function loadPortDetail() {
try {
// 从 URL 拿 portId
const urlParams = new URLSearchParams(location.search)
const portId = parseInt(urlParams.get('id')) || 1
// 调 API
const response = await fetch(`${API_BASE}/ports/${portId}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const port = await response.json()
// 渲染到页面
renderPortDetail(port)
} catch (error) {
console.error('加载港口详情失败:', error)
showError(error.message)
}
}
function renderPortDetail(port) {
// 改页面标题
document.title = `${port.name} - 海口三港 AI 智慧管控平台`
// 改 gang_title 里的当前港口名
const currentPortName = document.querySelector('.gang_title a:first-child')
if (currentPortName) {
currentPortName.textContent = port.name
}
// 改 location(如果有)
const locationEl = document.getElementById('port-location')
if (locationEl) {
locationEl.textContent = port.location
}
// 改 capacity(如果有)
const capacityEl = document.getElementById('port-capacity')
if (capacityEl) {
capacityEl.textContent = port.capacity.toLocaleString()
}
console.log(`✅ 已加载港口:${port.name}`, port)
}
function showError(message) {
const mainbox = document.querySelector('.mainbox') || document.body
const errorDiv = document.createElement('div')
errorDiv.style.cssText = 'color: #ff6b6b; padding: 20px; text-align: center; font-size: 16px;'
errorDiv.innerHTML = `
❌ 加载港口详情失败:${message}<br>
<small>请确保 uvicorn 在 ${API_BASE} 运行</small>
`
mainbox.insertBefore(errorDiv, mainbox.firstChild)
}
// 页面加载时执行
loadPortDetail()
第 2 步:3 个 HTML 各加一行
xiuying.html、xinhai.html、tielunan.html 都修改:
<!-- 修改前 -->
<script src="js/echarts.js"></script>
<script src="js/jquery.js"></script>
<script src="js/xiuying.js"></script>
<script src="js/sailing_xiuying.js"></script>
<!-- 修改后(只加最后一行) -->
<script src="js/echarts.js"></script>
<script src="js/jquery.js"></script>
<script src="js/xiuying.js"></script>
<script src="js/sailing_xiuying.js"></script>
<!-- D10:共用 JS 文件,3 个详情页共享 -->
<script src="js/port-detail.js"></script>
第 3 步:index.html 加 ?id=N
const PORT_PAGE_MAP = {
1: 'xiuying.html?id=1', // D10:加 ?id=N
2: 'xinhai.html?id=2',
3: 'tielunan.html?id=3',
}
五、D10 完成后的关键验证
静态验证(必做)
| 步骤 | 预期 |
|---|---|
| 1. 点击 index.html 的"秀英港" | 跳转到 xiuying.html?id=1 |
| 2. 等页面加载 | gang_title 显示"秀英港"(从 API) |
| 3. F12 看 console | ✅ 已加载港口:秀英港 {...} |
| 4. 点击"新海港" | 跳转到 xinhai.html?id=2 |
| 5. 等待加载 | gang_title 显示"新海港"(从 API) |
⭐ 动态验证(高级)
-
Apifox 改港口名:
PUT http://127.0.0.1:8000/ports/1 Body: {"name": "秀英港(测试版)"} -
不要刷新 index.html,直接点击"秀英港"卡片
-
新页面应该显示"秀英港(测试版)" ⭐
-
改回"秀英港" → 详情页又显示原来的名字
🎯 真正的全栈联动:改数据库 → 进入详情页 → 显示新名字。这就是 v1.0 的"动态化"含义。
六、DRY 原则:Don't Repeat Yourself
D10 是 DRY 原则的经典应用:
| 反面(重复) | 正面(共享) |
|---|---|
| 3 个 HTML 各复制一份 | 1 个 JS 文件 + 3 个 HTML 引用 |
| 改一次要改 3 处 | 改一次全生效 |
| 容易漏改、出错 | 单一来源,不容易错 |
DRY 不只是技术,是一种工程思维:
- 业务逻辑(JS):只写一次
- 数据(API):只存一份
- 配置(端口、地址):集中管理
七、我踩过的坑(D10)
坑 1:URL 参数拿不到
症状:urlParams.get('id') 返回 null。
原因:URL 写成 xiuying.html 而不是 xiuying.html?id=1。
修复:index.html 的 PORT_PAGE_MAP 加 ?id=N。
坑 2:parseInt 失败返回 NaN
症状:portId 是 NaN,API 请求 /ports/NaN 返回 422。
修复:parseInt(urlParams.get('id')) || 1 加 fallback。
坑 3:页面没看到 console 输出
症状:renderPortDetail 应该 console.log 但没看到。
原因:F12 的 Console 标签可能没打开。
修复:F12 → Console 标签。
坑 4:CSS 选择器没匹配上
症状:document.querySelector('.gang_title a:first-child') 返回 null。
原因:HTML 结构略有不同,或者 gang_title 不存在。
修复:加 if (currentPortName) 防御,或者在浏览器 console 看实际 HTML 结构。
八、D10 与 D9 的对比
| 维度 | D9 (index.html) | D10 (3 个详情页) |
|---|---|---|
| 改造范围 | 1 个 HTML | 3 个 HTML + 1 个 JS |
| JS 文件数 | 内联在 HTML | 独立 JS 文件 |
| 浏览器缓存 | 不缓存 JS | 缓存 JS |
| 代码复用 | 不复用 | 3 页共用 1 JS |
| 改动量 | 中 | 小(每个 HTML 1 行) |
九、D10 关键收获
- 模式 1 的精髓:JS 分离 + 多 HTML 引用
- URLSearchParams:从 URL 拿参数的关键 API
- DRY 原则:不重复自己
- **
|| 兜底**:防 NaN、防 null if (xxx) {}防御:DOM 操作前先检查元素是否存在
十、模式演进:从 D9 到 D10
D9:模式 0(JS 内联) 适合:1 个页面的 demo
D10:模式 1(JS 分离) 适合:多个相似页面
D11+:模式 2(JS 模块化) 适合:大型项目
未来:模式 3(Vue/React 框架) 适合:复杂单页应用
D10 是从"玩具项目"到"工程实践"的关键一步。
下一步
D10 我完成了 3 个详情页的动态化。D11 我会:
- 改造 platform.html 大屏
- 加"数据状态条"显示实时港口数据(端口数 + 总容量 + 在线状态)
- 引入"自动刷新"(每 30 秒拉一次)
- v1.0 全部完成!
参考资料
- MDN - URLSearchParams:https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams
- MDN -
<script>标签:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script - 《JavaScript 高级程序设计》第 2 章(HTML 中的 JavaScript)
- DRY 原则:https://en.wikipedia.org/wiki/Don't_repeat_yourself

浙公网安备 33010602011771号