[实践总结]给Vue项目封装一个带代码提示的api插件

前言

前端经常要和后端对接接口了,封装一个通用的接口请求很有必要,不过光有统一的接口库还不够库,能不能用typescriptd.ts文件外加webpack插件实现代码提示的自动化呢?今天我来分享我的项目实践总结。

效果

GIF.gif

原理

在热更新时每次读取api目录下所有js文件,解析每个接口函数的jsdoc风格的注释,生成一个对应的d.ts描述文件

动手做

创建plugins目录

src目录创建plugins目录,再新建一个api目录

image.png

目录中手动新建index.jsrunner.jswebpack.js三个文件,apis.jsindex.d.ts是由webpack插件生成的,这里暂时先不管。

index.js

这是请求的主体了,一般的api请求封装就写在这里,最后注入到Vue的原型上,vue中可以用this.$api调用接口,下面放上我的代码

import Vue from 'vue'
import axios from 'axios'
import config from '../../../local_env.json'
import store from '../../stores'

// 将所有接口合并到apis.js中,这个文件由webpack插件生成
import { normalAPIs, successMessageAPIs } from './apis' 

let instance = null
let api = null

Vue.use({
  install(Vue, option) {
    // 实例化axios,并且设置一些通用信息,例如请求地址
    instance = axios.create({
      baseURL: option.baseURL || '',
      headers: option.headers || {},
    })

    // 合并到接口列表
    const APIs = {
      ...normalAPIs,
      ...successMessageAPIs
    }

    // 接口包装,将axios实例传入接口函数调用网络请求
    const result = {}
    for (const k in APIs) {
      result[k] = async (data) => {
        // 网络请求需要捕获错误
        try {
          const reqRes = await APIs[k](instance, data)
          if (successMessageAPIs[k] && (reqRes.data.msg || reqRes.data.message)) {
            store.commit('alert', {
              type: 'success',
              message: reqRes.data.msg || reqRes.data.message
            })
          }
          return reqRes.data
        } catch (e) {
          // 如果返回结果报错,打印报错信息
          if (e.response) {
            store.commit('alert', {
              type: 'error',
              message: e.response.data.msg || e.response.data.message
            })
            throw e
          } else { // 代码内其他报错
            store.commit('alert', {
              type: 'error',
              message: e.message
            })
            throw new Error(e.message)
          }
        }
      }
    }
    // 注入到Vue的原型,在vue实例中可以通过this.$api调用接口
    api = result
    Vue.prototype.$api = result
  }
}, {
  baseURL: config.api,
  headers: config.headers
})

export default api
 

这里我将接口的函数限定为接收两个参数的函数,第一个参数是axios的实例,第二个参数是要发给接口的参数,这样能统一写接口函数的格式。
这里还做了一些别的事情,代码里调用store.commit的部分是为了方便给有提示信息的接口发送一个通知,直接把后台返回的message信息交给一个全局的toast之类的组件来弹出提示,所以相应的把接口放在了normalAPI和successMessageAPI两个对象里

runner.js

这个文件是当文件发生变化时,将各个接口的normalAPIs和successMessageAPIs合并成一个总的对象,生成apis.js;将每个接口函数的注释中,分解出描述、参数以及返回值,最终一股脑塞到d.ts的declare interface部分,代码如下

const fs = require('fs')
const path = require('path')
/**
* 将各个api文件中的接口合并成一个文件
*/
function mergeApis () {
let list = fs.readdirSync(path.resolve(__dirname, '../../api')).filter((f) => {
return f != 'index.js' && f.endsWith('.js')
})
let apis = list.map((f) => {
let objName = path.basename(f).split(path.extname(f))[0]
let filename = f
return {
objName,
filename,
importStr: `import ${objName} from '../../api/${filename}'`
}
})
let filecontent = `
${apis.map(({importStr}) => {
return importStr
}).join('\n')
}
const normalAPIs = {
${apis.map((api) => {
return `...${api.objName}.normalAPIs`
}).join(', ')}
}
const successMessageAPIs = {
${apis.map((api) => {
return `...${api.objName}.successMessageAPIs`
}).join(', ')}
}
export {
normalAPIs,
successMessageAPIs
}
`
fs.writeFileSync(path.resolve(__dirname, './apis.js'), filecontent)
return filecontent
}
/**
* 更新api的index.d.ts文件
*/
function updateApiTypeList() {
let list = fs.readdirSync(path.resolve(__dirname, '../../api')).filter((f) => {
return f != 'index.js'
})
let finalFuncs = []
let interfaces = []
for(let j of list) {
// console.log(j)
// js文件处理
if (j.endsWith('.js')) {
finalFuncs = finalFuncs.concat(handleJsFile(j))
}
// d.ts文件处理
if (j.endsWith('.d.ts')) {
interfaces.push(handleDesTypeFile(j))
}
}
let filecontent = `
declare interface IApi {
${finalFuncs.join(',\n')}
}
${interfaces.join('\n')}
declare module 'vue/types/vue' {
interface Vue {
$api: IApi
}
}
declare var api: IApi
export default api
`
fs.writeFileSync(path.resolve(__dirname, './index.d.ts'), filecontent)
}
function handleJsFile (filename) {
// 读取文件
let module = fs.readFileSync(path.resolve(__dirname, `../../api/${filename}`), 'utf-8')
// 注释import
module = module.replace(/import(\s+)/g, '//')
// 去掉es6 module
let res = module.match(/export(\s+)default(\s+){([\sa-zA-Z0-9,]+)}/)
module = module.split(res[0])
// 包裹代码为自调用函数,返回接口对象
let moduleObj = eval(`
(() => {
${module[0]}
let result = Object.assign(normalAPIs, successMessageAPIs)
return result
})()`)
let funcs = Object.values(moduleObj)
const finalFuncs = []
for(let f of funcs) {
// console.log(f.name, '------------------')
// 获取注释的正则
let regStr = new RegExp(`(/(\\**[^\\*]*(\\*[^\\*]+)+\\*)?\\/[^\\r\\n]*)(\\s+)(const|let)(\\s+)${f.name} `)
// 获取注释
let comment = (module[0].match(regStr) || [])[0] || ''
if (comment.startsWith('}')) {
comment = comment.slice(1)
}
if (comment.endsWith(`${f.name} `)) {
let endReg = new RegExp(`(const|let)(\\s)+${f.name} `)
comment = comment.replace(endReg, '')
}
// 获取参数,暂时不支持用解构来写参数,定义接口 的时候要注意
let define = (f.toString().match(/\([a-zA-z\d,\s]+\)(\s+)=>(\s+){/) || [])[0]
if (!define) continue
define = define.replace(/\)(\s+)=>(\s+){/, '')
define = define.replace(/\((\s*)rq(\s*)([,]*)/, '')
// 参数类型从注释中获取
let defineType = ''
// 参数注释正则
const paramCommentReg = new RegExp(`@param \{([A-Za-z0-9\\[\\]<>]+)\} ${define.trim()}`)
if (define) {
const match = comment.match(paramCommentReg)
if (match) {
defineType = match[1]
}
}
// 返回结果类型获取
let returnType = 'any'
const returnCommentReg = new RegExp(`@return \{([A-za-z0-9\\[\\]<>]+)\}`)
const returnMatch = comment.match(returnCommentReg)
// console.log(f.name ,comment, returnMatch)
if (returnMatch) {
returnType = returnMatch[1]
}
// console.log(returnType)
finalFuncs.push(`${comment}${f.name}(${define.trim()}${defineType ? `:${defineType}` : ''}): Promise<${returnType}>`)
}
return finalFuncs
}
function handleDesTypeFile (filename) {
const str = fs.readFileSync(path.resolve(__dirname, `../../api/${filename}`), 'utf-8')
return str
}
module.exports = {updateApiTypeList, mergeApis}

webpack.js

这个文件是webpack插件的定义部分了,代码如下

const {updateApiTypeList, mergeApis} = require('./runner')
function AutoApiPlugin(options) {}
AutoApiPlugin.prototype.apply = function(compiler) {
let filelist = mergeApis()
compiler.plugin('emit', function(compilation, callback) {
try {
updateApiTypeList()
} finally {
callback()
}
})
}
module.exports = AutoApiPlugin

这里写得不是很严谨,如果有新增或者删除文件的话,需要重启项目,正确的写法应该是在每次变更时重新获取文件列表,再执行更新

注册插件

main.js中引入api

import './plugins/api'

vue.config.js中添加webpack插件

const AutoApiPlugin = require('./src/plugins/api/webpack')
module.exports = {
configureWebpack: (config) => {
config.plugins.push(
new AutoApiPlugin({})
)
}
}

写一个接口试试

src/api目录下新建一个demo.js,代码如下

/**
* 
* @param {*} rq AxiosInstance
* @param {IProduct} data 添加物料的格式
* @return {IReturn}
*/
const CreateProduct = async (rq, data) => {
let { files=[], ...rest } = data
files = files.map((file) => {
return {
file_url: file.url,
name: file.name
}
})
const postData = {
files,
...rest
}
let res = await rq.post('product/create', postData)
return res
}

在同目录下建一个demo.d.ts,代码如下

interface IProduct {
name: string,
code: string,
remark: string
}
interface IReturn {
message: string,
data: IProduct
}

运行项目npm run serve,插件执行完之后,会在plugins/api目录下生成apis.jsindex.d.ts文件
在编辑器中也就能看到代码提示啦
GIF.gif

如果第一次运行没有看到代码提示,那就重新开启项目,再启动一次。

总结

通过这么一次实验,自己尝试了一下简单的webpack插件编写,通过d.ts规范了接口文件的书写,后续可以结合swagger或者其他的插件完成更多简化接口编写的工作

(0)
上一篇 2021年3月27日 上午8:39
下一篇 2021年3月27日 上午8:52

相关推荐

发表回复

登录后才能评论