当前位置: 首页 > Web前端 > vue.js

还记得在老Vue2项目中引入TypeScript和结合Api的艰辛历程

时间:2023-03-31 17:37:15 vue.js

最初是2年前的一个现有项目创建的。随着时间的推移,代码量暴涨到近万个文件,但工程也慢慢到了无法维护的地步。想给他一个大改,但是侵入式的代码配置太多了。。。最后折衷的引入了TypeScript,结合Api,vueuse来完善项目。工程标准化程度,整个过程让我感觉挺一般的,记录一下。首先配置一些TypeScript相关库的安装配置。由于webpack的版本还是3.6,我几次尝试升级到4和5,但是因为大量的配置和侵入性的代码修改工作而放弃了,所以我直接找到了下面的库npmi-Dts-loader@3.5.0tslint@6.1.3tslint-loader@3.6.0fork-ts-checker-webpack-plugin@3.1.1接下来就是更改webpack的配置,修改main.js文件为主。ts,并在文件的第一行添加//@ts-nocheck,让TS忽略检查这个文件,相应的将webpack.base.config.js的入口改成webpack.base.config中的main.ts。在jsresolve的扩展中添加.ts和.tsx,在别名规则中添加'vue$':'vue/dist/vue.esm.js'添加插件选项到webpack.base.config.js添加fork-ts-checker-webpack-plugin将tscheck的任务放到一个单独的进程中,以减少开发服务器的启动时间在webpack.base.config.js文件的规则中添加两个配置和fork-ts-checker-webpack-插件配置{test:/\.ts$/,exclude:/node_modules/,enforce:'pre',loader:'tslint-loader'},{test:/\.tsx?$/,loader:'ts-loader',exclude:/node_modules/,options:{appendTsSuffixTo:[/\.vue$/],transpileOnly:true//禁用类型检查器-我们将在fork插件中使用它}},,//...plugins:[newForkTsCheckerWebpackPlugin()],//独立进程处理ts-checker,缩短webpack服务冷启动和热更新时间https://github.com/T在ypeStrong/ts-loader#faster-builds根目录下添加tsconfig.json文件补充相应配置,并添加vue-shim.d.ts声明文件tsconfig.json{"exclude":["node_modules","static","dist"],"compilerOptions":{"strict":true,"module":"esnext","outDir":"dist","target":"es5","allowJs":true,"jsx":"preserve","resolveJsonModule":true,"downlevelIteration":true,"importHelpers":true,"noImplicitAny":true,"allowSyntheticDefaultImports":true,"moduleResolution":"node","isolatedModules":false,"experimentalDecorators":true,"emitDecoratorMetadata":true,"lib":["dom","es5","es6","es7","dom.iterable","es2015.promise"],"sourceMap":true,"baseUrl":".","paths":{"@/*":["src/*"],},"pretty":true},"include":["./src/**/*","typings/**/*.d.ts"]}vue-shim.d.tsdeclaremodule'*.vue'{importVuefrom'vue'exportdefaultVue}路由配置改进原创路由配置通过配置路径,name,component,在开发和维护的过程中存在一些不足:在使用的时候,path或者name的使用可能不规范,不统一。文件不方便手动避免路由名称和路径与其他路由冲突。路由路径根据业务分为不同的枚举。在枚举中定义可以防止路由路径冲突,也可以更加语义化地定义枚举的key。也可以借助Typescript的类型推断能力快速完成。在查找单个文件对应的路由时,可以一步完成。为什么不用name,因为name只是这条路由的语义标识。当我们使用枚举类型的路径时,枚举Key就足以充当语义路径。path的name属性只是没有存在的必要,我们在声明路由的时候不需要声明name属性,我们只需要path和component字段demoexportenumROUTER{home='/xxx/home',About='/xxx/about',}exportdefault[{path:ROUTER.Home,component:()=>import(/*webpackChunkName:'Home'*/'views/Home')},{path:ROUTER.About,component:()=>import(/*webpackChunkName:'About'*/'views/About')}]我们项目中的常量和枚举也通过将所有常量提取到services/const中来管理。现在集成Typescript之后,我们之后,项目可以在services/constant中管理常量,在services/enums中管理枚举。比如普通接口返回的code可以声明为一个枚举,这样使用的时候就不需要写类似if(res.code===200)的判断,直接通过declared获取即可RES_CODEenumeration所有接口返回码类型//services/enums/index.ts/**RES_CODEEnum*/exportenumRES_CODE{SUCCESS=200//xxx}比如我们可以在services/constant/storage中声明存储key。ts/**userInfo-storageKey*/exportconstUSERINFO_STORE_KEY='userInfo'/**用户相关的key可以通过构造一个带有业务属性参数的纯函数来声明*/exportconstUserSpecialInfo=(userId:string)=>{return`specialInfo-${userId}`}类型声明文件规范全局类型声明文件统一维护在根目录的typings文件夹中(可复用数据类型)相对于业务中组装数据过程中的类型,直接在component类型封装请求库可以维护接口中的类封装逻辑(不易复用的数据结构)。在utils文件夹下添加一个requestWrapper.ts文件,然后所有request基类方法封装都可以在这个文件中维护'//请求参数在后面打包的时候是特定于某个类型的。这里使用unknown语句返回值是一个泛型类型S,使用导出函数时填写具体类型PostWrapper(url:string,data:unknown,timeout?:number){return(request({url,方法:'post',data,timeout})asAxiosResponse['data'])asBASE.BaseResWrapper//BASE是在typings中定义的命名空间,后面会有代码说明}在具体业务层进行封装使用在api/user新建一个index.ts文件,与之前的比较即可done够简洁了,还能提供类型提示,知道请求是什么,传参的参数和返回值这个接口在注释里,我们不需要用注释来标识我们需要什么类型的参数,TS会帮我们完成,我们只需要填写请求参数的类型和返回参数的类型约束使用请求方法/**获取用户信息*/exportfunctiongetUserInfo(query:User.UserInfoReqType){returnPostWrapper('/api/userinfo',query)}需要提供接口支持的类型,需要在api/**/*中声明.ts文件,并传递给对应的函数注解参数requesttype和responsetype如果结构极其简单,则不需要在typings/request/*.d.ts中维护,直接声明类型即可在封装界面。如果参数多一点,应该在typings中维护在/request/*.d.ts中,以免混淆。业务中服务端返回的接口,基本都是被一层描述对象包裹起来的。业务数据在对象的请求字段中。基于此,我们对接口进行封装,只需要在typings/request/index.d.ts中声明请求返回的基类结构,在具体的xxx.d.ts中完成具体的请求类型声明,比如报错user.d.ts中的interface,在这个文件中声明了全局命名空间User来管理所有此类job接口的请求和响应数据类型typings/request/index.d.tsimport{RES_CODE}from'@/services/enums'declareglobal{//*all基类在这里声明类型。namespaceBASE{//请求返回的封装类型声明提供给封装类型BaseRes=Promise>//分页接口类型BasePagination={content:Tnow:stringpage:numbersize:numbertotalElements:numbertotalPages:number}}typings/request/user.d.tsdeclarenamespaceUser{/**响应参数*/类型UserInfoResType={id:number|stringname:string//...}/**请求参数*/typeUserInfoReqType={id:number|string//...}至此,TypeScript相关就结束了,接下来就是使用组合Api在Vue2中安装组合Api@vue/composition-apipmi@vue/composition-api可以用在main.tstousecombinedAPIin.vuefileimportVueCompositionAPIfrom'@vue/composition-api'//...Vue.use(VueCompositionAPI)Vue2中使用组合API的一些注意事项组合API文档,不懂的朋友可以先参考文档学习,在页面比较复杂,组件较多的情况下,组合API比传统的OptionsAPI更灵活,可以将逻辑抽取出来封装成一个单独的使用函数,让组件代码结构更清晰并且更容易重用业务逻辑组合Api中的所有api都需要从@vue/composition-api导入,然后使用exportdefaultdefineComponent({})替换原来的exportdefault{}来启用组合Api语法和Typescript类型推导(脚本需要添加对应的lang="ts"属性)模板中的写法与Vue2一致,无需关注Vue3中的v-model和类似.native的事件修饰符。Vue3中取消等其他break变化,在子组件中调用父组件中的方法,在setup(props,ctx)中使用ctx.emit(eventName,params),挂载在Vue实例对象上的属性和方法可以通过ctx.root.xxx获取,包括$route、$router等。为了使用方便,建议通过setup第一行的结构体声明ctx.root上的属性。如果之前有业务属性相关的属性或方法添加到Vue实例对象中,可以通过在模块vue/types/vue上扩展Vue接口来添加业务属性相关的类型:typings/common/index.d.ts//1.确保在声明扩充类型之前导入'vue'importVuefrom'vue'//2.指定一个包含您要扩充的类型的文件//Vue在types/vue.d.tsdeclare模块中具有构造函数类型'vue/types/vue'{//3.为Vue接口声明扩充Vue{/**当前环境是否为IE*/isIE:boolean//...可以根据自己的业务情况自行添加}}模板中使用的所有变量、方法和对象都需要在setup中返回,其他在页面逻辑内部建议根据页面展示元素和用户与用户之间的交互行为在setup中定义方法这一页。越复杂的逻辑细节和数据的处理,越要尽可能的抽离到外部。.vue文件中的代码逻辑清晰。在需求开发之前,根据服务端接口数据的定义,可以制定页面组件中数据和方法的接口。可以预先声明类型,然后在开发过程中实现具体的方法。在Vue2.6版本中,通过@vue/composition-api使用组合Api,无法使用setup语法糖。Vue2.7版本发布后,我们再观察一下。其他一些注意事项和限制基于反应式商店的样式规范。针对Vuex中访问TS的不便和Vuex使用场景的必要性,在组合Api中提供了一个最佳实践:将需要响应的数据声明在一个ts文件中,通过react包初始化对象,暴露一个updated方法,可以实现原来在Vuex中更新store中state的效果,使用computed实现getter的效果,哪些组件需要获取和修改数据只需要引入,change可以直接实现response影响!提供一个demo,大家可以对这部分内容的封装有不同意见:xxxHelper.tsimport{del,reactive,readonly,computed,set}from'@vue/composition-api'//定义数据类型在thestore,for数据结构受限interfaceCompositionApiTestStore{c:number[propName:string]:any}//初始值constinitState:CompositionApiTestStore={c:0}conststate=reactive(initState)/**暴露的store是只读的,只能通过下面的updateStore来改变*/exportconststore=readonly(state)/**可以达到原来Vuex中getter方法的效果*/exportconstupperC=computed(()=>{returnstore.c.toUpperCase()})/**暴露改变状态的方法。参数是状态对象的子集或没有参数。如果没有参数,方便当前对象删除所有子对象。否则,我需要更新或删除*/exportfunctionupdateStore(params:Partial|undefined){console.log('updateStore',params)if(params===undefined){for(const[k,v]ofObject.entries(state)){del(state,`${k}`)}}else{for(const[k,v]ofObject.entries(params)){if(v===undefined){del(state,`${k}`)}else{set(state,`${k}`,v)}}}}vueusevueuse是一个非常有用的库。它的安装和使用非常简单,但它有许多强大的功能。这部分我就不展开细说了,还是去看官方文档吧!综上所述,这次项目升级真的是不得已而为之。没有别的办法。该项目已经很大并且仍然与IE兼容。使用的脚手架和相关库已经很久没有更新了。自项目创建以来,已经欠下了很多技术债。是的,引起了后面的开发和维护人员的吐槽(其实是我,项目是别人做的,逃避。。。),各位大佬们在开始新项目的时候一定要考虑脚手架和技术栈,别让前人挖坑给后人填……如果你也在维护这样的项目,受够了这种糟糕的开发经历,可以参考我的经验改造你的项目。如果对你有帮助,欢迎评论一键三连~