这一章说一些基础的东西,主要谈论一些常规需求的开发思路
layout 构建
什么是 layout,就是进入系统内部后,根据页面结构划分出的布局组件。 构建一个优秀的 layout 组件对用户的体验尤为重要,对大部分维护中后台项目的开发人员来说,每次分配到的任务几乎只活动在这一部分。 但这并不意味着其他开发人员不需要关注一个中后台的布局,设计好一个优秀的中后台布局很大程度上可以降低后期的维护成本。
那么一个 layout 应该有那些部分组成呢,求同存异来看,一个系统内部基本应该具备以下结构。
-
导航栏
-
菜单栏
-
内容区
- 页眉
- 主内容
- 页脚
光有这些组成还不够,还需要可配置化 + 响应式 可配置化是什么,就是可以通过开关来控制组件的显示与否, 响应式是当浏览器视口处于不同大小时,前端页面应合理调整以提高用户体验。
我们可以以 ant design pro 为例,看看一个天花板级别的布局是怎样的
由此我们可以看出,一个优秀的 layout 布局组件应该具备以下特点
- 具备菜单栏,内容区,导航栏的可配置化
- 支持在项目不同设备上的适配
- 支持菜单栏支持响应式折叠
布局可配置化
布局可配置化,本质上就是通过状态去决定部分组件的展示与否 由于这些状态分布在不同层级,同时需求也要求一个全局的抽屉组件可以控制这些状态,因此通过 pinia 维护 这些 state 最合适不过,至于需不需要持久化或者保存在后端根据业务决定。
store 状态定义如下 navbar menu footer 控制导航栏、菜单栏、页尾的显示与隐藏 menuCollapse 控制菜单折叠 menuWidth 控制菜单宽度
js
import { defineStore } from 'pinia'
export type AppState = {
navbar: boolean
menu: boolean
menuCollapse: boolean
footer: boolean
menuWidth: number
[key: string]: unknown
}
export default defineStore('appStore', {
state(): AppState {
return {
navbar: true,
menu: true,
menuCollapse: false,
footer: true,
menuWidth: 220,
}
},
actions: {
updateSettings(partial: Partial<AppState>) {
this.$patch(partial)
},
resetSetting() {
this.$reset()
},
}
})
这样我们可以写一个 DOM 结构,然后通过全局状态来控制这些组件的渲染与否。
js
<>
<Layout>
{appStore.navbar && <Navbar />}
<Layout>
{appStore.menu && (
<Layout.Sider
width={appStore.menuWidth}
breakpoint="xl"
collapsible
hideTrigger
collapsed={appStore.menuCollapse}
onCollapse={(val) => (appStore.menuCollapse = val)}
>
<MenuComponent></MenuComponent>
</Layout.Sider>
)}
<Layout >
<TabBar />
<BreadcrumbComponent />
<Layout.Content>
<PageComponent />
</Layout.Content>
{appStore.footer && <FooterComponent />}
</Layout>
</Layout>
</Layout>
<AppSetting />
</>
响应式布局
菜单的响应式折叠简单,通过媒体查询宽度控制菜单折叠状态即可 但是项目支持移动端设备呢? 事实上,项目支持移动端设备的成本是很高的,即便是 ant-design-pro ,也会出现文字超出边框的样式问题,因此,大部分中后台管理系统做到布局可配置化,跟布局响应式即可,至于是否支持移动设备,再跟产品经理跟业务诉求确认即可。
ant design pro 的设计理念是避免横向滚动条,宽度自适应的模式开发。 但对于主要用户为桌面端的中后台管理系统来说,横向滚动条并不是很影响体验。
由此,我们可以采取另外一种比较经济的解决方案,通过将内容的盒子撑满到整个屏幕,内容区给一个 min-width 限制宽度下限,通过 padding 预留出导航栏跟菜单栏的位置,这样浏览器纵向滚动条跟横向滚动条都是来控制内容区域的滚动(这也是大部分静态文档站点的布局思路),这样的好处是我们 router-view 导航的组件有一个最小宽度,因此不必再进行内部的响应式调整。
浏览器端数据处理
前端侧也需要对一些数据进行管控,比如导航路由表,多语言支持的语料库(不考虑服务端路由跟多语言版本构建),这些数据都是写死在前端侧的数据,打包后一起存放在静态资源服务器的。 前端对其中内容的消费并不复杂,只需读取就以足够,因此可以利用 vite 的 提供的批量处理的功能做一些自动化的引入
Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:
js
const modules = import.meta.glob('./dir/*.js')
以上将会被转译为下面的样子:
js
// vite 生成的代码
const modules = {
'./dir/foo.js': () => import('./dir/foo.js'),
'./dir/bar.js': () => import('./dir/bar.js'),
}
你可以遍历 modules 对象的 key 值来访问相应的模块:
js
for (const path in modules) {
modules[path]().then((mod) => {
console.log(path, mod)
})
}
匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入 { eager: true } 作为第二个参数
因此我们对这些结构类似的数据可以使用批量导入的方式,进行 import.meta.glob 处理 批量引入后,通过建立循环,构建一个新的 object 赋值给 createI18n
路由表也是同理,通过批量导入收集到页面级别的路由数据后提供给菜单进行消费
构建代码提交工具链
真实项目中,尤其是多人协作的场景下,代码规范就十分重要,它可以用来统一团队代码风格,避免不同风格的代码混杂到一起难以阅读。其中 lint -staged 对这条工具链的定位比较形象,git 仓库中不允许 💩 进入。
那么这条工具链是如何做到的呢? 核心在于 husky,大家都知道渲染框架会把一些组件的声明周期向外暴露给开发中传入回调函数,如 onMounted,git 也有这样的钩子向外暴露,叫 git hook,但这种钩子并不是开箱即用的, husky 就是将其封装成了工具包,可以让开发者通过一些命令行指令,快速构建一些脚本文件 比如这条命令
sql
npx husky add .husky/pre-commit "npm test"
git add .husky/pre-commit
就是在用户每次 commit 之前都去执行 npm test 的命令,
由此,我们可以围绕这个工具加上现有的各种格式化,质量工具搭建一条代码风格质量工作流
- commit-msg + commitlint
利用 commit-msg 的钩子检测每次 commit 的 message 是否符合团队规范
没有类型的 commit message 是会被【打回】的
- pre-commit + formatter + linter
利用 pre-commit 的钩子在每次 commit 之前进行代码质量检测以及代码格式化操作
但是这么做是随着项目体积的增大,lint 跟 formatter 的时间也会变长的,因为每次提交的内容只是一小部分,却需要对整个项目进行检测跟格式化。
lint-staged 就出现了 lint-staged 的官方介绍是这样的
Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow, and linting results can be irrelevant. Ultimately you only want to lint files that will be committed. This project contains a script that will run arbitrary shell tasks with a list of staged files as an argument, filtered by a specified glob pattern.
该工具脚本将运行任意 shell 任务,并以暂存文件列表作为参数,并按指定的 glob 模式进行过滤,以此达到每次 commit 的内容进行检测的效果。
这样,每次我们开发的内容,经过 commit 的信息检测,代码质量校验,代码自动格式化后,才会成功 commit。
工具链配置实际上是较为复杂的东西,内部还有许多坑要踩,上述只是对大致流程进行了说明,有兴趣的同学可以参考 ant-design-pro 的配置 跟 linter上手指南 。
但是事实上这条工具链也是可以进行 commit 逃逸的,就是不去安装 husky。
提取业务类型
使用 typescript 的好处封装每一个接口调用可以对响应数据进行推断,请求参数进行限制,但是接口的类型如何获取呢?
有些开源的工具可以进行转换,比如JSON to TypeScript
但是这种工具是有限制的,Object 中可以提取的信息十分有限, 理想的提取信息的数据源应该是 JSON 和 YAML ,同时上述工具另外如果涉及到大量的接口转换就变得繁琐。
这样就可以使用 swagger-typescript-api,这个工具组件的用途在于,可以自动化的从后端接口文档中提取出前端需要的需要的业务类型,或者直接帮你生成接口函数
这里以开源的 swagger 文档 petstore3.swagger.io/#/ 为例,
如何快速提取出这个文档内的所有接口类型呢?我们只需输入
bash
npx swagger-typescript-api -p https://petstore3.swagger.io/api/v3/openapi.json -o ./src -n myApi.ts
typescript
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface Order {
/**
* @format int64
* @example 10
*/
id?: number
/**
* @format int64
* @example 198772
*/
petId?: number
/**
* @format int32
* @example 7
*/
quantity?: number
/** @format date-time */
shipDate?: string
/**
* Order Status
* @example "approved"
*/
status?: 'placed' | 'approved' | 'delivered'
complete?: boolean
}
export interface Customer {
/**
* @format int64
* @example 100000
*/
id?: number
/** @example "fehguy" */
username?: string
address?: Address[]
}
export interface Address {
/** @example "437 Lytton" */
street?: string
/** @example "Palo Alto" */
city?: string
/** @example "CA" */
state?: string
/** @example "94301" */
zip?: string
}
export interface Category {
/**
* @format int64
* @example 1
*/
id?: number
/** @example "Dogs" */
name?: string
}
export interface User {
/**
* @format int64
* @example 10
*/
id?: number
/** @example "theUser" */
username?: string
/** @example "John" */
firstName?: string
/** @example "James" */
lastName?: string
/** @example "john@email.com" */
email?: string
/** @example "12345" */
password?: string
/** @example "12345" */
phone?: string
/**
* User Status
* @format int32
* @example 1
*/
userStatus?: number
}
export interface Tag {
/** @format int64 */
id?: number
name?: string
}
export interface Pet {
/**
* @format int64
* @example 10
*/
id?: number
/** @example "doggie" */
name: string
category?: Category
photoUrls: string[]
tags?: Tag[]
/** pet status in the store */
status?: 'available' | 'pending' | 'sold'
}
export interface ApiResponse {
/** @format int32 */
code?: number
type?: string
message?: string
}
export type QueryParamsType = Record<string | number, any>
export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'>
export interface FullRequestParams extends Omit<RequestInit, 'body'> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean
/** request path */
path: string
/** content type of request body */
type?: ContentType
/** query params */
query?: QueryParamsType
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat
/** request body */
body?: unknown
/** base url */
baseUrl?: string
/** request cancellation token */
cancelToken?: CancelToken
}
export type RequestParams = Omit<FullRequestParams, 'body' | 'method' | 'query' | 'path'>
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string
baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'>
securityWorker?: (
securityData: SecurityDataType | null
) => Promise<RequestParams | void> | RequestParams | void
customFetch?: typeof fetch
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D
error: E
}
type CancelToken = Symbol | string | number
export enum ContentType {
Json = 'application/json',
FormData = 'multipart/form-data',
UrlEncoded = 'application/x-www-form-urlencoded',
Text = 'text/plain'
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = '/api/v3'
private securityData: SecurityDataType | null = null
private securityWorker?: ApiConfig<SecurityDataType>['securityWorker']
private abortControllers = new Map<CancelToken, AbortController>()
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams)
private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer'
}
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig)
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data
}
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key)
return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}`
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key])
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key]
return value.map((v: any) => this.encodeQueryParam(key, v)).join('&')
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {}
const keys = Object.keys(query).filter((key) => 'undefined' !== typeof query[key])
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key)
)
.join('&')
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery)
return queryString ? `?${queryString}` : ''
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === 'object' || typeof input === 'string')
? JSON.stringify(input)
: input,
[ContentType.Text]: (input: any) =>
input !== null && typeof input !== 'string' ? JSON.stringify(input) : input,
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key]
formData.append(
key,
property instanceof Blob
? property
: typeof property === 'object' && property !== null
? JSON.stringify(property)
: `${property}`
)
return formData
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input)
}
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {})
}
}
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
return abortController.signal
}
return void 0
}
const abortController = new AbortController()
this.abortControllers.set(cancelToken, abortController)
return abortController.signal
}
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
abortController.abort()
this.abortControllers.delete(cancelToken)
}
}
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{}
const requestParams = this.mergeRequestParams(params, secureParams)
const queryString = query && this.toQueryString(query)
const payloadFormatter = this.contentFormatters[type || ContentType.Json]
const responseFormat = format || requestParams.format
return this.customFetch(
`${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`,
{
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { 'Content-Type': type } : {})
},
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
body: typeof body === 'undefined' || body === null ? null : payloadFormatter(body)
}
).then(async (response) => {
const r = response as HttpResponse<T, E>
r.data = null as unknown as T
r.error = null as unknown as E
const data = !responseFormat
? r
: await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data
} else {
r.error = data
}
return r
})
.catch((e) => {
r.error = e
return r
})
if (cancelToken) {
this.abortControllers.delete(cancelToken)
}
if (!response.ok) throw data
return data
})
}
}
/**
* @title Swagger Petstore - OpenAPI 3.0
* @version 1.0.17
* @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
* @termsOfService http://swagger.io/terms/
* @baseUrl /api/v3
* @externalDocs http://swagger.io
* @contact <apiteam@swagger.io>
*
* This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
* Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
* You can now help us improve the API whether it's by making changes to the definition itself or to the code.
* That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
*
* Some useful links:
* - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
* - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
*/
export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
pet = {
/**
* @description Update an existing pet by Id
*
* @tags pet
* @name UpdatePet
* @summary Update an existing pet
* @request PUT:/pet
* @secure
*/
updatePet: (data: Pet, params: RequestParams = {}) =>
this.request<Pet, void>({
path: `/pet`,
method: 'PUT',
body: data,
secure: true,
type: ContentType.Json,
format: 'json',
...params
}),
/**
* @description Add a new pet to the store
*
* @tags pet
* @name AddPet
* @summary Add a new pet to the store
* @request POST:/pet
* @secure
*/
addPet: (data: Pet, params: RequestParams = {}) =>
this.request<Pet, void>({
path: `/pet`,
method: 'POST',
body: data,
secure: true,
type: ContentType.Json,
format: 'json',
...params
}),
/**
* @description Multiple status values can be provided with comma separated strings
*
* @tags pet
* @name FindPetsByStatus
* @summary Finds Pets by status
* @request GET:/pet/findByStatus
* @secure
*/
findPetsByStatus: (
query?: {
/**
* Status values that need to be considered for filter
* @default "available"
*/
status?: 'available' | 'pending' | 'sold'
},
params: RequestParams = {}
) =>
this.request<Pet[], void>({
path: `/pet/findByStatus`,
method: 'GET',
query: query,
secure: true,
format: 'json',
...params
}),
/**
* @description Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*
* @tags pet
* @name FindPetsByTags
* @summary Finds Pets by tags
* @request GET:/pet/findByTags
* @secure
*/
findPetsByTags: (
query?: {
/** Tags to filter by */
tags?: string[]
},
params: RequestParams = {}
) =>
this.request<Pet[], void>({
path: `/pet/findByTags`,
method: 'GET',
query: query,
secure: true,
format: 'json',
...params
}),
/**
* @description Returns a single pet
*
* @tags pet
* @name GetPetById
* @summary Find pet by ID
* @request GET:/pet/{petId}
* @secure
*/
getPetById: (petId: number, params: RequestParams = {}) =>
this.request<Pet, void>({
path: `/pet/${petId}`,
method: 'GET',
secure: true,
format: 'json',
...params
}),
/**
* No description
*
* @tags pet
* @name UpdatePetWithForm
* @summary Updates a pet in the store with form data
* @request POST:/pet/{petId}
* @secure
*/
updatePetWithForm: (
petId: number,
query?: {
/** Name of pet that needs to be updated */
name?: string
/** Status of pet that needs to be updated */
status?: string
},
params: RequestParams = {}
) =>
this.request<any, void>({
path: `/pet/${petId}`,
method: 'POST',
query: query,
secure: true,
...params
}),
/**
* No description
*
* @tags pet
* @name DeletePet
* @summary Deletes a pet
* @request DELETE:/pet/{petId}
* @secure
*/
deletePet: (petId: number, params: RequestParams = {}) =>
this.request<any, void>({
path: `/pet/${petId}`,
method: 'DELETE',
secure: true,
...params
}),
/**
* No description
*
* @tags pet
* @name UploadFile
* @summary uploads an image
* @request POST:/pet/{petId}/uploadImage
* @secure
*/
uploadFile: (
petId: number,
data: File,
query?: {
/** Additional Metadata */
additionalMetadata?: string
},
params: RequestParams = {}
) =>
this.request<ApiResponse, any>({
path: `/pet/${petId}/uploadImage`,
method: 'POST',
query: query,
body: data,
secure: true,
format: 'json',
...params
})
}
store = {
/**
* @description Returns a map of status codes to quantities
*
* @tags store
* @name GetInventory
* @summary Returns pet inventories by status
* @request GET:/store/inventory
* @secure
*/
getInventory: (params: RequestParams = {}) =>
this.request<Record<string, number>, any>({
path: `/store/inventory`,
method: 'GET',
secure: true,
format: 'json',
...params
}),
/**
* @description Place a new order in the store
*
* @tags store
* @name PlaceOrder
* @summary Place an order for a pet
* @request POST:/store/order
*/
placeOrder: (data: Order, params: RequestParams = {}) =>
this.request<Order, void>({
path: `/store/order`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params
}),
/**
* @description For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
*
* @tags store
* @name GetOrderById
* @summary Find purchase order by ID
* @request GET:/store/order/{orderId}
*/
getOrderById: (orderId: number, params: RequestParams = {}) =>
this.request<Order, void>({
path: `/store/order/${orderId}`,
method: 'GET',
format: 'json',
...params
}),
/**
* @description For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
*
* @tags store
* @name DeleteOrder
* @summary Delete purchase order by ID
* @request DELETE:/store/order/{orderId}
*/
deleteOrder: (orderId: number, params: RequestParams = {}) =>
this.request<any, void>({
path: `/store/order/${orderId}`,
method: 'DELETE',
...params
})
}
user = {
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name CreateUser
* @summary Create user
* @request POST:/user
*/
createUser: (data: User, params: RequestParams = {}) =>
this.request<any, User>({
path: `/user`,
method: 'POST',
body: data,
type: ContentType.Json,
...params
}),
/**
* @description Creates list of users with given input array
*
* @tags user
* @name CreateUsersWithListInput
* @summary Creates list of users with given input array
* @request POST:/user/createWithList
*/
createUsersWithListInput: (data: User[], params: RequestParams = {}) =>
this.request<User, void>({
path: `/user/createWithList`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params
}),
/**
* No description
*
* @tags user
* @name LoginUser
* @summary Logs user into the system
* @request GET:/user/login
*/
loginUser: (
query?: {
/** The user name for login */
username?: string
/** The password for login in clear text */
password?: string
},
params: RequestParams = {}
) =>
this.request<string, void>({
path: `/user/login`,
method: 'GET',
query: query,
format: 'json',
...params
}),
/**
* No description
*
* @tags user
* @name LogoutUser
* @summary Logs out current logged in user session
* @request GET:/user/logout
*/
logoutUser: (params: RequestParams = {}) =>
this.request<any, void>({
path: `/user/logout`,
method: 'GET',
...params
}),
/**
* No description
*
* @tags user
* @name GetUserByName
* @summary Get user by user name
* @request GET:/user/{username}
*/
getUserByName: (username: string, params: RequestParams = {}) =>
this.request<User, void>({
path: `/user/${username}`,
method: 'GET',
format: 'json',
...params
}),
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name UpdateUser
* @summary Update user
* @request PUT:/user/{username}
*/
updateUser: (username: string, data: User, params: RequestParams = {}) =>
this.request<any, void>({
path: `/user/${username}`,
method: 'PUT',
body: data,
type: ContentType.Json,
...params
}),
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name DeleteUser
* @summary Delete user
* @request DELETE:/user/{username}
*/
deleteUser: (username: string, params: RequestParams = {}) =>
this.request<any, void>({
path: `/user/${username}`,
method: 'DELETE',
...params
})
}
}
甚至内部还对每个接口进行了封装,但是如果我们如果不想要这种封装,只需要类型呢,只需加参数 --no-client
bash
npx swagger-typescript-api -p https://petstore3.swagger.io/api/v3/openapi.json -o ./src -n myApi.ts --no-client
就可得到
php
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface Order {
/**
* @format int64
* @example 10
*/
id?: number
/**
* @format int64
* @example 198772
*/
petId?: number
/**
* @format int32
* @example 7
*/
quantity?: number
/** @format date-time */
shipDate?: string
/**
* Order Status
* @example "approved"
*/
status?: 'placed' | 'approved' | 'delivered'
complete?: boolean
}
export interface Customer {
/**
* @format int64
* @example 100000
*/
id?: number
/** @example "fehguy" */
username?: string
address?: Address[]
}
export interface Address {
/** @example "437 Lytton" */
street?: string
/** @example "Palo Alto" */
city?: string
/** @example "CA" */
state?: string
/** @example "94301" */
zip?: string
}
export interface Category {
/**
* @format int64
* @example 1
*/
id?: number
/** @example "Dogs" */
name?: string
}
export interface User {
/**
* @format int64
* @example 10
*/
id?: number
/** @example "theUser" */
username?: string
/** @example "John" */
firstName?: string
/** @example "James" */
lastName?: string
/** @example "john@email.com" */
email?: string
/** @example "12345" */
password?: string
/** @example "12345" */
phone?: string
/**
* User Status
* @format int32
* @example 1
*/
userStatus?: number
}
export interface Tag {
/** @format int64 */
id?: number
name?: string
}
export interface Pet {
/**
* @format int64
* @example 10
*/
id?: number
/** @example "doggie" */
name: string
category?: Category
photoUrls: string[]
tags?: Tag[]
/** pet status in the store */
status?: 'available' | 'pending' | 'sold'
}
export interface ApiResponse {
/** @format int32 */
code?: number
type?: string
message?: string
}
swagger-typescript-api 内部参数比较丰富,足以满足日常开发需求
类似的工具还有 pont 跟 openapi-typescript。
标签页需求
这是一个在 arco-design-pro 内未经过测试的一个组件,需求大致等同于浏览器上的标签页条,Vue-TSX-Admin 进行了复现,我觉得也很有意思,分享给大家。 标签页一般来说需要的功能有
- 记录打开的菜单,进行组件缓存
- 标签页可以关闭,清理缓存
- 标签页具备右击出现选项,可以批量对标签页进行操作
- 系统目前所处路由必须跟高亮标签页保持一致
毫无疑问,这个需求要用到 KeepAlive,如果说需要缓存, Component 组件被 KeepAlive 包裹即可 但就目前来讲,KeepAlive 提供控制缓存的 prop 是即为有限的,我们只能可以通过 exclude 跟 include 配置那些页面缓存,那些页面不缓存。 于是首先需要将 route 的 name 跟组件的 name 设置为一致的 value,或者说建立一个映射,能够根据 route 的 name 读取到组件的name,因为 include 跟 exclude 针对组件名称的进行缓存控制,而我们进行页面路由跳转时只能获取到 route 的信息,并不能得到组件名。
之后就是需求的难点,如何清理缓存呢?我最开始尝试使用 exclude 来控制,但是这样就出现一个问题,exclude 后的页面组件,如何去重新进行缓存呢,exclude 绑定的值一定是一个动态的值,因为关闭掉标签页之后,缓存清理,重新打开这个标签页,要进行新的缓存,KeepAlive 内部逻辑是异步缓存,并不能同步的通过 exclude 属性的赋值又清空来控制,因此我在使用 exclude 的过程中并没有一个合适的时机去重新缓存标签页,因此只能转向 include 这个属性。 include 就很简单了,当我们跳去到一个新的路由时,根据获取到的组件名,修改 KeepAlive 绑定的 include 值,这样就达到了缓存当前路由页面的效果,清理缓存时,只需要对绑定的 list 进行修改即可。
另外关于右键批量操作,我们可以写一个 switch 进行功能维护,内部逻辑也是通过 store 的 action 对 list 进行更新的操作,也就不多细讲了。 这样大致就完成了一个通过标签页跳转缓存页面的初步模型,由于个人精力有限,并没有向下开发并进行测试,有兴趣的同学自行拓展。
本文所涉及的技术在 vue-tsx-admin 中可以找到完整的实例,希望对你写 Vue 的项目有所帮助。欢迎 star 和提出不足。
系列文章: