Node.js Stream - 实战篇

前面两篇(基础篇和进阶篇)主要介绍流的基本用法和原理,本篇从应用的角度,介绍如何使用管道进行程序设计,主要内容包括:

  1. 管道的概念
  2. Browserify的管道设计
  3. Gulp的管道设计
  4. 两种管道设计模式比较
  5. 实例

所谓“管道”,指的是通过a.pipe(b)的形式连接起来的多个Stream对象的组合。

假如现在有两个Transformboldred,分别可将文本流中某些关键字加粗和飘红。 可以按下面的方式对文本同时加粗和飘红:

// source: 输入流
// dest: 输出目的地
source.pipe(bold).pipe(red).pipe(dest)

bold.pipe(red)便可以看作一个管道,输入流先后经过boldred的变换再输出。

但如果这种加粗且飘红的功能的应用场景很广,我们期望的使用方式是:

// source: 输入流
// dest: 输出目的地
// pipeline: 加粗且飘红
source.pipe(pipeline).pipe(dest)

此时,pipeline封装了bold.pipe(red),从逻辑上来讲,也称其为管道。 其实现可简化为:

var pipeline = new Duplex()
var streams = pipeline._streams = [bold, red]// 底层写逻辑:将数据写入管道的第一个Stream,即bold
pipeline._write = function (buf, enc, next) {streams[0].write(buf, enc, next)
}// 底层读逻辑:从管道的最后一个Stream(即red)中读取数据
pipeline._read = function () {var bufvar reads = 0var r = streams[streams.length - 1]// 将缓存读空while ((buf = r.read()) !== null) {pipeline.push(buf)reads++}if (reads === 0) {// 缓存本来为空,则等待新数据的到来r.once('readable', function () {pipeline._read()})}
}// 将各个Stream组合起来(此处等同于`bold.pipe(red)`)
streams.reduce(function (r, next) {r.pipe(next)return next
})

pipeline写数据时,数据直接写入bold,再流向red,最后从pipeline读数据时再从red中读出。

如果需要在中间新加一个underline的Stream,可以:

pipeline._streams.splice(1, 0, underline)
bold.unpipe(red)
bold.pipe(underline).pipe(red)

如果要将red替换成green,可以:

// 删除red
pipeline._streams.pop()
bold.unpipe(red)// 添加green
pipeline._streams.push(green)
bold.pipe(green)

可见,这种管道的各个环节是可以修改的。

stream-splicer对上述逻辑进行了进一步封装,提供splicepushpop等方法,使得pipeline可以像数组那样被修改:

var splicer = require('stream-splicer')
var pipeline = splicer([bold, red])
// 在中间添加underline
pipeline.splice(1, 0, underline)// 删除red
pipeline.pop()// 添加green
pipeline.push(green)

labeled-stream-splicer在此基础上又添加了使用名字替代下标进行操作的功能:

var splicer = require('labeled-stream-splicer')
var pipeline = splicer(['bold', bold,'red', red,
])// 在`red`前添加underline
pipeline.splice('red', 0, underline)// 删除`bold`
pipeline.splice('bold', 1)

由于pipeline本身与其各个环节一样,也是一个Stream对象,因此可以嵌套:

var splicer = require('labeled-stream-splicer')
var pipeline = splicer(['style', [ bold, red ],'insert', [ comma ],
])pipeline.get('style')     // 取得管道:[bold, red].splice(1, 0, underline) // 添加underline

Browserify的功能介绍可见substack/browserify-handbook,其核心逻辑的实现在于管道的设计:

var splicer = require('labeled-stream-splicer')
var pipeline = splicer.obj([// 记录输入管道的数据,重建管道时直接将记录的数据写入。// 用于像watch时需要多次打包的情况'record', [ this._recorder() ],// 依赖解析,预处理'deps', [ this._mdeps ],// 处理JSON文件'json', [ this._json() ],// 删除文件前面的BOM'unbom', [ this._unbom() ],// 删除文件前面的`#!`行'unshebang', [ this._unshebang() ],// 语法检查'syntax', [ this._syntax() ],// 排序,以确保打包结果的稳定性'sort', [ depsSort(dopts) ],// 对拥有同样内容的模块去重'dedupe', [ this._dedupe() ],// 将id从文件路径转换成数字,避免暴露系统路径信息'label', [ this._label(opts) ],// 为每个模块触发一次dep事件'emit-deps', [ this._emitDeps() ],'debug', [ this._debug(opts) ],// 将模块打包'pack', [ this._bpack ],// 更多自定义的处理'wrap', [],
])

每个模块用row表示,定义如下:

{// 模块的唯一标识id: id,// 模块对应的文件路径file: '/path/to/file',// 模块内容source: '',// 模块的依赖deps: {// `require(expr)`expr: id,}
}

wrap阶段前,所有的阶段都处理这样的对象流,且除pack外,都输出这样的流。 有的补充row中的一些信息,有的则对这些信息做一些变换,有的只是读取和输出。 一般row中的sourcedeps内容都是在deps阶段解析出来的。

下面提供一个修改Browserify管道的函数。

var Transform = require('stream').Transform
// 创建Transform对象
function through(write, end) {return Transform({transform: write,flush: end,})
}// `b`为Browserify实例
// 该插件可打印出打包时间
function log(b) {// watch时需要重新打包,整个pipeline会被重建,所以也要重新修改b.on('reset', reset)// 修改当前pipelinereset()function reset () {var time = nullvar bytes = 0b.pipeline.get('record').on('end', function () {// 以record阶段结束为起始时刻time = Date.now()})// `wrap`是最后一个阶段,在其后添加记录结束时刻的Transformb.pipeline.get('wrap').push(through(write, end))function write (buf, enc, next) {// 累计大小bytes += buf.lengththis.push(buf)next()}function end () {// 打包时间var delta = Date.now() - timeb.emit('time', delta)b.emit('bytes', bytes)b.emit('log', bytes + ' bytes written ('+ (delta / 1000).toFixed(2) + ' seconds)')this.push(null)}}
}var fs = require('fs')
var browserify = require('browserify')
var b = browserify(opts)
// 应用插件
b.plugin(log)
b.bundle().pipe(fs.createWriteStream('bundle.js'))

事实上,这里的b.plugin(log)就是直接执行了log(b)

在插件中,可以修改b.pipeline中的任何一个环节。 因此,Browserify本身只保留了必要的功能,其它都由插件去实现,如watchify、factor-bundle等。

除了了上述的插件机制外,Browserify还有一套Transform机制,即通过b.transform(transform)可以新增一些文件内容预处理的Transform。 预处理是发生在deps阶段的,当模块文件内容被读出来时,会经过这些Transform处理,然后才做依赖解析,如babelify、envify。

Gulp的核心逻辑分成两块:任务调度与文件处理。 任务调度是基于orchestrator,而文件处理则是基于vinyl-fs。

类似于Browserify提供的模块定义(用row表示),vinyl-fs也提供了文件定义(vinyl对象)。

Browserify的管道处理的是row流,Gulp管道处理vinyl流:

gulp.task('scripts', ['clean'], function() {// Minify and copy all JavaScript (except vendor scripts) // with sourcemaps all the way down return gulp.src(paths.scripts).pipe(sourcemaps.init()).pipe(coffee()).pipe(uglify()).pipe(concat('all.min.js')).pipe(sourcemaps.write()).pipe(gulp.dest('build/js'));
});

任务中创建的管道起始于gulp.src,终止于gulp.dest,中间有若干其它的Transform(插件)。

如果与Browserify的管道对比,可以发现Browserify是确定了一条具有完整功能的管道,而Gulp本身只提供了创建vinyl流和将vinyl流写入磁盘的工具,管道中间经历什么全由用户决定。 这是因为任务中做什么,是没有任何限制的,文件处理也只是常见的情况,并非一定要用gulp.srcgulp.dest

Browserify与Gulp都借助管道的概念来实现插件机制。

Browserify定义了模块的数据结构,提供了默认的管道以处理这样的数据流,而插件可用来修改管道结构,以定制处理行为。

Gulp虽也定义了文件的数据结构,但只提供产生、消耗这种数据流的接口,完全由用户通过插件去构造处理管道。

当明确具体的处理需求时,可以像Browserify那样,构造一个基本的处理管道,以提供插件机制。 如果需要的是实现任意功能的管道,可以如Gulp那样,只提供数据流的抽象。

本节中实现一个针对Git仓库自动生成changelog的工具,完整代码见ezchangelog。

ezchangelog的输入为git log生成的文本流,输出默认为markdown格式的文本流,但可以修改为任意的自定义格式。

输入示意:

commit 9c5829ce45567bedccda9beb7f5de17574ea9437
Author: zoubin <zoubin04@gmail.com>
Date:   Sat Nov 7 18:42:35 2015 +0800CHANGELOGcommit 3bf9055b732cc23a9c14f295ff91f48aed5ef31a
Author: zoubin <zoubin04@gmail.com>
Date:   Sat Nov 7 18:41:37 2015 +08004.0.3commit 87abe8e12374079f73fc85c432604642059806ae
Author: zoubin <zoubin04@gmail.com>
Date:   Sat Nov 7 18:41:32 2015 +0800fix readmeadd more tests

输出示意:

* [[`9c5829c`](https://github.com/zoubin/ezchangelog/commit/9c5829c)] CHANGELOG## [v4.0.3](https://github.com/zoubin/ezchangelog/commit/3bf9055) (2015-11-07)* [[`87abe8e`](https://github.com/zoubin/ezchangelog/commit/87abe8e)] fix readmeadd more tests

其实需要的是这样一个pipeline

source.pipe(pipeline).pipe(dest)

可以分为两个阶段: * parse:从输入文本流中解析出commit信息 * format: 将commit流变换为文本流

默认的情况下,要想得到示例中的markdown,需要解析出每个commit的sha1、日期、消息、是否为tag。 定义commit的格式如下:

{commit: {// commit sha1long: '3bf9055b732cc23a9c14f295ff91f48aed5ef31a',short: '3bf9055',},committer: {// commit datedate: new Date('Sat Nov 7 18:41:37 2015 +0800'),},// raw message linesmessages: ['', '    4.0.3', ''],// raw headers before the messagesheaders: [['Author', 'zoubin <zoubin04@gmail.com>'],['Date', 'Sat Nov 7 18:41:37 2015 +0800'],],// the first non-empty message linesubject: '4.0.3',// other message linesbody: '',// git tagtag: 'v4.0.3',// link to the commit. opts.baseUrl should be specified.url: 'https://github.com/zoubin/ezchangelog/commit/3bf9055',
}

于是有:

var splicer = require('labeled-stream-splicer')
pipeline = splicer.obj(['parse', [// 按行分隔'split', split(),// 生成commit对象,解析出sha1和日期'commit', commit(),// 解析出tag'tag', tag(),// 解析出url'url', url({ baseUrl: opts.baseUrl }),],'format', [// 将commit组合成markdown文本'markdownify', markdownify(),],
])

至此,基本功能已经实现。 现在将其封装并提供插件机制。

function Changelog(opts) {opts = opts || {}this._options = opts// 创建pipelinethis.pipeline = splicer.obj(['parse', ['split', split(),'commit', commit(),'tag', tag(),'url', url({ baseUrl: opts.baseUrl }),],'format', ['markdownify', markdownify(),],])// 应用插件;[].concat(opts.plugin).filter(Boolean).forEach(function (p) {this.plugin(p)}, this)
}Changelog.prototype.plugin = function (p, opts) {if (Array.isArray(p)) {opts = p[1]p = p[0]}// 执行插件函数,修改pipelinep(this, opts)return this
}

上面的实现提供了两种方式来应用插件。 一种是通过配置传入,另一种是创建实例后再调用plugin方法,本质一样。

为了使用方便,还可以简单封装一下。

function changelog(opts) {return new Changelog(opts).pipeline
}

这样,就可以如下方式使用:

source.pipe(changelog()).pipe(dest)

这个已经非常接近我们的预期了。

现在来开发一个插件,修改默认的渲染方式。

var through = require('through2')function customFormatter(c) {// c是`Changelog`实例// 添加解析author的transformc.pipeline.get('parse').push(through.obj(function (ci, enc, next) {// parse the author name from: 'zoubin <zoubin04@gmail.com>'ci.committer.author = ci.headers[0][1].split(/\s+/)[0]next(null, ci)}))// 替换原有的渲染c.pipeline.get('format').splice('markdownify', 1, through.obj(function (ci, enc, next) {var sha1 = ci.commit.shortsha1 = '[`' + sha1 + '`](' + c._options.baseUrl + sha1 + ')'var date = ci.committer.date.toISOString().slice(0, 10)next(null, '* ' + sha1 + ' ' + date + ' @' + ci.committer.author + '\n')}))
}source.pipe(changelog({baseUrl: 'https://github.com/zoubin/ezchangelog/commit/',plugin: [customFormatter],})).pipe(dest)

同样的输入,输出将会是:

* [`9c5829c`](https://github.com/zoubin/ezchangelog/commit/9c5829c) 2015-11-07 @zoubin
* [`3bf9055`](https://github.com/zoubin/ezchangelog/commit/3bf9055) 2015-11-07 @zoubin
* [`87abe8e`](https://github.com/zoubin/ezchangelog/commit/87abe8e) 2015-11-07 @zoubin

可以看出,通过创建可修改的管道,ezchangelog保持了本身逻辑的单一性,同时又提供了强大的自定义空间。

  • GitHub,substack/browserify-handbook
  • GitHub,zoubin/streamify-your-node-program

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/477088.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我们的Web3创业项目,黄了

文 | 邬宇琛源 | 投资界PEdaily这是今年VC圈最具争议的赛道&#xff0c;如今也可能是熄灭最快的赛道。三个月&#xff0c;换了三个方向。今年春天&#xff0c;北京某出海互联网公司在内部筹划起一个新的项目&#xff0c;项目直指风口——Web3。毫无Web3经验的互联网运营经理吴欣…

决策树(Decision Tree,DT)

文章目录1. 决策树模型与学习2. 特征选择2.1 特征选择Python代码3. 决策树的生成3.1 Python代码4. 决策树的剪枝5. CART 算法6. sklearn 例子6.1 书上贷款例子6.2 鸢尾花 及 决策树可视化附. 本文完整代码决策树&#xff08;decision tree&#xff09;是一种基本的分类与回归方…

使用模板快速编写测试用例

在高速发展的互联网公司&#xff0c;由于产品的开发迭代太快&#xff0c;产品测试经常遇到以下几个问题&#xff1a; 1. 如何在快速的产品开发迭代中迅速地完成对产品功能的测试&#xff1f; 2. 面对用户众多、环境多样&#xff0c;如何尽可能地测试全面&#xff1f; 3. 公司扩…

最新!Kaggle所有竞赛开源方案和Top思路汇总,共477场竞赛!

数据竞赛能帮助我们快速提升数据分析和建模能力&#xff0c;非常多的朋友也非常希望学习Top比赛的方案。之前也有非常多的朋友问我某某竞赛的方案有没有&#xff0c;有时我不是很忙的时候就会把对应的竞赛中把Top的链接找出来一起整理发过去&#xff0c;但也有的时候可能会比较…

LeetCode 65. 有效数字(逻辑题,难)

1. 题目 验证给定的字符串是否可以解释为十进制数字。 例如: "0" > true " 0.1 " > true "abc" > false "1 a" > false "2e10" > true " -90e3 " > true " 1e" > false "…

Mock Server实践

背景 在美团服务端测试中&#xff0c;被测服务通常依赖于一系列的外部模块&#xff0c;被测服务与外部模块间通过REST API或是Thrift调用来进行通信。要对被测服务进行系统测试&#xff0c;一般做法是&#xff0c;部署好所有外部依赖模块&#xff0c;由被测服务直接调用。然而有…

美团上交开源PromptDet:无需标注,开放世界的目标检测器

文 | 冯承健源 | 极市平台本文提出了一个开放世界的目标检测器PromptDet&#xff0c;它能够在没有任何手动标注的情况下检测新类别&#xff08;如下图绿色检测框&#xff09;&#xff0c;其中提出区域prompt学习方法和针对网络图像的自训练方法&#xff0c;性能表现SOTA。主页&…

Docker系列之二:基于容器的自动构建

自动构建系统是从美团的自动部署系统发展出来的一个新功能。每当开发人员提交代码到仓库后&#xff0c;系统会自动根据开发人员定制的构建配置&#xff0c;启动新的Docker容器&#xff0c;在其中对源代码进行构建&#xff08;build&#xff09;&#xff0c;包括编译&#xff08…

剑指Offer - 面试题56 - I. 数组中数字出现的次数(异或,分组)

1. 题目 一个整型数组 nums 里除两个数字之外&#xff0c;其他数字都出现了两次。请写程序找出这两个只出现一次的数字。 要求时间复杂度是O(n)&#xff0c;空间复杂度是O(1)。 示例 1&#xff1a; 输入&#xff1a;nums [4,1,4,6] 输出&#xff1a;[1,6] 或 [6,1]示例 2&a…

自动驾驶技术简史

文 | Bernard_Han自动驾驶是一个最近在产业界炙手可热的关键词。无论是与人工智能相关的顶级会议还是各大造车厂商甚至各大投资商都为这个成长初期的蓝海产业下注了美好的未来。但是“汽车自动化”的理论与自动驾驶不同&#xff0c;提出至今已有近百年的历史。从最初的遥控到如…

你是什么时候对深度学习失去信心的?

文 | 霍华德源 | 知乎最近几天在知乎上有个问题火了&#xff1a;你是什么时候对深度学习失去信心的&#xff1f;在此推荐一下知乎大V霍华德的回答&#xff0c;以下为原回答。对于深度学习的现状&#xff0c;工业界还是很清楚的。如果没有变革性的突破&#xff0c;弱人工智能时代…

OpenTSDB 造成 Hbase 整点压力过大问题的排查和解决

业务背景 OpenTSDB 是一款非常适合存储海量时间序列数据的开源软件&#xff0c;使用 HBase 作为存储让它变的非常容易扩展。我们在建设美团性能监控平台的过程中&#xff0c;每天需要处理数以亿计的数据&#xff0c;经过几番探索和调研&#xff0c;最终选取了 OpenTSDB 作为数据…

LintCode 183. 木材加工(二分查找)

1. 题目 有一些原木&#xff0c;现在想把这些木头切割成一些长度相同的小段木头&#xff0c;需要得到的小段的数目至少为 k。当然&#xff0c;我们希望得到的小段越长越好&#xff0c;你需要计算能够得到的小段木头的最大长度。 样例 1 输入: L [232, 124, 456] k 7 输出: …

AC算法在美团上单系统的应用

1.背景 在美团&#xff0c;为了保证单子质量&#xff0c;需要对上单系统创建的每一个产品进行审核。为了提高效率&#xff0c;审核人员积累提炼出了一套关键词库&#xff0c;先基于该词库进行自动审核过滤&#xff0c;对于不包括这些关键词的产品信息不再需要进行人工审核。因此…

LintCode 600. 包裹黑色像素点的最小矩形(BFS)

1. 题目 一个由二进制矩阵表示的图&#xff0c;0 表示白色像素点&#xff0c;1 表示黑色像素点。 黑色像素点是联通的&#xff0c;即只有一块黑色区域。 像素是水平和竖直连接的&#xff0c;给一个黑色像素点的坐标 (x, y) &#xff0c;返回囊括所有黑色像素点的矩阵的最小面积…

浙大、阿里提出DictBERT,字典描述知识增强的预训练语言模型

文 | 刘聪NLP源 | NLP工作站写在前面大家好&#xff0c;我是刘聪NLP。今天给大家带来一篇IJCAI2022浙大和阿里联合出品的采用对比学习的字典描述知识增强的预训练语言模型-DictBERT&#xff0c;全名为《Dictionary Description Knowledge Enhanced Language Model Pre-training…

LintCode 207. 区间求和 II(线段树)

1. 题目 在类的构造函数中给一个整数数组, 实现两个方法 query(start, end) 和 modify(index, value): 对于 query(start, end), 返回数组中下标 start 到 end 的 和。对于 modify(index, value), 修改数组中下标为 index 上的数为 value. 样例1 输入: [1,2,7,8,5] [query(0…

深入解析String#intern

在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快&#xff0c;更节省内存&#xff0c;都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。 8种基本类型的常量池都是系统协调的&#xff0c;String类型的常量池…

想通这点,治好 AI 打工人的精神内耗

文 | 天于刀刀受到疫情影响&#xff0c;今年公司的校招生报道日还未到来&#xff0c;23 年的秋招提前批就已经是如火如荼地开展。而诸神黄昏算法岗&#xff0c;作为招聘中最靓眼的仔&#xff0c;简历门槛早已是硕士打底博士起步&#xff0c;项目竞赛多多益善的情况了。面临着今…

DHL

有句俗语谓&#xff1a;“不看不知道&#xff0c;一看吓一跳”&#xff0c;这次通过“中外运-敦豪”的一次快递&#xff0c;亲身感受到这种“吓一跳”的滋味。 MS 总部从 1 月 26 日寄出 MVP Award 快递包之后&#xff0c;在随后的电子邮件中给出了每个人的 DHL 快件追踪号&…