news 2026/4/18 4:59:51

前端新人必学:手把手封装 fetch,告别重复请求代码(附实战技巧)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端新人必学:手把手封装 fetch,告别重复请求代码(附实战技巧)


前端新人必学:手把手封装 fetch,告别重复请求代码(附实战技巧)

  • 前端新人必学:手把手封装 fetch,告别重复请求代码(附实战技巧)
    • 为什么每次写接口都要复制粘贴?看看你项目里是不是一堆重复的 fetch 逻辑
    • fetch 是什么,又不是什么
    • 从零开始封装一个实用的 fetch 工具
      • 1. 统一配置
      • 2. 序列化 QueryString
      • 3. 核心函数 `request`
      • 4. 工具函数
      • 5. 顺手导出 get/post 等快捷方法
    • 让错误处理不再头疼
    • 拦截器机制怎么加进去
    • 支持取消请求和防重发
    • 自动重试与降级策略
    • 开发调试神器:请求日志与 Mock 支持
    • 真实项目中的封装姿势
      • 1. 与 TypeScript 甜蜜双排
      • 2. 环境变量与多基址
      • 3. 模块拆分
    • 遇到“请求发不出去”怎么办
    • 进阶技巧:缓存策略与性能优化
    • 别再裸奔了,给你的 fetch 加点“盔甲”
    • 封装完别急着提交,先测一测
    • 你以为封装完了?其实才刚开始

前端新人必学:手把手封装 fetch,告别重复请求代码(附实战技巧)

警告:本文会让你的fetch代码从“能跑”进化到“能炫”,请自备咖啡和键盘,阅读过程中若忍不住ctrl+c/ctrl+v,作者概不负责。


为什么每次写接口都要复制粘贴?看看你项目里是不是一堆重复的 fetch 逻辑

先别急着否认,打开你最近的services文件夹,大概率能看到这样的“奇观”:

// user.jsexportfunctiongetUser(id){returnfetch(`/api/user/${id}`,{headers:{'Content-Type':'application/json',Authorization:`Bearer${localStorage.token}`}}).then(res=>res.json())}// order.jsexportfunctiongetOrder(id){returnfetch(`/api/order/${id}`,{headers:{'Content-Type':'application/json',Authorization:`Bearer${localStorage.token}`}}).then(res=>res.json())}

这两段代码像失散多年的双胞胎,除了 URL 不一样,其他连空格都懒得换。需求一改——“超时时间改成 8 秒”、“错误码 401 要跳登录”、“所有请求加个Request-ID”——你就得全局搜索替换,一不小心就把同事的代码给“误杀”。

痛定思痛,咱们来造一个“万能胶”:一次封装,终身受用,后续只写业务,不写废话。


fetch 是什么,又不是什么

fetch当成“原生提供的快递小哥”:

  • ✅ 免费、浏览器自带、支持 Promise、基于标准
  • ❌ 不会自动带 cookie(默认same-origin)、不会自动序列化对象、不会默认超时、不会自动根据状态码抛异常、不支持请求进度监控、不支持 Node(18 以前)

而 axios 像“顺丰”:包装好、功能全、拦截器、超时、自动 JSON、错误处理一条龙,但体积 +13 kB(gzip 后约)。
XMLHttpRequest 则是“邮政包裹”:老派、啰嗦、回调地狱,现在基本退居二线。

结论:
“小哥”虽然青涩,但胜在轻量、可控、可塑性强;自己给它配辆电动车(封装一层),就能媲美顺丰,还不用给快递费。


从零开始封装一个实用的 fetch 工具

先搭骨架,再填血肉。我们起名request.js,放在src/utils下,日后所有接口都靠它刷脸。

1. 统一配置

constDEFAULT_CONFIG={baseURL:'',timeout:6000,headers:{'Content-Type':'application/json'}}

2. 序列化 QueryString

functionqsStringify(obj){returnnewURLSearchParams(obj).toString()}

3. 核心函数request

asyncfunctionrequest(url,options={}){// 1. 合并配置constconfig=mergeConfig(DEFAULT_CONFIG,options)// 2. 拼接 baseURLurl=config.baseURL+url// 3. 处理 paramsif(config.params){constsep=url.includes('?')?'&':'?'url+=sep+qsStringify(config.params)}// 4. 超时处理constcontroller=newAbortController()consttimer=setTimeout(()=>controller.abort(),config.timeout)// 5. 自动 JSON 化if(config.data&&typeofconfig.data==='object'){config.body=JSON.stringify(config.data)}try{constresponse=awaitfetch(url,{...config,signal:controller.signal})clearTimeout(timer)// 6. 自动反序列化constcontentType=response.headers.get('Content-Type')||''letdataif(contentType.includes('application/json')){data=awaitresponse.json()}else{data=awaitresponse.text()}// 7. 状态码校验if(!response.ok){returnPromise.reject(createError(response.status,data))}returndata}catch(err){clearTimeout(timer)// 网络错误 / 超时thrownormalizeNetworkError(err)}}

4. 工具函数

functionmergeConfig(def,opt){return{...def,...opt,headers:{...def.headers,...opt.headers}}}functioncreateError(code,data){consterr=newError(`Request failed with status${code}`)err.code=code err.data=datareturnerr}functionnormalizeNetworkError(err){if(err.name==='AbortError'){err.message='Request timeout'}returnerr}

5. 顺手导出 get/post 等快捷方法

exportconstget=(url,params,opts)=>request(url,{...opts,method:'GET',params})exportconstpost=(url,data,opts)=>request(url,{...opts,method:'POST',data})exportdefaultrequest

一行import { get, post } from '@/utils/request'就能到处浪。


让错误处理不再头疼

后端返回格式千奇百怪:
{ code: 500, msg: '服务器冒烟了' }
{ error: { detail: '参数不对' } }
{ status: 'fail', reason: '余额不足' }

前端如果每个接口都if (res.code !== 200) alert(res.msg),迟早精神分裂。

统一“翻译器”安排上:

functiontransformError(data){// 优先取常用字段returndata?.msg||data?.message||data?.error?.detail||'系统繁忙'}

request里一旦捕获异常,就交给全局提示:

import{toast}from'react-hot-toast'// 或 antd/element-plusrequest.interceptors={response:[asyncres=>res,err=>{constmsg=transformError(err.data)||err.message toast.error(msg)returnPromise.reject(err)}]}

小贴士:把提示函数作为参数注入,避免工具层依赖 UI 库,测试时也能静默处理。


拦截器机制怎么加进去

fetch原生没有拦截器,但我们可以“外包”一层数组,循环执行:

classRequest{constructor(config){this.config=configthis.interceptors={request:[],response:[]}}asyncrun(url,options){// 请求拦截for(constfnofthis.interceptors.request){({url,options}=awaitfn({url,options})||{url,options})}letresponsetry{response=awaitfetch(url,options)}catch(e){// 网络层错误response={ok:false,status:0,statusText:e.message}}// 响应拦截for(constfnofthis.interceptors.response){response=awaitfn(response)||response}returnresponse}}

自动带 token 的场景:

request.interceptors.request.push(({url,options})=>{consttoken=localStorage.getItem('token')if(token){options.headers=options.headers||{}options.headers['Authorization']=`Bearer${token}`}return{url,options}})

日志上报:

request.interceptors.response.push(asyncres=>{if(!res.ok){report({url:res.url,status:res.status,timestamp:Date.now()})}returnres})

支持取消请求和防重发

用户狂点“提交”按钮,后端收到 5 次同款订单,老板连夜买站票跑路。

利用AbortController做“上一次没回来就取消”:

constpendingMap=newMap()functiongenKey(url,method,data){return[url,method,JSON.stringify(data)].join('&')}functioncancelRepeat(config){constkey=genKey(config.url,config.method,config.data)if(pendingMap.has(key)){pendingMap.get(key).abort()}constcontroller=newAbortController()pendingMap.set(key,controller)config.signal=controller.signal}// 响应后删除functionremovePending(config){constkey=genKey(config.url,config.method,config.data)pendingMap.delete(key)}

在拦截器里调用即可。

进阶:对非幂等请求(POST/PUT/PATCH)才做取消,GET 请求保留缓存即可。


自动重试与降级策略

网络抖动 502,立刻抛错太绝情。给关键接口 3 次机会:

asyncfunctionrequestWithRetry(url,options,retry=3){for(leti=0;i<retry;i++){try{returnawaitrequest(url,options)}catch(e){constisLast=i===retry-1if(isLast||e.code===400)throwe// 业务错误不重试awaitsleep(Math.pow(2,i)*1000)// 指数退避}}}functionsleep(ms){returnnewPromise(r=>setTimeout(r,ms))}

降级:如果连续失败,返回本地兜底数据,让页面不至于空白:

const降级数据=require('@/mock/fallback.json')try{returnawaitrequestWithRetry('/api/critical')}catch{return降级数据}

开发调试神器:请求日志与 Mock 支持

本地想看“发出去的是什么妖魔鬼怪”?一行拦截器搞定:

if(process.env.NODE_ENV==='development'){request.interceptors.request.push(({url,options})=>{console.groupCollapsed(`[${options.method}]${url}`)console.log('headers:',options.headers)console.log('body:',options.body)console.groupEnd()return{url,options}})}

Mock 数据?用vite-plugin-mockmsw都行,这里给个极简内存版:

constmockRoutes={'GET /api/user':{id:1,name:'纸糊小能手'}}request.interceptors.request.push(({url,options})=>{constkey=`${options.method}${url}`if(mockRoutes[key]){returnPromise.resolve(mockRoutes[key])}return{url,options}})

注意:如果返回的是 Promise,会短路真实请求,直接作为响应。


真实项目中的封装姿势

1. 与 TypeScript 甜蜜双排

interfaceBaseResponse<T=any>{code:numberdata:Tmsg?:string}exportasyncfunctionget<T=any>(url:string,params?:object):Promise<T>{constres=awaitrequest(url,{method:'GET',params})return(resasBaseResponse<T>).data}

调用时享受类型推导:

interfaceUser{id:number;name:string}constuser=awaitget<User>('/api/user')// user 自动提示 name 属性

2. 环境变量与多基址

.env文件:

VITE_API_BASE=https://api.example.com VITE_API_BASE_DEV=http://localhost:3001
constbaseURL=import.meta.env.DEV?import.meta.env.VITE_API_BASE_DEV:import.meta.env.VITE_API_BASE

3. 模块拆分

src/ api/ index.ts // 导出 request 实例 modules/ user.ts order.ts

user.ts只写业务:

importrequestfrom'../index'exportconstgetUser=(id:number)=>request.get(`/user/${id}`)

遇到“请求发不出去”怎么办

  1. CORS:
    浏览器报错No 'Access-Control-Allow-Origin'时,先确认后端Access-Control-Allow-Headers是否包含自定义头(如Authorization)。本地开发用 vite 代理:

    // vite.config.jsserver:{proxy:{'/api':{target:'http://localhost:3001',changeOrigin:true}}}
  2. HTTPS 混合内容:
    页面https却请求http,浏览器直接拦截。把后端也上证书,或者前端统一走/api代理。

  3. 证书无效:
    本地自签证书不被信任,chrome 地址栏输入thisisunsafe先跳过,或者把.crt加入系统受信任根证书。


进阶技巧:缓存策略与性能优化

GET 请求在内存里缓存 5 分钟,减少“抖动刷新”:

constcache=newMap()functionwithCache(fn,ttl=5*60*1000){returnasync(...args)=>{constkey=JSON.stringify(args)if(cache.has(key)){const{data,expiry}=cache.get(key)if(Date.now()<expiry)returndata}constresult=awaitfn(...args)cache.set(key,{data:result,expiry:Date.now()+ttl})returnresult}}exportconstgetUserCache=withCache(getUser)

配合 React 的useSWR

importuseSWRfrom'swr'const{data}=useSWR('/api/user',getUserCache)

秒杀“重复请求地狱”。


别再裸奔了,给你的 fetch 加点“盔甲”

  1. 敏感信息过滤:
    上报日志时,把手机号、身份证replace*

  2. XSS 注入:
    任何拼接在 URL 上的参数都要encodeURIComponent,别给坏人留?redirect=<script>的口子。

  3. CSRF:
    同源接口如果依赖 cookie,一定让后端开启SameSite=Strict,或者加自定义头X-Requested-With: fetch,后端校验缺失则拒绝。


封装完别急着提交,先测一测

单元测试用Jest+msw(Mock Service Worker):

// request.test.tsimport{rest}from'msw'import{setupServer}from'msw/node'importrequestfrom'../src/utils/request'constserver=setupServer(rest.get('/api/test',(req,res,ctx)=>{returnres(ctx.json({hello:'world'}))}))beforeAll(()=>server.listen())afterAll(()=>server.close())test('should return json',async()=>{constdata=awaitrequest('/api/test')expect(data).toEqual({hello:'world'})})

跑完npm run test:ci全绿再git push,否则半夜被测试姐姐 @ 别怪我没提醒。


你以为封装完了?其实才刚开始

项目像养娃,接口会越来越多,需求会越来越骚:
“我要 GraphQL!” “我要批量请求!” “我要插件化,自由插拔!”

把核心拆成“中间件”模式,参考koa-compose

classCore{use(fn){this.middlewares.push(fn)returnthis}exec(context){constdispatch=i=>{constfn=this.middlewares[i]if(!fn)returnreturnfn(context,()=>dispatch(i+1))}returndispatch(0)}}

链式调用:

request.use(cache).use(retry).use(logger)

每新增能力就是一条中间件,而不用回炉重造。

最后,记住一句话:
“好的封装不是让你少写代码,而是让你把精力花在更值得折腾的地方——比如给产品小姐姐调像素。”

祝你与fetch白头偕老,永无502

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!