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