[译]2016

WBOY
풀어 주다: 2016-06-24 11:27:24
원래의
1159명이 탐색했습니다.

原文 发表于2016年1月25日

好吧好吧,在一月份宣布这是 什么什么是年度最佳这种话题 的确有点大胆,但是web stream API展现出来的潜力真是让我兴奋不已。

TL;DR:流可以用来做许多有趣的事情,诸如把“云”变成“屁股”,把MPEG格式编码为GIF格式,但是最重要的是,你可以结合service workers来成为最快速的提供内容的方式.

流?他们擅长什么

当然是某些东西啦

Promises是一个很好的方式去表示一个异步取回来的值,但是如果有多个值呢?或者那些从一个极大值中拆分出来,逐步到达的部分值呢?

假设我们想获取并展现一张图片,这包括了以下部分:

  1. 从网络上获得一些数据
  2. 运行之,并把从压缩的数据转化为原始的像素数据。
  3. 渲染它

我们可以一步步的完成,又或者可以使用流处理的方式。

如果我们一比特一比特地处理响应,我们可以更快的渲染 部分 图像。我们甚至可以更快地渲染整个图像,因为处理和获取数据是并行的。这就是流处理!我们一边 读取 从网络过来的流,一边将其从压缩的数据 转换 像素数据,而同时我们把他 绘制 到屏幕上。

你可以用事件做到类似的效果,但是流有以下的优点:

  • 开始/结束感知 - 尽管流可以是无限大的
  • 缓冲尚未阅读的值 - 而在监听器连接成功前的数据,事件会将其丢失掉
  • 通过管道进行连接 - 你可以让流都流过管道从而形成一个异步序列
  • 内置错误处理 - 错误随着流往下传播
  • 支持取消 - 取消消息会传回到管道里
  • 流控制 - 你可以对读取的速度作出反应

最后一个优点十分重要。想象一下我们在下载并且展示一个视频。如果我们一秒内可以下载并且解码200帧,但是一秒内只能展示24帧,我们最终可能会因为解码帧的大量积压而耗尽了内存。

这就是我们需要流控制的地方。负责渲染的流每秒从解码流中拉取24次解码帧。解码器因此注意到他解码的速率远大于帧被读取的速率,因此降低了解码速率。接着,网络流注意到他获取数据的速率远大于数据被解码的速率,从而降低下载速率。

因为流和读取器之间紧密的关系,一个流只可以用一个读取器。然而,一个未被读取的流是可以被“teed”的,这意味着他可以分裂成两个拥有一样数据的流。在这种情况下三通处理器为两个读取器保存缓冲数据。

好的,这些只是理论,我知道你还不打算交出你心里的2016最佳的奖杯,但是请跟着我阅读。

浏览器默认会使用流来处理。当你看到浏览器一边下载,一边展示一个页面/图像/视频的时候,这要感谢流处理。然而,这只是最近,得益于 标准化工作 ,流已经暴露给脚本了。

流和fetch API

Response 对象,就像 fetch规范 中定义的,你可以用多种格式来读取响应,但是 response.body 让你直接访问底层流。 response.body 已经被目前的chrome稳定浏览器版本所支持。

假设我想获取一个响应的内容长度,而不希望依赖于头部,也不想在内存中保存整个响应。我可以通过流这样做:

// fetch() 返回一个promise一旦头部被获取就resolvefetch(url).then(response => {  // response.body是一个可读流  // 调用getReadeer()函数让我们可以独占到流内容的访问  var reader = response.body.getReader();  var bytesReceived = 0;  // read() 返回一个promise当值被读取到时会进行resolve  reader.read().then(function processResult(result) {    // 结果对象含有一下两个属性    // done  - 当流给予了你所有数据的时候会设为true    // value - 一些数据。当done是true的时候,为undefined    if (result.done) {      console.log("Fetch complete");      return;    }    // fetch流的result.value是一个Uint8Array    bytesReceived += result.value.length;    console.log('Received', bytesReceived, 'bytes of data so far');    // 阅读更多,再次调用这个函数    return reader.read().then(processResult);  });});
로그인 후 복사

观看示例 (1.3mb)

这个示例从服务器获取了一个大小为1.3兆的gzipped压缩的HTML文件,解压后大小为7.7兆。然而,结果并不是放置在缓存中。每个块的大小都被记录了下来,但是每个块本身则被垃圾回收了。

result.value 是无论什么流都会提供的,他可以是任何东西:字符串、数字、日期对象、图片数据、DOM元素……但是,在fetch流中,他永远是二进制的 Uint8Array 。整个响应就是每一个 Unit8Array 被组合到一起。如果你想响应变成文本格式,你可以使用 TextDecoder :

var decoder = new TextDecoder();var reader = response.body.getReader();// read() returns a promise that resolves// when a value has been receivedreader.read().then(function processResult(result) {  if (result.done) return;  console.log(    decoder.decode(result.value, {stream: true})  );  // Read some more, and recall this function  return reader.read().then(processResult);});
로그인 후 복사

{stream:true} 意味着如果 result.value 在通过一个UTF-8编码点的时候中断,解码器会保持一个缓冲,例如一个字符♥会用3个比特来表示: [0xE2, 0x99, 0xA5] 。

TextDecoder 目前是有一些笨拙,但是他有可能在未来变为一个变换流(一旦变换流被定义)。变换流是一个拥有可写流 .writable 和可读流 .readable 的对象。它通过可写流获取块文件并且处理他们,然后通过可读流传出内容。使用变换流会是这样子的:

假设这是未来的代码:

var reader = response.body  .pipeThrough(new TextDecoder()).getReader();reader.read().then(result => {  // result.value will be a string});
로그인 후 복사

浏览器应该有能力优化到上述程度,因为响应流和 TextDecoder 变换流都是浏览器本身的。

取消fetch

我们可以利用 stream.cancel() 或者 reader.cancel() 来取消一个流(同样的对应fetch使用 response.body.cancel() )。fetch会对此作出停止下载的反应。

观看示例 (注意一下JSBin给予我的神器的随机URL).

这个示例是搜索一个大型文档里面的一个术语,每次只在内存中保留一小部分,一旦找到匹配的部分就停止获取。

无论如何,这些都是2015的东西了。下面将会是一些新的东西。

创建你的可读取流

在Chrome Canary版本中使用"实验网络平台功能",你可以创建属于你自己的流。

var stream = new ReadableStream({  start(controller) {},  pull(controller) {},  cancel(reason) {}}, queuingStrategy);
로그인 후 복사
  • start 会被马上调用。使用它去设置一些底层数据源(这意味着,你可以从任何地方去得到你的数据,数据可能是事件、其他流、一个变量、一个字符串)。如果你从这里返回一个promise对象并且reject,它会通过流发送一个错误。
  • pull 会在你的流缓冲尚未满的时候被调用,他会被重复调用直至你的缓冲区已经满了。同样地,如果你从这里返回一个promise并且他被reject了,它会通过流发送一个错误。另外,在返回的promise完成前, pull 不会被再次调用。
  • cancel 会在stream被取消的时候调用。可以利用它去取消任意的底层数据。
  • queuingStrategy 定义了这个流理想情况下可以缓冲多少数据,默认是一个 - 我不打算深入这一部分, 规范上面有更多细节

至于说到 controller :

  • controller.enqueue(whatevet) - 让数据在流的缓冲区中排队
  • controller.close() - 标志了流的结束
  • conttoller.error(e) - 标志了一个终端错误
  • controller.desiredSize - 缓冲区中留有的数据数量,当缓冲区溢出的时候可能是负数。这个数据是利用 queuingStrategy 计算出的。

所以如果我想创造一个流可以每秒产生一个随机数,直到他产生的数字大于0.9.我会这么做:

var interval;var stream = new ReadableStream({  start(controller) {    interval = setInterval(() => {      var num = Math.random();      // Add the number to the stream      controller.enqueue(num);      if (num > 0.9) {        // Signal the end of the stream        controller.close();        clearInterval(interval);      }    }, 1000);  },  cancel() {    // This is called if the reader cancels,    //so we should stop generating numbers    clearInterval(interval);  }});
로그인 후 복사

观看运行示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

你可以控制什么时候把数据传送给 controller.enqueue 。你可以在你拥有数据需要发送的时候调用,使你的流成为一个“推送源”。当然你也可以选择等待到 pull 被调用,然后使用它作为一个信号去收集底层数据然后让他 enqueue ,使你的流成为“拉取源”。或者你可以结合两种方式,无论你想用哪种。

遵循 controller.desiredSize 意味着流正在用最有效率的方式传输数据。这被称作“背压支持”(backpressure support),意味着你的流会对读取器的读取速率作出反应(和之前提到的视频解码例子一样)。然而,忽略掉 desiredSize 并不会破坏什么东西,除了你可能会消耗掉整个设备的内存。在规范上面有 创造一个背压支持的流 的例子。

创建一个自己的流并不是一件十分有趣的是,因为他们是新的,所以没有特别多的API支持他们,不过有这么一条

new Response(readableStream)

你可以创建一个HTTP响应对象,他的body是一个流,然后你可以把这个对象应用到service worker上。

缓慢地提供一个字符串

观看示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

你会看到一个被(故意)渲染的非常慢的HTML页面。这个响应完全由service worker生成。下面是代码:

// In the service worker:self.addEventListener('fetch', event => {  var html = '…html to serve…';  var stream = new ReadableStream({    start(controller) {      var encoder = new TextEncoder();      // 我们目前的位置是在HTML中      var pos = 0;      // 每一次服务器推送的个数      var chunkSize = 1;      function push() {        // 推送完毕了吗        if (pos >= html.length) {          controller.close();          return;        }        // 推送一些html。并且把它转化为一个utf-8数据的Unit8Array        controller.enqueue(          encoder.encode(html.slice(pos, pos + chunkSize))        );        // 移动位置        pos += chunkSize;        // 5毫秒后再次推送        setTimeout(push, 5);      }      // 出发      push();    }  });  return new Response(stream, {    headers: {'Content-Type': 'text/html'}  });});
로그인 후 복사

当浏览器读取到相应的内容他希望读取到 Unit8Array 块,如果传入了其他格式的数据比如一个空白的字符串,他会读取失败。幸运的是, TextEncoder 可以传入一个字符串,然后返回一个 Unit8Array 格式的比特代表这是字符串。

诸如 TextDecoder , TextEncoder 在以后会成为一个变换流。

提供一个变换流

像我说的那样,变换流尚未被定义,但是你可以通过从其他数据源创造一个可读流来实现相同目的。

“云”变为“屁股”(cloud to butt)

观看示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

这个页面 摘自维基百科的云计算相关的文章,但是在里面每一个“云”的单词都被“屁股”所代替了。这样做的好处是,你在从源头下载数据的同时你可以变换其中的内容。

这里是代码 , 包括了一些edge的样例。

MPEG到GIF

视频代码是十分高效的,但是在手机上他不能自动播放。GIF格式在手机上可以自动播放,但是他十分巨大。好吧,以下是一个 非常傻 的解决方式:

观看示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

在这里流十分有用,因为那样子我们可以在MPEG帧仍在解码的时候播放GIF的第一帧。

所以你就这么做吧!一个26兆的GIF只需要用0.9兆的MPEG就可以传输了!完美!但是他不是实时的,并且会耗费大量CPU资源。浏览器应该允许在手机上自动播放视频,尤其是静音的视频。Chromes正在朝这方面努力着。

披露:其实在demo里面我感觉我有点被骗了,因为他是下载了整个MPEG后才开始播放的。我希望可以用流的方式获取,但是我碰到了 OutOfSkillError 。同样的,下载的时候GIF也不应该循环播放,这是需要我们研究的bug。

创造一个多源流来压缩页面渲染时间

这可能是流+service worker中最实际的应用了。单就性能表现而言,他的优点十分 巨大 。

几个月前我构建了 离线优先版本维基百科的样例 。我希望能创造一个速度快,遵循逐步联网策略的网页应用,并且利用更多的现代功能去增强他。

我下面引用的关于性能的数字是基于OSX的网络模拟出的较差的3G网络。

缺少了service worker,他展示的内容是来自服务器的。我投入了大量的精力在性能优化方面,然而结果是这样的:

观看示例

不算太差,我加入了service worker来引入一些离线优先的优点从而使性能优化更多,结果呢?

观看示例

可以看到,首屏渲染加快了,但是在内容渲染方面仍然有巨大的改进空间。

最快的渲染方法是从缓存里加载整个页面,但是这意味你要缓存所有的维基百科。反之,我提供了一个页面包括CSS,JavaScript和header,从而得到了一个极快速的首屏渲染体验,然后我利用页面的JavaScript去获取文章内容。这就是我丢失性能的地方 - 客户端渲染。

HTML一旦下载它就会被渲染,无论是来自服务器或是service worker。但是我利用JavaScript来获取页面的内容,它会利用 innerHTML 来渲染而不是流解释器。正因为如此,这个内容部分直到他下载完毕才被渲染,这就是造成那两秒延迟的原因。你下载的内容越多,缺少流造成的影响就越大,而不幸的事,维基百科的文章真的很大(google的文章大小是100k)。

这就是为什么你看到我总会抱怨由JavaScript驱动的网络应用和框架 - 他们总是抛弃流从头开始,造成性能损耗极大。

我试图利用预取流和伪流来挽回一些性能。伪流是一个颇为hack的方法。页面获取到文章内容然后利用流的方式进行读取,当他读取到的数据量达到9k的时候,使用 innerHTML 的方法将内容写入,然后当其余的内容获得后,再次使用 innerHTML 写入。这其实是挺恐怖的,因为他会把一些元素创造两次,不过这是值得的。

观看示例

所以hacks改进了心梗,但是相比于服务端渲染,他还是落后了,这真是难以令人接受。此外,把内容通过 innerHTML 加入到页面中和常规的内容解析表现不大一样。值得注意的是,内联的

인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿