1
头图

reason

An existing project was created 2 years ago. With the passage of time, the amount of code has soared to nearly tens of thousands of files, but the engineering has slowly reached an unmaintainable state. I want to give him a big change, but intrusion There are too many code configurations..., and finally introduced TypeScript, combined Api, and vueuse in a compromised way, which improved the engineering standardization of the project.

First configure TypeScript related

Installation and configuration of some libraries

  1. Since the version of webpack 3.6 , I tried to upgrade to 4、5 several times and gave up because of a large number of configuration and intrusive code modifications, so I 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 webpack configuration, and modify main.js file main.ts , and add in the first line of the file // @ts-nocheck let TS from checking the file in webpack.base.config.js entrance of the corresponding change main.ts
  3. In webpack.base.config.js of resolve in extensions increase in .ts and .tsx , alias rules add a 'vue$': 'vue/dist/vue.esm.js'
  4. Add plugins option in webpack.base.config.js fork-ts-checker-webpack-plugin , put ts check a separate process, reduce the development server startup time
  5. In webpack.base.config.js file rules increase in two configurations and fork-ts-checker-webpack-plugin plug-in configuration

    {
      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 file to the root directory to supplement the corresponding configuration, and add the vue-shim.d.ts declaration file to the src

    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
    }

Improvements in routing configuration

The original routing configuration is by configuring path , name and component , so there are some disadvantages in the process of development and maintenance:

  1. path or name , it may be inconsistent 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 routes name and path do not conflict with other routes

All routing paths are extracted into different enumerations according to the business. The definition in the enumeration can prevent the path , it can also key more semantically, and it can be Typescript , which can be done in one step when looking for a route corresponding to a single file

Why not use name , because name is just a semantic to identify this route, when we use the enumerated type path , the enumerated Key is enough to act as a semantic path path the name attribute does not need to exist, we are declaring the route When you don't need to declare the name attribute, you only need 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 Enums

Before our project is also constant through all pulled into services/const in management, now integrated Typescript After that, we will be in after the project services/constant manage constants in services/enums management enumeration.

Such as common interfaces returned code can be declared as an enumeration, do not use the time you needed handwriting if (res.code === 200) similar judgment, directly by declaring good RES_CODE enumerate all the interfaces directly to the return code type

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

For example, storage of key can be declared in services/constant/storage.ts

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

/** 与用户相关的key可以通过构造一个带业务属性参数的纯函数来声明 */
export const UserSpecialInfo = (userId: string) => {
  return `specialInfo-${userId}`
}

Type Declaration File Specification

The global type declaration files are uniformly typings folder of the root directory (reusable data types)

In comparison, the types in the process of assembling data in the partial business can be maintained directly in the component where they are located (data structure that is not easy to reuse)

Type encapsulation in interfaces

Request base class encapsulation logic

requestWrapper.ts file under the utils folder, after which all the request base class method encapsulation can be maintained in this file

// 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中定义的一个命名空间 后面会有代码说明
}

used after encapsulation in the specific business layer

Create a new index.ts file in api/user . Compared with the previous one, it can be concise enough, and can also provide type hints to know what the request is and the parameters and return values of the parameters.

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

// 此处只需要在注释中标注这个接口是什么接口,不需要我们通过注释来标识需要什么类型的参数,TS会帮我们完成, 只需要我们填充请求参数的类型和返回参数的类型即可约束请求方法的使用
/** 获取用户信息 */
export function getUserInfo(query: User.UserInfoReqType) {
  return PostWrapper<User.UserInfoResType>(
    '/api/userinfo',
    query
  )
}
  • The interface that needs to provide type support needs to be declared in the api/**/*.ts file, and the corresponding function marked with the parameter request type and response type
  • If the structure is very simple, you don't need typings/request/*.d.ts , you can directly declare the type at the package interface. If there are a little more parameters, they should be typings/request/*.d.ts to avoid confusion.

Now, the interface returned by the server in the business is 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 to declare the base class structure returned by the request typings/request/index.d.ts In the specific xxx.d.ts , complete the specific request type declaration, such as user.d.ts User , declare the global namespace 061dfa3d3a1202 in this file to manage the request and response data types of all such job interfaces
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
  // ...
}

At this point, the TypeScript related is over, and the next is the combined API.

Using Combined Api in Vue2

  1. install @vue/componsition-api
npm i @vue/componsition-api
  1. Combined API can be used in .vue file in main.ts in use
import VueCompositionAPI from '@vue/composition-api'
// ...
Vue.use(VueCompositionAPI)

Some notes on using composite APIs in Vue2

  1. The combined Api document , those who don’t know it, can refer to the document to learn it first. In the case of more complex pages and many components, the combined API is Options API , and the logic can be extracted and packaged separately The use function makes the component code structure clearer and more convenient to reuse business logic.
  2. All the modular Api api required from @vue/composition-api introduced, and then use export default defineComponent({ }) replace the original export default { } wording of syntax and to enable modular Api Typescript type inference ( script need to add corresponding lang="ts" of attribute )
  3. template in the wording and Vue2 consistent, without paying attention to Vue3 in v-model and similar .native event modifier Vue3 cancel the other of break change
  4. The method in the parent component is called in the child component by using setup(props, ctx) in ctx.emit(eventName, params) . The properties and methods mounted on the Vue ctx.root.xxx , including $route , $router etc. For the convenience of use, the first setup The line is to declare the ctx.root on 061dfa3d3a14b4 through the structure. If there are properties or methods related to business properties previously added to the Vue instance object, the types related to business properties Vue interface on the 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 {
     /** 当前环境是否是IE */
     isIE: boolean
     // ... 各位根据自己的业务情况自行添加
     }
    }
  5. All template used to variables method, the object needs setup the return , other use within a logical page need not return
  6. setup according to the page display elements and the interaction between the user and the page. The more complex logic details and data processing should be extracted to the outside as much as possible, and the code logic in the .vue
  7. Before requirement development, according to the definition of server-side interface data, the interface of data and methods in page components can be formulated. Types can be declared in advance, and then specific methods can be implemented in the development process.
  8. In the moment Vue2.6 version by @vue/composition-api using modular Api can not use setup syntactic sugar , later to be Vue2.7 version release then observed after some other precautions and restrictions

A style guide for reactive-based stores

In view of the inconvenience of accessing TS Vuex Vuex using 061dfa3d3a1624, a best practice is provided in the combined Api: declare the data that needs to be responded to in a ts file and wrap the initialization object reactive method, you can achieve the original Vuex of updating store in state , and use computed to achieve getter . Which components need to acquire and modify data only need to be imported, and the change can directly achieve the response effect! Provide a demo, you can have different opinions on the packaging of this part of the content:

// 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 is a very useful library. The specific installation and use are very simple, but it has many powerful functions. I will not go into details in this part. Let's go to the official documentation!

Summarize

This project upgrade is really a last resort. There is no other way. The project is already huge and it needs to be compatible with IE. The scaffolding and related libraries used have not been updated for a long time, and a lot of technical debts have been owed since the project was created. , causing the developers and maintenance staff to complain incessantly (actually, it was me, the project was done by someone else, escape...), when you start a new project, you must consider the scaffolding and technology stack. now...

If you are also maintaining such a project, and you are fed up with this bad development experience, you can refer to my experience to transform your project. If you feel that it is helpful to you, please give a one-click three-link ~


Senar
35 声望8 粉丝