D9 学习笔记:前端从静态到动态——fetch + CORS + 前后端联调

系列:海口三港 AI 全栈实战 · 从参赛大屏到 AI 平台
仓库:https://github.com/2003Tim/haikou-ai-port


前言:D9 是 v1.0 的"破冰之旅"

D8 我有了一个能持久化的数据库。但前端还是硬编码的:index.html 里写死了 3 个港口,改名字要改 HTML,删港口要改 JS。

D9 我做了真正让前后端打通的事:

  • fetch 让前端调后端 API
  • 配置 CORS 解决跨域
  • JS 分离到独立文件(模式 1)
  • Network 标签观察请求
  • 改数据库 → 刷新页面 → 看到效果

这是第一次前端真正从后端拿数据,v1.0 的关键破冰。


一、为什么要让前端调后端?(前后端分离的意义)

D8 之前的问题

<!-- index.html:硬编码 -->
<a class="port" href="xiuying.html">
    <div class="port_image">
        <img src="images/xiuying0.jpg">
    </div>
    <div class="port_text">
        <span class="port_title">秀英港</span>
        <span class="port_address">海口市秀英区滨海大道96号</span>
    </div>
</a>

问题:

  • 改港口名要改 HTML
  • 新增港口要改 HTML
  • 多页面数据不一致(同一个港口在 3 个地方写)

D9 之后

<div class="ports" id="ports-container">
    <!-- JS 动态填充 -->
</div>

<script>
    async function loadPorts() {
        const response = await fetch('http://127.0.0.1:8000/ports');
        const ports = await response.json();
        // 动态渲染...
    }
    loadPorts();
</script>

好处:

  • 改数据库 → 前端立即反映
  • 新增港口不用改前端
  • 数据一致(都从同一个 API 来)

二、CORS:跨域问题详解

D9 我第一次遇到 CORS。这个概念很多人卡在这里,我详细讲。

什么是"源"(Origin)

"源" = 协议 + 域名 + 端口,三者都相同才是"同源"。

我的前端:http://127.0.0.1:5500
后端 API:http://127.0.0.1:8000

协议 ✓   域名 ✓   端口 ✗ (5500 vs 8000)
       ↓ 不同源!

浏览器默认禁止跨源请求

CSRF 防护的一部分:如果允许任意网站调你的 API,你登录的 cookie 会被恶意网站利用

浏览器: "API,前端 http://127.0.0.1:5500 想调你,你同意吗?"
        ↓
后端(CORS): "看 allow_origins 配置..."
        ↓
        ↓ 看到 ["*"] (允许所有)
        ↓
后端: "同意,我在响应头加 Access-Control-Allow-Origin: *"
        ↓
浏览器: "后端同意,放行!"

CORS 中间件代码详解

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],   # 允许哪些源
    allow_credentials=True, # 是否允许带认证
    allow_methods=["*"],   # 允许哪些 HTTP 方法
    allow_headers=["*"],   # 允许哪些请求头
)
配置 作用 生产环境应该
allow_origins=["*"] 允许任意源 ["https://yourdomain.com"]
allow_credentials=True 允许带 cookie 按需
allow_methods=["*"] 允许所有方法 ["GET", "POST"]
allow_headers=["*"] 允许所有头 按需

⚠️ 生产环境警告:allow_origins=["*"] + allow_credentials=True 不能同时用!这是浏览器安全策略。生产要写具体域名。

中间件是什么?

中间件 = 拦截器,所有请求/响应都先经过它。

请求进来 → CORS 中间件(加响应头) → 路由处理 → 响应回去

就像快递站的"安检"——所有包裹都要过一遍。


三、fetch API:浏览器发请求的方式

fetch 基础

const response = await fetch('http://127.0.0.1:8000/ports')
//                       ↑ URL                   ↑ await 等待响应

fetch 返回一个 Response 对象,不是直接的数据

async/await 异步

// ❌ 错误写法(同步)
const response = fetch('/api/ports')  // 返回 Promise 对象
const data = response.json()         // 报错:response 不是 Promise
// ✅ 正确写法(异步)
const response = await fetch('/api/ports')  // 等响应回来
const data = await response.json()          // 等 JSON 解析

为什么需要 async/await?

fetch异步操作(网络请求需要时间)。如果同步执行,代码会"卡住"等响应。async/await 让代码:

  • 像同步代码一样易读
  • 实际是异步执行(不卡 UI)

完整 fetch 模板

async function loadPorts() {
    try {
        const response = await fetch(API_BASE + '/ports')
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`)
        }
        const data = await response.json()
        return data
    } catch (error) {
        console.error('加载失败:', error)
        // 显示错误给用户
        return []
    }
}

关键点:

  • try/catch:网络错误(断网、404、CORS)都会被 catch
  • response.ok:HTTP 状态码 200-299 为 true
  • await response.json():把 JSON 字符串解析成 JavaScript 对象

四、JS 文件分离:模式 1

模式 0 vs 模式 1

模式 0(JS 写在 HTML 里):

<script>
    // 100 行 JS 代码
</script>

模式 1(JS 分离):

<script < src="js/main.js"></script>  <!-- 外部文件 -->

为什么分离

模式 0 模式 1
HTML 文件
浏览器缓存 ❌ 不缓存 缓存 JS,二次访问快
多页面共用 ❌ 每个 HTML 复制 多个 HTML 引同一 JS
调试 HTML/CSS/JS 混在一起 JS 独立,易调试

多页面共用(我们的 D10 用到)

D10 我创建 js/port-detail.js,3 个详情页共用:

<!-- xiuying.html / xinhai.html / tielunan.html 都加这一行 -->
<script src="js/port-detail.js"></script>

改一次 JS,3 个页面同时生效


五、index.html 实战改造(D9)

改造前(硬编码)

<div class="ports">
    <a class="port" href="xiuying.html">...</a>
    <a class="port" href="xinhai.html">...</a>
    <a class="port" href="tielunan.html">...</a>
</div>

改造后(动态加载)

HTML 主体:

<div class="ports" id="ports-container">
    <!-- JS 会动态填充 -->
</div>

JS 部分:

const API_BASE = 'http://127.0.0.1:8000'

// id → 子页面映射
const PORT_PAGE_MAP = {
    1: 'xiuying.html',
    2: 'xinhai.html',
    3: 'tielunan.html',
}

// 加载港口列表
async function loadPorts() {
    try {
        const response = await fetch(`${API_BASE}/ports`)
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        const ports = await response.json()
        
        renderPorts(ports)
    } catch (error) {
        console.error('加载港口失败:', error)
        // 显示错误
    }
}

// 渲染港口列表
function renderPorts(ports) {
    const container = document.getElementById('ports-container')
    container.innerHTML = ports.map(port => `
        <a class="port" href="${PORT_PAGE_MAP[port.id] || '#'}">
            <div class="port_image">
                <img src="${port.image_url || 'images/xiuying0.jpg'}" alt="${port.name}">
            </div>
            <div class="port_text">
                <span class="port_title">${port.name}</span>
                <span class="port_address">${port.location}</span>
            </div>
        </a>
    `).join('')
}

// 页面加载时执行
loadPorts()

关键步骤讲解

1. 找容器

const container = document.getElementById('ports-container')

getElementById('xxx') 找 HTML 里 id="xxx" 的元素。

2. 生成 HTML 字符串

ports.map(port => `<a>${port.name}</a>`).join('')
  • ports.map(...):遍历每个港口,生成 HTML 片段
  • .join('')'):把多个片段连起来(否则是数组)

3. 把 HTML 塞进容器

container.innerHTML = '...'

innerHTML 会替换元素内部所有内容。浏览器看到 DOM 变了 → 重新渲染。


六、Network 标签观察请求(关键调试技能)

打开浏览器 F12 → Network 标签,刷新页面,你会看到:

字段 含义
Name 请求名(如 ports)
Status HTTP 状态码(200/404/500)
Type 类型(fetch / css / js / img)
Initiator 谁发起的请求
Size 响应大小

点击某个请求看详细信息:

字段 含义
Request URL 完整 URL
Request Method GET / POST
Status Code 200 OK
Remote Address 服务器地址
Origin 来源页面(http://127.0.0.1:5500)
Sec-Fetch-Mode cors 表示跨源请求

🎯 关键观察:看到 Sec-Fetch-Mode: cors + 后端 CORS 放行 → 跨源成功!


七、动态验证:改数据库 → 刷新页面 ⭐

这是见证 v1.0 真正打通的步骤:

  1. Apifox 改数据库:

    PUT http://127.0.0.1:8000/ports/1
    Body: {"name": "秀英港(测试版)"}
    
  2. 刷新 index.html

  3. 看到港口名变了!

这就证明了:

  • 前端真的从后端拿数据(不是硬编码)
  • 后端真的从数据库读数据(不是内存)
  • 三者联动:浏览器 → uvicorn → SQLAlchemy → SQLite → ports.db → 数据回传 → 渲染

八、我踩过的坑(D9)

坑 1:fetch 没调用就跑了

症状:页面打开,fetch 没执行,console 没报错。

原因:loadPorts() 没在末尾调用,只在函数定义。

修复:在 <script> 末尾加 loadPorts()

教训:函数定义 ≠ 函数调用

坑 2:CORS 报错

症状:

Access to fetch at 'http://127.0.0.1:8000/ports' from origin
'http://127.0.0.1:5500' has been blocked by CORS policy

原因:后端没装 CORS 中间件。

修复:在 main.pyapp.add_middleware(CORSMiddleware, ...)

坑 3:数据库表缺字段

症状:fetch 返回 500,后端日志报 no such column: ports.image_url

原因:D9 加了 image_url 字段,但旧数据库没这列。

修复:删除 ports.db 重启 uvicorn(SQLAlchemy 会自动重建)。

坑 4:uvicorn 启动报错 src\backend\__init__.py

症状:uv add sqlalchemy 报这个错。

原因:pyproject.toml 里 [build-system] 让 uv 试图 build 当前项目。

修复:移除 [build-system][project.scripts]


九、HTML / CSS / JS 互动原理

三者的角色

类比 做什么
HTML 房子骨架 定义元素
CSS 房子装修 定义样式
JS 房子水电 让它动起来

JS 修改 HTML 的核心 API

// 1. 找元素
const el = document.getElementById('ports-container')

// 2. 改内容
el.innerHTML = '<a>新内容</a>'  // 替换
el.textContent = '纯文本'       // 只改文本

// 3. 改样式
el.style.color = 'red'
el.classList.add('active')

// 4. 加元素
el.appendChild(newElement)

// 5. 监听事件
el.addEventListener('click', () => { ... })

我们的 D9 流程

1. 浏览器加载 HTML
2. 看到 <script> → 执行 JS
3. JS 末尾调用 loadPorts()
4. fetch 发请求到后端
5. 后端从数据库读数据 → 返回 JSON
6. JS 拿到 JSON → 生成 HTML 字符串
7. container.innerHTML = 字符串
8. 浏览器看到 DOM 变了 → 重新渲染
9. 用户看到港口卡片

十、主流的"前后端互动"模式

方式 代码量 学习曲线 主流程度
原生 JS + fetch(我们 D9) 平缓 ⭐⭐⭐
jQuery + $.ajax(老项目) 平缓 ⭐⭐
Vue 3 + fetch(现代) 较陡 ⭐⭐⭐⭐⭐
React + fetch ⭐⭐⭐⭐⭐

我们当前用原生 JS,后续会升级到 Vue 3(主流)。


十一、D9 的关键收获

  1. CORS 是前后端分离的必修课——不解决就调不通
  2. fetch + async/await 是浏览器发请求的标准方式
  3. JS 分离让多页面共用代码,浏览器缓存提速
  4. Network 标签是调试前后端联调的关键工具
  5. 改数据库 + 刷新页面 = 真正的"全栈联动"

下一步

D9 我有了第一个动态化的页面(index.html)。D10 我会:

  • 创建共享 JS 文件 js/port-detail.js
  • 改造 3 个港口详情页(xiuying / xinhai / tielunan)
  • 3 个页面共用 1 个 JS 文件(模式 1 的精髓)

D11 我会:

  • 改造 platform.html 大屏
  • 加"数据状态条"显示实时港口数据
  • v1.0 全部完成!

参考资料

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