☰
FastAPI与原生JS前后端分离:电脑组装报价工具实战指南
2026/9/30 12:51:31 网站建设 项目流程

FastAPI + 原生 JS 做一套电脑组装报价指南

作为一个平时主要写后端接口的开发者,我很少碰前端,但前段时间朋友接了个活,帮一家电脑店做"选配件自动出报价"的小工具。店里的人不懂技术,就想打开网页,勾选CPU、主板、显卡,右边就实时显示总价。

接到这个需求,我第一反应是:别上Vue、React那一套重型框架,也别引入Webpack、Vite这些构建工具。直接一点,用FastAPI做后端接口,前端用原生JavaScript写,一个仓库两个目录,把这套前后端分离的小项目跑通。

这篇文章我把整个项目的设计思路、关键实现、踩坑过程完整写下来。如果你也想做一个前后端分离的小工具,或者正在学FastAPI和原生JS怎么配合,这篇应该能给你一个能直接抄作业的参考。

项目本身不复杂,核心就两块:

  • 后端维护一份配件库(CPU、主板、内存、显卡、固态、电源、机箱),通过接口暴露数据,根据用户提交的配置生成报价单。
  • 前端是几个下拉框加一个汇总区域,用户选完配件,页面实时计算总价,最后可以生成一张简单的报价单。

别看功能简单,这里面涵盖了前后端分离项目最常见的问题:跨域、异步请求顺序、状态管理、数据校验、部署路径。把这些东西全部跑通之后,你再去套Vue、React,会发现心里特别有底。

1. 为什么用 FastAPI 加原生 JS 做组装报价

1.1 选题背后的真实需求

电脑组装报价这个场景,我在网上调研了一圈,发现大多数现成方案要么是纯静态页面写死配件价格,要么是Excel表格手工报价。纯静态页面改一次价格就得改HTML,Excel表格没法让顾客自助查询。

朋友要的东西,本质上是一个"数据与展示分离"的小系统:

  • 配件数据存在后端,随时可以增删改。
  • 前端只是读取数据做展示和计算。
  • 顾客在浏览器里操作,电脑店不需要安装任何软件。

这就天然是一个前后端分离的架构模型。前后端分离这个词听着玄乎,落到这个项目里就是两个独立的部分:FastAPI跑在8000端口提供JSON数据,前端页面用fetch去拿数据。中间通过HTTP协议通信,两边约定好JSON格式就行,不需要任何复杂的框架来支撑。

1.2 技术选型对比

选型的时候,我列了一张对比清单,把几套常见方案都过了一遍:

  • Spring Boot + Vue:功能全,上手成本高,适合中大型团队,一个人快速交付的场景用它有点杀鸡用牛刀。
  • 若依框架:自带权限、菜单、代码生成,功能确实全,但引入它之后你要处理的东西比业务本身还多,不划算。
  • Flask + 原生JS:Flask能用,但异步支持和数据校验手感不如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

数据流是这样的:

  1. 用户在页面上选择CPU、主板、显卡等配件。
  2. 前端把所有选中配件的ID收集起来,通过fetch发送到后端的报价接口。
  3. 后端根据ID查配件库,累加价格,返回报价明细和总价。
  4. 前端把结果渲染到页面。

这里有个关键决定:总价计算必须放在后端,不能让前端自己算。

原因是价格、库存、兼容性这些逻辑应该由后端统一控制。如果前端也存一份价格数据,改价的时候就得改两个地方,今天店长调了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_code=404, detail=f"{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里挂上CORSMiddleware:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, 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 11,Python 3.11。前端就是两个静态文件,不需要任何构建工具。

虚拟环境操作步骤:

cd backend python -m venv venv venv\Scripts\activate pip install fastapi uvicorn

requirements.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里配置了CORS,allow_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", html=True), name="frontend")

前端里的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%的跨域坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询