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

51次阅读

共计 7389 个字符,预计需要花费 19 分钟才能阅读完成。

原由

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

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

正文完
 0