Bootstrap

vue3+vite插件开发

插件开发目的:由于我司使用的前端技术栈为vue3+ts+vite2.X+axios,在前端代码框架设计初期,做了把axios挂载到proxy对象上的操作,具体可见我的另一篇文章vue3+TS自动化封装全局api_ts 封装+腾讯位置api-CSDN博客

现在可以实现vue2的类似this.$api.xxx去调用接口,但是vue2源码使用的是flow来实现,并且搭配typeScript不太友好(由于装饰器语法过于复杂,这里不讨论vue2+装饰器来使用typeScript),故vue2项目没有开发webpack插件去实现代码补全

这篇文章主要介绍的是vue3+ts+vite来开发时的代码补全情况

在使用vue/react+ts开发时,我们把api挂载到全局后,例如封装好axios后需要按模块划分请求,此时会创建一个modules文件夹,里面存放各个模块的请求,当把module所有的文件都动态挂载到proxy实例上时,我们可以通过proxy.$api.文件名.请求名去发起请求

例如:proxy.$api.test_api.test()

1.把modules下所有的api挂载到proxy对象上

这里以vite2和vite4.X举例

// vite 2.X
// src/api/index.ts
// 动态加载module下所有的文件
const files = import.meta.globEager('./module/*.ts')
const models= {}
for (const key in files) {
  const keys = {}


  for (const v in files[key]) {
    Object.assign(keys, { [v]: files[key][v] })
  }
    
  models[key.replace(/(\.\/module\/|\.ts)/g, '')] = keys
}
export default models
// vite4.X以上版本
// src/api/index.ts

const models = {}

const modules = import.meta.glob('./module/*.ts')
const moduleEntries = Object.entries(modules)

for (const [key, apiFetchFn] of moduleEntries) {
  try {
    const module = await apiFetchFn()
    const keys = Object.fromEntries(Object.entries(module))
    models[key.replace(/(\.\/module\/|\.ts)/g, '')] = keys
  } catch (error) {
    console.error('API挂载初始化异常:', error)
  }
}

export default models
// src/main.ts
import { createApp } from 'vue'
import api from '@/api'
const app = createApp(App)
app.config.globalProperties.$api = api
//src/xxx.vue
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()!

async function foo(){
    const res = await proxy.$api.test_api.testFetch()
}

2.为proxy使用$api添加类型提示

第一点我们已经完成把所有的请求挂载proxy上并且可以通过proxy.$api.xxx.xxx去发起请求,为proxy添加拥有$api的类型很简单,只需要在xxx.d.ts文件声明@vue/runtime-core添加类型即可

//src/global.d.ts
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $api: any
  }
}

以上我们为proxy添加了$api属性,值为any,所以我们在使用proxy是会出现$api的代码提示,如下图

3.为$api添加代码提示

众所周知,在vue+ts开发的项目中,只要全局声明了XXX.d.ts文件,并且在里面使用declare声明的类型可以在全局访问到,那么我们思路可以放在声明一个xxx.d.ts文件,该文件的key可以取api/module下面的所有文件名,值直接使用typeScript的typeof import该文件即可获取到

3.1在api文件下新建Api.d.ts文件,里面声明所需要的文件类型,如下

        这里假使module文件夹已有test_api文件

declare namespace GlobalApi {
  interface Api {
    test_api: typeof import('@/api/module/test_api')
  }
}

3.2使用$api时出现代码提示

4.如何自动生成Api.d.ts文件内容呢

我想大家第一反应肯定是写脚本去处理,当module文件下的文件变动时,重新执行脚本即可,那么如何监听到module文件夹下文件的改动呢?但是我不想手动执行脚本怎么处理,这里我们可以使用nodejs的fs模块

4.1在根目录声明一个ts文件,起名为generateApi.ts(随意)

import fs from 'fs'
import type { Plugin } from 'vite'

/**
 * @type 插件配置项
 */
interface IPluginConfig {
  /**生成的.d.ts文件名 */
  fileName: string
  /**查找的文件夹名 */
  folderName: string
}

/**
 * @method 更新Api.d.ts内容
 * @description 生成Api类型后挂载到proxy上获取proxy?.$api.xxx代码提示
 */
function updateApiDeclaration(config: IPluginConfig) {
  const { fileName, folderName } = config
  const moduleFiles = fs.readdirSync(folderName)
  const apiContent = generateApiDeclaration(moduleFiles)
  fs.writeFileSync(fileName, apiContent)
}

/**
 * @method 根据api/module文件名生成Api.d.ts内容
 * @param {string[]}  moduleFiles 文件名
 */
function generateApiDeclaration(moduleFiles: string[]) {
  const interfaceEntries = moduleFiles.map((filename) => `  ${filename.replace('.ts', '')}: typeof import('@/api/module/${filename.replace('.ts', '')}')`).join('\n  ')
  return `declare namespace GlobalApi {
  interface Api {
  ${interfaceEntries}
  }
}
`
}

/**
 * @method 使用fs观察module文件夹变动
 * @description 观察到文件夹内的文件变动后重新生成Api.d.ts文件
 */
export default function watchFolderChange(pluginConfig: IPluginConfig): Plugin {
  return {
    name: 'watch-folder-plugin',
    config(config, { mode }) {
      if (mode === 'development') {
        fs.watch(pluginConfig.folderName, (eventType) => {
          if (['change', 'rename'].includes(eventType)) {
            updateApiDeclaration(pluginConfig)
          }
        })
      }
    },
  }
}

4.2 在vite.config.ts里引入

import watchFolderPlugin from './generateApi'

export default defineConfig({
  plugins: [
     //...otherPlugins
     watchFolderPlugin({
       fileName: pathResolve('src/api/Api.d.ts'),
       folderName: pathResolve('src/api/module'),
     }),

  ],
    // ...otherConfig


})

;