前后端分离初探:用 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)、URLHTTP 版本号

常用的请求方法:

方法 用途
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,通常在代码里单独命名为 endpointbaseURL,方便统一管理和替换。

try/catch:网络请求可能因为各种原因失败(网络断开、服务器挂了、响应超时),用 try/catch 捕获异常,避免程序直接崩掉。


八、总结

今天的核心链路:

json-server 启动本地 API
        ↓
fetch 发出 GET 请求
        ↓
await 等待响应
        ↓
res.json() 解析响应体
        ↓
map + 模板字符串 构建 HTML
        ↓
innerHTML 渲染到页面

几条值得记住的原则:

HTTP 只传字符串,JS 对象进出都要经过 JSON.stringifyresponse.json() 转换。

async/await 的本质是控制顺序,渲染必须等数据回来,顺序错了页面就是空的。

LLM API 和普通接口没有本质区别,都是 HTTP 请求,只是 endpoint、headers、body 的内容不同。

前后端分离的核心思想:前端只负责展示,数据从后端接口取,两者通过 HTTP 协议通信,互不依赖。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐