教你如何开发一个数据转换插件

在开发过程中,或多或少都会遇到数据格式转换的问题,如果只是简单的数据,那自然用什么方式都可以,如果遇到数据非常多、层级复杂、关联性强的数据,则需要摸索出一套合适的,本文会介绍两种比较可行的转换模型,他们各自适合不同的场景和喜好。
 
在不同的平台上开发导入/导出插件,自然会受到该平台硬性条件的约束,例如供主系统调用的函数名、约定的传入和返回的参数形式、与主系统共享的环境变量等等。
 
这些方面各自的平台难免会有所差异,难以统一讨论,但其本质是数据结构的转换,也是本文想要讨论的主题。 无论在功能上是导入或导出,我们所做的事情是类似的,都是将一种数据结构转换成另一种数据结构。
在开始之前,我们先约定一下术语,我们将转换前的数据称之为源数据,将转换后的数据,称之为目标数据。 作为例子,我们有一份自己系统所属的接口数据,需要将其转换成 postman 平台的数据,本文将探讨如何处理各种场景。
 
字段差异
最简单的情况,字段内容一样,仅仅是字段名不一样,例如在我们的数据中,接口请求地址叫做 url, 在目标数据格式中,请求接口地址叫做 uri,那只需要做一下简单的转换即可:
 
const translate = ({ url }) => {
    return {
        uri: url
    }
}

  

我们将难度稍微提升一点,考虑更复杂一些的情况,是字段之间的关系是一对多,或多对一。例如在源数据中,需要用 host + path两个字段来描述接口地址,而在目标数据中仅用一个字段url来描述。在这种情况下,我们最好使用另一个小函数来处理:

 

const translatePath = ({ host, path }) => ({
        url: host + path
})

const translate = ({ host, path }) => {
    return {
        ...translatePath({ host, path })
    }
}

  

在一对多、或多对一、多对多这几种字段关系中,都可以用该思路,灵活地封装出translateRequesttranslateResponse等多个函数,将他们在返回值中堆叠在一起:

 

const translate = ({ host, path, request, response }) => {
    return {
        ...translatePath({ host, path }),
        ...translateRequest({ path, request }),
        ...translateResponse({ response })
    }
}

  

大致的处理模型如此,由于每一个子函数都是独立运行、独立解析,不存在缓存和副作用,因而较好维护,在传参、取参时可以灵活控制自己需要或不要的参数;一些非空处理、边界处理,也都可以在对应的子函数中操作;庆幸 ES6+ 提供了比较优雅的解构语法,能让程序整体看起来较为简洁。
 
 
层级差异
 
上面所说的都是字段与字段之间的关系,但数据结构之间的差异不只是有字段,还有层级。如果沿用上面的处理模式,遇到层级较深、并且含有级联关系的数据结构,可能会难于处理。因此,在这里需要介绍另一种处理模型——基于 class 语法的模型。
 
为什么需要引入class
 
并非是要对数据结构进行抽象,而是基于class比较容易使用链式调用语法,如有需要,我们还可以比较方便地使用缓存。
 
还是以接口数据为例,我们考虑这样一种场景,在源数据中,接口的rest参数,定义在最外层,而在目标数据中,rest参数定义在request字段中,两者之间差了一个层级。我希望以这样的语法处理数据结构间的转换:
 
 
new Translate(source).translateRest()

  

同理,也是一个函数只做一件事,只不过在语法上,各个子函数以链式调用的形式结合在一起:

 

new Translate(source)
    .translateRest()
    .translateRequest()
    .translateResponse()

  

Translate类中大致需要这么几个部件:
  • 至少需要两个缓存变量,这里暂时称之为sourceresult,前者用于存放源数据,后者用于存放转换中的数据。
  • constructor中,将接受到的源数据缓存起来,方便后面的子函数取用。
  • 各个子函数每次都从缓存中取出源数据,根据源数据的数据特征,从特定的层级中取出特定的字段,进行转换,然后塞入到result对应的层级中,随后记得返回this。
  • 在转换结束后,调用getResult方法,将result取出,在这一步就不再返回this了。
  • 最后一个,根据我自己的经验,还需要有一个方便打印数据的函数,暂且叫log吧。

 

我们即刻实现一下基础架构,除了Class的语法外,还可以结合前半段介绍的处理模型来用:
 
 
const translateRest = ({ rest }) => ({
    ...    // 返回处理好的格式
})

class Translate {
    constructor(source) {
        // 初始化
        this.source = source
        this.result = {}
    }
    translateRest() {
        // 在该函数中,比较好地处理了层级问题
        this.result.request = {
            ...translateRest(source)
        }
        return this
    }
    translateResponse() {
        this.result.response = {
            ...
        }
        return this
    }
    log() {
        // 格式化输出
        console.log(JSON.stringify(this.result, null, 2))
        return this
    }
    getResult() {
        // 这是链式处理的最后一步
        return this.result
    }
}

  

这样的处理模型,确实是破坏了子函数的原子性,因为它不得不对外部的缓存进行读写,引入了一些副作用,但基本的缓存只有两个,复杂不会太高。但是用这个模型,能让处理层级差异变得简单,你可以用多个子函数处理某一层级中的多个字段(可能要注意先后顺序),比如下个例子就用三个子函数,来处理request层级下的多个字段:

 

new Translate(source)
    .translateRequestHeader()
    .translateRequestBody()
    .translateRequestQuery()
    ...

  

总结起来,该模型有以下几个明显的优势:
  1. 得益于缓存,子函数可以任意读取源数据中多个层级的字段。同理地,子函数也可以在任意一个层级写入目标数据。
  2. 由于是链式语法,在处理链路中,可以比较灵活地插入子函数、调整顺序,比如在任意一个子函数后面插入log函数进行打印,这一特点在开发和调试中尤其方便。

 

new Translate(source)
    .translateRest()
    ...
    .log()    // 随时插入打印
    .translateResponse()
    .getResult()

  

需要注意是,由于使用了缓存,因此在开发过程中,需要注意深浅拷贝的问题。
 
结尾
本文大致由浅及深地介绍了两种处理模型,应该足够应付大部分数据结构转译的场景。当实现了整个算法后,接下来要做的事情就只是按照平台的插件规则,将其包装成一个合格的插件。
我们就是基于这样的独立模型,在开源产品 Eoapi 产品中实现 OpenAPI 格式的导入和导出插件。当然,OpenAPI 的数据结构比较复杂,我们目前只是开个了头,正在逐步完善。
 
感兴趣可以关注 Eoapi 的插件仓库
感谢阅读本文,希望你从此也能够轻松处理数据结构的转换。
 
Eoapi 是一款类 Postman 的开源 API 工具,它更轻量,同时可拓展。
 
 
在开发过程中,或多或少都会遇到数据格式转换的问题,如果只是简单的数据,那自然用什么方式都可以,如果遇到数据非常多、层级复杂、关联性强的数据,则需要摸索出一套合适的,本文会介绍两种比较可行的转换模型,他们各自适合不同的场景和喜好。
在不同的平台上开发导入/导出插件,自然会受到该平台硬性条件的约束,例如供主系统调用的函数名、约定的传入和返回的参数形式、与主系统共享的环境变量等等。
 
这些方面各自的平台难免会有所差异,难以统一讨论,但其本质是数据结构的转换,也是本文想要讨论的主题。 无论在功能上是导入或导出,我们所做的事情是类似的,都是将一种数据结构转换成另一种数据结构。
在开始之前,我们先约定一下术语,我们将转换前的数据称之为源数据,将转换后的数据,称之为目标数据。 作为例子,我们有一份自己系统所属的接口数据,需要将其转换成 postman 平台的数据,本文将探讨如何处理各种场景。
 
字段差异
最简单的情况,字段内容一样,仅仅是字段名不一样,例如在我们的数据中,接口请求地址叫做 url, 在目标数据格式中,请求接口地址叫做 uri,那只需要做一下简单的转换即可:
const translate = ({ url }) => { return { uri: url } }
我们将难度稍微提升一点,考虑更复杂一些的情况,是字段之间的关系是一对多,或多对一。例如在源数据中,需要用 host + path两个字段来描述接口地址,而在目标数据中仅用一个字段url来描述。在这种情况下,我们最好使用另一个小函数来处理:
const translatePath = ({ host, path }) => ({ url: host + path }) const translate = ({ host, path }) => { return { ...translatePath({ host, path }) } }
在一对多、或多对一、多对多这几种字段关系中,都可以用该思路,灵活地封装出translateRequesttranslateResponse等多个函数,将他们在返回值中堆叠在一起:
const translate = ({ host, path, request, response }) => { return { ...translatePath({ host, path }), ...translateRequest({ path, request }), ...translateResponse({ response }) } }
大致的处理模型如此,由于每一个子函数都是独立运行、独立解析,不存在缓存和副作用,因而较好维护,在传参、取参时可以灵活控制自己需要或不要的参数;一些非空处理、边界处理,也都可以在对应的子函数中操作;庆幸 ES6+ 提供了比较优雅的解构语法,能让程序整体看起来较为简洁。
层级差异
上面所说的都是字段与字段之间的关系,但数据结构之间的差异不只是有字段,还有层级。如果沿用上面的处理模式,遇到层级较深、并且含有级联关系的数据结构,可能会难于处理。因此,在这里需要介绍另一种处理模型——基于 class 语法的模型。
为什么需要引入class
并非是要对数据结构进行抽象,而是基于class比较容易使用链式调用语法,如有需要,我们还可以比较方便地使用缓存。
还是以接口数据为例,我们考虑这样一种场景,在源数据中,接口的rest参数,定义在最外层,而在目标数据中,rest参数定义在request字段中,两者之间差了一个层级。我希望以这样的语法处理数据结构间的转换:
new Translate(source).translateRest()
同理,也是一个函数只做一件事,只不过在语法上,各个子函数以链式调用的形式结合在一起:
new Translate(source) .translateRest() .translateRequest() .translateResponse()
Translate类中大致需要这么几个部件:
  • 至少需要两个缓存变量,这里暂时称之为sourceresult,前者用于存放源数据,后者用于存放转换中的数据。
  • constructor中,将接受到的源数据缓存起来,方便后面的子函数取用。
  • 各个子函数每次都从缓存中取出源数据,根据源数据的数据特征,从特定的层级中取出特定的字段,进行转换,然后塞入到result对应的层级中,随后记得返回this。
  • 在转换结束后,调用getResult方法,将result取出,在这一步就不再返回this了。
  • 最后一个,根据我自己的经验,还需要有一个方便打印数据的函数,暂且叫log吧。
我们即刻实现一下基础架构,除了Class的语法外,还可以结合前半段介绍的处理模型来用:
const translateRest = ({ rest }) => ({ ... // 返回处理好的格式 }) class Translate { constructor(source) { // 初始化 this.source = source this.result = {} } translateRest() { // 在该函数中,比较好地处理了层级问题 this.result.request = { ...translateRest(source) } return this } translateResponse() { this.result.response = { ... } return this } log() { // 格式化输出 console.log(JSON.stringify(this.result, null, 2)) return this } getResult() { // 这是链式处理的最后一步 return this.result } }
这样的处理模型,确实是破坏了子函数的原子性,因为它不得不对外部的缓存进行读写,引入了一些副作用,但基本的缓存只有两个,复杂不会太高。但是用这个模型,能让处理层级差异变得简单,你可以用多个子函数处理某一层级中的多个字段(可能要注意先后顺序),比如下个例子就用三个子函数,来处理request层级下的多个字段:
new Translate(source) .translateRequestHeader() .translateRequestBody() .translateRequestQuery() ...
总结起来,该模型有以下几个明显的优势:
  1. 得益于缓存,子函数可以任意读取源数据中多个层级的字段。同理地,子函数也可以在任意一个层级写入目标数据。
  2. 由于是链式语法,在处理链路中,可以比较灵活地插入子函数、调整顺序,比如在任意一个子函数后面插入log函数进行打印,这一特点在开发和调试中尤其方便。
new Translate(source) .translateRest() ... .log() // 随时插入打印 .translateResponse() .getResult()
需要注意是,由于使用了缓存,因此在开发过程中,需要注意深浅拷贝的问题。
结尾
本文大致由浅及深地介绍了两种处理模型,应该足够应付大部分数据结构转译的场景。当实现了整个算法后,接下来要做的事情就只是按照平台的插件规则,将其包装成一个合格的插件。
我们就是基于这样的独立模型,在开源产品 Eoapi 产品中实现 OpenAPI 格式的导入和导出插件。当然,OpenAPI 的数据结构比较复杂,我们目前只是开个了头,正在逐步完善。
感兴趣可以关注 Eoapi 的插件仓库
感谢阅读本文,希望你从此也能够轻松处理数据结构的转换。
 
Eoapi 是一款类 Postman 的开源 API 工具,它更轻量,同时可拓展。
官网地址https://www.eoapi.io/
如果你对于 Eoapi 有任何疑问或者建议,都可以去 Github 或者来这里找我,提个 Issue,我看到了都会及时回复的。
posted @ 2022-08-26 10:19  Postcat小助手  阅读(72)  评论(0编辑  收藏  举报