前端新人必学:手把手封装 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-mock或msw都行,这里给个极简内存版:
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:3001constbaseURL=import.meta.env.DEV?import.meta.env.VITE_API_BASE_DEV:import.meta.env.VITE_API_BASE3. 模块拆分
src/ api/ index.ts // 导出 request 实例 modules/ user.ts order.tsuser.ts只写业务:
importrequestfrom'../index'exportconstgetUser=(id:number)=>request.get(`/user/${id}`)遇到“请求发不出去”怎么办
CORS:
浏览器报错No 'Access-Control-Allow-Origin'时,先确认后端Access-Control-Allow-Headers是否包含自定义头(如Authorization)。本地开发用 vite 代理:// vite.config.jsserver:{proxy:{'/api':{target:'http://localhost:3001',changeOrigin:true}}}HTTPS 混合内容:
页面https却请求http,浏览器直接拦截。把后端也上证书,或者前端统一走/api代理。证书无效:
本地自签证书不被信任,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 加点“盔甲”
敏感信息过滤:
上报日志时,把手机号、身份证replace成*。XSS 注入:
任何拼接在 URL 上的参数都要encodeURIComponent,别给坏人留?redirect=<script>的口子。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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!