从 fetch 到 async/await:前后端分离与 LLM API 调用的完整异步实战
前后端分离初探:用 fetch + async/await 从后端拿数据渲染到页面
一、今天做的是什么
一个很小的项目:页面上有一张表格,数据不是写死在 HTML 里的,而是从后端接口请求来的,请求回来后用 JS 动态渲染进去。
[浏览器] → HTTP 请求 → [json-server 本地后端]
↓
返回 JSON 数据
↓
JS 渲染到表格里
就这么一条链路,但把前后端分离、异步编程、HTTP 协议几个概念都串起来了。
二、先把后端跑起来
项目里没有真实的后端服务,用 json-server 代替——它能把一个 JSON 文件直接变成一个可以请求的 REST API 接口,零配置,适合学习和原型开发。
package.json 里配了启动命令:
{
"scripts": {
"start": "json-server demo1/backend/data.json --port 3000"
},
"dependencies": {
"json-server": "1.0.0-beta.15"
}
}
data.json 就是"数据库":
{
"friends": [
{ "id": 1, "name": "张三" },
{ "id": 2, "name": "李四" }
]
}
终端里执行 npm start,json-server 启动,自动生成接口:
http://localhost:3000/friends → 返回 friends 数组
这个地址就是今天的 endpoint(接口终点)。
三、HTTP 请求由什么组成
发一条 HTTP 请求,本质上是向某个地址发一封"信",这封信有固定的格式,三个部分:
3.1 请求行
POST https://api.deepseek.com/chat/completions HTTP/1.1
包含三样东西:请求方法(GET/POST/PUT/DELETE)、URL、HTTP 版本号。
常用的请求方法:
| 方法 | 用途 |
|---|---|
GET |
取数据,不带请求体 |
POST |
提交数据,带请求体 |
PUT |
更新数据 |
DELETE |
删除数据 |
今天从 json-server 取数据用 GET,调用 DeepSeek 用 POST(要传 messages)。
3.2 请求头(Headers)
请求头是附加信息,告诉服务器"这封信的格式是什么、身份是谁":
const headers = {
'Content-Type': 'application/json', // 告诉服务器请求体是 JSON 格式
'Authorization': `Bearer ${apiKey}`, // 身份验证,带上 API Key
}
Content-Type 是最常见的一个——不声明格式,服务器可能不知道怎么解析请求体。
Authorization: Bearer xxx 是 API 鉴权的标准格式,Bearer 是固定前缀,后面跟实际的 key。
3.3 请求体(Body / Payload)
GET 请求没有请求体,POST 请求需要带数据。
关键点:HTTP 传输的是字符串,不能直接传 JS 对象。所以需要 JSON.stringify() 把对象序列化成字符串再发出去,服务器收到后再反序列化回来。
const payload = {
model: 'deepseek-v4-pro',
messages: [
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: '你好' }
]
}
body: JSON.stringify(payload) // 对象 → JSON 字符串
收到响应后反向操作:
const data = await response.json() // JSON 字符串 → JS 对象
四、async/await:把异步代码写得像同步
4.1 为什么需要它
fetch 是异步操作,发出请求后不会停在那里等,而是继续往下执行。如果不处理这个问题:
// 错误示范
const res = fetch('http://localhost:3000/friends')
console.log(res) // 打印出来是 Promise 对象,不是数据
renderData() // 这时候数据还没回来,渲染个寂寞
数据还没回来,渲染就已经跑完了,页面是空的。
4.2 async/await 的解法
async function loadData() {
const endpoint = 'http://localhost:3000/friends'
const res = await fetch(endpoint) // 等待请求完成
const data = await res.json() // 等待解析完成
friends = data
}
await 的作用是:暂停当前函数的执行,等这个异步操作完成,再继续往下走。
async 是前提——只有被 async 标记的函数里才能用 await。
4.3 执行顺序保证
async function init() {
console.log('init start')
await loadData() // 等数据加载完
renderData() // 数据到位了再渲染
}
await loadData() 保证了 renderData() 一定在数据拿到之后才执行。这是今天最核心的控制逻辑。
如果去掉 await:
async function init() {
loadData() // 发出请求,但不等它
renderData() // 立刻执行,friends 还是空数组
}
渲染就会失败,因为 friends 还没被赋值。
4.4 Promise 与 async/await 的关系
async/await 是 Promise 的语法糖,两种写法等价:
// Promise 链式写法
fetch(endpoint)
.then(res => res.json())
.then(data => {
friends = data
renderData()
})
// async/await 写法(更清晰)
const res = await fetch(endpoint)
const data = await res.json()
friends = data
renderData()
两者结果完全一样,但 async/await 读起来更接近同步代码的思路,逻辑更直观。
五、数据渲染:模板字符串 + map
拿到数据之后,把它动态写进 HTML 表格:
function renderData() {
const oBody = document.querySelector('table tbody')
if (friends.length > 0) {
oBody.innerHTML = friends.map(friend => `
<tr>
<td>${friend.id}</td>
<td>${friend.name}</td>
</tr>
`).join('')
}
}
拆开来看:
friends.map():遍历数组,把每个 friend 对象转换成一段 HTML 字符串。
模板字符串(反引号包裹):用 ${} 直接嵌入变量,比字符串拼接干净得多。
.join(''):map 返回的是字符串数组,join('') 把它们拼成一整段 HTML,不加分隔符。
innerHTML:把拼好的 HTML 字符串直接写入 DOM,浏览器自动解析渲染。
整个数据流是:
JSON 数组 → .map() 转换 → HTML 字符串数组 → .join() 合并 → innerHTML 渲染
六、延伸:调用 LLM API 是同一套逻辑
今天还写了调用 DeepSeek API 的代码,结构和从 json-server 取数据完全一样,只是细节不同:
const endpoint = 'https://api.deepseek.com/chat/completions'
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
}
const payload = {
model: 'deepseek-v4-pro',
messages: [
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: '你好' }
]
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(payload)
})
const data = await response.json()
document.getElementById('reply').innerHTML = data.choices[0].message.content
} catch (error) {
console.error('请求失败:', error)
}
和 json-server 的区别:
| 对比项 | json-server(本地) | DeepSeek API(远程) |
|---|---|---|
| 请求方法 | GET | POST |
| 请求头 | 无需额外配置 | 需要 Content-Type + Authorization |
| 请求体 | 无 | JSON 序列化的 messages |
| 响应取值 | data[0].name |
data.choices[0].message.content |
本质上是同一套 HTTP 请求模式,换了个地址和参数而已。
七、几个容易混的概念
IP 地址 vs 域名:127.0.0.1 是本机的 IP 地址,localhost 是它的域名别名,两者等价。访问 www.baidu.com 时,DNS 服务器会把域名解析成对应的 IP 地址,再去建立连接。
端口号:同一台机器上可以跑很多服务,端口号用来区分它们。http://127.0.0.1:3000 里的 3000 就是端口号,json-server 默认监听这个端口。
endpoint:就是 API 请求的终点 URL,通常在代码里单独命名为 endpoint 或 baseURL,方便统一管理和替换。
try/catch:网络请求可能因为各种原因失败(网络断开、服务器挂了、响应超时),用 try/catch 捕获异常,避免程序直接崩掉。
八、总结
今天的核心链路:
json-server 启动本地 API
↓
fetch 发出 GET 请求
↓
await 等待响应
↓
res.json() 解析响应体
↓
map + 模板字符串 构建 HTML
↓
innerHTML 渲染到页面
几条值得记住的原则:
HTTP 只传字符串,JS 对象进出都要经过 JSON.stringify 和 response.json() 转换。
async/await 的本质是控制顺序,渲染必须等数据回来,顺序错了页面就是空的。
LLM API 和普通接口没有本质区别,都是 HTTP 请求,只是 endpoint、headers、body 的内容不同。
前后端分离的核心思想:前端只负责展示,数据从后端接口取,两者通过 HTTP 协议通信,互不依赖。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)