当前位置: 首页 > Web前端 > JavaScript

用于程序设计优化的流水线数据流

时间:2023-03-27 14:37:48 JavaScript

摘要学计算机的同学对流水线这个词应该不陌生,尤其是在Linux系统中,流水线算子得到了广泛的应用,给我们带来了极大的便利。在前端领域比较突出的脚手架“gulp”,也以流水线操作着称。今天我们就来一步一步的看一下如何设计前端领域的“流水线数据流”。1.前言有计算机基础的同学对管道这个词应该不陌生,尤其是在Linux系统中,管道运算符得到了广泛的应用,给我们的改造带来了极大的便利。流水线操作通常分为单向流水线和双向流水线。当数据从上一个管道流到下一个管道时,我们的数据会经过这个管道处理,处理完之后再送到下一个管道,以此类推,这样就可以在连续的管道流中不断地处理一些原始数据,最终可以得到我们想要的目标数据。在我们日常的编程开发过程中,也可以尝试使用流水线数据的概念,在一定程度上优化我们的程序架构,让我们程序的数据流转更加清晰,可以让我们像流水线一样,每个pipeline专门负责各自的工作,并对数据源进行粗略的处理,达到职责清晰和程序解耦的目的。2.程序设计现在我们使用Typescript来实现一个基本的流水线类设计。我们今天使用的管道是单向管道。2.1管道适配器顾名思义,适配器就是需要将不同的多节管道连接在一起,形成一个整体管道的连接端口。通过这个连接器,我们可以控制数据的流向,让数据流向它真正应该去的地方。首先,让我们设计适配器的类型结构:typePipeline={/***Linkmulti-sectionpipelines*e.g.*constapp=newBaseApp();*app.pipe(newTestApp1()).pipe(newTestApp2()).pipe(newTestApp3()).pipe(newOutput()).pipe(newEnd())*@param_next*/管道(_next:管道):管道;};上面的代码描述了一个支持管道数据的类需要什么样的适配器。在编程中,我们的适配器实际上是一个函数,用于将多段管道相互链接起来。从上面的代码可以看出,为了程序的高复用性,我们选择对管道中传输的数据类型进行泛化,这样我们在实现某个程序的时候,可以更加灵活的使用类型。例如://时间类型的管道DatePipeline=Pipeline//数组类型的管道ArrayPipeLine=Pipeline//自定义数据类型的管道CustomPipeLine=Pipeline<{name:string,age:number}>另外,我们函数的传入参数和返回值也是有讲究的。从上面的代码可以看出,我们接收的是管道类型的数据,返回的是管道类型的数据。其中,传入的参数是下一段管道,所以我们将两段管道连接在一起。之所以要返回一个管道类型的数据,是为了让我们在使用的时候能够链式调用,更符合管道数据的设计理念,比如:constapp=newAppWithPipleline();app.pipe(newWorkerPipeline1()).pipe(newWorkerPipeline2()).pipe(newWorkerPipeline3()).pipe(newWorkerPipeline4())换句话说,我们返回的实际上是对下一节管道的引用。2.2push-waterpump有了adapter之后,我们还需要一个“waterpump”来不断的将我们的数据推送到不同的pipeline,最终到达目标点。typePipeline={/***实现该方法可以通过管道逐层传递数据*@paramdata*/push(data:T[]):Promise;/***链接多节管道*e.g.*constapp=newBaseApp();*app.pipe(newTestApp1()).pipe(newTestApp2()).pipe(newTestApp3()).pipe(newOutput()).pipe(newEnd())*@param_next*/管道(_next:管道):管道;};为了适应更多的场景,我们设计这个水泵接受类型为T[]的数组,在管道的第一段,当我们得到初始数据源时,我们可以使用这个水泵(方法)来推送数据出来,让后续的各个加工车间对数据进行处理。2.3resolveData——处理车间当我们的数据被推送到流水线的某一段时,就会有一个处理车间,根据它们不同的流程对推送过来的数据进行粗略的处理。注意:我们每个加工车间要尽可能保证职责分离。每个加工车间负责一部分工作,对数据进行粗加工,而不是把所有的工作都放在一个加工车间,否则流水线数据会丢失。意义。typePipeline={/***实现该方法可以通过管道逐层传递数据*@paramdata*/push(data:T[]):Promise;/***链接多节管道*e.g.*constapp=newBaseApp();*app.pipe(newTestApp1()).pipe(newTestApp2()).pipe(newTestApp3()).pipe(newOutput()).pipe(newEnd())*@param_next*/管道(_next:管道):管道;/***用于接受上一段管道传来的数据进行处理,然后传递给下一段管道*@paramdata*/resolveData(data:T[]):T[]|Promise;};处理车间仍然接收到一个T[]类型的数据数组,拿到这个数据后,按照各自的流程对数据进行处理。加工完成后放回流水线的传送带上(返回值),送至下一条流水线的加工车间进行进一步加工。3.具体实现上面我们只是定义了一个pipeline应该具备的最基本的行为。只有具备以上行为能力的类才能被认为是合格的管道。那么,接下来,我们就来看看一个管道类需要如何实现。3.1基本管道模型类classBaseAppimplementsPipeline

{constructor(data?:P[]){data&&this.push(data);}/***仅供内部使用,下一节管道参考*/protectednext:Pipeline

|不明确的;/***收到数据后,使用resolveData处理得到新书店,将新数据推送到管道的下一段*@paramdata*/asyncpush(data:P[]):Promise{data=awaitthis.resolveData(data);this.next?.push(数据);}/***Chainpipeline*让pipe的返回值永远是pipeline的下一段,这样就可以链式调用*@param_next*@returns*/pipe(_next:Pipeline

):Pipeline

{this.next=_next;返回_next;}/***数据处理,返回最新的数据对象*@paramdata*@returns*/resolveData(data:P[]):P[]|Promise{返回数据;}}我们定义了一个实现了Pipleline接口的基类,用来描述所有的管道是什么样子的,我们所有的管道都需要继承这个基类。在构造函数中,我们接受一个可选参数,它代表我们的初始数据源。只有流水线的第一段需要传入此参数,才能将初始数据注入整个流水线。在我们得到这个初始数据之后,我们将使用水泵(push)将这些数据推出。3.2管道统一数据对象通常在程序实现时,我们会定义一个统一的数据对象作为管道中流动的数据,这样更便于维护和管理。输入PipeLineData={数据源:{用户信息:{名字:字符串;姓氏:字符串;age:number,}}}3.3首段管道由于首段管道之前没有管道,我们要让数据流动起来,就需要在首段使用水泵pipeline为数据提供初始动能,使其可以流动。因此,第一段管道的实现会与其他管道略有不同。导出类PipelineWorker1extendsBaseApp{constructor(data:T[]){super(data);}}管道的第一段主要作用是接受原始数据源,使用水泵将数据发送出去,所以实现比较简单,只需要继承我们的基类BaseApp,并提交初始数据源到基类,然后基类可以用水泵将数据推出。3.4OtherpipelinesOtherpipelines每个pipeline都会有一个数据处理车间来处理流向当前pipeline的数据,所以我们还需要重写基类的resolveData方法。导出类PipelineWorker2扩展BaseApp{constructor(){super();}resolveData(数据:PipeLineData[]):PipeLineData[]|Promise{//这里我们可以对数据进行一些特定的操作Processing//注意我们尽量对传入的数据进行操作,保持引用data.forEach(item=>{item.userInfo.name=`${item.userInfo.firstName}${item.userInfo.lastName}`});//最后调用基类的resolveData方法传入处理后的数据,//这样就完成了一个流程的处理returnsuper.resolveData(data);}}exportclassPipelineWorker3extendsBaseApp{constructor(){super();}resolveData(数据:PipeLineData[]):PipeLineData[]|Promise{//这里我们可以对数据做一些具体的处理//注意我们尽量对传入的数据进行操作,保持引用data.forEach(item=>{item.userInfo.年龄+=10;});//最后我们调用基类的resolveData方法处理好数据传入,//这样就完成了一个流程的处理returnsuper.resolveData(data);}}exportclassOutputextendsBaseApp{constructor(){极好的();}resolveData(数据:PipeLineData[]):PipeLineData[]|Promise{//在这里我们可以对数据做一些具体的处理//注意我们尽量对传入的数据进行操作,一直参考console.log(data);//最后调用基类的resolveData方法传入处理后的数据,//这样就完成了一个流程的处理returnsuper.resolveData(data);}}//我们还可以利用流水线组装灵活的特性,开发各种插件,随时插拔。导出类Plugin1扩展BaseApp{constructor(){super();}resolveData(数据:PipeLineData[]):PipeLineData[]|Promise{//这里我们可以对数据进行一些具体的处理//注意我们尝试对传入的数据进行操作,一直引用console.log("Thisisaplug-in");//最后调用基类的resolveData方法传入处理后的数据,//这样就完成了一个流程的处理returnsuper.resolveData(data);}}3.5组装流水线上面我们已经准备好了每一段流水线,现在我们需要组装它们并使用它们constdatasource={userInfo:{firstName:"kiner",lastName:"tang",age:18}};constapp=newPipelineWorker1(datasource);//管道可以自由组合app.pipe(newOutput()).pipe(newPipelineWorker2()).pipe(newOutput()).pipe(newPipelineWorker3()).pipe(newOutput()).pipe(newPlugin1());4.结论至此,我们完成了一个流水线架构的设计。是不是觉得使用了流水线数据之后,我们整个程序代码的数据流向更加清晰,各个模块之前的分工更加明确,模块之间和模块之前的项目之间的配合更加灵活?使用流水线设计也让我们扩展了一个额外的插件库,用户可以随意定制满足各种业务场景的插件,让我们的程序具有极强的可扩展性。