奈何本人没文化,后台管理走天下(二) 通用需求的开发思路

发布时间 2024-01-02 18:59:52作者: manyuemeiquqi

这一章分享些基础的东西,主要谈论一些常规需求的开发思路

layout 构建

什么是 layout,就是进入中后台系统内部后,根据页面结构划分出的布局组件,也就是页面的骨架。
构建一个优秀的 layout 组件对用户的体验尤为重要,对大部分维护中后台项目的开发人员来说,每次分配到的任务几乎只活动在内容区这一部分,其余地方并不需要考虑。
image.png
但这并不意味着其他开发人员不需要关注一个中后台的布局,设计好一个优秀的中后台布局很大程度上可以降低后期的维护成本。


那么一个 layout 应该有那些部分组成呢,求同存异来看,一个系统内部基本应该具备以下结构。

  • 导航栏
  • 菜单栏
  • 内容区
    • 页眉
    • 主内容
    • 页脚

光有这些组成还不够,在这个基础上还需要支持可配置化 + 响应式
可配置化是可以通过开关来控制布局结构
响应式是当浏览器视口处于不同大小时,前端页面应合理调整以提高用户体验。

我们可以 ant design pro 为例,看看一个天花板级别的布局是怎样的
动画.gif
image.png
image.png

由此我们可以看出,一个优秀的 layout 布局组件应该具备以下特点

  • 具备菜单栏,内容区,导航栏的可配置化
  • 支持在项目不同设备上的适配
  • 支持菜单栏支持响应式折叠

布局可配置化

布局可配置化,本质上就是通过状态去决定部分组件的展示与否
由于这些状态分布在不同层级,同时需求也要求一个全局的抽屉组件可以控制这些状态,因此通过 pinia 维护 这些 state 最合适不过,至于需不需要持久化或者保存在后端根据业务决定。

store 状态定义如下
navbar menu footer 控制导航栏、菜单栏、页尾的显示与隐藏
menuCollapse 控制菜单折叠
menuWidth 控制菜单宽度

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()
    },

  }
})

依据这些状态,写出页面的结构,然后通过全局状态来控制这些组件的渲染与否。

     <>
          <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 />
        </>

这样一个 layout 的基本骨架已经完成。

响应式布局

动画.gif

菜单的响应式折叠比较容易开发,通过媒体查询宽度控制菜单折叠状态即可
但是如何让项目支持移动端设备呢?
事实上,项目支持移动端设备的成本是很高的,即便是 ant-design-pro ,也会出现文字超出边框的样式问题,因此,大部分中后台管理系统做到布局可配置化,跟布局响应式即可,至于是否支持移动设备,再跟产品经理跟业务诉求确认即可。

image.png

ant design pro 的设计理念是避免横向滚动条,宽度自适应的模式开发。
但对于主要用户为桌面端的中后台管理系统来说,横向滚动条并不是很影响体验,由此,我们可以采取另外一种比较经济的解决方案,通过将内容区的盒子撑满到整个屏幕,内容区给一个 min-width 限制宽度下限,通过 padding 预留出导航栏跟菜单栏的位置,这样浏览器纵向滚动条跟横向滚动条都是控制内容区域的滚动(这也是大部分静态文档站点的布局思路),这样做的好处是我们 router-view 导航的组件有一个最小宽度,因此不必再进行内部的响应式调整。
image.png

浏览器端数据处理

前端侧也需要对一些数据进行管控,比如导航路由表,多语言支持的语料库(不考虑服务端路由跟多语言版本构建),这些数据都是写死在前端的数据,打包后一起存放在静态资源服务器的。
前端对其中内容的消费并不复杂,只需读取就以足够,因此可以利用 vite 的 提供的批量处理的功能做一些自动化的引入
image.png

Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:

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 处理,建立静态数据自动化引入的工作流
image.png
批量引入后,通过建立循环,构建一个新的 object 赋值给 createI18n

路由表也是同理,通过批量导入收集到页面级别的路由数据后提供给菜单进行使用
image.png

构建代码提交工具链

真实项目中,尤其是多人协作的场景下,代码规范就十分重要,它可以用来统一团队代码风格,避免不同风格的代码混杂到一起难以阅读。
这条工具链的作用只有一个,git 仓库中不允许 ? 进入。
image.png

那么这条工具链是如何做到的呢?
核心在于 git hook,大家都知道渲染框架会把一些组件的生命周期向外暴露给开发者进行回调函数的注册,比如 onMounted 等,git 也有这样的钩子向外暴露,叫 git hook,但这种钩子并不是开箱即用的, husky 就是将其封装成了工具包,可以让开发者通过键入一些命令行指令,快速构建 git hook 的脚本文件
比如这条命令

npx husky add .husky/pre-commit "npm test"
git add .husky/pre-commit

目的就是在用户每次 commit 之前都去执行 npm test 的命令

由此,我们可以围绕这个 husky 加上现有的格式化、代码质量检测工具搭建一条团队内部的代码风格质量工作流

这里我们利用 commit-msg 的钩子,检测每次 commit 的 message 是否符合团队规范
image.png

如果 message 不符合规范commit lint 规范,本次 commit 是会被【打回】的
image.png

  • pre-commit + formatter + linter

还可以利用 pre-commit 的钩子,在每次 commit 之前进行代码质量检测以及代码格式化操作
image.png

但这么也带来了缺点,随着项目体积的增大,lint 跟 formatter 也需要更多的时间去遍历整个项目,导致每次 commit 耗费在 pre-commit 的时间越来越长。

那么有没有方案解决这个问题呢 ?
工具链上在添加 lint -staged ,这个工具的目的是为了提高 lint 跟 formatter 的速度,每次工具只针对暂存区的文件进行处理,即增量化处理。这样,一条检测 commit message,commit 代码质量,进行代码格式化的工具链基本搭建完毕了。

实际上是上述工具链内部还有许多坑要踩,上述只是对大致流程进行了说明,有兴趣的同学可以参考 ant-design-pro 的配置linter上手指南 即可。

如果某次迭代特别紧急,我不想进行这些 commit 校验该怎么做呢?事实上这条工具链也是可以进行 commit 逃逸的,就是不去安装 husky。

提取业务类型

使用 typescript 的好处封装每一个接口调用可以对响应数据进行推断,请求参数进行限制,但是接口的类型如何获取呢?
image.png

业界有开源的工具可以进行转换,比如JSON to TypeScript,左边输入 JSON,右侧就同步出对应的类型。
image.png

但是这种工具是有限制的,后端返回的响应体中可以提取的信息十分有限,
理想的提取信息的数据源应该是 OpenAPI Specification,即 swagger 文档里的 JSON 和 YAML
同时上述工具另外如果涉及到大量的接口转换就变得无能为力了。

这里就可以使用 swagger-typescript-api了,这个 node 工具的用途在于,可以自动化的从后端接口文档中提取出前端需要的需要的业务类型,或者直接帮你生成接口函数。

这里以开源的 swagger 文档 https://petstore3.swagger.io/#/ 为例,
image.png

如何快速提取出这个文档内的所有接口呢?
我们只需输入

npx swagger-typescript-api -p https://petstore3.swagger.io/api/v3/openapi.json -o ./src -n myApi.ts

动画.gif

就可以得到下面的输出代码

/* 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
      })
  }
}

甚至内部还对每个接口进行了封装

但是如果我们如果不想要这种封装,只需要类型呢

npx swagger-typescript-api -p https://petstore3.swagger.io/api/v3/openapi.json -o  ./src -n myApi.ts  --no-client 

就可得到

/* 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 功能十分强大,可以使用的参数还有很多,甚至也可以提供给用户自定义请求封装模板的接口,足以满足日常开发需求
image.png

类似的工具还有 pont 跟 openapi-typescript

标签页需求

这是一个在 arco-design-pro 内未经过测试的一个组件,需求大致等同于浏览器上的标签页条,Vue TSX Admin 进行了复现,我觉得也很有意思,分享给大家。动画.gif
标签页一般来说需要的功能有

  • 记录打开的菜单,进行组件缓存
  • 标签页可以关闭,清理缓存
  • 标签页具备右击出现选项,可以批量对标签页进行操作
  • 系统目前所处路由必须跟高亮标签页保持一致

image.png

毫无疑问,这个需求要用到 KeepAlive,如果说需要缓存, Component 组件被 KeepAlive 包裹即可
image.png
但就目前来讲,KeepAlive 提供控制缓存的 prop 是即为有限的,只能通过 exclude 跟 include 配置那些页面缓存,那些页面不缓存。
于是首先需要将 route 的 name 跟组件的 name 设置为一致的 value,或者说建立一个映射,能够根据 route 的 name 读取到组件的name,因为 include 跟 exclude 针对组件名称的进行缓存控制,而我们进行页面路由跳转时只能获取到 route 的信息,并不能得到组件名。
image.png

之后就是需求的难点,如何清理缓存呢?我最开始尝试使用 exclude 来控制,但是这样就出现一个问题,exclude 后的页面组件,如何去重新进行缓存呢,exclude 绑定的值一定是一个动态的值,因为关闭掉标签页之后,缓存清理,重新打开这个标签页,要进行新的缓存,KeepAlive 内部逻辑是异步缓存,并不能同步的通过 exclude 属性的赋值又清空来控制,因此我在使用 exclude 的过程中并没有一个合适的时机去重新缓存标签页,因此只能转向 include 这个属性。
include 就很简单了,当我们跳去到一个新的路由时,根据获取到的组件名,修改 KeepAlive 绑定的 include 值,这样就达到了缓存当前路由页面的效果,清理缓存时,只需要对绑定的 list 进行修改即可。
image.png

另外关于右键批量操作,我们可以写一个 switch 进行功能维护,内部逻辑也是通过 store 的 action 对 list 进行更新的操作,也就不多细讲了。
image.png
这样大致就完成了一个通过标签页跳转缓存页面的初步模型,由于个人精力有限,并没有向下开发并进行测试,有兴趣的同学自行拓展。

本文所涉及的技术在 vue-tsx-admin 中可以找到完整的实例,希望对你写 Vue 的项目有所帮助。欢迎 star 和提出不足。
系列文章: