关于前端工程化:记一次在老掉牙的Vue2项目中引入TypeScript和组合式Api的艰辛历程

原由

现有的一个我的项目2年前创立的,随着工夫流逝,代码量曾经暴增到了将近上万个文件,然而工程化曾经缓缓到了不可保护的状态,想给他来一次大换血,然而侵入式代码配置太多了……,最终以一种斗争的形式引入了TypeScript、组合式Api、vueuse,晋升了我的项目的工程化标准水平,整个过程让我颇有感概,记录一下。

先配置TypeScript相干的

一些库的装置和配置

  1. 因为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
  2. 接下来就是改webpack的配置了,批改main.js文件为main.ts,并在文件的第一行增加// @ts-nocheckTS疏忽查看此文件,在webpack.base.config.js的入口中相应的改为main.ts
  3. webpack.base.config.jsresolve中的extensions中减少.ts.tsx,alias规定中减少一条'vue$': 'vue/dist/vue.esm.js'
  4. webpack.base.config.js中减少plugins选项增加fork-ts-checker-webpack-plugin,将ts check的工作放到独自的过程中进行,缩小开发服务器启动工夫
  5. 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
  6. 根目录中减少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
    }

路由配置的改善

原有路由配置是通过配置pathnamecomponent,这样在开发和保护的过程中有一些毛病:

  1. 应用的时候可能呈现应用path或者应用name不标准不对立的状况
  2. 开发人员在保护老代码的时候查找路由对应的单文件不不便
  3. 要手动防止路由的namepath不与其余路由有抵触

将所有的路由的门路依照业务抽离到不同的枚举中。在枚举中定义能够避免路由 path 抵触,也能够将枚举的 key 定义的更加语义化,又能够借助Typescript的类型推导能力疾速补全,在查找路由对应单文件的时候能够一步到位

为什么不必name,因为name只是一个标识这个路由的语义,当咱们应用枚举类型的path之后,枚举的Key就足以充当语义化的门路path这个name属性就没有存在的必要了,咱们在申明路由的时候就不须要申明name属性,只须要pathcomponent字段就能够了

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
}

比方storagekey咱们就能够申明在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.ts
import { 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

  1. 装置@vue/componsition-api
npm i @vue/componsition-api
  1. main.tsuse即可在.vue文件中应用组合式 API
import VueCompositionAPI from '@vue/composition-api'
// ...
Vue.use(VueCompositionAPI)

Vue2 中应用组合式 Api 中的一些注意事项

  1. 组合式 Api文档,不理解的小伙伴能够先参照文档学习一下,在比较复杂的页面,组件多的状况下组合式 API 相比传统的Options API更灵便,能够把逻辑抽离进来封装为独自的use函数,使组件代码构造更为清晰,也更不便复用业务逻辑。
  2. 所有的组合式 Api 中的api都须要从@vue/composition-api中引入,而后应用export default defineComponent({ })替换原有的export default { }的写法,即可启用组合式 Api 语法和Typescript的类型推导(script须要增加对应的lang="ts"attribute)
  3. template中的写法和Vue2中统一,无需留神Vue3中的v-model和相似.native的事件修饰符在Vue3中勾销等其余的break change
  4. 子组件中调用父组件中的办法应用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 types
    import Vue from 'vue'
    // 2. Specify a file with the types you want to augment
    //    Vue has the constructor type in types/vue.d.ts
    declare module 'vue/types/vue' {
     // 3. Declare augmentation for Vue
     interface Vue {
     /** 以后环境是否是IE */
     isIE: boolean
     // ... 各位依据本人的业务状况自行添加
     }
    }
  5. 所有template中应用到的变量、办法、对象都须要在setupreturn,其余的在页面逻辑外部应用的不须要return
  6. 举荐依据页面展现元素和用户与页面的交互行为定义setup中的办法,比较复杂的逻辑细节和对数据的解决尽量抽离到内部,放弃.vue文件中的代码逻辑清晰
  7. 在需要开发前,依据服务端接口数据的定义,来制订页面组件中的数据和办法的接口,能够提前申明类型,之后在开发过程中实现具体的办法
  8. 在当下的Vue2.6版本中通过@vue/composition-api应用组合式 Api 不能应用setup语法糖,待之后的Vue2.7版本release之后再察看,其余的一些 注意事项和限度

基于 reactive 的 store 的格调标准

鉴于在Vuex中接入TS的不便和Vuex应用场景的必要性,在组合式 Api 中提供了一个最佳实际:将须要响应的数据申明在一个ts文件中通过reactive包裹初始化对象,暴漏出一个更新的办法,即可达到原有在Vuex中更新storestate的成果,应用computed能够达到getter的成果,哪些组件须要对数据进行获取和批改只须要引入即可,更改间接就能够达到响应成果!提供一份Demo,各位对于这部分内容的封装能够见仁见智:

// xxxHelper.ts
import { 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,用的脚手架及相干库也都很久没有更新版本,在我的项目创立开始就曾经欠下了很多的技术债了,导致前面开发保护人员叫苦不迭(其实就是我,我的项目是别个搞的,逃…),各位老大哥在新起我的项目的时候肯定要斟酌脚手架和技术栈啊,不要前人挖坑前人填了……

如果你也在保护这样的我的项目,并且也受够了这种蹩脚的开发体验,能够参照我的教训来革新下你的我的项目,如果看过感觉对你有帮忙,也请给个一键三连~

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理