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.htmlxinhai.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)

⭐ 动态验证(高级)

  1. Apifox 改港口名:

    PUT http://127.0.0.1:8000/ports/1
    Body: {"name": "秀英港(测试版)"}
    
  2. 不要刷新 index.html,直接点击"秀英港"卡片

  3. 新页面应该显示"秀英港(测试版)"

  4. 改回"秀英港" → 详情页又显示原来的名字

🎯 真正的全栈联动:改数据库 → 进入详情页 → 显示新名字。这就是 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. 模式 1 的精髓:JS 分离 + 多 HTML 引用
  2. URLSearchParams:从 URL 拿参数的关键 API
  3. DRY 原则:不重复自己
  4. **|| 兜底**:防 NaN、防 null
  5. 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 全部完成!

参考资料

posted @ 2026-08-28 16:58  吴逸清  阅读(7)  评论(0)    收藏  举报