ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FastAPI与原生JS前后端分离:电脑组装报价工具实战指南

FastAPI与原生JS前后端分离:电脑组装报价工具实战指南 FastAPI 原生 JS 做一套电脑组装报价指南作为一个平时主要写后端接口的开发者我很少碰前端但前段时间朋友接了个活帮一家电脑店做选配件自动出报价的小工具。店里的人不懂技术就想打开网页勾选CPU、主板、显卡右边就实时显示总价。接到这个需求我第一反应是别上Vue、React那一套重型框架也别引入Webpack、Vite这些构建工具。直接一点用FastAPI做后端接口前端用原生JavaScript写一个仓库两个目录把这套前后端分离的小项目跑通。这篇文章我把整个项目的设计思路、关键实现、踩坑过程完整写下来。如果你也想做一个前后端分离的小工具或者正在学FastAPI和原生JS怎么配合这篇应该能给你一个能直接抄作业的参考。项目本身不复杂核心就两块后端维护一份配件库CPU、主板、内存、显卡、固态、电源、机箱通过接口暴露数据根据用户提交的配置生成报价单。前端是几个下拉框加一个汇总区域用户选完配件页面实时计算总价最后可以生成一张简单的报价单。别看功能简单这里面涵盖了前后端分离项目最常见的问题跨域、异步请求顺序、状态管理、数据校验、部署路径。把这些东西全部跑通之后你再去套Vue、React会发现心里特别有底。1. 为什么用 FastAPI 加原生 JS 做组装报价1.1 选题背后的真实需求电脑组装报价这个场景我在网上调研了一圈发现大多数现成方案要么是纯静态页面写死配件价格要么是Excel表格手工报价。纯静态页面改一次价格就得改HTMLExcel表格没法让顾客自助查询。朋友要的东西本质上是一个数据与展示分离的小系统配件数据存在后端随时可以增删改。前端只是读取数据做展示和计算。顾客在浏览器里操作电脑店不需要安装任何软件。这就天然是一个前后端分离的架构模型。前后端分离这个词听着玄乎落到这个项目里就是两个独立的部分FastAPI跑在8000端口提供JSON数据前端页面用fetch去拿数据。中间通过HTTP协议通信两边约定好JSON格式就行不需要任何复杂的框架来支撑。1.2 技术选型对比选型的时候我列了一张对比清单把几套常见方案都过了一遍Spring Boot Vue功能全上手成本高适合中大型团队一个人快速交付的场景用它有点杀鸡用牛刀。若依框架自带权限、菜单、代码生成功能确实全但引入它之后你要处理的东西比业务本身还多不划算。Flask 原生JSFlask能用但异步支持和数据校验手感不如FastAPI顺手。Gradio适合做AI演示工具不适合做这种带多交互表格的报价页面。FastAPI 原生JS最终选择原因在后面。这里顺带提一下很多人看到前后端分离第一个想到的就是Spring Boot配Vue或者若依这类脚手架。但我这个场景有一个核心特点业务逻辑极其简单数据量几十条用户量就是一个门店的店员加顾客并发基本可以忽略。这种项目上一个全家桶部署麻烦学习成本高后续维护反而更痛苦。1.3 为什么 FastAPI 在这个场景里合适FastAPI有几个很实际的优点让我确定就是它了自带数据校验和自动生成OpenAPI文档。前端联调的时候直接访问/docs页面就能看到每个接口的入参和出参格式省掉大量接口到底要传什么字段的扯皮。异步支持好接口响应快。我在本地实测下来接口延迟都在10ms以内用户操作时基本无感知。用Python的类型注解写Pydantic模型既能当请求参数校验模型又能当数据模型一套代码吃透减少重复定义。1.4 整体架构设计整个项目的目录结构我设计得比较朴素就一个仓库两个目录pc-price-guide/ ├── backend/ │ ├── main.py │ ├── data.py │ ├── models.py │ └── requirements.txt └── frontend/ ├── index.html ├── style.css └── app.js数据流是这样的用户在页面上选择CPU、主板、显卡等配件。前端把所有选中配件的ID收集起来通过fetch发送到后端的报价接口。后端根据ID查配件库累加价格返回报价明细和总价。前端把结果渲染到页面。这里有个关键决定总价计算必须放在后端不能让前端自己算。原因是价格、库存、兼容性这些逻辑应该由后端统一控制。如果前端也存一份价格数据改价的时候就得改两个地方今天店长调了CPU价格明天发现电脑上显示的还是旧价格迟早出问题。后端的配件库是唯一数据源前端只负责展示用户的选择。2. 后端设计FastAPI 怎么管好配件和报价2.1 数据模型设计配件数据是核心。设计模型时我犯过一个错误刚开始把所有配件混在一个列表里用type字段区分。后来发现行不通因为不同类型的配件属性差异太大——CPU要标核心数和主频显卡要标显存电源要标功率机箱要标类型。虚拟属性堆在一个模型里要么空着要么写一堆可选字段代码乱得没法看。后来我用Pydantic给每个配件类型建了独立模型from pydantic import BaseModel class CPU(BaseModel): id: int name: str price: int cores: int class Motherboard(BaseModel): id: int name: str price: int cpu_socket: str class GraphicsCard(BaseModel): id: int name: str price: int vram: int价格字段我特意用整数而不是浮点数。报价场景里价格都取整而且浮点数做累加会有精度问题。用整数算总价时绝对不会出现那种差几分钱的对不上的尴尬。2.2 接口路径怎么设计接口规划遵循一个简单原则资源用名词动作用HTTP方法。我设计了三个核心接口GET /api/parts/{part_type}获取某种类型的配件列表比如/api/parts/cpu返回所有CPU。GET /api/parts/{part_type}/{id}获取单个配件详情用于后续扩展。POST /api/quote提交用户选择的配件ID列表返回报价明细和总价。这里有一个取舍问题要不要做一个汇总接口/api/parts一次把所有类型全部返回一开始我觉得这样省事前端一次请求全搞定。真做之后发现不是那么回事。前端要在一堆数据里反复filter代码难读而且某个类型数据量大时响应也会变慢。拆成按类型区分的资源接口前端代码反而更清晰。2.3 报价计算逻辑报价接口的核心逻辑是后端接收一套ID列表逐一去配件库查找并累加价格app.post(/api/quote) async def create_quote(items: QuoteRequest): total 0 details [] for item in items.parts: part find_part(item.part_type, item.part_id) if part is None: raise HTTPException(status_code404, detailf{item.part_type} id{item.part_id} 不存在) total part.price details.append({ type: item.part_type, id: part.id, name: part.name, price: part.price }) return { total: total, details: details }这段代码看着简单里面有两个细节值得展开查找不到配件时必须返回明确错误不能跳过。否则用户选了个已经下架或者不再兼容的配件页面不知道最后总价少一截报价单就是废的。这个校验逻辑是报价系统的底线。入参用QuoteRequest模型校验class PartItem(BaseModel): part_type: str part_id: int class QuoteRequest(BaseModel): parts: list[PartItem]前端传错字段名后端直接返回422状态码和详细错误信息。联调的时候一眼就能看出问题。2.4 CORS 配置前后端分离项目里跨域是最容易让人头大的问题。我这个项目开发阶段的架构是前端页面运行在5500端口通过python http.server后端接口跑在8000端口。两个端口不一样浏览器就会做跨域限制。处理方式很直接在FastAPI里挂上CORSMiddlewarefrom fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsFalse, allow_methods[*], allow_headers[*], )注意allow_origins用*在开发阶段没问题部署到生产环境就不能这么干了必须指定具体域名。这个我在后面部署时栽了跟头后文会细说。3. 前端实现原生 JS 维持前后端分离的边界3.1 页面结构设计前端不借助任何框架只用一个HTML文件、一个CSS文件、一个JS文件。页面结构上我分成三个区域左侧是配件选择区每种配件类型对应一个下拉框。右侧顶部是已选配件清单区域实时列出当前选择的所有配件。右侧底部是总价和生成报价单按钮。这个布局把选择和结果分开贴合店里接待顾客的实际场景顾客在左边挑店员和顾客一起盯着右边看总价。操作路径短顾客自己都能上手。关于要不要用Vue我也纠结过。Vue的响应式绑定在这个场景里确实好用但仔细想想这个页面没复杂到需要框架配件类型大概7种每种是一个下拉框。用户选完之后需要实时显示已选清单和总价。中间穿插联动规则比如选了AMD的CPU主板平台自动切到AM5。用原生JS完全能搞定而且不用引入框架就不用处理构建工具链整个项目可以在任何一台机器上直接打开调试。3.2 数据加载与渲染页面加载时通过fetch从后端拉配件数据async function loadParts() { const response await fetch(http://localhost:8000/api/parts/cpu); const data await response.json(); const select document.getElementById(cpu-select); data.forEach(part { const option document.createElement(option); option.value part.id; option.textContent ${part.name}${part.price}; select.appendChild(option); }); }这里有个小坑渲染文本时用textContent而不是innerHTML。原因很简单如果将来开放给店员维护配件数据难免有人输入一些奇奇怪怪的字符用textContent可以避免把用户输入当成HTML解析防止XSS注入。多写几行代码胜在安全。3.3 状态管理原生JS没有响应式系统如果不做统一管理代码会越来越乱。我的做法是维护一个全局状态对象let state { cpuId: null, motherboardId: null, gpuId: null, memoryId: null, storageId: null, powerId: null, caseId: null };每个下拉框的change事件只更新state里对应的字段然后调用一个统一的updateQuote()函数重新计算和渲染。所有交互逻辑都收敛到一个函数里不会出现改了这里的值还要去另一个地方手动同步的尴尬。这个做法的本质是单向数据流界面操作更新状态状态变化触发重新渲染。和Vue的v-model相比少了很多魔法层面上的自动追踪但逻辑完全可控哪里出问题直接定位到对应函数就行。3.4 价格联动与汇总用户每选择一个配件前端把state里所有的ID收集起来调用报价接口async function updateQuote() { const parts []; for (const [type, id] of Object.entries(state)) { if (id) { parts.push({ part_type: type, part_id: id }); } } const response await fetch(http://localhost:8000/api/quote, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ parts }) }); const result await response.json(); renderQuote(result.details, result.total); }这段代码里藏着一个绕不开的问题每次下拉框变化都会触发fetch用户连续改动时前端会同时发出好几个请求。如果不做控制后发的请求先返回先发的请求后返回最终显示的价格就是错的用户会觉得这个页面有bug。解决这个异步竞态问题我用了一个计数器加锁的方法let quoteRequestId 0; async function updateQuote() { const currentRequestId quoteRequestId; const response await fetch(...); const result await response.json(); if (currentRequestId ! quoteRequestId) return; renderQuote(result.details, result.total); }每次发起请求前先把当前请求号加一请求回来时检查这个请求号是不是最新的如果不是就直接丢弃。这个方法虽然简单但实测下来非常稳。连续快速切换十几个配件显示的总价始终是最终结果。3.5 兼容性联动报价工具里有个比较有意思的需求CPU和主板必须兼容。比如选Intel第14代CPU要配LGA1700接口的主板选AMD Ryzen 7000系列要配AM5接口的主板。这个逻辑放在后端还是前端我认真考虑过。最终决定在前端做过滤后端返回主板数据时带上cpu_socket字段前端根据已选CPU的socket类型过滤主板下拉框的选项。理由是交互友好。用户一旦选定CPU主板下拉框立刻收窄到兼容的型号不需要请求后端再计算一次也没有等待感。但要注意不能选只是软校验。真正的硬校验必须放在后端报价接口里如果前端传了不匹配的CPU和主板组合后端要拒绝这次报价。这是前后端分离项目里很重要的观念前端的过滤只是锦上添花后端的校验才是最后一道防线。前端可以被绕过后端不能被绕过。4. 实操过程与三个典型的坑4.1 搭建环境的完整步骤先交代一下我的本地环境Windows 11Python 3.11。前端就是两个静态文件不需要任何构建工具。虚拟环境操作步骤cd backend python -m venv venv venv\Scripts\activate pip install fastapi uvicornrequirements.txt内容fastapi uvicorn[standard]启动后端uvicorn main:app --reload --host 0.0.0.0 --port 8000--reload是开发时改代码自动重启--host 0.0.0.0是为了让局域网里其他设备也能访问接口方便手机测试。前端启动cd frontend python -m http.server 5500然后在浏览器访问http://localhost:5500。这就是标准的前后端分离开发模式两个进程两个端口通过HTTP通信。4.2 坑一CORS 导致的前端请求全部失败先说坑一也是最常见的坑CORS。最开始前端不通过HTTP服务启动我直接在浏览器里双击打开index.html。页面能正常打开但下拉框里一片空白控制台报错Access to fetch at http://localhost:8000/api/parts/cpu from origin null has been blocked by CORS policy大家注意这个origin null。当你以file://协议直接打开本地HTML文件时浏览器的Origin是字符串null不是http://localhost:5500。我当时已经在FastAPI里配置了CORSallow_origins写的是[http://localhost:5500]所以null这个来源被拦截了。解决办法有两种在allow_origins里加上null这个字符串。更规范的做法让前端也走HTTP服务设置与后端一致的来源。我选择了后者。这不仅仅是CORS能解决还避免了file协议下浏览器的各种安全限制。之后在浏览器访问http://localhost:5500后端CORS配置里也加上了这个端口问题迎刃而解。4.3 坑二接口入参格式不匹配第二个坑是前后端数据结构约定出了问题。前端最初提交的数据结构是这样的{ cpu: 101, motherboard: 202 }后端模型定义的是class QuoteRequest(BaseModel): parts: list[PartItem]前端传过去后后端直接返回422错误422 Unprocessable Entity这个报错对新手来说有点抽象翻译过来就是你传来的请求体和我约定的模型对不上。字段名不匹配、字段类型错误、缺字段都会触发这个错误。排查过程其实很简单打开FastAPI自动生成的/docs页面直接查看QuoteRequest模型的示例结构对照一看就发现前端少了parts这一层封装。后来我统一了数据结构前端把所有选中配件生成一个parts数组每个元素包含part_type和part_id。两边对着/docs页面确认无误后再开始联调。这一步省下来的时间远比当时扯皮的要多。4.4 坑三并发请求显示错乱第三个坑是被联动触发出来的。在实现CPU和主板兼容过滤后每次切换CPU前端会同时触发出两个请求获取主板列表因为CPU变了主板下拉框需要重新过滤。获取报价因为CPU变了总价可能变化。两个请求并发发出响应顺序并不固定。如果先发出的请求后返回页面就会被旧数据刷新总价显示乱跳。处理思路我在前面已经提了用requestId计数器。这个思路本质上是一个轻量级的取消机制不真正取消前一个请求而是忽略它的响应。原生JS的AbortController理论上能真正断开请求但在我这个场景里忽略响应更快更简单代码只多三行。实际测试效果很好连续快速切换CPU、GPU、内存十几下报价单内容和总价始终是最终状态没出现过一次错乱。4.5 联调阶段的经验前后端分离项目里联调是最容易出问题的地方。我的经验是接口文档先行。FastAPI自动生成/docs页面在/docs里能看到每个接口的入参模型、返回示例。每次前端改了请求格式后端改了返回结构都先去看一眼文档两边按照文档对齐不要凭记忆开发。我这次项目深有体会所谓前后端分离不只是部署分离更重要的是契约分离。JSON结构就是这个契约。契约清晰双方各做各的效率翻倍。5. 部署、优化和后续扩展5.1 部署方式本地调试跑通之后朋友要求把这个工具部署到店里的电脑上让店员通过浏览器访问不用每次都打开开发环境。部署方案我选了最省事的在FastAPI里挂载静态文件让前端也由后端统一服务。这样就没有跨域问题了因为前端和后端同源。from fastapi.staticfiles import StaticFiles app.mount(/, StaticFiles(directory../frontend, htmlTrue), namefrontend)前端里的API地址也要跟着改不能用写死的http://localhost:8000改成相对路径const API_BASE ; // 空字符串请求走同源这样请求/api/parts/cpu和/api/quote都会打到同一个域名和端口上。启动命令还是那个uvicorn main:app --host 0.0.0.0 --port 8000店员访问http://服务器IP:8000就能直接用。路由器在同一个局域网下手机也能访问。5.2 一个值得注意的部署陷阱部署时我踩了一个隐蔽的坑把allow_origins改成了具体域名后前端页面能打开但接口还是报跨域错误。排查了很久发现问题出在静态文件服务和API在同一个源上理论上不该有CORS但浏览器在 http://192.168.1.100:8000 访问时依然报错。后来仔细看请求原来是前端代码里还有一处写死了http://localhost:8000/api/quote的绝对地址。所以代码里不要混用绝对地址和相对地址。统一用相对路径后问题消失了。这也提醒我部署前要做一次地址替换审计全局搜索 localhost 和 127.0.0.1全部改成相对路径或者环境变量配置。5.3 项目目录结构心得经历过这些折腾后我对小型前后端分离项目的目录结构有了更清晰的认识pc-price-guide/ ├── backend/ │ ├── main.py # 入口路由和CORS配置 │ ├── models.py # Pydantic模型 │ ├── data.py # 配件数据库可替换成SQLite/MySQL │ └── requirements.txt ├── frontend/ │ ├── index.html │ ├── style.css │ └── app.js └── README.md有人可能会问要不要用SQLite存数据配件几十条数据每次启动从JSON文件读入内存其实就够用了。真要换SQLite或者MySQL只需要改动data.py里的查找函数接口层的逻辑一行都不用动。这就是把数据访问层独立出来的好处。5.4 后续还能加什么功能这个报价指南第一期做完后我又想到了几个可以扩展的方向添加价格区间约束用户设个预算超出时前端给出提示后端在报价时返回超预算标记。导出PDF报价单前端拿到报价JSON后通过打印模板生成一张带样式和店名的报价单。配件图片接入给每个配件加图片URL字段前端渲染时展示缩略图能明显提升门店洽谈效果。后台管理页面做一个简单的密码校验页面认证通过后可以增删改配件数据。用FastAPI的OAuth2密码流程就能实现。多套配置方案对比用户同时维护两三个配置方案并排对比性能和价格。这些扩展点都在当前架构上可以平滑地加进去后端加接口前端加区块不需要推翻重来。如果之后业务量变大想上Vue前端部分可以保留当前的核心逻辑只是把状态管理替换成Vue的响应式写法。后端完全不用动。这就是前后端分离换来的灵活性。就我个人这次实践而言最大的收获是用最快的速度把一个前后端分离的小项目完整跑通。FastAPI处理接口和数据校验原生JS处理交互和渲染中间用一份清晰的JSON约定完成沟通。没有框架和构建工具包袱反而把前端的请求是怎么到后端的接口返回的数据到底长什么样跨域到底怎么回事这些问题想得特别明白。如果你也是第一次自己动手做前后端分离的小项目我建议先学会用这套笨办法跑通再上手Vue、React也不迟。地基打扎实了上面盖什么楼都稳。另外有个小技巧本地开发时给前端页面也起一个HTTP服务不要直接双击打开HTML文件虽然听起来麻烦一点但能少踩90%的跨域坑。
返回列表