zoukankan      html  css  js  c++  java
  • 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, '
    ']
    
            return result.join("
    ")
        }
    }
    
    
    

    两个工厂有了,合成一下。 当然其实调用逻辑还可以再封装一层。

    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, "
    ", data.resBody].join("
    ");
    
                copyText(fullContent, ()=>{
                    alert("复制成功");
                })
    
            });
        })
    
        $("#btnDownload").click(function () {
            startTrans(function (err, data) {
                if (err) {
                    return alert(err);
                }
                const fullContent = [data.reqBody, "
    ", data.resBody].join("
    ");
                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-ex-api-ts-generator

    安装chrome插件

    • 下载上面的项目
    • npm install
    • 修改 src/content_scriptyour host 为具体的值,或者删除
      if (document.location.host !== "your host") {
            return;
     }
    
    • npm run build
    • chrome浏览器打开 chrome://extensions/
    • 项目dist文件拖拽到 chrome://extensions/ tab页面
  • 相关阅读:
    诡异的命名空间问题
    如何为自定义属性提供表达式绑定支持
    为SSIS编写自定义数据流组件(DataFlow Component)之进阶篇:自定义编辑器
    SSAS : 外围应用配置器
    SSAS : 数据访问接口整理汇总
    SSAS : ADOMDConnection.ConnectionString的参数列表
    SSIS中的字符映射表转换组件
    SSAS: Discover 何处寻? 一切尽在GetSchemaDataSet
    为SSIS编写简单的同步转换组件
    如何在同步转换组件中增加输出列
  • 原文地址:https://www.cnblogs.com/cloud-/p/13704020.html
Copyright © 2011-2022 走看看