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

发布时间:2026/7/2 5:19:27
从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装 这是一个系列 Blog作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-mallgithub | 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.scss、element.scss、app.scss三个文件了其中element.scss和app.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}fromvueuse/core// 路由加载后router.afterEach((to){if(window.loading){loading.hide()}NProgress.done()// 设置浏览器标题consttitleKeyto?.meta?.titleasstring|undefinedconsttitletitleKeyi18n.global.te(titleKey)?i18n.global.t(titleKey):useTitle().valuetitle?${title}- AI GO MALL:AI GO MALL})先使用了 i18n.global.te 确定翻译 key 是存在的然后才翻译最后使用useTitle直接完成浏览器标题的设置。网络请求封装网络请求库当然是使用 axios这是 90% 的项目在使用的网络请求库我们主要需要封装的内容如下请求 loading可以配置一个网络请求是否显示全屏 loading 态如果显示会在请求完成时自动关闭自动取消重复请求根据请求的 get/param/data 等参数生成一个唯一的 key如果一个请求未结束再发起 key 相同的请求就会被自动取消此功能也可以配置是否开启使用 TypeScript 实现可以指定相应数据的类型操作成功/操作失败的自动提示开关开启则自动使用 Element plus 的 API 弹出提示信息成功默认不显示失败默认显示自动携带用户 token从stores\adminInfo.ts状态商店获取当前用户 token将以上需求发送给 cc总共只建立了utils\request.ts一个文件且未改动任何其他文件人工 review 发现以下可以优化的地方后端统一响应结构放到types\common.d.ts里边去定义参考服务端的..\internal\response\response.go目前 AI 定义的响应结构是错误的自定义成功/失败提示语去掉网络请求的 loading 不要使用首屏 loading改用 Element Plus 的 ElLoading API封装单独的 getBaseUrl 和 getBaseUrlPort 函数以便项目它处使用默认导出内部直接返回 axios 实例本身不要加一层 then 或将 request 函数改为异步函数请求的网络错误提示单独一个开关比如 404、500 错误默认开启顶层变量归类并使用对象存储比如 loadingInstance 和 loadingCount 可以放到一个对象里边进行初始化完成变更如下 types/common.d.ts — 新增全局接口 ApiResponseT字段对齐后端 response.gocode/message/time/data成功判断为 code0。 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 实例本身...最后再对一些有顾虑的地方进行确定和补充再次次分析 buildRequestKey此函数建立的请求唯一 key 合理吗如果不合理如何优化封装的部分自定义函数逻辑是否可以用 lodash-es 库中的函数代替第1点完成。主要变更点 - method → 统一 .toLowerCase()- params → 普通对象按 key 排序后 stringifyURLSearchParams 调用 .sort()再 .toString()- data → 同 params 排序字符串直接使用FormData/Blob/ArrayBuffer 用[object FormData]等类型名标记相同类型的请求仍会去重若需区分并发上传请对该请求设 cancelDuplicate: false - 最终 key → 改用 JSON.stringify({method, url, params, data})消除分隔符碰撞问题 第2点整个文件中只有一处值得替换typeof config.datastring→ isString(config.data)其余自定义逻辑用原生写法已是最简形式引入 lodash-es 反而增加复杂度。单独为一行引入一个import成本大于收益建议维持现状不做替换。利用 AI 封装网络请求完成最终代码如下types/common.d.ts增加了 ApiResponse 响应数据结构interfaceApiResponseTany{code:numbermessage:stringtime:numberdata:T}对utils/request.ts的内容进行人工微调比如constBASE_URLimport.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}fromaxiosimportaxiosfromaxiosimport{ElLoading,ElMessage}fromelement-plusimporti18nfrom//langimport{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:nullasReturnTypetypeofElLoading.service|null,}functionshowLoading(){if(loadingState.count0){loadingState.instanceElLoading.service({fullscreen:true})}loadingState.count}functionhideLoading(){loadingState.countMath.max(0,loadingState.count-1)if(loadingState.count0){loadingState.instance?.close()loadingState.instancenull}}// 重复请求取消 interfacePendingEntry{controller:AbortController hasLoading:boolean}constpendingMapnewMapstring,PendingEntry()functionsortedStringify(obj:Recordstring,any):string{returnJSON.stringify(Object.fromEntries(Object.entries(obj).sort(([a],[b])a.localeCompare(b))))}/** * 根据请求参数为请求生成唯一标识 */functionbuildRequestKey(config:InternalAxiosRequestConfig):string{constmethod(config.method??get).toLowerCase()consturlconfig.url??letparamsif(config.params!null){if(config.paramsinstanceofURLSearchParams){constcopynewURLSearchParams(config.params)copy.sort()paramscopy.toString()}else{paramssortedStringify(config.paramsasRecordstring,any)}}letdataif(config.data!null){if(typeofconfig.datastring){dataconfig.data}elseif(config.datainstanceofFormData||config.datainstanceofBlob||config.datainstanceofArrayBuffer){// 无法稳定序列化用类型名标记如需区分多个并发上传请将 cancelDuplicate 设为 falsedataObject.prototype.toString.call(config.data)}else{datasortedStringify(config.dataasRecordstring,any)}}returnJSON.stringify({method,url,params,data})}functionaddPending(config:InternalRequestConfig):void{constkeybuildRequestKey(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)}constcontrollernewAbortController()config.signalcontroller.signal pendingMap.set(key,{controller,hasLoading:config.requestOptions?.loading??false,})}functionremovePending(config:InternalAxiosRequestConfig):void{pendingMap.delete(buildRequestKey(config))}// Axios 实例 constinstance:AxiosInstanceaxios.create({baseURL:getBaseUrl(),timeout:10000,})instance.interceptors.request.use((config:InternalRequestConfig){constoptsconfig.requestOptions??{}if(opts.cancelDuplicate!false)addPending(config)if(opts.loading)showLoading()constadminInfouseAdminInfo()if(adminInfo.token){config.headers.set(Authorization,Bearer${adminInfo.token})}returnconfig},(error)Promise.reject(error))instance.interceptors.response.use((response){constconfigresponse.configasInternalRequestConfigconstoptsconfig.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)constconfigerror.configasInternalRequestConfig|undefinedconstoptsconfig?.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 functionrequestTany(config:RequestConfig){returninstanceApiResponseT(config)}exportdefaultrequest