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
Since the version of
webpack
3.6
, I tried to upgrade to4、5
several times and gave up because of a large number of configuration and intrusive code modifications, so I directly found the following librariesnpm i -D ts-loader@3.5.0 tslint@6.1.3 tslint-loader@3.6.0 fork-ts-checker-webpack-plugin@3.1.1
- The next step is to change
webpack
configuration, and modifymain.js
filemain.ts
, and add in the first line of the file// @ts-nocheck
letTS
from checking the file inwebpack.base.config.js
entrance of the corresponding changemain.ts
- In
webpack.base.config.js
ofresolve
inextensions
increase in.ts
and.tsx
,alias
rules add a'vue$': 'vue/dist/vue.esm.js'
- Add
plugins
option inwebpack.base.config.js
fork-ts-checker-webpack-plugin
, putts check
a separate process, reduce the development server startup time In
webpack.base.config.js
filerules
increase in two configurations andfork-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
tsconfig.json
file to the root directory to supplement the corresponding configuration, and add thevue-shim.d.ts
declaration file to thesrc
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:
path
orname
, it may be inconsistent and inconsistent- It is inconvenient for developers to find the single file corresponding to the route when maintaining the old code
- To manually avoid routes
name
andpath
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 correspondingfunction
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 betypings/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
- install
@vue/componsition-api
npm i @vue/componsition-api
- Combined API can be used in
.vue
file inmain.ts
inuse
import VueCompositionAPI from '@vue/composition-api'
// ...
Vue.use(VueCompositionAPI)
Some notes on using composite APIs in Vue2
- 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 Theuse
function makes the component code structure clearer and more convenient to reuse business logic. - All the modular Api
api
required from@vue/composition-api
introduced, and then useexport default defineComponent({ })
replace the originalexport default { }
wording of syntax and to enable modular ApiTypescript
type inference (script
need to add correspondinglang="ts"
ofattribute
) template
in the wording andVue2
consistent, without paying attention toVue3
inv-model
and similar.native
event modifierVue3
cancel the other ofbreak change
The method in the parent component is called in the child component by using
setup(props, ctx)
inctx.emit(eventName, params)
. The properties and methods mounted on theVue
ctx.root.xxx
, including$route
,$router
etc. For the convenience of use, the firstsetup
The line is to declare thectx.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 propertiesVue
interface on thevue/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 // ... 各位根据自己的业务情况自行添加 } }
- All
template
used to variables method, the object needssetup
thereturn
, other use within a logical page need notreturn
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
- 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.
- In the moment
Vue2.6
version by@vue/composition-api
using modular Api can not usesetup
syntactic sugar , later to beVue2.7
versionrelease
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 ~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。