preface

The front end often interconnects with the back end. It is necessary to encapsulate a common interface request, but a common interface library is not enough. Can we use typescript D.ts files and webpack plug-ins to automate code prompting? Today I would like to share my summary of project practice.

The effect

The principle of

Read all JS files in API directory every time during hot update, parse jsDoc style comments of each interface function, and generate a corresponding D.ts description file

And began to make

Create the plugins directory

Create the plugins directory in the SRC directory and create a new API directory

Js, runner. Js, and webpack.js. Apis. Js and index.d.ts are generated by webpack plug-in.

index.js

This is the body of the request, the general API request wrapper is written here, and finally injected into the prototype of the Vue, which can call the interface using this.$API. Here is my code

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

// Merge all interfaces into apis. Js, which is generated by the Webpack plugin
import { normalAPIs, successMessageAPIs } from './apis' 

let instance = null
let api = null

Vue.use({
  install(Vue, option) {
    // Instantiate axios and set some general information, such as the request address
    instance = axios.create({
      baseURL: option.baseURL || ' '.headers: option.headers || {},
    })

    // Merge to interface list
    constAPIs = { ... normalAPIs, ... successMessageAPIs }// Interface wrapping, passing the AXIos instance to the interface function to call the network request
    const result = {}
    for (const k in APIs) {
      result[k] = async (data) => {
        // Network requests need to catch errors
        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 an error occurs, print an error message
          if (e.response) {
            store.commit('alert', {
              type: 'error'.message: e.response.data.msg || e.response.data.message
            })
            throw e
          } else { // Other errors in the code
            store.commit('alert', {
              type: 'error'.message: e.message
            })
            throw new Error(e.message)
          }
        }
      }
    }
    // Inject a prototype into the Vue. In the Vue instance the interface can be called through the this.$API
    api = result
    Vue.prototype.$api = result
  }
}, {
  baseURL: config.api,
  headers: config.headers
})

export default api
Copy the code

Here I limit the interface function to a function that takes two arguments, the first being an instance of AXIos and the second being the argument to be sent to the interface, so that I can write the interface function in a uniform format. There are a few other things that are done here. The part of the code that calls store.mit is to send a notification to the interface that has the prompt, and to send the message that is returned in the background directly toa global toast or something like that to pop up the prompt. So we put interfaces into normalAPI and successMessageAPI objects accordingly

runner.js

This file generates apis. Js when the file changes by combining normalAPIs and successMessageAPIs into a single object. Decompress the description, parameters, and return values of each interface function into the d.TS declare Interface section, as shown below

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

/** * Merge the interfaces from each API file into a single file */
function mergeApis () {
  let list = fs.readdirSync(path.resolve(__dirname, '.. /.. /api')).filter((f) = > {
    returnf ! ='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
}

/** * Update API index.d.ts file */
function updateApiTypeList() {
  let list = fs.readdirSync(path.resolve(__dirname, '.. /.. /api')).filter((f) = > {
    returnf ! ='index.js'
  })
  let finalFuncs = []
  let interfaces = []
  for(let j of list) {
    // console.log(j)
    // js file processing
    if (j.endsWith('.js')) {
      finalFuncs = finalFuncs.concat(handleJsFile(j))
    }
    // d.ts file processing
    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) {
  // Read the file
  let module = fs.readFileSync(path.resolve(__dirname, `.. /.. /api/${filename}`), 'utf-8')
  / / comment import
  module = module.replace(/import(\s+)/g.'/ /')

  // Remove the ES6 module
  let res = module.match(/export(\s+)default(\s+){([\sa-zA-Z0-9,]+)}/)
  module = module.split(res[0])
  // The wrapping code is a self-calling function that returns an interface object
  let moduleObj = eval(` (() = > {The ${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, '------------------')
    // Get the re for the comment
    let regStr = new RegExp(` (/ \ \] [^ \ \ * * * * (* *] [^ \ \ \ \ +) + \ \ *)? \\/[^\\r\\n]*)(\\s+)(const|let)(\\s+)${f.name} `)
    // Get comments
    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, ' ')}// Get parameters. There is no support for writing parameters with deconstruction
    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*)([,]*)/.' ')
    // The parameter type is obtained from the comment
    let defineType = ' '
    // Parameter annotation regex
    const paramCommentReg = new RegExp(`@param \{([A-Za-z0-9\\[\\]<>]+)\} ${define.trim()}`)
    if (define) {
      const match = comment.match(paramCommentReg)
      if (match) {
        defineType = match[1]}}// Get the result type
    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}
Copy the code

webpack.js

This file is the definition part of the WebPack plug-in, and the code is as follows

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
Copy the code

If a file is added or deleted, you need to restart the project. The correct way to write this is to retrieve the file list at each change and then perform the update

To register the plugin

Introduce the API in main.js

import './plugins/api'
Copy the code

Add the WebPack plug-in to vue.config.js

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

Try writing an interface

Create a new demo.js file in the SRC/API directory

/ * * * *@param {*} rq AxiosInstance
 * @param {IProduct} Data Format for adding materials *@return {IReturn}* /
const CreateProduct = async (rq, data) => {
    let{ files=[], ... rest } = data files = files.map((file) = > {
        return {
            file_url: file.url,
            name: file.name
        }
    })
    constpostData = { files, ... rest }let res = await rq.post('product/create', postData)
    return res
}
Copy the code

Create a demo.d.ts in the same directory as the code below

interface IProduct {
    name: string.code: string.remark: string
}

interface IReturn {
    message: string.data: IProduct
}
Copy the code

Run the projectnpm run serveAfter the plug-in is executed, theplugins/apiDirectory generationapis.jsandindex.d.tsFiles in the editor can also see the code prompt

If you don’t see the code prompt the first time you run it, restart the project and start again.

conclusion

Through such an experiment, I tried to write a simple Webpack plug-in by myself, and standardized the writing of interface files through D. TS. In the future, I can combine Swagger or other plug-ins to complete more work of simplifying interface writing