编程技术文章分享与教程

网站首页 > 技术文章 正文

这个前端黑科技可能是YouTube比B站、优酷、爱奇艺加载快的原因

hmc789 2024-11-14 16:36:54 技术文章 2 ℃

↑更多精彩,请点击上方蓝字关注我们!


“为什么同样是视频网站,Youtube 感觉加载很快、很轻,B 站、优酷、爱奇艺就感觉慢且重呢?”这是知乎上面的一个浏览次数很高的帖子,而在下面,有人就回答说


虽然我们并不确定这是否是主要因素,但是Service Worker对于前端性能优化的增益效果足以可见了。

之前在上篇超过100位程序员大佬:这是现代前端性能优化必会的黑科技中我们已经为大家介绍了Service Worker的概览,以及生命周期。今天我们将在下篇内容里为大家介绍service worker与APIs、使用场景以及小贴士。



APIs


Service Worker依赖于包括cachefetch在内的许多相关的浏览器API来使得网页应用功能更加丰富,越来越接近原生应用的体验。(注:这里的cache和fetch是指浏览器接口,与上文的Service Worker的生命周期事件注意区分)


Cache & Fetch API

作为一个网页应用,除了前端无法控制的服务接口,最重要的无非是获取和存储资源了,fetchcache两个接口作为现代浏览器的重要接口,Service Worker的fetch事件的响应几乎可以说是重度依赖于他们。如果浏览器版本稍老一些,可能还需要使用腻子脚本(polyfill)来提供这两个接口。


通过它们,Service Worker可以在截取页面的请求后,修改请求的参数内容,修改请求路径(注意跨域),延迟响应,修改响应内容,使用缓存内容伪造响应内容,甚至只用缓存构造一个完全不再依赖于网络的页面等等。




cache



添加资源到缓存对象中:

self.addEventListener('install', function(event) {
  // In install event, cache the resources first
  event.waitUntil(
    caches.open('my-cache-identifier')   // Open/create a cache with identifier
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll([    
          '/',
          '/styles/main.css',
          '/script/main.js'
        ]); // Cache the major HTML, CSS, JS file
      })
  );
});


需要注意,一个缓存对象可以添加多个资源路径->响应结果。




fetch


Fetch API*是浏览器新的API标准,在Service Worker中通常被用于在FetchEvent*中转发请求。


当网页正在发送HTTP请求,触发了Service Worker的fetch事件,我们就能在劫持HTTP包中的内容,按照需要进行读取或者修改,然后再继续发送或者转发给其他目标。


通常有以下操作流程或其中一部分:

  • 检查请求目标是否已被缓存,如果已存在直接回复缓存内容;
  • 拆包分析请求内容,筛选请求,阻止某些请求被发送,或者修改其内容;
  • 使用fetch API发送请求,或者转发请求;
  • 当得到响应,拆包检查响应状态,类型或者其他的HTTP头,按照需要筛选或者修改回复体的内容;
  • 按需缓存响应结果;


流程代码示例:


self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        return fetch(event.request).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});




存储 & 通信API

Fetch是通信用的API, cache是存储用的。但他们通常适用于“代理网页的网络请求”和 “缓存网页的资源”。


如果Service Worker还有其他的通信需求(如直接和页面通信,或者其他的Web Worker通信),或者存储不同需求的数据(如页面层数据,浏览器层数据,更大量的数据等),还有以下常用的API。




进程间通信


上文多次提及Web Worker, Service Worker与常规的Web Worker都是独立于渲染上下文的独立线程,所以它们都是无法直接操作DOM或者window对象的。如果我们有和其他Worker或者页面交互的需求,可以使用 postMessage* API和message事件来进行进程/线程间通信。


  • 在页面的主线程创建消息频道 MessageChannel*,使用 postMessage向频道上发送消息并且监听上面来的message事件:
function sendMessage(message) {
  // This wraps the message posting/response in a promise, which will resolve if the response doesn't
  // contain an error, and reject with the error if it does. If you'd prefer, it's possible to call
  // controller.postMessage() and set up the onmessage handler independently of a promise, but this is
  // a convenient wrapper.
  return new Promise(function(resolve, reject) {
    var messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = function(event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };

    // This sends the message data as well as transferring messageChannel.port2 to the service worker.
    // The service worker can then use the transferred port to reply via postMessage(), which
    // will in turn trigger the onmessage handler on messageChannel.port1.
    // See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
    navigator.serviceWorker.controller.postMessage(message,
      [messageChannel.port2]);
  });
}
  • 在Service Worker中,使用Client对象上的 Client.postMessage*来发送消息, 并且监听 message Service Worker自己的消息事件:

// Consume the message from host thread (or other Workers)
addEventListener('message', (event) => {
    console.log(`The client sent me a message: ${event.data}`);
});

{
  // Send message to host thread (or other Workers)
  clients.matchAll(event.clientId).postMessage({
    msg: "Hey I just got a fetch from you!",
  });
}

发送消息的demo

https://googlechrome.github.io/samples/service-worker/post-message/index.html




数据存储


Service Worker中也能使用各种新旧浏览器标准下的Web存储API来持久化数据,我们可以依照不同的需要来选择:



  1. 注意Service Worker的官方标准提到它是完全基于Promise的异步非阻塞 (it's designed to be fully asynchronous),同步的XHR请求和 localStorage (LocalStorage请求都是完全同步的*) 在Service Worker不可以被使用。
  2. 对于真正需要被长期持久化且在浏览器重启之后也需要被复用的内容,使用IndexedDB*是更加建议的方案,甚至可以在这之上做类似基于数据库的数据同步。




更多Web应用的API

Service Worker作为PWA的核心概念之一,它也是将web应用变得更接近原生应用的出发点。在它之上,更多浏览器特性提供了类似原生应用的支持。





使用场景


Server Worker在PWA之外也有诸多应用,基于它对HTTP请求和响应的强大管理能力,它可以作为多种依赖网络的应用的核心流程管理器。


  1. 全静态站点如果一个网站只包含静态数据而无需服务, 我们可以缓存所有的html页面,css样式,脚本和图片等资源,来使得这个页面在初次打开后可以被完全地离线访问。(宝可梦图鉴:https://pokedex.org/
  2. 预加载为了优化首屏渲染,页面上非必要的资源通常被延迟加载直到它们被需要。这类资源使用Server Worker来加载既可以使得在需要被加载时有良好的体验,又不会影响到首屏性能。(Demo:https://googlechrome.github.io/samples/service-worker/prefetch/index.html) / (Demo prefetch video:https://googlechrome.github.io/samples/service-worker/prefetch-video/index.html
  3. 应变响应有时候HTTP请求可能会因为不确定因素失败(如服务器离线,网络中断等),此时为用户提供一个应变的响应比如展示上一次成功加载的资源/数据。(例如:实时数据监测)Service worker可以帮助验证请求是否成功,如果失败就提供应变策略的回复。 (Demo:https://googlechrome.github.io/samples/service-worker/fallback-response/index.html
  4. 仿造响应仿造响应是非常有用的。它可以帮助我们隔离部分特定的请求来使用给定的回复,或者我们可以用它来测试一些尚不可用,或者不能稳定重现问题的资源或者REST API.
  5. 窗口缓存Service Worker来承担缓存数据的责任,页面可以直接使用window.cache来访问缓存。通过窗口缓存作为媒介可以间接实现service worker向页面的数据传递,也可以将Service Worker用作缓存的生产者而页面作为消费者。Demo:https://googlechrome.github.io/samples/service-worker/window-caches/index.html
  6. ......




小贴士


  • 在Service Worker中使用fetch API来转发请求,请求中默认不会包含cookie等中的用户认证信息。如果需要为转发请求附带认证信息, 在fetch请求中添加'credentials'的参数:
  • fetch(url, { credentials: 'include'})
  • 跨域资源默认是不支持缓存的,需要额外参数。如果目标资源支持CORS,在构建请求需要附带参数 {mode: 'cors'} 。如果目标资源不支持CORS或者不确定, 我们可以使用 non-cors模式,但这会导致"不透明"的响应, 意味着Service Worker不能判断响应中的状态,不透明的结果被缓存后仍被页面消费成non-cors的响应。
  • cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) { return new Request(urlToPrefetch, { mode: 'no-cors' });})).then(function() { console.log('All resources have been fetched and cached.');});
  • 30X的HTTP状态码尚不支持离线请求重定向, 这是一个已知的issue(https://github.com/w3c/ServiceWorker/issues/1457)。建议在官方支持离线重定向前,根据你的使用场景寻找其他方案。
  • 在使用Service Worker代理HTTP的响应体时,务必记住clone* response,而不要直接消费掉响应体。原因是HTTP response是一个流, 它的内容只能被消费一次。只要我们仍然希望既能让浏览器正确地获得响应体中的内容,又能是它被缓存或者在Service Worker作内容检查,请不要忘记复制一个响应体。




网址导航:

  • Fetch API:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
  • FetchEvent:https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent
  • postMessage:https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
  • MessageChannel:https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
  • Client.postMessage:https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage
  • LocalStorage请求都是完全同步的:https://stackoverflow.com/questions/20231163/is-html5-localstorage-asynchronous
  • IndexedDB:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
  • clone:https://fetch.spec.whatwg.org/#dom-response-clone




引用:

  • Chrome官方在线demo:https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker
  • Service Worker简介:https://developers.google.com/web/fundamentals/primers/service-workers
  • Service Worker API - MDN:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
  • W3C官方标准:https://w3c.github.io/ServiceWorker/
  • Service Worker相关的资源:https://jakearchibald.github.io/isserviceworkerready/resources.html#moar

Tags:

标签列表
最新留言