最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [实践总结]给Vue项目封装一个带代码提示的api插件

    正文概述 掘金(flashtd1就是我)   2021-03-27   744

    前言

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

    效果

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

    原理

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

    动手做

    创建plugins目录

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

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

    目录中手动新建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文件 在编辑器中也就能看到代码提示啦 [实践总结]给Vue项目封装一个带代码提示的api插件

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

    总结

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


    起源地下载网 » [实践总结]给Vue项目封装一个带代码提示的api插件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元