前言
前端经常要和后端对接接口了,封装一个通用的接口请求很有必要,不过光有统一的接口库还不够库,能不能用typescript的d.ts文件外加webpack插件实现代码提示的自动化呢?今天我来分享我的项目实践总结。
效果
![[实践总结]给Vue项目封装一个带代码提示的api插件](https://img.wangzhan5u.com/images/5/[sjzjo42wwrq51ov.jpg)
原理
在热更新时每次读取api目录下所有js文件,解析每个接口函数的jsdoc风格的注释,生成一个对应的d.ts描述文件
动手做
创建plugins目录
在src目录创建plugins目录,再新建一个api目录
![[实践总结]给Vue项目封装一个带代码提示的api插件](https://img.wangzhan5u.com/images/5/[sjzjbg13qnv5yt4.jpg)
目录中手动新建index.js、runner.js、webpack.js三个文件,apis.js和index.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.js和index.d.ts文件
在编辑器中也就能看到代码提示啦
![[实践总结]给Vue项目封装一个带代码提示的api插件](https://img.wangzhan5u.com/images/5/[sjzjo42wwrq51ov.jpg)
如果第一次运行没有看到代码提示,那就重新开启项目,再启动一次。
总结
通过这么一次实验,自己尝试了一下简单的webpack插件编写,通过d.ts规范了接口文件的书写,后续可以结合swagger或者其他的插件完成更多简化接口编写的工作
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
 - 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
 
- 提示下载完但解压或打开不了?
 
- 找不到素材资源介绍文章里的示例图片?
 
- 模板不会安装或需要功能定制以及二次开发?
 
                    
    
发表评论
还没有评论,快来抢沙发吧!