原由
现有的一个我的项目2年前创立的,随着工夫流逝,代码量曾经暴增到了将近上万个文件,然而工程化曾经缓缓到了不可保护的状态,想给他来一次大换血,然而侵入式代码配置太多了……,最终以一种斗争的形式引入了TypeScript、组合式Api、vueuse,晋升了我的项目的工程化标准水平,整个过程让我颇有感概,记录一下。
先配置TypeScript相干的
一些库的装置和配置
因为
webpack
的版本还是3.6
,尝试数次降级到4、5
都因为大量的配置侵入性代码的大量批改工作放弃了,所以就间接找了上面这些库npm i -D ts-loader@3.5.0 tslint@6.1.3 tslint-loader@3.6.0 fork-ts-checker-webpack-plugin@3.1.1
- 接下来就是改
webpack
的配置了,批改main.js
文件为main.ts
,并在文件的第一行增加// @ts-nocheck
让TS
疏忽查看此文件,在webpack.base.config.js
的入口中相应的改为main.ts
- 在
webpack.base.config.js
的resolve
中的extensions
中减少.ts
和.tsx
,alias
规定中减少一条'vue$': 'vue/dist/vue.esm.js'
- 在
webpack.base.config.js
中减少plugins
选项增加fork-ts-checker-webpack-plugin
,将ts check
的工作放到独自的过程中进行,缩小开发服务器启动工夫 在
webpack.base.config.js
文件的rules
中减少两条配置和fork-ts-checker-webpack-plugin
的插件配置{ test: /\.ts$/, exclude: /node_modules/, enforce: 'pre', loader: 'tslint-loader'},{ test: /\.tsx?$/, loader: 'ts-loader', exclude: /node_modules/, options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true // disable type checker - we will use it in fork plugin }},,// ...plugins: [new ForkTsCheckerWebpackPlugin()], // 在独立过程中解决ts-checker,缩短webpack服务冷启动、热更新工夫 https://github.com/TypeStrong/ts-loader#faster-builds
根目录中减少
tsconfig.json
文件补充相应配置,src
目录下新增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.ts
declare module '*.vue' { import Vue from 'vue' export default Vue}
路由配置的改善
原有路由配置是通过配置path
、name
和component
,这样在开发和保护的过程中有一些毛病:
- 应用的时候可能呈现应用
path
或者应用name
不标准不对立的状况 - 开发人员在保护老代码的时候查找路由对应的单文件不不便
- 要手动防止路由的
name
和path
不与其余路由有抵触
将所有的路由的门路依照业务抽离到不同的枚举中。在枚举中定义能够避免路由 path
抵触,也能够将枚举的 key
定义的更加语义化,又能够借助Typescript
的类型推导能力疾速补全,在查找路由对应单文件的时候能够一步到位
为什么不必name
,因为name
只是一个标识这个路由的语义,当咱们应用枚举类型的path
之后,枚举的Key
就足以充当语义化的门路path
这个name
属性就没有存在的必要了,咱们在申明路由的时候就不须要申明name
属性,只须要path
和component
字段就能够了
demo
export enum ROUTER { Home = '/xxx/home', About = '/xxx/about',}export default [ { 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)
相似的判断了,能够间接通过申明好的RES_CODE
枚举间接获取到所有的接口返回code
类型
// services/enums/index.ts/** RES_CODE Enum */export enum RES_CODE { SUCCESS = 200 // xxx}
比方storage
的key
咱们就能够申明在services/constant/storage.ts
中
/** userInfo-storageKey */export const USERINFO_STORE_KEY = 'userInfo'/** 与用户相干的key能够通过结构一个带业务属性参数的纯函数来申明 */export const UserSpecialInfo = (userId: string) => { return `specialInfo-${userId}`}
类型申明文件标准
全局类型申明文件对立在根目录的
typings
文件夹中保护(可复用的数据类型)比拟偏业务中组装数据过程中的类型间接在所在组件中保护即可(不易复用的数据结构)
接口中的类型封装
申请基类封装逻辑
在 utils 文件夹下新增requestWrapper.ts
文件,之后所有的申请基类办法封装能够在此文件中进行保护
// src/utils/requestWrapper.tsimport { AxiosResponse } from 'axios'import request from '@/utils/request'// 申请参数在之后具体封装的时候才具体到某种类型,在此应用unknown申明,返回值为泛型S,在应用的时候填充具体类型export function PostWrapper<S>( url: string, data: unknown, timeout?: number) { return (request({ url, method: 'post', data, timeout }) as AxiosResponse['data']) as BASE.BaseResWrapper<S> // BASE是在typings中定义的一个命名空间 前面会有代码阐明}
在具体的业务层进行封装后的应用
在api/user
中新建一个index.ts
文件,比照之前的能够做到足够简洁,也能够提供类型提醒,通晓这个申请是什么申请以及参数的参数以及返回值
import { PostWrapper } from '@/utils/requestWrapper'// 此处只须要在正文中标注这个接口是什么接口,不须要咱们通过正文来标识须要什么类型的参数,TS会帮咱们实现, 只须要咱们填充申请参数的类型和返回参数的类型即可束缚申请办法的应用/** 获取用户信息 */export function getUserInfo(query: User.UserInfoReqType) { return PostWrapper<User.UserInfoResType>( '/api/userinfo', query )}
- 须要提供类型反对的接口,须要申明在
api/**/*.ts
文件中,并通过给对应的function
标注参数申请类型和响应类型 - 如果构造极为简洁,能够不须要在
typings/request/*.d.ts
中保护,间接在封装接口处申明类型即可,如果参数稍多,都应在typings/request/*.d.ts
中保护,防止凌乱
当初业务中的服务端的接口返回的根本都是通过一层描述性对象包裹起来的,业务数据都在对象的request
字段中,基于此咱们封装接口就在typings/request/index.d.ts
中申明申请返回的基类构造,在具体的xxx.d.ts
中欠缺具体的申请类型申明,例如user.d.ts
中的一个报错的接口,在此文件中申明全局的命名空间User
来治理所有此类作业接口的申请和响应的数据类型
typings/request/index.d.ts
import { RES_CODE } from '@/services/enums'declare global { // * 所有的基类在此申明类型 namespace BASE { // 申请返回的包裹层类型申明提供给具体数据层进行包装 type BaseRes<T> = { code: RES_CODE result?: T info?: string time: number traceId: string } type BaseResWrapper<T> = Promise<BASE.BaseRes<T>> // 分页接口 type BasePagination<T> = { content: T now: string page: number size: number totalElements: number totalPages: number } }
typings/request/user.d.ts
declare namespace User {/** 响应参数 */type UserInfoResType = { id: number | string name: string // ...}/** 申请参数 */type UserInfoReqType = { id: number | string // ...}
到此TypeScript相干的就完结了,接下来是组合式Api的
Vue2中应用组合式Api
- 装置
@vue/componsition-api
npm i @vue/componsition-api
- 在
main.ts
中use
即可在.vue
文件中应用组合式 API
import VueCompositionAPI from '@vue/composition-api'// ...Vue.use(VueCompositionAPI)
Vue2 中应用组合式 Api 中的一些注意事项
- 组合式 Api文档,不理解的小伙伴能够先参照文档学习一下,在比较复杂的页面,组件多的状况下组合式 API 相比传统的
Options API
更灵便,能够把逻辑抽离进来封装为独自的use
函数,使组件代码构造更为清晰,也更不便复用业务逻辑。 - 所有的组合式 Api 中的
api
都须要从@vue/composition-api
中引入,而后应用export default defineComponent({ })
替换原有的export default { }
的写法,即可启用组合式 Api 语法和Typescript
的类型推导(script
须要增加对应的lang="ts"
的attribute
) template
中的写法和Vue2
中统一,无需留神Vue3
中的v-model
和相似.native
的事件修饰符在Vue3
中勾销等其余的break change
子组件中调用父组件中的办法应用
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. Make sure to import 'vue' before declaring augmented typesimport Vue from 'vue'// 2. Specify a file with the types you want to augment// Vue has the constructor type in types/vue.d.tsdeclare module 'vue/types/vue' { // 3. Declare augmentation for Vue interface Vue { /** 以后环境是否是IE */ isIE: boolean // ... 各位依据本人的业务状况自行添加 }}
- 所有
template
中应用到的变量、办法、对象都须要在setup
中return
,其余的在页面逻辑外部应用的不须要return
- 举荐依据页面展现元素和用户与页面的交互行为定义
setup
中的办法,比较复杂的逻辑细节和对数据的解决尽量抽离到内部,放弃.vue
文件中的代码逻辑清晰 - 在需要开发前,依据服务端接口数据的定义,来制订页面组件中的数据和办法的接口,能够提前申明类型,之后在开发过程中实现具体的办法
- 在当下的
Vue2.6
版本中通过@vue/composition-api
应用组合式 Api 不能应用setup
语法糖,待之后的Vue2.7
版本release
之后再察看,其余的一些 注意事项和限度
基于 reactive 的 store 的格调标准
鉴于在Vuex
中接入TS
的不便和Vuex
应用场景的必要性,在组合式 Api 中提供了一个最佳实际:将须要响应的数据申明在一个ts
文件中通过reactive
包裹初始化对象,暴漏出一个更新的办法,即可达到原有在Vuex
中更新store
中state
的成果,应用computed
能够达到getter
的成果,哪些组件须要对数据进行获取和批改只须要引入即可,更改间接就能够达到响应成果!提供一份Demo,各位对于这部分内容的封装能够见仁见智:
// xxxHelper.tsimport { del, reactive, readonly, computed, set } from '@vue/composition-api'// 定义store中数据的类型,对数据结构进行束缚interface CompositionApiTestStore { c: number [propName: string]: any}// 初始值const initState: CompositionApiTestStore = { c: 0 }const state = reactive(initState)/** 暴露出的store为只读,只能通过上面的updateStore进行更改 */export const store = readonly(state)/** 能够达到原有Vuex中的getter办法的成果 */export const upperC = computed(() => { return store.c.toUpperCase()})/** 暴漏出更改state的办法,参数是state对象的子集或者无参数,如果是无参数就便当以后对象,将子对象全副删除, 否则俺需更新或者删除 */export function updateStore( params: Partial<CompositionApiTestStore> | undefined) { console.log('updateStore', params) if (params === undefined) { for (const [k, v] of Object.entries(state)) { del(state, `${k}`) } } else { for (const [k, v] of Object.entries(params)) { if (v === undefined) { del(state, `${k}`) } else { set(state, `${k}`, v) } } }}
vueuse
vueuse是一个很好用的库,具体的装置和应用非常简单,然而性能很多很弱小,这部分我就不开展细说了,大家去看官网文档吧!
总结
这次的我的项目降级切实是无可奈何,没方法的方法,我的项目曾经宏大无比还要兼容IE,用的脚手架及相干库也都很久没有更新版本,在我的项目创立开始就曾经欠下了很多的技术债了,导致前面开发保护人员叫苦不迭(其实就是我,我的项目是别个搞的,逃…),各位老大哥在新起我的项目的时候肯定要斟酌脚手架和技术栈啊,不要前人挖坑前人填了……
如果你也在保护这样的我的项目,并且也受够了这种蹩脚的开发体验,能够参照我的教训来革新下你的我的项目,如果看过感觉对你有帮忙,也请给个一键三连~