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)都会被 catchresponse.ok:HTTP 状态码 200-299 为 trueawait 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 真正打通的步骤:
-
Apifox 改数据库:
PUT http://127.0.0.1:8000/ports/1 Body: {"name": "秀英港(测试版)"} -
刷新 index.html
-
看到港口名变了! ⭐
这就证明了:
- 前端真的从后端拿数据(不是硬编码)
- 后端真的从数据库读数据(不是内存)
- 三者联动:浏览器 → 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.py 加 app.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 的关键收获
- CORS 是前后端分离的必修课——不解决就调不通
- fetch + async/await 是浏览器发请求的标准方式
- JS 分离让多页面共用代码,浏览器缓存提速
- Network 标签是调试前后端联调的关键工具
- 改数据库 + 刷新页面 = 真正的"全栈联动"
下一步
D9 我有了第一个动态化的页面(index.html)。D10 我会:
- 创建共享 JS 文件
js/port-detail.js - 改造 3 个港口详情页(xiuying / xinhai / tielunan)
- 3 个页面共用 1 个 JS 文件(模式 1 的精髓)
D11 我会:
- 改造 platform.html 大屏
- 加"数据状态条"显示实时港口数据
- v1.0 全部完成!
参考资料
- MDN - Fetch API:https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API
- MDN - CORS:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
- FastAPI CORS 文档:https://fastapi.tiangolo.com/tutorial/cors/
- 《JavaScript 高级程序设计》第 23 章(网络请求)
- freeCodeCamp 中文:https://chinese.freecodecamp.org/

浙公网安备 33010602011771号