Remember the hard journey of introducing TypeScript and composite Api into the old Vue2 project

Reason

An existing project was created two years ago. With the passage of time, the amount of code has soared to nearly tens of thousands of files, but the engineering has gradually become unmaintainable. I want to give him a big change, but there are too many intrusive code configurations... Finally, TypeScript, combinatorial Api and vueuse are introduced in a compromise way, It has improved the engineering standardization of the project. The whole process makes me feel very general and record it.

Configure TypeScript related first

Installation and configuration of some libraries

  1. Since the version of webpack is still 3.6, several attempts to upgrade to 4 and 5 were abandoned because of a large number of modifications to the configuration intrusive code, so we directly found the following libraries

    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. The next step is to change the configuration of webpack and modify main JS file is main TS, and add / / @ TS nocheck in the first line of the file to let TS ignore checking the file. In webpack base. config. The corresponding in the entry of JS is changed to main ts
  3. In webpack base. config. JS is added to extensions in resolve ts and Tsx, add a 'Vue $' in the alias rule: 'Vue / dist / Vue esm. js'
  4. In webpack base. config. Add the plugins option in JS, add fork TS checker webpack plugin, and put the task of ts check into a separate process to reduce the startup time of the development server
  5. In webpack base. config. Two configurations and the plug-in configuration of fork TS checker webpack plugin are added to the rules of JS file

    {
      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()], // Process TS checker in an independent process to shorten the cold start and hot update time of webpack service https://github.com/TypeStrong/ts-loader#faster-builds
  6. Add tsconfig. In the root directory The JSON file supplements the corresponding configuration, and Vue shim. Exe is added in the src directory d. TS declaration file

    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
    }

Improvement of routing configuration

The original route is configured by configuring path, name and component, which has some disadvantages in the process of development and maintenance:

  1. The use of path or name may be nonstandard and inconsistent
  2. It is inconvenient for developers to find the single file corresponding to the route when maintaining the old code
  3. To manually avoid that the name and path of the route do not conflict with other routes

The paths of all routes are extracted into different enumerations according to services. Defining in enumeration can prevent route path conflicts, or define the enumerated key more semantically, and can be quickly completed with the help of Typescript's type derivation ability. It can be in place in one step when finding the corresponding single file of route

Why not use name? Because name is just a semantics that identifies the route. When we use an enumerated path, the enumerated Key is sufficient to serve as a semantic path. The name attribute does not exist. When we declare the route, we do not need to declare the name attribute, but only the path and component fields

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')
  }
]

Constants and enumerations

Previously, in our project, all constants were managed by pulling them out of services/const. Now, after integrating Typescript, we can manage constants in services/constant and enums in services/enums.

For example, the code returned by the common interface can be declared as enumeration, so you don't need to hand write if (res.code === 200) similar judgments when using it. You can directly use the declared res_ The code enumeration directly obtains the return code type of all interfaces

// services/enums/index.ts
/** RES_CODE Enum */
export enum RES_CODE {
  SUCCESS = 200
  // xxx
}

For example, the key of storage can be declared in services / constant / storage In TS

/** userInfo-storageKey */
export const USERINFO_STORE_KEY = 'userInfo'

/** User related key s can be declared by constructing a pure function with business attribute parameters */
export const UserSpecialInfo = (userId: string) => {
  return `specialInfo-${userId}`
}

Type declaration file specification

Global type declaration files are uniformly maintained in the types folder of the root directory (reusable data types)

The types in the process of assembling data in partial business can be maintained directly in the component (data structures that are not easy to reuse)

Type encapsulation in interface

Request base class encapsulation logic

Add requestwrapper.exe in utils folder TS file, and then all request base class method encapsulation can be maintained in this file

// src/utils/requestWrapper.ts
import { AxiosResponse } from 'axios'
import request from '@/utils/request'

// The request parameter is specific to a type only after specific encapsulation. Here, use the unknown declaration, and the return value is generic S. fill in the specific type when using
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 is a namespace defined in types, followed by a code description
}

Use after encapsulation in specific business layer

Create a new index. In api/user TS file can be concise enough compared with the previous one, and can also provide type prompt to know what the request is, the parameters of the request parameters and the return value

import { PostWrapper } from '@/utils/requestWrapper'

// Here, we only need to mark the interface in the annotation. We do not need to identify the required parameters through annotation. TS will help us complete it. We only need to fill in the types of request parameters and return parameters to restrict the use of request methods
/** Get user information */
export function getUserInfo(query: User.UserInfoReqType) {
  return PostWrapper<User.UserInfoResType>(
    '/api/userinfo',
    query
  )
}
  • Interfaces that need type support need to be declared in API / * * / * TS file, and mark the parameter request type and response type to the corresponding function
  • If the structure is extremely concise, you don't need to type / request / * d. Maintain in TS and directly declare the type at the encapsulation interface. If there are a few parameters, they should be in types / request / * d. Maintenance in TS to avoid confusion

At present, the returned interfaces of the service side in the business are basically wrapped by a layer of descriptive objects, and the business data is in the request field of the object. Based on this, we encapsulate the interface in types / request / index d. TS declares the base class structure returned by the request in the specific XXX d. Improve the specific request type declaration in TS, such as User d. An error reporting interface in TS, in which the global namespace User is declared to manage the data types of requests and responses of all such job interfaces
typings/request/index.d.ts

import { RES_CODE } from '@/services/enums'

declare global {
  // *All base classes declare types here
  namespace BASE {
    // The package layer type declaration returned by the request is provided to the specific data layer for packaging
    type BaseRes<T> = {
      code: RES_CODE
      result?: T
      info?: string
      time: number
      traceId: string
    }
    type BaseResWrapper<T> = Promise<BASE.BaseRes<T>>
    // Paging interface
    type BasePagination<T> = {
      content: T
      now: string
      page: number
      size: number
      totalElements: number
      totalPages: number
    }
  }

typings/request/user.d.ts

declare namespace User {

/** Response parameters */
type UserInfoResType = {
  id: number | string
  name: string
  // ...
}

/** Request parameters */
type UserInfoReqType = {
  id: number | string
  // ...
}

At this point, the TypeScript related work is over, and then the combined Api

Combined Api used in Vue2

  1. Install @ Vue / composition API
npm i @vue/componsition-api
  1. In main use in TS use composite API s in vue files
import VueCompositionAPI from '@vue/composition-api'
// ...
Vue.use(VueCompositionAPI)

Some considerations in using composite APIs in Vue2

  1. Combined Api file , if you don't know, you can learn from the documentation first. In the case of complex pages and many components, the combined API is more flexible than the traditional Options API. You can isolate the logic and package it into a separate use function, which makes the component code structure clearer and easier to reuse the business logic.
  2. APIs in all composite APIs need to be introduced from @ Vue / composition api, and then use export default defaultecomponent ({}) to replace the original writing method of export default {}, so as to enable the type derivation of composite api syntax and Typescript (the script needs to add the corresponding lang="ts" attribute)
  3. The writing method in template is the same as that in Vue2, and there is no need to pay attention to v-model and similar in Vue3 The event modifier of native is cancelled in Vue3 break change
  4. The method to invoke parent component in child component uses ctx. in setup(props, ctx). Just emit (eventName, params). The properties and methods attached to the Vue instance object can be accessed through CTX root. XXX, including $route, $router, etc. for ease of use, it is recommended to declare CTX through the structure in the first line of setup Attributes on root. If there are business attribute related attributes or methods previously added on the Vue instance object, you can add business attribute related types through the Vue interface on the extension module vue/types/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 {
     /** Is the current environment IE */
     isIE: boolean
     // ...  You can add it yourself according to your business situation
     }
    }
  5. All variables, methods and objects used in the template need to return in setup, and others used inside the page logic do not need to return
  6. It is recommended to define the methods in setup according to the page display elements and the interaction behavior between users and pages. The more complex logic details and data processing should be separated from the outside as far as possible to maintain The code logic in vue file is clear
  7. Before requirement development, the interface of data and methods in the page component is formulated according to the definition of server interface data. Types can be declared in advance, and then specific methods can be implemented in the development process
  8. In the current vue2 In version 6, composite Api is used through @ Vue / composition Api, and setup cannot be used Grammar sugar , vue2 Observe after the release of version 7. Others Precautions and limitations

Style specification of reactive store

In view of the inconvenience of accessing ts in Vuex and the necessity of Vuex usage scenarios, a best practice is provided in the combined Api: declare the data to be responded in a TS file, initialize the object through reactive package, and leak an update method to achieve the original effect of updating the state in the store in Vuex, and use computed to achieve the effect of getter, Which components need to acquire and modify data only need to be imported, and the change can achieve the response effect directly! A Demo is provided. You can have different opinions on the encapsulation of this part:

// xxxHelper.ts
import { del, reactive, readonly, computed, set } from '@vue/composition-api'

// Define the type of data in the store and constrain the data structure
interface CompositionApiTestStore {
  c: number
  [propName: string]: any
}

// Initial value
const initState: CompositionApiTestStore = { c: 0 }

const state = reactive(initState)

/** The exposed store is read-only and can only be changed through the updateStore below */
export const store = readonly(state)

/** It can achieve the effect of the getter method in the original Vuex */
export const upperC = computed(() => {
  return store.c.toUpperCase()
})

/** The method of changing state is exposed. The parameter is a subset of the state object or no parameter. If there is no parameter, it will facilitate the current object and delete all the sub objects. Otherwise, I need to update or delete */
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 It is a very easy to use library. The specific installation and use are very simple, but many functions are very powerful. I won't elaborate on this part. Let's go to the official documents!

summary

This project upgrade is really a last resort. There is no way. The project is huge and compatible with IE. the scaffolds and related libraries used have not been updated for a long time. They have owed a lot of technical debts since the project was created, resulting in constant complaints from the later development and maintenance personnel (in fact, I, the project is a different one, escape...), Big brothers, when starting a new project, we must consider the scaffold and technology stack. Don't dig the pit before others and fill it after others

If you are also maintaining such a project and have had enough of this bad development experience, you can refer to my experience to transform your project. If you have seen it and feel it is helpful to you, please give me a key three times~

Keywords: Front-end TypeScript Vue.js

Added by space1 on Thu, 13 Jan 2022 06:10:00 +0200