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读取,并获取状态和value
    const { 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 sink
    write(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 info
          const 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 dimensions
          this._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 info
          const 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 stream
          const bytesPerCol = this.bytesPerPixel();
          const bytesPerRow = this._width * bytesPerCol + 1;
          let result = chunk.subarray(position + 8, position + 8 + dataSize);

          // Decompress the data
          result = pako.inflate(result);

          // Remove PNG filters from each scanline
          result = this.removeFilters(result, bytesPerCol, bytesPerRow);

          // Actually grayscale the image
          result = this.grayscale(result, bytesPerCol, bytesPerRow);

          // Compress with Deflate
          result = pako.deflate(result);

          // Write data to target
          target.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 chunk
          target.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 image
  fetch('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>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深度学习(2)--卷积神经网络(CNN)

卷积神经网络&#xff08;Convolutional Neural Networks&#xff09;是一种深度学习模型或类似于人工神经网络的多层感知器&#xff0c;常用来分析视觉图像。 一.卷积神经网络基础概念 传统网络是二维的&#xff0c;而卷积网络是三维的。 例如32x32x3的图片&#xff0c;在传…

阿里云云原生弹性方案:用弹性解决集群资源利用率难题

作者&#xff1a;赫曦 随着上云的认知更加普遍&#xff0c;我们发现除了以往占大部分的互联网类型的客户&#xff0c;一些传统的企业&#xff0c;一些制造类的和工业型企业客户也都开始使用云原生的方式去做 IT 架构的转型&#xff0c;提高集群资源使用率也成为企业上云的一致…

告别繁琐配置!JVS低代码逻辑引擎让你轻松实现高效数据处理

在当今高度数字化的世界中&#xff0c;逻辑引擎作为数据处理和业务逻辑的核心组件&#xff0c;其重要性不言而喻。它不仅关乎企业数据的准确处理&#xff0c;还影响着业务决策的效率和准确性。为了确保逻辑引擎的正常运行和准确性&#xff0c;配置和测试环节显得尤为重要。 本…

C++ 类与对象Oop

类与对象Oop 一、类&#xff1a;用户定义的数据类型&#xff0c;用于封装数据和方法1.1 对比结构体警告-->主要目的&#xff1a;初始化 1.2 定义类的过程并定义一个对象1.2.1 定义类例子 1.2.2 定义一个对象1.2.3 注意事项例子1.2.4 分成头文件和源文件的方式&#xff08;0&…

k8s之对外服务ingress

一、service 1、service作用 ①集群内部&#xff1a;不断跟踪pod的变化&#xff0c;不断更新endpoint中的pod对象&#xff0c;基于pod的IP地址不断变化的一种服务发现机制&#xff08;endpoint存储最终对外提供服务的IP地址和端口&#xff09; ②集群外部&#xff1a;类似负…

canal调度控制器CanalController源码分析

canal调度控制器初始化&#xff1a; public CanalController(final Properties properties) 1. 初始化instance公共全局配置&#xff1a; canal.instance.global.(mode、lazy、manager.address以及spring.xml&#xff0c;并放入内存 InstanceConfig globalInstanceConfig; …

看完买,开放式耳机质量榜单:南卡夺冠、韶音第5、Cleer排第7

​作为一名拥有丰富经验的开放式耳机用户&#xff0c;我想在此提醒大家&#xff0c;选择耳机时&#xff0c;千万不要盲目跟风或过于信赖所谓的“网红”或“大牌产品”。毕竟&#xff0c;每个人的需求和使用环境都是独一无二的&#xff0c;因此&#xff0c;适合自己的耳机才是最…

AP5191DC-DC宽电压LED降压恒流驱动器

产品描述 AP5191是一款PWM工作模式,高效率、外围 简 单、外置功率MOS管&#xff0c;适用于4.5-150V输入的高 精度降压LED恒流驱动芯片。输出最大功率150W&#xff0c; 最大电流6A。 AP5191可实现线性调光和PWM调光&#xff0c;线性 调 光脚有效电压范围0.55-2.6V. AP519…

centos7 arm服务器编译安装PaddlePaddle

前言 随着国产服务器发展&#xff0c;部署项目需要用在国产服务器上&#xff0c;官方教程里面很多没有讲解到&#xff0c;安装过程中出现了各种各样的问题&#xff0c;以下是对官方教程的补充&#xff0c;有什么问题&#xff0c;欢迎指正&#xff01; 一、环境准备 gcc: 8.2版…

(工具变量)各地区-距沿海港口最短距离汇总数据集

全国各省距沿海港口最短距离数据提供了中国各省份与最近海港之间的最短地理距离信息。这些数据对于理解和分析中国各省的地理优势、物流效率以及对外贸易潜力有一定帮助。沿海港口作为国际贸易的重要节点&#xff0c;其距离对于省份的出口入口物流成本、运输时间以及总体贸易便…

vue $attrs和$listenners

Vue2.x 中的a t t r s 和 attrs和attrs和listeners 或许很多Vue小白跟我一样, 在之前不太了解a t t r s 和 attrs和attrs和listenners这两个API是干嘛的, 甚至没有听过或者使用过。下面我来浅述一下我对这两个API的理解。 下文将基于下面这张图片来进行解释&#xff0c;现在我…

《Python数据分析技术栈》第01章 02 Jupyter入门(Getting started with Jupyter notebooks)

02 Jupyter入门&#xff08;Getting started with Jupyter notebooks&#xff09; 《Python数据分析技术栈》第01章 02 Jupyter入门&#xff08;Getting started with Jupyter notebooks&#xff09; Before we discuss the essentials of Jupyter notebooks, let us discuss…

VSCode使用Makefile Tools插件开发C/C++程序

提起Makefile&#xff0c;可能有人会觉得它已经过时了&#xff0c;毕竟现在有比它更好的工具&#xff0c;比如CMake&#xff0c;XMake&#xff0c;Meson等等&#xff0c;但是在Linux下很多C/C源码都是直接或者间接使用Makefile文件来编译项目的&#xff0c;可以说Makefile是基石…

RT Thread Stdio生成STM32L431RCT6工程后如何修改外部时钟

一、简介 RT Thread Stdio生成STM32L431RCT6工程后默认为内部时钟&#xff0c;如何修改为外部时钟呢&#xff1f; 二、修改时钟步骤 本方案修改外部时钟为直接修改代码&#xff0c;不通过STM32CubeMX 进行配置&#xff08;使用这个软件会编译出错&#xff09; &#xff08;…

AEB滤镜再破碎,安全焦虑「解不开」?

不久前&#xff0c;理想L7重大交通事故&#xff0c;再次引发了公众对AEB的热议。 根据理想汽车公布的事故视频显示&#xff0c;碰撞发生前3秒&#xff0c;车速在178km/h时驾驶员采取了制动措施&#xff0c;但车速大幅超出AEB&#xff08;自动紧急刹车系统&#xff09;的工作范…

为什么 Golang Fasthttp 选择使用 slice 而非 map 存储请求数据

文章目录 Slice vs Map&#xff1a;基本概念内存分配和性能Fasthttp 中的 SliceMap性能优化的深层原因HTTP Headers 的特性CPU 预加载特性 结论 Fasthttp 是一个高性能的 Golang HTTP 框架&#xff0c;它在设计上做了许多优化以提高性能。其中一个显著的设计选择是使用 slice 而…

用sdkman在linux上管理多个java版本

概述&#xff1a; SDKMAN 是一个用于管理软件开发工具的工具&#xff0c;允许您轻松地安装、升级和切换不同版本的 JDK、Maven、Gradle 等工具。以下是在 Linux 上安装 SDKMAN! 的基本步骤&#xff1a; 安装SdkMan 使用 curl 安装 SDKMAN!: 打开终端&#xff0c;并运行以下命…

SpringCloud之Nacos

一、微服务介绍 1. 什么是微服务 2014年,Martin Fowler(马丁福勒 ) 提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模…

eNSP学习——配置通过Telnet登陆系统

实验内容&#xff1a; 模拟公司网络场景。R1是机房的设备&#xff0c;办公区与机房不在同一楼层&#xff0c;R2和R3模拟员工主机&#xff0c; 通过交换机S1与R1相连。 为了方便用户的管理&#xff0c;需要在R1上配置Telnet使员工可以在办公区远程管理机房设备。 为…

德施曼智能锁×去哪儿跨界联名,送你一场说走就走的新年旅行~

2024年农历新年即将来临&#xff0c;智能锁行业领军企业德施曼携手中国领先在线旅游平台去哪儿&#xff0c;紧扣“旅游过年”的新年趋势&#xff0c;推出“新年去哪儿&#xff0c;德施曼替你看家”跨界联名活动&#xff0c;为广大用户带来一场说走就走的旅行。 德施曼X去哪儿 …