news 2026/7/2 5:19:23

从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装

这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-mall(github | gitee)开源项目的制作,全程记录分享。

在上一期,我们已经完成 “静态登录页制作”,本期将完成:优化细节、网络请求封装

优化细节

全局基本样式初始化

全局字体设定,还有部分浏览器标签默认样式消除等(比如 Chrome 浏览器的 body 标签,默认会有 8px 的 margin),建立 app.scss 文件,写入全局默认样式:

src\styles\app.scss 文件

*, *::before, *::after{margin:0;padding:0;box-sizing:border-box;}html, body{margin:0;padding:0;width:100%;height:100%;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;color:var(--el-text-color-primary);font-size:var(--el-font-size-base);}

再建立src\styles\element.scss文件,写入全局的 element plus 样式优化代码:

/* 修复 Chrome 浏览器输入框内选中字符行高异常的问题 */.el-input{.el-input__inner{line-height:calc(var(--el-input-height,40px)- 4px);}}

至此,我们的styles目录已经有loading.scsselement.scssapp.scss三个文件了,其中element.scssapp.scss都是需要全局引入的样式文件,我们可以直接于main.ts文件中逐一引入,但是考虑到未来这类全局样式文件还会增加,我们单独建立一个index.scss文件合并所有全局样式,然后main.ts内只导入它即可:

// src\styles\index.scss 文件@use'/@/styles/app.scss';@use'/@/styles/element.scss';// 未来增加全局样式文件,在此增加一行即可,无需改动 main.ts 文件

main.ts 增加一行代码即可

import'/@/styles/index.scss'

路由切换更新浏览器标题

我们已经于静态路由配置中设定了各个路由的 title,接下来只需要在src\router\index.ts里边的路由加载后钩子中,将meta.title设置到浏览器标题栏即可:

// src\router\index.ts 文件import{useTitle}from'@vueuse/core'// 路由加载后router.afterEach((to)=>{if(window.loading){loading.hide()}NProgress.done()// 设置浏览器标题consttitleKey=to?.meta?.titleasstring|undefinedconsttitle=titleKey&&i18n.global.te(titleKey)?i18n.global.t(titleKey):''useTitle().value=title?`${title}- AI GO MALL`:'AI GO MALL'})

先使用了 i18n.global.te 确定翻译 key 是存在的,然后才翻译,最后使用useTitle直接完成浏览器标题的设置。

网络请求封装

网络请求库当然是使用 axios,这是 90% 的项目在使用的网络请求库,我们主要需要封装的内容如下:

  1. 请求 loading,可以配置一个网络请求是否显示全屏 loading 态,如果显示会在请求完成时自动关闭
  2. 自动取消重复请求,根据请求的 get/param/data 等参数生成一个唯一的 key,如果一个请求未结束,再发起 key 相同的请求就会被自动取消,此功能也可以配置是否开启
  3. 使用 TypeScript 实现,可以指定相应数据的类型
  4. 操作成功/操作失败的自动提示开关,开启则自动使用 Element plus 的 API 弹出提示信息,成功默认不显示,失败默认显示
  5. 自动携带用户 token(从stores\adminInfo.ts状态商店获取当前用户 token)

将以上需求发送给 cc,总共只建立了utils\request.ts一个文件,且未改动任何其他文件,人工 review 发现以下可以优化的地方:

  1. 后端统一响应结构放到@types\common.d.ts里边去,定义参考服务端的@..\internal\response\response.go(目前 AI 定义的响应结构是错误的)
  2. 自定义成功/失败提示语去掉
  3. 网络请求的 loading 不要使用首屏 loading,改用 Element Plus 的 ElLoading API
  4. 封装单独的 getBaseUrl 和 getBaseUrlPort 函数,以便项目它处使用
  5. 默认导出内部直接返回 axios 实例本身,不要:加一层 then 或将 request 函数改为异步函数
  6. 请求的网络错误提示,单独一个开关,比如 404、500 错误,默认开启
  7. 顶层变量归类,并使用对象存储,比如 loadingInstance 和 loadingCount 可以放到一个对象里边进行初始化
完成,变更如下: types/common.d.ts — 新增全局接口 ApiResponse<T>,字段对齐后端 response.go(code/message/time/data),成功判断为 code===0。 src/utils/request.ts — 四处修改:1. Loading 换用 ElLoading.service({fullscreen:true}),仍用引用计数管理并发请求2. 去掉 successMsg / errorMsg 两个自定义提示选项,直接使用 res.message3. ApiResponse 改用 types/common.d.ts 中的全局声明,不再本地export4. 新增 getBaseUrl / getBaseUrlPort 两个导出函数,用于在其他地方拼接完整请求地址(例如上传 action、文件下载链接等)5. 默认导出内部直接返回 axios 实例本身...

最后再对一些有顾虑的地方进行确定和补充:

  1. 再次次分析 buildRequestKey,此函数建立的请求唯一 key 合理吗?如果不合理,如何优化?
  2. 封装的部分自定义函数,逻辑,是否可以用 lodash-es 库中的函数代替
第1点完成。主要变更点: - method → 统一 .toLowerCase()- params → 普通对象按 key 排序后 stringify;URLSearchParams 调用 .sort()再 .toString()- data → 同 params 排序;字符串直接使用;FormData/Blob/ArrayBuffer 用[object FormData]等类型名标记(相同类型的请求仍会去重,若需区分并发上传请对该请求设 cancelDuplicate: false) - 最终 key → 改用 JSON.stringify({method, url, params, data})消除&分隔符碰撞问题 第2点:整个文件中只有一处值得替换:typeof config.data==='string'→ isString(config.data),其余自定义逻辑用原生写法已是最简形式,引入 lodash-es 反而增加复杂度。单独为一行引入一个import成本大于收益,建议维持现状,不做替换。

利用 AI 封装网络请求完成,最终代码如下:

types/common.d.ts增加了 ApiResponse 响应数据结构:

interfaceApiResponse<T=any>{code:numbermessage:stringtime:numberdata:T}

utils/request.ts的内容进行人工微调,比如:

constBASE_URL=import.meta.env.VITE_AXIOS_BASE_URLasstringexportfunctiongetBaseUrl():string{returnBASE_URL}exportfunctiongetBaseUrlPort():string{returnnewURL(BASE_URL).port}

修改为:

exportfunctiongetBaseUrl():string{returnimport.meta.env.VITE_AXIOS_BASE_URLasstring}exportfunctiongetBaseUrlPort():string{returnnewURL(getBaseUrl()).port}

避免顶层的 BASE_URL,不好看又徒增心智负担。request.ts完整代码:

importtype{AxiosInstance,AxiosRequestConfig,InternalAxiosRequestConfig}from'axios'importaxiosfrom'axios'import{ElLoading,ElMessage}from'element-plus'importi18nfrom'/@/lang'import{useAdminInfo}from'/@/stores/adminInfo'// ==================== 类型定义 ====================exportinterfaceRequestOptions{// 是否显示全屏 loading,默认 falseloading?:boolean// 是否自动取消重复请求,默认 truecancelDuplicate?:boolean// 是否显示操作成功提示,默认 falseshowSuccessMessage?:boolean// 是否显示业务错误提示(code !== 0),默认 trueshowErrorMessage?:boolean// 是否显示网络错误提示(HTTP 4xx/5xx 等),默认 trueshowNetworkErrorMessage?:boolean}exportinterfaceRequestConfigextendsAxiosRequestConfig{requestOptions?:RequestOptions}interfaceInternalRequestConfigextendsInternalAxiosRequestConfig{requestOptions?:RequestOptions}// ==================== Base URL 辅助函数 ====================exportfunctiongetBaseUrl():string{returnimport.meta.env.VITE_AXIOS_BASE_URLasstring}exportfunctiongetBaseUrlPort():string{returnnewURL(getBaseUrl()).port}// ==================== Loading ====================constloadingState={count:0,instance:nullasReturnType<typeofElLoading.service>|null,}functionshowLoading(){if(loadingState.count===0){loadingState.instance=ElLoading.service({fullscreen:true})}loadingState.count++}functionhideLoading(){loadingState.count=Math.max(0,loadingState.count-1)if(loadingState.count===0){loadingState.instance?.close()loadingState.instance=null}}// ==================== 重复请求取消 ====================interfacePendingEntry{controller:AbortController hasLoading:boolean}constpendingMap=newMap<string,PendingEntry>()functionsortedStringify(obj:Record<string,any>):string{returnJSON.stringify(Object.fromEntries(Object.entries(obj).sort(([a],[b])=>a.localeCompare(b))))}/** * 根据请求参数,为请求生成唯一标识 */functionbuildRequestKey(config:InternalAxiosRequestConfig):string{constmethod=(config.method??'get').toLowerCase()consturl=config.url??''letparams=''if(config.params!=null){if(config.paramsinstanceofURLSearchParams){constcopy=newURLSearchParams(config.params)copy.sort()params=copy.toString()}else{params=sortedStringify(config.paramsasRecord<string,any>)}}letdata=''if(config.data!=null){if(typeofconfig.data==='string'){data=config.data}elseif(config.datainstanceofFormData||config.datainstanceofBlob||config.datainstanceofArrayBuffer){// 无法稳定序列化,用类型名标记;如需区分多个并发上传请将 cancelDuplicate 设为 falsedata=Object.prototype.toString.call(config.data)}else{data=sortedStringify(config.dataasRecord<string,any>)}}returnJSON.stringify({method,url,params,data})}functionaddPending(config:InternalRequestConfig):void{constkey=buildRequestKey(config)if(pendingMap.has(key)){const{controller,hasLoading}=pendingMap.get(key)!controller.abort()if(hasLoading)hideLoading()pendingMap.delete(key)console.warn('[Request] The repeated request has been canceled:',key)}constcontroller=newAbortController()config.signal=controller.signal pendingMap.set(key,{controller,hasLoading:config.requestOptions?.loading??false,})}functionremovePending(config:InternalAxiosRequestConfig):void{pendingMap.delete(buildRequestKey(config))}// ==================== Axios 实例 ====================constinstance:AxiosInstance=axios.create({baseURL:getBaseUrl(),timeout:10000,})instance.interceptors.request.use((config:InternalRequestConfig)=>{constopts=config.requestOptions??{}if(opts.cancelDuplicate!==false)addPending(config)if(opts.loading)showLoading()constadminInfo=useAdminInfo()if(adminInfo.token){config.headers.set('Authorization',`Bearer${adminInfo.token}`)}returnconfig},(error)=>Promise.reject(error))instance.interceptors.response.use((response)=>{constconfig=response.configasInternalRequestConfigconstopts=config.requestOptions??{}removePending(response.config)if(opts.loading)hideLoading()if(response.data.code!==0){if(opts.showErrorMessage!==false){ElMessage.error(response.data.message||i18n.global.t('common.operationFailed'))}returnPromise.reject(newError(response.data.message||i18n.global.t('common.operationFailed')))}if(opts.showSuccessMessage){ElMessage.success(response.data.message||i18n.global.t('common.operationSuccess'))}returnresponse},(error)=>{if(axios.isCancel(error))returnPromise.reject(error)constconfig=error.configasInternalRequestConfig|undefinedconstopts=config?.requestOptions??{}if(config){removePending(error.config)if(opts.loading)hideLoading()}if(opts.showNetworkErrorMessage!==false){constmsg=(error.response?.dataasApiResponse|undefined)?.message??error.message??i18n.global.t('common.networkError')ElMessage.error(msg)}returnPromise.reject(error)})// ==================== 对外 API ====================functionrequest<T=any>(config:RequestConfig){returninstance<ApiResponse<T>>(config)}exportdefaultrequest
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 5:17:34

go-zero:3.3 万 Star 的 Go 微服务框架,大厂实战打磨出来的

文章目录go-zero&#xff1a;3.3 万 Star 的 Go 微服务框架&#xff0c;大厂实战打磨出来的解决什么问题代码生成省时间背后的故事AI 开发支持我的看法go-zero&#xff1a;3.3 万 Star 的 Go 微服务框架&#xff0c;大厂实战打磨出来的 最近在看 Go 微服务框架&#xff0c;发现…

作者头像 李华
网站建设 2026/7/2 5:14:36

告别词库迁移烦恼:深蓝词库转换工具让你的输入习惯无缝同步

告别词库迁移烦恼&#xff1a;深蓝词库转换工具让你的输入习惯无缝同步 【免费下载链接】imewlconverter ”深蓝词库转换“ 一款开源免费的输入法词库转换程序 项目地址: https://gitcode.com/gh_mirrors/im/imewlconverter 还在为更换输入法后词库无法迁移而头疼吗&…

作者头像 李华
网站建设 2026/7/2 5:11:30

Go 语言设计模式大全,2.8 万 Star 的编程参考手册

文章目录Go 语言设计模式大全&#xff0c;2.8 万 Star 的编程参考手册包含哪些设计模式为什么值得看适合谁Go 语言设计模式大全&#xff0c;2.8 万 Star 的编程参考手册 最近在 GitHub 上看到一个 Go 语言项目&#xff0c;Star 数已经到了 2.8 万。这项目不是框架&#xff0c;…

作者头像 李华
网站建设 2026/7/2 5:10:49

保冷管束用在哪里?六大核心应用场景全梳理

保冷管束用在哪里&#xff1f;六大核心应用场景全梳理做过暖通、制冷或工业管道项目的工程师&#xff0c;基本都绕不开保冷管束这个配件。很多人不清楚它的应用边界&#xff0c;在不该用的地方用了普通管夹&#xff0c;或者不知道某些场合必须用保冷管束才能保证系统完整性。本…

作者头像 李华
网站建设 2026/7/2 5:10:23

计算机大学浑浑噩噩摆烂四年还有翻盘机会吗?完整自救学习路线,零基础也能逆袭拿到技术 offer

计算机专业摆烂四年还有什么办法补救回来吗&#xff1f;看着身边同学拿到大厂offer&#xff0c;自己却连简历都填不满&#xff0c;难免会陷入“我是不是彻底没救了”的自我否定。 但作为深耕网安行业多年的老鸟&#xff0c;今天想明确告诉你&#xff1a;摆烂四年不代表人生报废…

作者头像 李华
网站建设 2026/7/2 5:10:22

手写数字识别实战:从MNIST到银行票据的全流程解析

1. 这不是魔法&#xff0c;是手写数字识别的完整实操现场你有没有在银行柜台填过单子&#xff1f;快递面单上签过名&#xff1f;老式收银机旁手写的价签&#xff1f;这些场景里&#xff0c;那些歪歪扭扭、粗细不一、连笔飞白的“0”到“9”&#xff0c;每天都在被成千上万台设备…

作者头像 李华