当前位置:   article > 正文

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

js file.stream()

目录

一、流的原理

二、流的分类

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控制器实例,同下文中提及的构造器参数

举个应用栗子

  1. // Fetch the original image
  2. fetch("./tortoise.png")
  3. // Retrieve its body as ReadableStream
  4. .then((response) => response.body)
  5. .then((body) => {
  6. const reader = body.getReader();
  7. // 使用 read读取,并获取状态和value
  8. const { done, value } = await reader.read();
  9. if (done) {
  10. console.log('End of file reached');
  11. // 释放读取器
  12. reader.releaseLock();
  13. } else {
  14. // 处理读取到的数据块
  15. console.log('Read:', value);
  16. // 继续递归读取下一个数据块
  17. // 可以递归读取
  18. }
  19. });

 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()方法获取其写入器

举个应用栗子

  1. const decoder = new TextDecoder("utf-8");
  2. const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });
  3. let result = "";
  4. const writableStream = new WritableStream(
  5. {
  6. // Implement the sink
  7. write(chunk) {
  8. return new Promise((resolve, reject) => {
  9. const buffer = new ArrayBuffer(1);
  10. const view = new Uint8Array(buffer);
  11. view[0] = chunk;
  12. const decoded = decoder.decode(view, { stream: true });
  13. const listItem = document.createElement("li");
  14. listItem.textContent = `Chunk decoded: ${decoded}`;
  15. list.appendChild(listItem);
  16. result += decoded;
  17. resolve();
  18. });
  19. },
  20. close() {
  21. const listItem = document.createElement("li");
  22. listItem.textContent = `[MESSAGE RECEIVED] ${result}`;
  23. list.appendChild(listItem);
  24. },
  25. abort(err) {
  26. console.error("Sink error:", err);
  27. },
  28. },
  29. queuingStrategy,
  30. );

3、转换流(TransformStream)

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

举个应用栗子

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

  1. class UpperCaseTransformer {
  2. constructor() {
  3. this.transformStream = new TransformStream({
  4. start(controller) {//开始钩子
  5. //----下面是给个示例!
  6. // 将会在对象创建时立刻执行,并传入一个流控制器
  7. controller.desiredSize
  8. // 填满队列所需字节数
  9. controller.enqueue(chunk)
  10. // 向可读取的一端传入数据片段
  11. controller.error(reason)
  12. // 同时向可读取与可写入的两侧触发一个错误
  13. controller.terminate()
  14. // 关闭可读取的一侧,同时向可写入的一侧触发错误
  15. },
  16. async transform(chunk, controller) {//中间chunck转换
  17. // 将 chunk 转换为大写
  18. const upperCaseChunk = chunk.toString().toUpperCase();
  19. // 块入队,将转换后的数据传递给下游
  20. controller.enqueue(upperCaseChunk);
  21. },
  22. flush(controller) {
  23. // 当可写入的一端得到的所有的片段完全传入 transform() 方法处理后,在可写入的一端即将关
  24. 闭时调用
  25. }
  26. },queuingStrategy); //queuingStrategy策略内容假设为 { highWaterMark: 1 }
  27. //获取自身的读写流主提
  28. this.readableStream = this.transformStream.readable;
  29. this.writableStream = this.transformStream.writable;
  30. }
  31. // 关闭流
  32. async close() {
  33. await this.writableStream.getWriter().close();
  34. }
  35. // 获取读取器
  36. getReader() {
  37. return this.readableStream.getReader();
  38. }
  39. // 获取写入器
  40. getWriter() {
  41. return this.writableStream.getWriter();
  42. }
  43. }

三、流中的Request和Response对象

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

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

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

  1. arrayBuffer() // 返回一个Promise,兑现为ArrayBuffer
  2. blob() // 返回一个Promise,兑现为blob
  3. formData() //如上
  4. text() //如上,返回字符串
  5. 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】

  1. const image = document.getElementById("target");
  2. // 请求原始图片
  3. fetch("./tortoise.png")
  4. // 取出 body
  5. .then((response) => response.body)
  6. .then((body) => {
  7. const reader = body.getReader();
  8. return new ReadableStream({
  9. start(controller) {
  10. return pump();
  11. function pump() {
  12. return reader.read().then(({ done, value }) => {
  13. // 读不到更多数据就关闭流
  14. if (done) {
  15. controller.close();
  16. return;
  17. }
  18. // 将下一个数据块置入流中
  19. controller.enqueue(value);
  20. return pump();
  21. });
  22. }
  23. },
  24. });
  25. })
  26. .then((stream) => new Response(stream))
  27. .then((response) => response.blob())
  28. .then((blob) => URL.createObjectURL(blob))
  29. .then((url) => console.log((image.src = url)))
  30. .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:

  1. class GrayscalePNGTransformer {
  2. constructor() {
  3. this._mode = 'magic';
  4. }
  5. /**
  6. * Called for every downloaded PNG data chunk to be grayscaled.
  7. *
  8. * @param {Uint8Array} chunk The downloaded chunk.
  9. * @param {TransformStreamDefaultController} controller The controller to euqueue grayscaled data.
  10. */
  11. transform(chunk, controller) {
  12. let position = chunk.byteOffset;
  13. let length = chunk.byteLength;
  14. const source = new DataView(chunk.buffer, position, length);
  15. const buffer = new Uint8Array(length);
  16. const target = new DataView(buffer.buffer, position, length);
  17. while (position < length) {
  18. switch (this._mode) {
  19. case 'magic': {
  20. const magic1 = source.getUint32(position);
  21. target.setUint32(position, magic1);
  22. position += 4;
  23. const magic2 = source.getUint32(position);
  24. target.setUint32(position, magic2);
  25. position += 4;
  26. const magic = magic1.toString(16) + '0' + magic2.toString(16);
  27. console.log('%cPNG magic: %c %o', 'font-weight: bold', '', magic);
  28. if (magic !== '89504e470d0a1a0a') {
  29. throw new TypeError('This is not a PNG');
  30. }
  31. this._mode = 'header';
  32. break;
  33. }
  34. case 'header': {
  35. // Read chunk info
  36. const chunkLength = source.getUint32(position);
  37. target.setUint32(position, chunkLength);
  38. position += 4;
  39. const chunkName = this.readString(source, position, 4);
  40. this.writeString(target, position, chunkName);
  41. position += 4;
  42. if (chunkName !== 'IHDR') {
  43. throw new TypeError('PNG is missing IHDR chunk');
  44. }
  45. // Read image dimensions
  46. this._width = source.getUint32(position);
  47. target.setUint32(position, this._width);
  48. position += 4;
  49. this._height = source.getUint32(position);
  50. target.setUint32(position, this._height);
  51. position += 4;
  52. console.log('%cPNG dimensions:%c %d x %d', 'font-weight: bold', '', this._width, this._height);
  53. this._bitDepth = source.getUint8(position);
  54. target.setUint8(position, this._bitDepth);
  55. position += 1;
  56. console.log('%cPNG bit depth: %c %d', 'font-weight: bold', '', this._bitDepth);
  57. this._colorType = source.getUint8(position);
  58. target.setUint8(position, this._colorType);
  59. position += 1;
  60. console.log('%cPNG color type:%c %s', 'font-weight: bold', '', this.colorType(this._colorType));
  61. const compression = source.getUint8(position);
  62. target.setUint8(position, compression);
  63. position += 1;
  64. console.log('%cPNG compressio:%c %d', 'font-weight: bold', '', compression);
  65. const filter = source.getUint8(position);
  66. target.setUint8(position, filter);
  67. position += 1;
  68. console.log('%cPNG filter: %c %d', 'font-weight: bold', '', filter);
  69. const interlace = source.getUint8(position);
  70. target.setUint8(position, interlace);
  71. position += 1;
  72. console.log('%cPNG interlace: %c %d', 'font-weight: bold', '', interlace);
  73. const chunkCrc = source.getUint32(position);
  74. target.setUint32(position, chunkCrc);
  75. position += 4;
  76. this._mode = 'data';
  77. break;
  78. }
  79. case 'data': {
  80. // Read chunk info
  81. const dataSize = source.getUint32(position);
  82. console.log('%cPNG data size: %c %d', 'font-weight: bold', '', dataSize);
  83. const chunkName = this.readString(source, position + 4, 4);
  84. if (chunkName !== 'IDAT') {
  85. throw new TypeError('PNG is missing IDAT chunk');
  86. }
  87. const crcStart = position + 4;
  88. // Extract the data from the PNG stream
  89. const bytesPerCol = this.bytesPerPixel();
  90. const bytesPerRow = this._width * bytesPerCol + 1;
  91. let result = chunk.subarray(position + 8, position + 8 + dataSize);
  92. // Decompress the data
  93. result = pako.inflate(result);
  94. // Remove PNG filters from each scanline
  95. result = this.removeFilters(result, bytesPerCol, bytesPerRow);
  96. // Actually grayscale the image
  97. result = this.grayscale(result, bytesPerCol, bytesPerRow);
  98. // Compress with Deflate
  99. result = pako.deflate(result);
  100. // Write data to target
  101. target.setUint32(position, result.byteLength);
  102. this.writeString(target, position + 4, 'IDAT');
  103. buffer.set(result, position + 8);
  104. position += result.byteLength + 8;
  105. const chunkCrc = crc32(chunkName, result);
  106. target.setUint32(position, chunkCrc);
  107. position += 4;
  108. this._mode = 'end';
  109. break;
  110. }
  111. case 'end': {
  112. // Write IEND chunk
  113. target.setUint32(position, 0);
  114. position += 4;
  115. this.writeString(target, position, 'IEND');
  116. position += 4;
  117. target.setUint32(position, 2923585666);
  118. position += 4;
  119. controller.enqueue(buffer.subarray(0, position));
  120. return;
  121. }
  122. }
  123. }
  124. }
  125. /**
  126. * @param {DataView} dataView
  127. * @param {number} position
  128. * @param {number} length
  129. */
  130. readString(dataView, position, length) {
  131. return new Array(length)
  132. .fill(0)
  133. .map((e, index) => String.fromCharCode(dataView.getUint8(position + index))).join('');
  134. }
  135. /**
  136. * @param {DataView} dataView
  137. * @param {number} position
  138. * @param {string} string
  139. */
  140. writeString(dataView, position, string) {
  141. string.split('').forEach((char, index) => dataView.setUint8(position + index, char.charCodeAt(0)));
  142. }
  143. //..........未完
  144. }

流读取和转换:

  1. <script type="application/javascript">
  2. const image = document.getElementById('target');
  3. // Fetch the original image
  4. fetch('tortoise.png')
  5. // Retrieve its body as ReadableStream
  6. .then(response => response.body)
  7. // Create a gray-scaled PNG stream out of the original
  8. .then(rs => rs.pipeThrough(new TransformStream(new GrayscalePNGTransformer())))
  9. // Create a new response out of the stream
  10. .then(rs => new Response(rs))
  11. // Create an object URL for the response
  12. .then(response => response.blob())
  13. .then(blob => URL.createObjectURL(blob))
  14. // Update image
  15. .then(url => image.src = url)
  16. .catch(console.error);
  17. </script>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/811343?site
推荐阅读
相关标签
  

闽ICP备14008679号