# 请求封装结合 webpack 智能提示

# 引言

工欲善其事,必先利其器。为了提高开发效率,减少傻瓜错误,提高维护性。我为前端发请求好好折腾了一下。

# 目标

  • 请求需要处理常规业务异常
  • 请求高度可配置
  • 调用时有参数智能提示
  • 由于 ts 接口写起来比较繁琐,有规律性的,希望利用 webpack 自动生成

# 1.请求封装

主要注意,不管是啥情况都要顺着 promise 往外抛,特别是异常情况,即便处理了也要给到调用方 catch 的机会


import axios from 'axios'
import Vue from 'vue'
import Router from '@/router'
import { message } from 'ant-design-vue'

export interface AxiosConfig{
    loading?: boolean;
    loadingDesc?: string;
    auth?: boolean;
    errorFuck?: boolean;
    success?:boolean;
    successText?:string;
    thirdApi?:boolean;
}

// eslint-disable-next-line max-len
export const $axios = function (apiKey: string, params = {}, config: AxiosConfig = {}, otherConfig = {}) {
  const apiConfig = {
    loading: true,
    loadingDesc: '加载中',
    auth: true,
    errorFuck: true,
    success: false,
    successText: '操作成功',
    thirdApi: false,
    ...config
  }

  if (otherConfig.headers) {
    otherConfig.headers.Authorization = `Bearer ${local.get('accessToken')}`
  } else {
    otherConfig.headers = { Authorization: `Bearer ${local.get('accessToken')}` }
  }

  apiConfig.loading && Vue.$xl.loading.show()

  const apiUrlArr = apiKey.split(' ')

  return axios({
    method: apiUrlArr[0],
    url: apiUrlArr[1],
    data: _params,
    params: apiUrlArr[0].toLowerCase() === 'get' ? _params : {},
    ...otherConfig
  }).then((res: any) => {
    apiConfig.loading && Vue.$xl.loading.hide()
    if (apiConfig.thirdApi) {
      return res.data
    }

    if (res.data.code === 2) {
        if (apiConfig.auth) {
            message.error('未登录,即将前往登录页面..')
            Router.replace({
                name: 'Login',
                query: { redirect: Router.currentRoute.fullPath }
            })
            throw res.data
        }
    }

    if (!res.data.success) {
      apiConfig.errorFuck && message.error(res.data.msg)
      throw res.data
    }
    apiConfig.success && message.info(apiConfig.successText)
    return res.data
  }).catch((err: any) => {
    apiConfig.loading && Vue.$xl.loading.hide()
    if (err.response) {
      switch (err.response.status) {
        case 500:
          message.error('系统异常')
          console.log('fuck 500')
          break
        default:
          console.log('fuck 500', err.response.status)
      }
    } else {
      console.log(err)
    }
    // eslint-disable-next-line no-throw-literal
    throw 'opps An unexpected error occured'
  })
}

# 2.利用 webpack 的 require.context 收集接口 url

我的目录是这样的


api
    ---@types
            ---api.ts // ts 接口用以做到智能提示
            ---generate.js // 生成 api.ts
    ---modules
            ---a.ts // 各个接口 url
            ---b.ts
            ...
    ---axiosSet.ts   // axios 封装后的代码
    ---index.ts  // api 处理完成的输出对象
    

index.ts 中需要收集所有的 modules 并且注册为调用 axios 的方法


// modules下的 xx.ts 长这样:

export default {
    getSth:'get /xx',
    postSth:'post /xxx',
    ...
}

index.ts 就可以:


import { $axios, AxiosConfig } from '@/Global/api/axiosSet'
import { Api } from '@/Global/api/@types/api'

console.time()

const originModule = require.context('./modules', true, /\.ts/)

const $api:Api = {}

originModule.keys().forEach((it) => {
  let o = originModule(it).default
  Object.keys(o).forEach(key => {
    o[`_${key}`] = o[key]
    o[key] = function (params:{} = {}, config:AxiosConfig = {}, otherConfig:{} = {}) {
      return $axios(o[`_${key}`], params, config, otherConfig)
    }
  })
  $api[it.replace(/\.\/(.+)\.ts$/, '$1')] = originModule(it).default
})

console.timeEnd()

export default Object.freeze($api)

现在导出的 $api 是这样的:

调用接口即: $api.a.getSth()

# 3.把index.ts导出挂载在 vue 实例上


// main.ts:
import $api from '@/api/index'
Vue.prototype.$api = $api

主要注意需要在 .d.ts中声明,在项目任何位置都可以,不过建议在@types中,创建 vue.d.ts:

import Vue from 'vue'
import api from '@/api/index'
declare module 'vue/types/vue'{
    interface Vue{
        $api:typeof api
    }
}

# 4.手写 @/types/api.ts 感受提示效果


import { AxiosConfig } from '@/api/axiosSet'
interface Res<T> {
    data:{},
    body:T
}
interface Params{
    pageSize?:number,
    page?:number,
    id?:number,
    [key:string]:any
}
interface ResFunction{
    (params:Params, config:AxiosConfig, otherConfig:{}):Promise<Res<any>>
}
export interface Api{
    a?:aContent,
    b?:bContent,
    [key:string]:any
}
export interface aContent{
    getSth:ResFunction,
    postSth:ResFunction,
    _getSth:string,
    _postSth:string,
    [key:string]:any
}

vue 实例中:

这样一来写起来已经相当爽了,但不得不面对一行行写 interface 的事实,仔细观察其实这个文件是很有规律性的,完全可以交给工具生成,你说没有现成的 webpack插件?那我们就来写一个。

# 5.自动生成@/types/api.ts


// generate.js

const fs = require('fs')
const path = require('path')
const join = require('path').join

const CONFIG = {
  // 源文件 根据这里生成 interface
  apiPath: join(__dirname, '../modules'),
  // 目标文件上方的固定内容
  baseContent: `import { AxiosConfig } from '@/api/axiosSet'
interface Res<T> {
    data:{},
    body:T
}
interface Params{
    pageSize?:number,
    page?:number,
    id?:number,
    [key:string]:any
}
interface ResFunction{
    (params:Params, config:AxiosConfig, otherConfig:{}):Promise<Res<any>>
}`,
  // 输出
  outPut: 'src/api/@types/api.ts'
}

/**
 * 读取文件路径
 * @param startPath  起始目录文件夹路径
 * @returns {Array}
 */
const findSync = (startPath) => {
  let result = []
  function finder (path) {
    let files = fs.readdirSync(path)
    files.forEach((val, index) => {
      let fPath = join(path, val)
      let stats = fs.statSync(fPath)
      if (stats.isDirectory()) finder(fPath)
      if (stats.isFile()) result.push(fPath)
    })
  }
  finder(startPath)
  return result
}

/**
 * 读取文件信息
 * @param {*} fileName 文件路径
 */
const getContent = (fileName) => {
  return new Promise(resolve => {
    fs.readFile(fileName, (err, data) => {
      if (err) {
        throw err
      }
      let res = data.toString('utf-8')
      res = res.replace('export default', '')
      // eslint-disable-next-line no-new-func
      const keys = Object.keys(new Function(`return ${res}`)())
      resolve({
        fileName: fileName.replace(/.+\/(.+)\.ts/, '$1'),
        keys
      })
    })
  })
}

/**
 * 创建或覆盖文件
 * @param {*} fileName 要覆盖的文件路径
 * @param {*} str 新内容
 */
const writeFileFn = (fileName, str) => {
  fs.writeFile(path.join(path.resolve('.'), fileName), str, (err) => {
    if (err) {
      console.log(err)
    } else {
      console.log(`create ${fileName} successs`)
    }
  })
}

/**
 * resolve ({
 *     fileName,
 *     origin,
 *     main
 * })
 */
const handleFileContent = () => {
  const apiPaths = findSync(CONFIG.apiPath)
  return new Promise(resolve => {
    Promise.all(apiPaths.map(it => getContent(it))).then(res => {
      resolve(res)
    })
  })
}

const newContent = (arr) => {
  let ApiStr = arr.map(it => `${it.fileName}?:${it.fileName}Content`).join(',\n    ')
  ApiStr = `
export interface Api{
    ${ApiStr},
    [key:string]:any
}`
  let contentInterface = arr.map(it => {
    const Interface = it.keys.map(item => `${item}:ResFunction`).join(',\n    ')
    const interfaceString = it.keys.map(item => `_${item}:string`).join(',\n    ')
    return `
export interface ${it.fileName}Content{
    ${Interface},
    ${interfaceString},
    [key:string]:any
}`
  }).join('')
  const str = `
${CONFIG.baseContent}
${ApiStr}
${contentInterface}
`
  return str
}

module.exports = () => {
  handleFileContent().then(res => {
    writeFileFn(CONFIG.outPut, newContent(res))
  })
}


node 文件编写好 导出为 webpack插件


// 创建 apiPlugin.js 文件

const fn = require('./src/api/@types/generate')
class ApiPlugin {
  apply (compiler) {
    // 指定要附加到的事件钩子函数
    compiler.hooks.emit.tapAsync(
      'ApiPlugin',
      (compilation, callback) => {
        fn()
        callback()
      }
    )
  }
}

module.exports = ApiPlugin

使用 webpack 插件,这里用的是 vue-cli3 ,所以直接修改 vue.config.js:


const ApiPlugin = require('./apiPlugin')
module.exports = {
  ...
  configureWebpack: {
    plugins: [
      new ApiPlugin()
    ]
  }
}

这样一来,每次 api/modules下文件变化,webpack都会自动生成对应的 api/@types/api.ts 帮助我们智能提示。