chrome插件: yapi 接口TypeScript代码生成器
前言
2020-09-12 天气晴,蓝天白云,微风,甚好。
前端Jser一枚,在公司的电脑前,浏览器打开着yapi的接口文档,那密密麻麻的接口数据,要一个一个的去敲打成为TypeScript的interface或者type。 心烦。
虽然这样的情况已经持续了大半年了,也没什么人去抱怨。 在程序中的any却是出现了不少, 今any , 明天又any, 赢得了时间,输了维护。 换个人来一看, what, 这个是what , 那个是what. 就算是过了一段时间,找来最初编写代码的人来看,也是what what what。 这时候 TS 真香定律就出来了。
我是开始写代码? 还是开始写生成代码的代码?
扔个硬币, 头像朝上开始写代码,反之,写生成代码的代码。 第一次头像朝上, 不行, 一次不算, 第二次, 还是头像朝上,那5局3剩吧, 第三次,依旧是头像朝上,呵呵,苦笑一下。 我就扔完五局,看看这上天几个意思。 第四次,头像朝向, 第五次头像朝向。 呵呵哒。
好吧,既然这样。 切,我命由我不由天,开始写生成代码的代码。
于是才有了今天的这篇博客。
分析
考虑到TypeScript代码生成这块, 大体思路
- 方案一: 现有开源的项目.
- 方案二: yapi插件
- 方案三: 后端项目开放权限, 直接copy。 因为后端也是使用node + TS 来编写的,这不太可能。
- 方案四: yapi上做手脚,读取原始的接口元数据,生成TS。
- 直接操作数据库
- 操作接口
- 页面里面扣
方案一
现在开源的项目能找到的是
- yapi-to-typescript
yapi-to-typescript非常强大和成熟, 推荐大家使用。 - sm2tsservice
这个也很强大,不过需要进行一些配置。
方案二
yapi-plugin-response-to-ts 看起来还不错,19年5月更新的。
接下来说的是 方案四的一种
经过对 /project/[project id]/interface/api/[api id] 页面的观察。
发现页面是在加载之后发出一个http请求,路径为 /api/interface/get?id=[api id], 这里的api id和页面路径上的那个api id是一个值。
接口返回的数据格式如下:
{
"errcode": 0,
"errmsg": "成功!",
"data": {
"query_path": {
"path": "/account/login",
"params": []
},
"req_body_is_json_schema": true,
"res_body_is_json_schema": true,
"title": "登录",
"path": "/account/login",
"req_params": [],
"res_body_type": "json",
"req_query": [],
// 请求的头信息
"req_headers": [
{
"required": "1",
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
],
// 请求的表单信息
"req_body_form": [
{
"required": "1",
"name": "uid",
"type": "text",
"example": "1",
"desc": "number"
},
{
"required": "1",
"name": "pwd",
"type": "text",
"example": "123456",
"desc": "string"
}
],
"req_body_type": "form",
// 返回的结果
"res_body": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"errCode\":{\"type\":\"number\",\"mock\":{\"mock\":\"0\"}},\"data\":{\"type\":\"object\",\"properties\":{\"token\":{\"type\":\"string\",\"mock\":{\"mock\":\"sdashdha\"},\"description\":\"请求header中设置Authorization为此值\"}},\"required\":[\"token\"]}},\"required\":[\"errCode\",\"data\"]}"
}
}
比较有用的信息是
- req_query
- req_headers
- req_body_type 请求的数据类型 form|json等等
- req_body_form req_body_type 为form时有数据,数据长度大于0
- req_body_other req_body_type 为json时有数据
- res_body_type 返回的数据类型 form | json
- res_body res_body_type返回为json时有数据
我们项目上, req_body_type只用到了 form 或者 json。 res_body_type只使用了 json.
我们把res_body格式化一下
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"errCode": {
"type": "number",
"mock": {
"mock": "0"
}
},
"data": {
"type": "object",
"properties": {
"token": {
"type": "string",
"mock": {
"mock": "sdashdha"
},
"description": "请求header中设置Authorization为此值"
}
},
"required": [
"token"
]
}
},
"required": [
"errCode",
"data"
]
}
上面大致对应如下的数据
{
"errCode": 0,
"data":{
"token": ""
}
}
到这里可以开始写生成代码的代码了。
这里要处理两个格式的数据,一种是form对应的数组数据,一种是json对应的数据。
代码
先定义一下接口
export interface BodyTransFactory<T, V> {
buildBodyItem(data: T): string;
buildBody(name: string, des: string, data: V): string;
}
form数据格式
定义类型为form数据的格式
export interface FormItem {
desc: string;
example: string;
name: string;
required: string;
type: string;
}
代码实现
import { BodyTransFactory, FormItem } from "../types";
const TYPE_MAP = {
"text": "string"
}
export default class Factory implements BodyTransFactory<FormItem, FormItem[]> {
buildBodyItem(data: FormItem) {
return `
/**
* ${data.desc}
*/
${data.name}${data.required == "0" ? "?" : ""}: ${TYPE_MAP[data.type] || data.type}; `
}
buildBody(name: string, des: string = "", data: FormItem[]) {
return (`
/**
* ${des}
*/
export interface ${name}{
${data.map(d => this.buildBodyItem(d)).join("")}
}`)
}
}
json数据格式
定义数据
interface Scheme {
// string|number|object|array等
type: string;
// type 为 object的时候,该属性存在
properties?: Record<string, Scheme>;
// type为object 或者array的时候,该属性存在,标记哪些属性是必须的
required?: string[];
// 描述
description?: string;
// 当type为array的时候,该属性存在
items?: Scheme;
}
这里的注意一下,如果type为object的时候,properties属性存在,如果type为array的时候, items存在。
这里的required标记哪些属性是必须的。
一看着数据格式,我们就会潜意思的想到一个词,递归,没错,递归。
属性每嵌套以及,就需要多四个(或者2个)空格,那先来一个辅助方法:
function createContentWithTab(content = "", level = 0) {
return " ".repeat(level) + content;
}
注意到了,这里有个level,是的,标记递归的深度。 接下来看核心代码:
trans方法的level,result完全可以放到类属性上,不放也有好处,完全独立。
核心就是区分是不是叶子节点,不是就递归,还有控制level的值。
import { BodyTransFactory } from "../types";
import { isObject } from "../util";
interface Scheme {
type: string;
properties?: Record<string, Scheme>;
required?: string[];
description?: string;
items?: Scheme;
}
function createContentWithTab(content = "", level = 0) {
return " ".repeat(level) + content;
}
export default class Factory implements BodyTransFactory<Scheme, Scheme> {
private trans(data: Scheme, level = 0, result = [], name = null) {
// 对象
if (data.type === "object") {
result.push(createContentWithTab(name ? `${name}: {` : "{", level));
level++;
const requiredArr = (data.required || []);
for (let p in data.properties) {
const v = data.properties[p];
if (!isObject(v)) {
if (v.description) {
result.push(createContentWithTab(`/**`, level));
result.push(createContentWithTab(`/*${v.description}`, level));
result.push(createContentWithTab(` */`, level));
}
const required = requiredArr.includes(p);
result.push(createContentWithTab(`${p}${required ? "" : "?"}: ${v.type};`, level));
} else {
this.trans(v, level, result, p);
}
}
result.push(createContentWithTab("}", level - 1));
} else if (data.type === "array") { // 数组
// required 还没处理呢,哈哈
// 数组成员非对象
if (data.items.type !== "object") {
result.push(createContentWithTab(name ? `${name}: ${data.items.type}[]` : `${data.items.type}[]`, level));
} else { // 数组成员是对象
result.push(createContentWithTab(name ? `${name}: [{` : "[{", level));
level++;
for (let p in data.items.properties) {
const v = data.items.properties[p];
if (!isObject(v)) {
if (v.description) {
result.push(createContentWithTab(`/**`, level));
result.push(createContentWithTab(`/*${v.description}`, level));
result.push(createContentWithTab(`*/`, level));
}
result.push(createContentWithTab(`${p}: ${v.type};`, level));
} else {
this.trans(v, level, result, p);
}
}
result.push(createContentWithTab("}]", level - 1));
}
}
return result;
}
buildBodyItem(data: Scheme) {
return null;
}
buildBody(name: string, des: string, data: Scheme) {
const header = [];
header.push(createContentWithTab(`/**`, 0));
header.push(createContentWithTab(`/*${des}`, 0));
header.push(createContentWithTab(`*/`, 0));
const resutlArr = this.trans(data, 0, []);
// 修改第一行
const fline = `export interface ${name} {`
resutlArr[0] = fline;
// 插入说明
const result = [...header, ...resutlArr, '\r\n']
return result.join("\r\n")
}
}
两个工厂有了,合成一下。 当然其实调用逻辑还可以再封装一层。
import SchemeFactory from "./scheme";
import FormFactory from "./form";
export default {
scheme: new SchemeFactory(),
form: new FormFactory()
}
在上主逻辑
import factory from "./factory";
import { ResApiData } from "./types";
import * as util from "./util";
import ajax from "./ajax";
// 拼接请求地址
function getUrl() {
const API_ID = location.href.split("/").pop();
return "/api/interface/get?id=" + API_ID;
}
// 提取地址信息
function getAPIInfo(url: string = "") {
const urlArr = url.split("/");
const len = urlArr.length;
return {
category: urlArr[len - 2],
name: urlArr[len - 1]
}
}
function onSuccess(res: ResApiData, cb) {
if (res.errcode !== 0 || !res.data) {
return alert("获取接口基本信息失败");
}
trans(res, cb);
}
// 核心流程代码
function trans(res: ResApiData, cb: CallBack) {
const apiInfo = getAPIInfo(res.data.path);
let reqBodyTS: any;
let resBodyTs: any;
const reqBodyName = util.fUp(apiInfo.name);
const reqBodyDes = res.data.title + "参数";
// 更合适是通过 res.data.req_body_type
if (res.data.req_body_other && typeof res.data.req_body_other === "string") {
reqBodyTS = factory.scheme.buildBody(reqBodyName + "Param", reqBodyDes, JSON.parse(res.data.req_body_other));
} else if (Array.isArray(res.data.req_body_form)) {
reqBodyTS = factory.form.buildBody(reqBodyName+ "Param", reqBodyDes, res.data.req_body_form)
}
const resBodyName = util.fUp(apiInfo.name);
const resBodyDes = res.data.title;
// // 更合适是通过 res.data.res_body_type
if (res.data.res_body_is_json_schema) {
resBodyTs = factory.scheme.buildBody(resBodyName+ "Data", resBodyDes, JSON.parse(res.data.res_body));
} else {
cb("res_body暂只支持scheme格式");
}
cb(null, {
reqBody: reqBodyTS,
resBody: resBodyTs,
path: res.data.path
})
}
export type CallBack = (err: any, data?: {
reqBody: string;
resBody: string;
path: string;
}) => void;
export default function startTrans(cb: CallBack) {
const url = getUrl();
console.log("api url", url);
ajax({
url,
success: res => {
onSuccess(res, cb)
},
error: () => {
cb("请求发生错误")
}
})
}
到这里,其实主要的处理流程都已经完毕了。 当然还存在不少的遗漏和问题。
比如
- form格式处理的时候
TYPE_MAP数据还不完善 - 请求数据格式只处理了form和json两种类型
- 返回数据数据格式只处理了json类型
- 数据格式覆盖问题
这毕竟只是花了半天时候弄出来的半成品,目前测试了不少接口,够用。
到这里,其实代码只能复制到浏览器窗体里面去执行,体验当然是太差了。
所以,我们再进一步,封装到chrome插件里面。
chrome插件
chrome插件有好几部分,我们这个只用content_script就应该能满足需求了。
核心脚本部分
- 检查域名和地址
- 动态注入html元素
- 注册事件监听
就这么简单。
import * as $ from 'jquery';
import startTrans from "./apits/index";
import { copyText, downloadFile } from "./apits/util";
; (function init() {
if (document.location.host !== "") {
return;
}
const $body = $(document.body);
$body.append(`
<div style="position:fixed; right:0; top:0;z-index: 99999;background: burlywood;">
<input type="button" value="复制到剪贴板" id='btnCopy' />
<input type="button" value="导出文件" id='btnDownload' />
</div>
`);
$("#btnCopy").click(function () {
startTrans(function (err, data) {
if (err) {
return alert(err);
}
const fullContent = [data.reqBody, "\r\n", data.resBody].join("\r\n");
copyText(fullContent, ()=>{
alert("复制成功");
})
});
})
$("#btnDownload").click(function () {
startTrans(function (err, data) {
if (err) {
return alert(err);
}
const fullContent = [data.reqBody, "\r\n", data.resBody].join("\r\n");
const name = data.path.split("/").pop();
downloadFile(fullContent, `${name}.ts`)
});
})
})();
如上可以看到有两种操作,一是复制到剪贴板,而是下载。
复制剪贴版,内容贴到textarea元素,选中,执行document.execCommand('copy');。
export function copyText(text, callback) {
var tag = document.createElement('textarea');
tag.setAttribute('id', 'cp_input_');
tag.value = text;
document.getElementsByTagName('body')[0].appendChild(tag);
(document.getElementById('cp_input_') as HTMLInputElement).select();
document.execCommand('copy');
document.getElementById('cp_input_').remove();
if (callback) { callback(text) }
}
下载的话, blob生成url, a标签download属性, 模拟点击。
export function downloadFile(content: string, saveName:string) {
const blob = new Blob([content]);
const url = URL.createObjectURL(blob);
var aLink = document.createElement('a');
aLink.href = url;
aLink.download = saveName || '';
var event;
if (window.MouseEvent) event = new MouseEvent('click');
else {
event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
}
aLink.dispatchEvent(event);
}
后续
- 只是生成了数据结构,其实后面还可以生成请求接口部分的代码,一键生成之后,直接调用接口就好,专心写你的业务。
- 只能一个一个接口的生成,能否直接生成一个project下的全部接口代码。 这都可以基于此来做
- 其他:等你来做
源码
这里采用了TypeScript chrome插件脚手架chrome-extension-typescript-starter。
安装chrome插件
- 下载上面的项目
- npm install
- 修改
src/content_script的your host为具体的值,或者删除
if (document.location.host !== "your host") {
return;
}
- npm run build
- chrome浏览器打开
chrome://extensions/ - 项目dist文件拖拽到
chrome://extensions/tab页面
chrome插件: yapi 接口TypeScript代码生成器的更多相关文章
- Chrome插件(Extensions)开发攻略
本文将从个人经验出发,讲述为什么需要Chrome插件,如何开发,如何调试,到哪里找资料,会遇到怎样的问题以及如何解决等,同时给出一个个人认为的比较典型的例子——获取网页内容,和服务器交互,再把信息反馈 ...
- Chrome插件整理
本文内容都来源于偶整理的fetool. 想让更多使用Chrome的小伙伴,体验到这些令人愉悦的小工具,所以单独整理了这篇文章. 如果你是 前端/服务端/设计/面向Github编程/视觉控,相信下列的插 ...
- 如何离线安装chrome插件【转】
http://blog.csdn.net/shuideyidi/article/details/45674601 原文链接 前言------可以不看: 实习做web,要弄单点登录SSO和验证Contr ...
- 【开发必备】吐血推荐珍藏的Chrome插件
[开发必备]吐血推荐珍藏的Chrome插件 一:(Lying人生感悟.可忽略) 青春浪漫,往往难敌事故变迁.生命对每一个人都是平等的,彼此所经历的那就一定是彼此所必须经历的,它一定不是只为了折磨.消耗 ...
- Chrome插件Visual Event查看Dom元素绑定事件的利器
找这工具找了好久,统一找着了,开发人员不可多得的好东东,收藏做一下分享. 用Chrome插件Visual Event查看Dom绑定的事件 Visual Event简介 Visual Event是一个开 ...
- Chrome插件i18n多语言实现
i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称.Chrome插件框架中i18n的封装API: chrome.i18n.ge ...
- 喂,前端,你应该知道的chrome插件
最近,优点闲. 压力,有点大,回顾,曾今被问,你怎么查看内存泄露,然后,一脸蒙. 工欲善其事, 必先利其器 最近在研究chrome devtools,发现,其实他很强.而且chrome6周一次的更新, ...
- 堪称神器的Chrome插件
前言 相信很多人都在使用 Chrome 浏览器,其流畅的浏览体验得到了不少用户的偏爱,但流畅只是一方面, Chrome 最大的优势还是其支持众多强大好用的扩展程序(Extensions).最近为了更好 ...
- 好用的Chrome插件推荐
无扩展,不 Chrome :几款 Chrome 扩展程序推荐 相信很多人都在使用 Chrome 浏览器,其流畅的浏览体验得到了不少用户的偏爱,但流畅只是一方面, Chrome 最大的优势还是其支持众多 ...
随机推荐
- 算法-图(3)用顶点表示活动的网络(AOV网络)Activity On Vertex NetWork
对于给定的AOV网络,必须先判断是否存在有向环. 检测有向环是对AOV网络构造它的拓扑有序序列,即将各个顶点排列成一个线性有序的序列,使得AOV网络中所有直接前驱和直接后继关系都能得到满足. 这种构造 ...
- Federated Learning: Challenges, Methods, and Future Directions
郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! arXiv:1908.07873v1 [cs.LG] 21 Aug 2019 Abstract 联邦学习包括通过远程设备或孤立的数据中心( ...
- 【转】在Python的struct模块中进行数据格式转换的方法
这篇文章主要介绍了在Python的struct模块中进行数据格式转换的方法,文中还给出了C语言和Python语言的数据类型比较,需要的朋友可以参考下 Python是一门非常简洁的语言,对于数据类型的表 ...
- Navicat12 for Mysql激活
1 下载 注册机和Navicat网盘下载地址 链接:https://pan.baidu.com/s/1AFpQIlHCXVHc8OuBZ9PAlA 提取码:xvi2 2 安装 2 ...
- 分享一个操作pdf文件的js文件-pdfObject.js(文件预览、下载、打印等操作都具备)
获取相关资料或者源码的朋友可以关注下公众号,回复关键字pdf20200518即可
- openCV - 5~7 图像混合、调整图像亮度与对比度、绘制形状与文字
5. 图像混合 理论-线性混合操作.相关API(addWeighted) 理论-线性混合操作 用到的公式 (其中 α 的取值范围为0~1之间) 相关API(addWeighted) 参数1:输入图像M ...
- centos 7 对用过yum更新的软件服务进行降级
centos 7 执行 yum update 会对现有服务软件进行更新,但是如果把不该升级的软件升级,彼此软件不兼容,如何进行降级,比如:kibana 必须与 elasticsearch 大版本相同, ...
- C语言内存泄露很严重,如何应对?
摘要:通过介绍内存泄漏问题原理及检视方法,希望后续能够从编码检视环节就杜绝内存泄漏导致的网上问题发生. 1. 前言 最近部门不同产品接连出现内存泄漏导致的网上问题,具体表现为单板在现网运行数月以后,因 ...
- LWPR
Scriptable Render Pipeline https://docs.unity3d.com/Manual/ScriptableRenderPipeline.html Unity轻量 ...
- 初学WebGL引擎-BabylonJS:第4篇-灯光动画与丛林场景
前几章接触的案例都是接近静态的,由这张开始开始接触大量动态的内容,包括 球体灯光,变动的形体,以及一个虚拟的丛林场景 下章我会试着结合1-9案例的内容做出一个demo出来 [playground]-l ...