Skip to content

异步与网络请求

JavaScript 里很多操作不是立刻完成的——setTimeout 要等几秒、fetch 要等服务器响应、读文件要等磁盘。这些"要等的操作"叫异步操作。同步代码是一行做完才做下一行,异步代码发出去之后不傻等,先去做别的,等结果回来再处理。

这篇覆盖两块:先弄懂 Promise 和 async/await 怎么处理"要等的操作",再用 fetch 调远程 API——fetch 就是前端跟后端通信的方式,后端的接口就是给 fetch 调的

一、同步和异步

先看同步代码——一行做完才做下一行:

js
console.log("第一步");
console.log("第二步");
console.log("第三步");
// 输出顺序:第一步 → 第二步 → 第三步

再看异步——setTimeout 设定 2 秒后执行,但 JS 不会干等 2 秒:

js
console.log("第一步");
setTimeout(() => console.log("第二步(2秒后)"), 2000);
console.log("第三步");
// 输出顺序:第一步 → 第三步 → (2秒后)第二步

setTimeout 注册了回调就返回了,JS 继续执行第三步。2 秒到了,回调才执行。"第二步"被丢到队列里,不阻塞后面代码——这就是异步。

二、Promise——异步操作的结果占位符

setTimeout 用回调函数处理结果,操作多了就层层嵌套——回调里套回调,代码越来越深,排查也痛苦。Promise 是另一种处理异步的方式:它代表一个"现在还没出结果,但将来会有"的值

js
// 创建一个 Promise
const promise = new Promise((resolve, reject) => {
    const success = true;

    if (success) {
        resolve("操作成功");    // 成功时调用 resolve,传结果
    } else {
        reject("操作失败");     // 失败时调用 reject,传错误
    }
});

resolvereject 是两个函数——成功调 resolve,失败调 reject

.then().catch() 接收结果:

js
promise
    .then((result) => {
        console.log(result);  // 操作成功
    })
    .catch((error) => {
        console.log(error);   // 操作失败(成功时不会走到这)
    });

.then 接收 resolve 传的结果,.catch 接收 reject 传的错误。链式调用可以串起多个步骤:

js
fetchArticle(1)
    .then((article) => {
        console.log(article.title);
        return fetchComments(article.id);  // 返回新的 Promise
    })
    .then((comments) => {
        console.log(comments);
    })
    .catch((error) => {
        console.log("出错了:", error);  // 任何一步出错都会走到这
    });

每个 .then 返回的值会传给下一个 .then。返回新的 Promise 时,下一个 .then 会等它完成。任何一步出错,都会跳到 .catch

三、async/await——用同步的写法写异步

.then() 链长了之后缩进和可读性都不理想。async/await 是 Promise 的语法糖——让异步代码看起来像同步代码,更好读更好写。

js
async function loadArticle(id) {
    try {
        const article = await fetchArticle(id);   // await 等 Promise 完成
        console.log(article.title);

        const comments = await fetchComments(article.id);
        console.log(comments);
    } catch (error) {
        console.log("出错了:", error);            // 跟 .catch 一样,任何 await 出错都走到这
    }
}

loadArticle(1);

几个要点:

  • async 写在函数声明前面,表示这个函数里有异步操作
  • await 只能在 async 函数里用,它等 Promise 完成再继续往下
  • try/catch 替代 .catch(),跟同步代码的错误处理写法一致
  • await 后面跟一个 Promise,拿到的是 resolve 传的值

async/await.then() 做的事完全一样,只是写法不同。新代码优先用 async/await,可读性明显更好。

四、fetch——请求后端 API

fetch 是浏览器内置的 HTTP 请求函数。它返回一个 Promise,天然配合 async/await前端调后端接口,基本都是用 fetch

1 GET 请求

js
async function loadArticles() {
    try {
        const response = await fetch("http://localhost:8000/api/articles");

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        const articles = await response.json();  // 把响应体解析成 JSON
        console.log(articles);
    } catch (error) {
        console.log("请求失败:", error);
    }
}

loadArticles();

fetch 返回的不是数据本身,是一个 Response 对象。要拿数据得调 response.json(),它把响应体从 JSON 文本解析成 JS 对象——这一步也是异步的,所以也要 await

response.ok 判断 HTTP 状态码是不是 2xx(200-299)。不是 2xx 时 fetch 不会自动抛异常(只有网络不通才抛),要自己判断 response.ok 然后手动 throw

2 POST 请求

提交数据用 POST,要传 methodheadersbody

js
async function createArticle(data) {
    const response = await fetch("http://localhost:8000/api/articles", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(data),  // JS 对象转成 JSON 字符串
    });

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
}

createArticle({ title: "新文章", content: "正文内容" })
    .then((result) => console.log("创建成功", result))
    .catch((error) => console.log("创建失败", error));

JSON.stringify 把 JS 对象转成 JSON 字符串——fetchbody 只接受字符串。Content-Type: application/json 告诉后端"我发的是 JSON 格式"。

3 带认证 Token

需要登录的接口通常要求在请求头里带 Token:

js
async function fetchWithAuth(url) {
    const token = localStorage.getItem("access_token");  // 从浏览器存储读 Token

    const response = await fetch(url, {
        headers: {
            Authorization: `Bearer ${token}`,  // Token 放在 Authorization 头
        },
    });

    if (response.status === 401) {
        // 401 说明 Token 过期或没带,跳登录页
        console.log("未登录或 Token 过期");
        return null;
    }

    return response.json();
}

localStorage 是浏览器提供的本地存储——登录成功后把 Token 存进去,后续请求从里面取。Bearer ${token} 是最常见的 Token 传递方式,后端会从 Authorization 头里解析 Token。

五、封装请求函数

每次写 fetch 都要处理 response.okresponse.json()、错误处理,重复且容易漏。封装成一个函数,所有页面共用

js
const BASE_URL = "http://localhost:8000/api";

async function request(path, options = {}) {
    const token = localStorage.getItem("access_token");

    const response = await fetch(`${BASE_URL}${path}`, {
        ...options,
        headers: {
            "Content-Type": "application/json",
            ...(token ? { Authorization: `Bearer ${token}` } : {}),
            ...options.headers,
        },
    });

    if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.detail || `HTTP ${response.status}`);
    }

    return response.json();
}

用起来就简单了:

js
// GET
const articles = await request("/articles");

// POST
const result = await request("/articles", {
    method: "POST",
    body: JSON.stringify({ title: "新文章" }),
});

// DELETE
await request("/articles/1", { method: "DELETE" });

这个封装在后端基础篇的项目实战里会直接用——前端调用 request("/articles"),后端提供 /api/articles 接口,两边就这样连上了

六、跨域和 CORS

浏览器有个安全限制叫同源策略:JS 默认只能请求跟当前页面同协议、同域名、同端口的地址。前端跑在 localhost:5173,后端跑在 localhost:8000,端口不一样就算跨域,浏览器会拦住请求。

js
// 前端在 localhost:5173
fetch("http://localhost:8000/api/articles");
// 浏览器控制台报错:CORS policy: No 'Access-Control-Allow-Origin'

这个限制是浏览器加的,不是网络层的问题——请求其实发出去了,后端也响应了,但浏览器发现后端没说"我允许你跨域",就把响应拦住了。

解决方法在后端:FastAPI 里配置 CORS 中间件,告诉浏览器"我允许这些来源访问"。这个在后端进阶的 CORS 篇里会详细配。

跨域只在浏览器里有——用 curl、Python requests、Node.js 发请求都不会遇到 CORS。这是前端开发特有的"坑",知道根因在后端配置就行