JavaScriptAPI之线程(web worker)

目录

1 Web Worker概述

1.1 新建和启动子线程

1.2 子线程的事件监听

1.3 主线程的事件监听

1.4 错误处理

1.5 关闭子线程

2 主线程与子线程的数据通信

3 同页面的Web Worker

4 Service Worker

4.1 首先,需要向浏览器登记Service Worker

4.2 安装和激活,主要通过事件来判断


1 Web Worker概述

JavaScript语言采用的是单线程模型,也就是说,所有任务排成一个队列,一次只能做一件事。随着电脑计算能力的增强,这一点带来很大的不便,无法充分发挥JavaScript的潜力。尤其考虑到,File API允许JavaScript读取本地文件,就更是如此了。

Web Worker的目的,就是为JavaScript创造多线程环境,允许主线程将一些任务分配给子线程。在主线程运行的同时,子线程在后台运行,两者互不干扰。等到子线程完成计算任务,再把结果返回给主线程。因此,每一个子线程就好像一个“工人”(worker),默默地完成自己的工作

普通的Wek Worker,只能与创造它们的主进程通信。还有另一类Shared worker,能被所有同源的进程获取(比如来自不同的浏览器窗口、iframe窗口和其他Shared worker)。

Web Worker有以下几个特点:

  • 同域限制。子线程加载的脚本文件,必须与主线程的脚本文件在同一个域。
  • DOM限制。子线程无法读取网页的DOM对象,即document、window、parent这些对象,子线程都无法得到。(但是,navigator对象和location对象可以获得。)
  • 脚本限制。子线程无法读取网页的全局变量和函数,也不能执行alert和confirm方法,不过可以执行setInterval和setTimeout,以及使用XMLHttpRequest对象发出AJAX请求。
  • 文件限制。子线程无法读取本地文件,即子线程无法打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

使用之前,检查浏览器是否支持这个API。支持的浏览器包括IE10、Firefox (从3.6版本开始)、Safari (从4.0版本开始)、Chrome 和 Opera 11,但是手机浏览器还不支持。

if (window.Worker) {
  // 支持
} else {
  // 不支持
}

1.1 新建和启动子线程

主线程采用new命令,调用Worker构造函数,可以新建一个子线程。

var worker = new Worker('work.js');

Worker构造函数的参数是一个脚本文件,这个文件就是子线程所要完成的任务,上面代码中是work.js。由于子线程不能读取本地文件系统,所以这个脚本文件必须来自网络端。如果下载没有成功,比如出现404错误,这个子线程就会默默地失败。

子线程新建之后,并没有启动,必需等待主线程调用postMessage方法,即发出信号之后才会启动。postMessage方法的参数,就是主线程传给子线程的信号。它可以是一个字符串,也可以是一个对象。

worker.postMessage("Hello World");
worker.postMessage({method: 'echo', args: ['Work']});

1.2 子线程的事件监听

在子线程内,必须有一个回调函数,监听message事件。

/* File: work.js */

self.addEventListener('message', function(e) {
  self.postMessage('You said: ' + e.data);
}, false);

self代表子线程自身,self.addEventListener表示对子线程的message事件指定回调函数(直接指定onmessage属性的值也可)。回调函数的参数是一个事件对象,它的data属性包含主线程发来的信号。self.postMessage则表示,子线程向主线程发送一个信号。

根据主线程发来的不同的信号值,子线程可以调用不同的方法。

/* File: work.js */

self.onmessage = function(event) {
  var method = event.data.method;
  var args = event.data.args;

  var reply = doSomething(args);
  self.postMessage({method: method, reply: reply});
};

1.3 主线程的事件监听

主线程也必须指定message事件的回调函数,监听子线程发来的信号。

/* File: main.js */

worker.addEventListener('message', function(e) {
    console.log(e.data);
}, false);

1.4 错误处理

主线程可以监听子线程是否发生错误。如果发生错误,会触发主线程的error事件。

worker.onerror(function(event) {
  console.log(event);
});

// or

worker.addEventListener('error', function(event) {
  console.log(event);
});

1.5 关闭子线程

使用完毕之后,为了节省系统资源,我们必须在主线程调用terminate方法,手动关闭子线程。

worker.terminate();

也可以子线程内部关闭自身。

self.close();

2 主线程与子线程的数据通信

前面说过,主线程与子线程之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,子线程对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给子线程,后者再将它还原。

主线程与子线程之间也可以交换二进制数据,比如File、Blob、ArrayBuffer等对象,也可以在线程之间发送。但是,用拷贝方式发送二进制数据,会造成性能问题。比如,主线程向子线程发送一个500MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。

如果要使用该方法,postMessage方法的最后一个参数必须是一个数组,用来指定前面发送的哪些值可以被转移给子线程。

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

3 同页面的Web Worker

通常情况下,子线程载入的是一个单独的JavaScript文件,但是也可以载入与主线程在同一个网页的代码。假设网页代码如下:


    
        
    

我们可以读取页面中的script,用worker来处理。

var blob = new Blob([document.querySelector('#worker').textContent]);

这里需要把代码当作二进制对象读取,所以使用Blob接口。然后,这个二进制对象转为URL,再通过这个URL创建worker。

var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

部署事件监听代码。

worker.addEventListener('message', function(e) {
   console.log(e.data);
}, false);

最后,启动worker。

worker.postMessage('');

整个页面的代码如下:



  

  


可以看到,主线程和子线程的代码都在同一个网页上面。

上面所讲的Web Worker都是专属于某个网页的,当该网页关闭,worker就自动结束。除此之外,还有一种共享式的Web Worker,允许多个浏览器窗口共享同一个worker,只有当所有网口关闭,它才会结束。这种共享式的Worker用SharedWorker对象来建立,因为适用场合不多,这里就省略了。

4 Service Worker

Service worker是一个在浏览器后台运行的脚本,与网页不相干,专注于那些不需要网页或用户互动就能完成的功能。它主要用于操作离线缓存。

Service Worker有以下特点

  • 属于JavaScript Worker,不能直接接触DOM,通过postMessage接口与页面通信。
  • 不需要任何页面,就能执行。
  • 不用的时候会终止执行,需要的时候又重新执行,即它是事件驱动的。
  • 有一个精心定义的升级策略。
  • 只在HTTPs协议下可用,这是因为它能拦截网络请求,所以必须保证请求是安全的。
  • 可以拦截发出的网络请求,从而控制页面的网路通信。
  • 内部大量使用Promise。

Service worker的常见用途

  • 通过拦截网络请求,使得网站运行得更快,或者在离线情况下,依然可以执行。
  • 作为其他后台功能的基础,比如消息推送和背景同步。

使用Service Worker有以下步骤。

4.1 首先,需要向浏览器登记Service Worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(registration) {
    // 登记成功
    console.log('ServiceWorker登记成功,范围为', registration.scope);
    }).catch(function(err) {
    // 登记失败
      console.log('ServiceWorker登记失败:', err);
    });
}

上面代码向浏览器登记sw.js脚本,实质就是浏览器加载sw.js。这段代码可以多次调用,浏览器会自行判断sw.js是否登记过,如果已经登记过,就不再重复执行了。注意,Service worker脚本必须与页面在同一个域,且必须在HTTPs协议下正常运行。

sw.js位于域名的根目录下,这表明这个Service worker的范围(scope)是整个域,即会接收整个域下面的fetch事件。如果脚本的路径是/example/sw.js,那么Service worker只对/example/开头的URL有效(比如/example/page1//example/page2/)。如果脚本不在根目录下,但是希望对整个域都有效,可以指定scope属性。

navigator.serviceWorker.register('/path/to/serviceworker.js', {
  scope: '/'
});

一旦登记完成,这段脚本就会用户的浏览器之中长期存在,不会随着用户离开你的网站而消失。

.register方法返回一个Promise对象。

登记成功后,浏览器执行下面步骤。

  1. 下载资源(Download)
  2. 安装(Install)
  3. 激活(Activate)

4.2 安装和激活,主要通过事件来判断

self.addEventListener('install', function(event) {
  event.waitUntil(
    fetchStuffAndInitDatabases()
  );
});

self.addEventListener('activate', function(event) {
  // You're good to go!
});

Service worker一旦激活,就开始控制页面。网页加载的时候,可以选择一个Service worker作为自己的控制器。不过,页面第一次加载的时候,它不受Service worker控制,因为这时还没有一个Service worker在运行。只有重新加载页面后,Service worker才会生效,控制加载它的页面。

你可以查看navigator.serviceWorker.controller,了解当前哪个ServiceWorker掌握控制权。如果后台没有任何Service worker,navigator.serviceWorker.controller返回null

Service worker激活以后,就能监听fetch事件。

self.addEventListener('fetch', function(event) {
  console.log(event.request);
});

fetch事件会在两种情况下触发。

  • 用户访问Service worker范围内的网页。
  • 这些网页发出的任何网络请求(页面本身、CSS、JS、图像、XHR等等),即使这些请求是发向另一个域。但是,iframe标签发出的请求不会被拦截。

    fetch事件的event对象的request属性,返回一个对象,包含了所拦截的网络请求的所有信息,比如URL、请求方法和HTTP头信息

    Service worker的强大之处,在于它会拦截请求,并会返回一个全新的回应。

    self.addEventListener('fetch', function(event) {
      event.respondWith(new Response("Hello world!"));
    });
    

    respondWith方法的参数是一个Response对象实例,或者一个Promise对象(resolved以后返回一个Response实例)。上面代码手动创造一个Response实例。

    下面是完整的代码。

    先看网页代码index.html

    
    
    
      
    
    
    
    

    然后是Service worker脚本sw.js

    // The SW will be shutdown when not in use to save memory,
    // be aware that any global state is likely to disappear
    console.log("SW startup");
    
    self.addEventListener('install', function(event) {
      console.log("SW installed");
    });
    
    self.addEventListener('activate', function(event) {
      console.log("SW activated");
    });
    
    self.addEventListener('fetch', function(event) {
      console.log("Caught a fetch!");
      event.respondWith(new Response("Hello world!"));
    });
    

    每一次浏览器向服务器要求一个文件的时候,就会触发fetch事件。Service worker可以在发出这个请求之前,前拦截它。

    self.addEventListener('fetch', function (event) {
      var request = event.request;
      ...
    });
    

    实际应用中,我们使用fetch方法去抓取资源,该方法返回一个Promise对象。

    self.addEventListener('fetch', function(event) {
      if (/\.jpg$/.test(event.request.url)) {
        event.respondWith(
          fetch('//www.google.co.uk/logos/example.gif', {
            mode: 'no-cors'
          })
        );
      }
    });
    

    上面代码中,如果网页请求JPG文件,就会被Service worker拦截,转而返回一个Google的Logo图像。fetch方法默认会加上CORS信息头,,上面设置了取消这个头。

    下面的代码是一个将所有JPG、PNG图片请求,改成WebP格式返回的例子。

    "use strict";
    
    // Listen to fetch events
    self.addEventListener('fetch', function(event) {
    
      // Check if the image is a jpeg
      if (/\.jpg$|.png$/.test(event.request.url)) {
        // Inspect the accept header for WebP support
        var supportsWebp = false;
        if (event.request.headers.has('accept')){
          supportsWebp = event.request.headers.get('accept').includes('webp');
        }
    
        // If we support WebP
        if (supportsWebp) {
          // Clone the request
          var req = event.request.clone();
          // Build the return URL
          var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";
          event.respondWith(fetch(returnUrl, {
            mode: 'no-cors'
          }));
        }
      }
    });
    

    如果请求失败,可以通过Promise的catch方法处理。

    self.addEventListener('fetch', function(event) {
      event.respondWith(
        fetch(event.request).catch(function() {
          return new Response("Request failed!");
        })
      );
    });
    

    登记成功后,可以在Chrome浏览器访问chrome://inspect/#service-workers,查看整个浏览器目前正在运行的Service worker。访问chrome://serviceworker-internals,可以查看浏览器目前安装的所有Service worker。

    一个已经登记过的Service worker脚本,如果发生改动,浏览器就会重新安装,这被称为“升级”。

    Service worker有一个Cache API,用来缓存外部资源。

    self.addEventListener('install', function(event) {
      // pre cache a load of stuff:
      event.waitUntil(
        caches.open('myapp-static-v1').then(function(cache) {
          return cache.addAll([
            '/',
            '/styles/all.css',
            '/styles/imgs/bg.png',
            '/scripts/all.js'
          ]);
        })
      )
    });
    
    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.match(event.request).then(function(response) {
          return response || fetch(event.request);
        })
      );
    });
    

    上面代码中,caches.open方法用来建立缓存,然后使用addAll方法添加资源。caches.match方法则用来建立缓存以后,匹配当前请求是否在缓存之中,如果命中就取出缓存,否则就正常发出这个请求。一旦一个资源进入缓存,它原来指定是否过期的HTTP信息头,就会被忽略。缓存之中的资源,只在你移除它们的时候,才会被移除。

    单个资源可以使用cache.put(request, response)方法添加。

    下面是一个在安装阶段缓存资源的例子。

    var staticCacheName = 'static';
    var version = 'v1::';
    
    self.addEventListener('install', function (event) {
      event.waitUntil(updateStaticCache());
    });
    
    function updateStaticCache() {
      return caches.open(version + staticCacheName)
        .then(function (cache) {
          return cache.addAll([
            '/path/to/javascript.js',
            '/path/to/stylesheet.css',
            '/path/to/someimage.png',
            '/path/to/someotherimage.png',
            '/',
            '/offline'
          ]);
        });
    };
    

    上面代码将JavaScript脚本、CSS样式表、图像文件、网站首页、离线页面,存入浏览器缓存。这些资源都要等全部进入缓存之后,才会安装。

    安装以后,就需要激活。

    self.addEventListener('activate', function (event) {
      event.waitUntil(
        caches.keys()
          .then(function (keys) {
            return Promise.all(keys
              .filter(function (key) {
                return key.indexOf(version) !== 0;
              })
              .map(function (key) {
                return caches.delete(key);
              })
            );
          })
      );
    });
    

     

    你可能感兴趣的:(JavaScript高级核心)