插件开发目的 :由于我司使用的前端技术栈为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举例
TypeScript
// 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
TypeScript
// vite4.X以上版本
// src/api/index.ts
const models = {}
const modules = import.meta.glob('./module/*.ts')
const moduleEntries = Object.entries(module)
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
TypeScript
// src/main.ts
import { createApp } from 'vue'
import api from '@/api'
const app = createApp(App)
app.config.globalProperties.$api = api
TypeScript
//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添加类型即可
TypeScript
//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文件
TypeScript
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(随意)
TypeScript
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里引入
TypeScript
import watchFolderPlugin from './generateApi'
export default defineConfig({
plugins: [
//...otherPlugins
watchFolderPlugin({
fileName: pathResolve('src/api/Api.d.ts'),
folderName: pathResolve('src/api/module'),
}),
],
// ...otherConfig
})