JS中的File(四):文件流Streams API使用详解

目录

一、流的原理

二、流的分类

1、可读流(ReadableStream)

3、转换流(TransformStream)

三、流中的Request和Response对象

四、综合应用


PS:涉及到一些基本的文件操作和格式内容知识,可以进入我的主页参考我之前的此系列文章。这个系列我还会继续更新下去的~

参考:

从 Fetch 到 Streams —— 以流的角度处理网络请求 - 掘金 (juejin.cn)

Stream API - Web API 接口参考 | MDN (mozilla.org)

一、流的原理

在流之前,如果想要对文件资源进行操作,需要先下载完整的文件,等待它反序列化成合适的格式,在完整地对接收到的内容进行统一处理。流出现之后,网络发送文件可以将文件以一块块的数据形式传输,这使得——视频缓冲区 和 逐渐加载播放其他类型的资源成为可能

*在阅读的时候,总会遇到一个词组叫做"Underlying Sink",我们翻译为"底层接收器"或者"底层汇聚器",指的是用于接收流写入数据的底层组件或实体,表达的是底层数据传输的目标或者终点

流的优点:

  • 在javascript中对流可以进行按块、按位处理,不再需要缓冲区、字符串或者blob;
  • 你可以检测流何时开始或结束,将流链接在一起,根据需要处理错误和取消流,并对流的读取速度做出反应。
  • 流的处理实现了异步,很多方法基于Promise,所以注意时序顺序

由于网络请求方法Fetch API,它区别于原始的XHR不仅仅在是基于Promise处理网络请求任务(XHR是基于事件驱动的callback),更是在于它是基于数据流来实现的。所以下文在描述流的操作时,不可避免要和fetch结合。另外,fetch API基于Request对象实现的,因此下文中提及的Request对象的基于流的使用和fetch API同理

二、流的分类

1、可读流(ReadableStream)

表示数据的可读流。在使用Fetch进行网络请求处理时,请求对象中的body属性Request.body 和 响应对象Response.body 属性中可以获取到可读流的主体,这是将主体内容暴露作为一个可读流的 getter。                                                                                             

 或者开发者可以自定义流(使用ReadableStream()进行构造,定义一些钩子比如start、cancel、pull等,参数的传入是controller控制器实例,同下文中提及的构造器参数

举个应用栗子

// Fetch the original image
fetch("./tortoise.png")// Retrieve its body as ReadableStream.then((response) => response.body).then((body) => {const reader = body.getReader();// 使用 read读取,并获取状态和valueconst { done, value } = await reader.read();if (done) {console.log('End of file reached');// 释放读取器reader.releaseLock();} else {// 处理读取到的数据块console.log('Read:', value);// 继续递归读取下一个数据块// 可以递归读取 }});

 ReadableStream中除了getReader()获取对可读流的读取器,还有一些常用方法

  • ReadableStream.tee():拷贝流,实现的是返回一个数组,包含对原始可读流的两个相同的副本可读流,然后可以独立的使用不同的 reader 读取
  • 链式管道传输ReadableStream.pipeThrough()和ReadableStream.pipeTo():实现的是从一个流输出到另一个。前者pipeThrough是将可读流管道输出至拥有一对writer/reader 的流中比如下文介绍的TransformStream,并将一种数据转换成另一种类型的流(比如输入WritableStream->TransformStream->返回输出ReadableStream);后者pipeTo是可读流管道传输至作为链式管道传输终点的 writer(转换为readableStream->writableStream

 2、可写流(WritableStream)

为将流写入目的地(称为接收器)的过程,提供了一个标准抽象。内置了背压和队列机制

*解释下背压和队列机制:

  1. 背压(Backpressure):

    • 背压是指在数据写入速度大于消费者(读取端)的处理速度时,为了防止数据溢出,写入端必须采取一些措施以减缓写入速度的现象。
    • 在可写流(WritableStream)中,当写入速度过快而消费者无法跟上时,可写流会发出背压信号,告诉写入端要减缓写入。这可以通过 writer.write() 返回的 Promise 来实现,当 Promise 处于挂起状态时,表示发生了背压。
  2. 队列机制:

    • 队列机制是一种用于缓存待写入数据的机制,以确保写入端和消费者之间的速度匹配。
    • WritableStream 中,有一个内部的队列用于存储待写入的数据块。写入端通过 writer.write(chunk) 将数据块推送到队列中。如果队列已满或发生背压,writer.write(chunk) 返回一个处于挂起状态的 Promise。
    • 当队列中的数据被消费者处理时,队列中的下一个数据块会被写入。

定义构造WritableStream有两个对象参数:第一个必选,用于配置一些写入流时的钩子;第二个可选,用于配置一些chunk入队和队列控制的策略(利用ByteLengthQueuingStrategy【按字节计量】CountQueuingStrategy【按元素数量计量】接口去定义策略)

在必选中,所有的对象字段都是可选的,如下:

  • start(controller):在WritableStream对象完成构造后立即调用controller method执行一次
  • write(chunk,controller):每当一个新的chunk准备写入接收器的时候,将调用方法
  • close(controller):当结束写入流时候调用该方法
  • abort(reason):当写入流被中断或者写入流进入错误状态的时候,调用该方法

构造完的WritableStream可以用getWriter()方法获取其写入器

举个应用栗子

const decoder = new TextDecoder("utf-8");
const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });
let result = "";
const writableStream = new WritableStream({// Implement the sinkwrite(chunk) {return new Promise((resolve, reject) => {const buffer = new ArrayBuffer(1);const view = new Uint8Array(buffer);view[0] = chunk;const decoded = decoder.decode(view, { stream: true });const listItem = document.createElement("li");listItem.textContent = `Chunk decoded: ${decoded}`;list.appendChild(listItem);result += decoded;resolve();});},close() {const listItem = document.createElement("li");listItem.textContent = `[MESSAGE RECEIVED] ${result}`;list.appendChild(listItem);},abort(err) {console.error("Sink error:", err);},},queuingStrategy,
);

3、转换流(TransformStream)

 代表一个即可写入又可读取的流,在读写之间起着中间流转换的作用。因此转换流比较简单,无实例方法,可自定义构造,只有两个只读的属性ReadableStream与 WritableStream,这两个都暴露的都是自身的可读流和可写流。通常,借助这两个属性来做中间转换!

举个应用栗子

实现输入的所有字符串都转为大写字母

class UpperCaseTransformer {constructor() {this.transformStream = new TransformStream({start(controller) {//开始钩子//----下面是给个示例!// 将会在对象创建时立刻执行,并传入一个流控制器controller.desiredSize// 填满队列所需字节数controller.enqueue(chunk)// 向可读取的一端传入数据片段controller.error(reason)// 同时向可读取与可写入的两侧触发一个错误controller.terminate()// 关闭可读取的一侧,同时向可写入的一侧触发错误},async transform(chunk, controller) {//中间chunck转换// 将 chunk 转换为大写const upperCaseChunk = chunk.toString().toUpperCase();// 块入队,将转换后的数据传递给下游controller.enqueue(upperCaseChunk);},flush(controller) {// 当可写入的一端得到的所有的片段完全传入 transform() 方法处理后,在可写入的一端即将关    闭时调用}},queuingStrategy); //queuingStrategy策略内容假设为 { highWaterMark: 1 }//获取自身的读写流主提this.readableStream = this.transformStream.readable;this.writableStream = this.transformStream.writable;}// 关闭流async close() {await this.writableStream.getWriter().close();}// 获取读取器getReader() {return this.readableStream.getReader();}// 获取写入器getWriter() {return this.writableStream.getWriter();}
}

三、流中的Request和Response对象

这两个对象的很多属性依然和普通网络请求一样,可以直接通过对象获取,比如headers、request中的url和credentials等、response中的status等。

但是,在fetch和Request对象下,这两个对象的body属性返回的ReadableStream

那么要如何转换成文件的其他资源格式和对象呢?request和response都有着相同的实例方法如下,只不过request是调用这些方法作为请求体body的内容格式,response是将响应体的body解析为对应格式

arrayBuffer() // 返回一个Promise,兑现为ArrayBuffer
blob() // 返回一个Promise,兑现为blob
formData() //如上
text() //如上,返回字符串clone()//将request或者response拷贝

直接单独说的是json()方法,request和response都有着相同的实例方法,可以将body内容兑现为json

但是response中还有一个静态方法json,可以在后端处理的时候,将输入的data直接以json格式塞入body而不是默认的readableStream;options配置status状态码、statusText状态信息、headers等,返回的是一个  以json内容为body   的response对象

Response.json(data, options)

举个应用栗子

fetch读取图像【涉及到如何从stream转为blob】

const image = document.getElementById("target");// 请求原始图片
fetch("./tortoise.png")// 取出 body.then((response) => response.body).then((body) => {const reader = body.getReader();return new ReadableStream({start(controller) {return pump();function pump() {return reader.read().then(({ done, value }) => {// 读不到更多数据就关闭流if (done) {controller.close();return;}// 将下一个数据块置入流中controller.enqueue(value);return pump();});}},});}).then((stream) => new Response(stream)).then((response) => response.blob()).then((blob) => URL.createObjectURL(blob)).then((url) => console.log((image.src = url))).catch((err) => console.error(err));

四、综合应用

将彩色图片转成由灰度级别信息(grayscale)表示的黑白图,达成以下效果

github链接:dom-examples/streams/grayscale-png at main · mdn/dom-examples (github.com)

这里仅粘贴与流操作有关的核心代码,其中转换代码在png-lib.js里面,流读取在index.html中,有兴趣的去github看,里面还有不少其他例子~

transform:

class GrayscalePNGTransformer {constructor() {this._mode = 'magic';}/*** Called for every downloaded PNG data chunk to be grayscaled.** @param {Uint8Array} chunk The downloaded chunk.* @param {TransformStreamDefaultController} controller The controller to euqueue grayscaled data.*/transform(chunk, controller) {let position = chunk.byteOffset;let length = chunk.byteLength;const source = new DataView(chunk.buffer, position, length);const buffer = new Uint8Array(length);const target = new DataView(buffer.buffer, position, length);while (position < length) {switch (this._mode) {case 'magic': {const magic1 = source.getUint32(position);target.setUint32(position, magic1);position += 4;const magic2 = source.getUint32(position);target.setUint32(position, magic2);position += 4;const magic = magic1.toString(16) + '0' + magic2.toString(16);console.log('%cPNG magic:     %c %o', 'font-weight: bold', '', magic);if (magic !== '89504e470d0a1a0a') {throw new TypeError('This is not a PNG');}this._mode = 'header';break;}case 'header': {// Read chunk infoconst chunkLength = source.getUint32(position);target.setUint32(position, chunkLength);position += 4;const chunkName = this.readString(source, position, 4);this.writeString(target, position, chunkName);position += 4;if (chunkName !== 'IHDR') {throw new TypeError('PNG is missing IHDR chunk');}// Read image dimensionsthis._width = source.getUint32(position);target.setUint32(position, this._width);position += 4;this._height = source.getUint32(position);target.setUint32(position, this._height);position += 4;console.log('%cPNG dimensions:%c %d x %d', 'font-weight: bold', '', this._width, this._height);this._bitDepth = source.getUint8(position);target.setUint8(position, this._bitDepth);position += 1;console.log('%cPNG bit depth: %c %d', 'font-weight: bold', '', this._bitDepth);this._colorType = source.getUint8(position);target.setUint8(position, this._colorType);position += 1;console.log('%cPNG color type:%c %s', 'font-weight: bold', '', this.colorType(this._colorType));const compression = source.getUint8(position);target.setUint8(position, compression);position += 1;console.log('%cPNG compressio:%c %d', 'font-weight: bold', '', compression);const filter = source.getUint8(position);target.setUint8(position, filter);position += 1;console.log('%cPNG filter:    %c %d', 'font-weight: bold', '', filter);const interlace = source.getUint8(position);target.setUint8(position, interlace);position += 1;console.log('%cPNG interlace: %c %d', 'font-weight: bold', '', interlace);const chunkCrc = source.getUint32(position);target.setUint32(position, chunkCrc);position += 4;this._mode = 'data';break;}case 'data': {// Read chunk infoconst dataSize = source.getUint32(position);console.log('%cPNG data size: %c %d', 'font-weight: bold', '', dataSize);const chunkName = this.readString(source, position + 4, 4);if (chunkName !== 'IDAT') {throw new TypeError('PNG is missing IDAT chunk');}const crcStart = position + 4;// Extract the data from the PNG streamconst bytesPerCol = this.bytesPerPixel();const bytesPerRow = this._width * bytesPerCol + 1;let result = chunk.subarray(position + 8, position + 8 + dataSize);// Decompress the dataresult = pako.inflate(result);// Remove PNG filters from each scanlineresult = this.removeFilters(result, bytesPerCol, bytesPerRow);// Actually grayscale the imageresult = this.grayscale(result, bytesPerCol, bytesPerRow);// Compress with Deflateresult = pako.deflate(result);// Write data to targettarget.setUint32(position, result.byteLength);this.writeString(target, position + 4, 'IDAT');buffer.set(result, position + 8);position += result.byteLength + 8;const chunkCrc = crc32(chunkName, result);target.setUint32(position, chunkCrc);position += 4;this._mode = 'end';break;}case 'end': {// Write IEND chunktarget.setUint32(position, 0);position += 4;this.writeString(target, position, 'IEND');position += 4;target.setUint32(position, 2923585666);position += 4;controller.enqueue(buffer.subarray(0, position));return;}}}}/*** @param {DataView} dataView* @param {number} position* @param {number} length*/readString(dataView, position, length) {return new Array(length).fill(0).map((e, index) => String.fromCharCode(dataView.getUint8(position + index))).join('');}/*** @param {DataView} dataView* @param {number} position* @param {string} string*/writeString(dataView, position, string) {string.split('').forEach((char, index) => dataView.setUint8(position + index, char.charCodeAt(0)));}//..........未完}

流读取和转换:

<script type="application/javascript">const image = document.getElementById('target');// Fetch the original imagefetch('tortoise.png')// Retrieve its body as ReadableStream.then(response => response.body)// Create a gray-scaled PNG stream out of the original.then(rs => rs.pipeThrough(new TransformStream(new GrayscalePNGTransformer())))// Create a new response out of the stream.then(rs => new Response(rs))// Create an object URL for the response.then(response => response.blob()).then(blob => URL.createObjectURL(blob))// Update image.then(url => image.src = url).catch(console.error);</script>

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

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

相关文章

书生·浦语大模型实战营第四次课堂笔记

先来看看参考作业 哈哈到这才想起来写笔记 倒回去看发现要求将不要葱姜蒜换成自己的名字和昵称&#xff01; 好好好我就是不配玩&#xff08;换成管理员也不行&#xff01;&#xff09; 诶怎么能进这个环境&#xff1f;要进双系统ubuntu&#xff1f; 现在看视频发现原来是…

k8s------Pod、Label、NameSpace

一、Pod: Kubernetes中的最小调度对象 1.1 说明 Pod(容器组)是k8s创建和调度的最小单元。一个Pod封装多个容器(container)、存储资源(volume)、一个独立网络ip和管理控制容器运行方式。 Pod可以单独运行一个容器&#xff0c;也可以兼容多个容器运行&#xff0c;多个容器共享…

周五的胡思乱想

众所周知 csdn 在程序员的心目中是比较逊色的, 因为博客水平的参差不齐, 大部分人也都是用来作为自己的笔记方便未来复制。这样就导致这里的文章都是点到为止&#xff0c;没有去深究问题的根本原因&#xff0c;大家也都是复制一下解决方案就关闭的页面。或许这就是 csdn 的价值…

使用 ClassFinal 对SpringBoot jar加密加固并进行机器绑定

写在前面&#xff1a;各位看到此博客的小伙伴&#xff0c;如有不对的地方请及时通过私信我或者评论此博客的方式指出&#xff0c;以免误人子弟。多谢&#xff01;如果我的博客对你有帮助&#xff0c;欢迎进行评论✏️✏️、点赞&#x1f44d;&#x1f44d;、收藏⭐️⭐️&#…

Linux搭建dns主从服务器

一、实验要求 配置Dns主从服务器&#xff0c;能够实现正常的正反向解析 二、知识点 1、DNS简介 DNS&#xff08;Domain Name System&#xff09;是互联网上的一项服务&#xff0c;它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便的访问互联网。…

Microsoft365域名DNS设置

DNS 基础 项目2023/09/227 个参与者DNS 基础 - Microsoft 365 admin | Microsoft Learn 反馈 本文内容 观看&#xff1a;域和 DNS&#xff1a;概述什么是域名&#xff1f;了解 DNS 记录类型DNS 如何工作&#xff1f; 显示另外 4 个 如果找不到要查找的内容&#xff0c;请…

1.php开发-个人博客项目文章功能显示数据库操作数据接收

&#xff08;2022-day12&#xff09; #知识点 1-php入门&#xff0c;语法&#xff0c;提交 2-mysql 3-HTMLcss ​ 演示案例 博客-文章阅读功能初步实现 实现功能&#xff1a; 前端文章导航&#xff0c;点入内容显示&#xff0c;更改ID显示不同内容 实现步骤&#xff1…

java8 列表通过 stream流 根据对象属性去重的三种实现方法

java8 列表通过 stream流 根据对象属性去重的三种实现方法 一、简单去重 public class DistinctTest {/*** 没有重写 equals 方法*/SetterGetterToStringAllArgsConstructorNoArgsConstructorpublic static class User {private String name;private Integer age;}/*** lombo…

LeetCode.2788. 按分隔符拆分字符串

题目 题目链接 分析 题目的意思是给我们一个字符串数组和一个分隔符&#xff0c;让我们按照分隔符把字符串数组分割成新的字符串数组。 看到这个描述&#xff0c;这不就是直接就是利用 按照分隔符分割字符串的系统库函数split()&#xff0c;这个函数的意思就是 把一个字符串…

JDBC编程详细教程与示例源码

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl JDBC概述 为了在Java语言中提供对数据库访问的支持&#xff0c;Sun公司于1996年提供了一套访问数据库的标准Java类库JDBC。JDBC的全称是Java数据库连接(Java Database Conn…

使用条件操作执行控制图

Condition Action Behavior 此示例显示了具有多个分段的转换路径中的简单条件操作的行为。该图表使用传出转换的隐式排序 起初&#xff0c;chart处于休眠状态。状态A处于活动状态。条件C_one和C_two为false。事件E_one发生并唤醒chart&#xff0c;该图表通过层次结构从根向下…

第5章 运算符重载

运算符概述 纯单目运算符&#xff0c;只能有一个操作数&#xff0c;包括&#xff1a;!、~、sizeof、new、delete 等 纯双目运算符&#xff0c;只能有两个操作数&#xff0c;包括&#xff1a;[]、->、% 、 等 三目运算符&#xff0c;有三个操作数&#xff0c;如“ ? : ” 既…

C#,字符串匹配(模式搜索)有限自动机(Finite Automata)算法的源代码

一、有限状态自动机 图中两个圆圈&#xff0c;也叫节点&#xff0c;用于表示状态&#xff0c;从图中可以看成&#xff0c;它有两个状态&#xff0c;分别叫0和1。从每个节点出发&#xff0c;都会有若干条边。当处于某个状态时&#xff0c;如果输入的字符跟该节点出发的某条边的内…

go语言(八)---- map

map的声明方式有以下三种。 package mainimport "fmt"func main() {//第一种声明方式//声明map1是一个map类型&#xff0c;key是String&#xff0c;value是Stringvar myMap1 map[string] stringif myMap1 nil {fmt.Println("myMap1 是一个空map")}//在使…

idea中使用git提交代码报 Nothing To commit No changes detected

问题描述 在idea中右键&#xff0c;开始将变更的代码进行提交的时候&#xff0c;【Commit Directory】点击提交的时候 报 Nothing To commit No changes detected解决方案 在这里点击Test 看看是不是能下面显示git版本&#xff0c;不行的话 会显示一个 fix的字样&#xff0c;行…

阿里云ECS使用docker搭建mysql服务

目录 1.确保正确安装好docker 2.安装mysql镜像 3.创建容器&#xff08;设置端口映射、目录映射&#xff09; 1.确保正确安装好docker 安装教程&#xff1a; 阿里云ECS(CentOS镜像)安装docker-CSDN博客https://blog.csdn.net/qq_62262918/article/details/135686614?spm10…

Android平台Unity下如何通过WebCamTexture采集摄像头数据并推送至RTMP服务器或轻量级RTSP服务

技术背景 我们在对接Unity下推送模块的时候&#xff0c;遇到这样的技术诉求&#xff0c;开发者希望在Android的Unity场景下&#xff0c;获取到前后摄像头的数据&#xff0c;并投递到RTMP服务器&#xff0c;实现低延迟的数据采集处理。 在此之前&#xff0c;我们已经有了非常成…

如何下载OpenStreetMap(OSM)最新数据

OpenStreetMap&#xff08;OSM&#xff09;是一个开源的地图项目&#xff0c;旨在创建和提供免费、可自由使用、可编辑的地图数据和地图服务。以下是关于OpenStreetMap的一些关键信息&#xff1a; 社区驱动&#xff1a; OpenStreetMap是由一个全球性的志愿者社区共同创建和维护…

新上线一个IT公司微信小程序

项目介绍 项目背景: 一家IT公司,业务包含以下六大块: 1、IT设备回收 2、IT设备租赁 3、IT设备销售 4、IT设备维修 5、IT外包 6、IT软件开发 通过小程序,提供在线下单,在线制单,在线销售,业务介绍,推广,会员 项目目的: 业务介绍: 包含企业业务介绍 客户需…

怎么样的布局是符合可制造性的PCB布局?

满足可制造性、可装配性、可维修性要求&#xff0c;方便调试的时候于检测和返修&#xff0c;能够方便的拆卸器件&#xff1a; 1&#xff09;极性器件的方向不要超过2种&#xff0c;最好都进行统一方向等要求&#xff0c;如图1-1所示&#xff1b; 图1-1 极性器件方向统一摆放 2…