用 template 生成其他语言协议

在前一章节 生成其他语言协议 里,我们展示了如何解析 protocol 协议来生成目标语言的代码。在这一节,我们将演示如何通过 handlebars 语法 来实现生成其他语言代码的方法。

本节的演示代码位于项目 niuhedemo

开启 template 功能

niuhe/.config.json5 文件里,我们可以通过配置 templates 选项来开启模板渲染功能。下面是 templates 配置项的声明:

interface TplConfig {
    /** 模板类型, 可选为 message, enum, route */
    type: 'message' | 'enum' | 'route'
    modes: string[]
    /** template 文件地址 */
    template: string
    /** 存放的目标地址 */
    output: string
}

// 入口文件定义的信息
interface EntryInfo {
    ...
    /** 是否显示烟花效果 */
    fire?: boolean
    /** template 渲染 */
    templates?: TplConfig[]
}

生成代码

我们通过在 niuhe/.config.json5 中添加配置,来演示生成 typescript 脚本所需的类型、常量和 API 请求这三部分代码。其他语言的示例可以根据对应语言的特性,结合 handlebar 语法进行修改。

templates:[{
    type: 'message', // 可选值有 message, route, enum 三个, 同 protocol 协议相同
    template: './templates/ts_message.tpl', // 模板文件地址
    output: './output/message.d.ts', // 输出文件地址
    modes: ["api"], // 生成的 mode, url 第一部分
  },{
    type: 'route',
    template: './templates/ts_route.tpl',
    output: './output/route.ts',
    modes: ["api"],
  },{
    type: 'enum',
    template: './templates/ts_enum.tpl',
    output: './output/enum.d.ts',
    modes: ["api"],
  }]

生成类型定义代码

./templates/ts_message.tpl 代码如下:



// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

interface Rsp<T> {
    result: number
    data: T
    message: string
}

declare namespace {{capitalize mod}} {
 {{#each items}}
 {{#if desc}}
    /** {{desc}} */
 {{/if}}
    interface {{name}} {
        {{#each fields}}
        {{#if desc}}
        /** {{desc}} */
        {{/if}}
        {{#if (isArray label)}}
        {{ json }}: {{ convertType type }}[], <!-- 处理数组 -->
        {{else}}
        {{ json }}: {{ convertType type }},
        {{/if}}
        {{/each}}
    }

{{/each}}
}

生成的 typescript 代码如下:



// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

interface Rsp<T> {
    result: number
    data: T
    message: string
}

declare namespace Demo {
    /** 分页查询Config信息 */
    interface ConfigPageReq {
        /** 页码 */
        page: number,
        /** 每页数量 */
        size: number,
        /** 配置名称 */
        name: string,
        /** 配置值 */
        value: number,
    }

    /** 系统配置表 */
    interface ConfigItem {
        /** id */
        id: number,
        /** 配置名称 */
        name: string,
        /** 配置值 */
        value: number,
        /** 创建时间 */
        create_at: string,
        /** 更新时间 */
        update_at: string,
    }

    /** 分页查询Config信息 */
    interface ConfigPageRsp {
        /** 总数 */
        total: number,
        /** Config信息 */
        items: ConfigItem[],
    }

    /** 请求Config信息 */
    interface ConfigFormReq {
        id: number,
    }

    /** 批量删除Config信息 */
    interface ConfigDeleteReq {
        /** 记录id列表 */
        ids: number[],
    }

    /** Config无数据返回 */
    interface ConfigNoneRsp {
    }

    /** 测试请求 */
    interface HelloReq {
        /** 用户名 */
        name: string,
    }

    /** 测试响应 */
    interface HelloRsp {
        /** 问候语 */
        greeting: string,
    }

    /** 空请求 */
    interface NoneReq {
    }

    /** 空响应 */
    interface NoneRsp {
    }

    /** RPC测试用例请求参数 */
    interface XxxYyyReqMsg {
        /** 用户名 */
        name: string,
        /** 密码 */
        password: string,
    }

    /** RPC测试用例返回参数 */
    interface XxxYyyRspMsg {
        /** 用户open_id */
        open_id: string,
        /** 账户信息 */
        account_info: string,
    }

}

这里只展示了类型代码的生成,由于篇幅限制,常量和 API 代码请访问 示例项目查看

添加模板代码助手

在插件里,我们添加了 capitalizeisArraycameCase 这三个代码助手。在上一小节的演示中,用到了 capitalizeisArray,它们的定义如下:

// 首字母大写 Helper
handlebars.registerHelper('capitalize', (str: string): string => {
    if (!str) {
        return str;
    }
    if (str.length === 1) {
        return str.toUpperCase();
    }
    return str.charAt(0).toUpperCase() + str.slice(1);
});

// 数组 Helper
handlebars.registerHelper('isArray', (label: string): boolean => {
    return label === 'repeated';
});
// 驼峰命名 Helper
handlebars.registerHelper('cameCase', (text: string): string => {
    return camelCase(text);
});

添加自定义代码助手

当通过模板功能成功生成一次代码后, 会创建 niuhe/.template.tpl.ts 文件(这是自定义代码助手文件入口, 也可手动创建)。其默认内容如下:

// templates 注入 例子。 不能使用 js 高级语法。 
// 详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper('funName', function(args){
   return args;
});

为了在 template 里更简洁、灵活地编写模板代码,我们可以根据需求添加更多代码助手。

在上面的演示代码中,使用到了自定义的 convertTypeisInteger 两个代码助手,下面是演示代码中完整的 niuhe/.templates.tpl.js 内容:


// templates 注入 例子。 不能使用 js 高级语法。 
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper('funName', function(args){
   return args;
});

handlebars.registerHelper('convertType', function (type) {
    const typeMap = {
        'string': 'string',
        'stringenum': 'string',
        'number': 'number',
        'float': 'number',
        'double': 'number',
        'int': 'number',
        'long': 'number',
        'enum': 'number',
        'integer': 'number',
        'boolean': 'boolean',
        'object': 'any',
        'array': 'any[]',
        'map': 'any',
    };
    return typeMap[type] || type;
});

handlebars.registerHelper('isInteger', function (type) {
    return type === 'integer';
});

高效调试

在正确配置 niuhe/.config.json5 中的 templates 项后, 插件会在调用 handlebars 从模板文件编译目标文件前, 打印传入的数据内容(console.log({config, data}))。其中 data 是待编译的具体数据结构和数值。 我们可以通过 帮助 -> 切换开发人员工具 -> 控制台 页面查看打印的数据信息。 debug 截图

完整的数据结构定义

在编写 *.tpl 文件时, 数据项(message), 常量(enum)和请求方法(route) 定义结构如下。参考下面的定义,有助于我们高效、快速地编写模板代码。

declare namespace Tpl {
    /**
     * 字段定义属性, enum 比 message 多一个 value 字段
     */
    interface TplField {
        name: string;
        /** 字段描述 */
        desc?: string;
        /** 类型 专属字段 */
        type?: string;
        /** json 名 */
        json: string;
        /** 引用的结构 */
        ref?: TplMessage;

        /** 标签, required, optional, repeated, mapping */
        label: string;
        /** 字段版本 */
        version?: string;
        /** 是否废弃 */
        deprecated?: boolean

        /** enum 专属字段 */
        value?: string;
    }
    interface TplMessage {
        mode: string
        /* 类名 */
        name: string;
        /** 类描述 */
        desc?: string;
        /** 字段列表 */
        fields: TplField[];
        /** 是否废弃 */
        deprecated?: boolean;
        /** enum 专属字段, 为整数常量和字符串常量 */
        type?: 'integer' | 'string';
    }

    interface TplRoute {
        /** 如: api, url 第一段内容 */
        mode: string
        /** 路由方法, post, get, put, option... */
        method: string;
        /** api 路径 */
        url: string;
        /* 路由名 */
        name: string;
        /** 描述 */
        desc?: string;
        /** 返回结构 */
        rsp?: TplMessage;
        /** 请求结构 */
        req?: TplMessage;
        /** RPC定义返回的错误代码列表 */
        codes: TplField[]
    }
    interface Template {
        /** 数据项定义 */
        messages: TplMessage[]
        /** 常量定义 */
        enums: TplMessage[]
        /** 请求方法定义 */
        routes: TplRoute[]
    }
}