优化实战 第 05 期 - 共享Worker实现数据预获取

2,460 阅读3分钟

工作线程(Web Worker)允许开发人员编写能够脱离主线程、长时间运行而不被用户所中断的后台程序,去执行 计算密集型高延迟 的事务或者逻辑,并同时保证页面对用户的及时响应

web_worker.jpg

Web Worker 分为 专用线程 Dedicated Worker 和 共享线程 Shared Worker

浏览器作为宿主环境提供了多线程运行 JS 的能力,可通过并行的方式高效地提升执行效率

contrast.jpeg

共享 worker 可以在多个浏览器页面中访问同一个 Worker 实例,本期我们将基于共享 Worker 来设计符合业务场景的优化方案

shared_worker.jpg

在 脚本URL、Worker名称、文档源 origin 保持一致的情况下,多个页面共享同一个 SharedWorker。可以实现多页面共享数据,共享 webSocket 连接

主线程环境

  • 创建一个共享Worker

    const sharedWorker = new SharedWorker('prefetch.worker.js',{
      name: 'prefetch_worker'
    })
    

    注:共享 Worker 可以被多个上下文共同使用

  • 消息的发送和接收

    const worker = sharedWorker.port
    worker.postMessage()
    worker.onmessage = ({ data }) => {
      // event.data <=> data
    }
    

工作线程环境

  • 消息的接收和发送

    self.addEventListener('connect', ({ ports }) => {
      const client = ports[0]
      client.onmessage = ({ data }) => {
        // event.data <=> data
      }
      client.postMessage(data)
    })
    

    SharedWorker 是通过 port 来发送和接收消息的

  • 加载外部脚本或第三方库

    self.importScripts(urlA, urlB)
    

    注:所引入的脚本与库都会绑定在子线程的全局对象上,即 selfthis

注意事项

  • 同源策略的限制

    Worker 线程中运行的脚本文件必须与主线程的脚本文件同源

  • 操作 DOM 的限制

    Worker 线程中无法读取主线程所在视图的 DOM 对象,也无法使用 windowdocument 对象,但可以使用他们的子对象

  • 本地文件的限制

    Worker 线程中无法读取本地文件,因此它所加载的脚本必须来自于网络

  • 消息通信

    Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息的发送和接收来通信

场景示例

  • 业务场景

    有些固定参数的业务请求相对来说比较耗时,好几十秒都没有响应结果。导致页面呈现延迟,这样的用户体验极差

  • 生活场景

    公司大楼的电梯,每逢上班的时间点,都要等很长的时间,让人非常的烦躁。

    如果说我们早一点,就可以直接坐上电梯了,避免了不必要地等待

共享线程数据预获取

  • 工作线程(worker/worker.prefetch.js)

    配置请求类型映射策略

    const strategies = Object.assign(Object.create(null), {
      'GET': (url, options) => {
        const { params = {}, headers = {}, ...config } = options
        const queryString = Object.keys(params).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&')
        Object.assign(config, {
          url: queryString ? `${url}?${queryString}` : url,
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded', ...headers 
          }
        })
        return config
      },
      'POST': (url, options) => {
        const { data = null, headers = {}, ...config } = options
        Object.assign(config, { 
          url,
          body: data ? JSON.stringify(data) : null,
          headers: {
            'Content-Type': 'application/json; charset=utf-8', ...headers
          }
        })
        return config
      }
    })
    

    根据请求类型读取映射策略,发起请求获取数据

    self.addEventListener('connect', ({ ports }) => {
      const client = ports[0]
      client.onmessage = ({ data: [url, options] }) => {
        const { url: fetchUrl, ...adjunction } = strategies[options.method ? options.method.toUpperCase() : 'GET'](url, options)
        fetch(fetchUrl, { mode: 'cors', credentials: 'same-origin', ...adjunction})
          .then(async response => {
            if (!response.ok) {
              throw new Error('Network response was not ok.')
            } else {
              const data = await response.json()
              client.postMessage(data)
            }
          })
      }
    })
    
  • 主线程封装(worker/share.index.js)

    const wfetch = (url, options = {}) => {
      Object.assign(options, {
        'headers': { 'Authorization': localStorage.getItem('AUTH_TOKEN') }
      })
      const sharedWorker = new SharedWorker(`${location.origin}/worker/worker.prefetch.js`, { name: 'prefetch_worker' })
      const worker = sharedWorker.port
      return new Promise(resolve => {
        worker.postMessage([url, options])
        worker.onmessage = ({ data }) => resolve(data)
      })
    }
    export default wfetch
    
  • 主线程处理

    import wfetch from 'worker/share.index.js'
    wfetch(url).then(data => {
       console.log('Response Data:', data)
    })
    
  • 基于 Webpack5 的进一步优化

    const sharedWorker = new SharedWorker(new URL('./worker.prefetch.js', import.meta.url), { name: 'prefetch_worker' })
    

    通过以上语法可以实现不使用 bundler 就可以运行代码,也可以在浏览器中的原生 ECMAScript 模块中使用

浏览器调试

  • 图示

    chrome.png

  • 图解

    首先,通过 chrome://inspect 打开浏览器新标签页

    然后,选择左侧 Shared workers 选项查看执行中的线程列表

    最后,选择相应的执行线程,点击 inspect 打开调试控制台

浏览器兼容性

  • 在现代浏览器和移动端上的支持性并不是很理想

    shared_web_workers.png

    共享 Worker 在 Safari 浏览器中的支持比较糟糕,在项目中使用要做好优雅降级的方案

  • 判断浏览器对共享 Worker 的支持性

    if (window.hasOwnProperty('SharedWorker')) {
      console.log('支持,请放心使用!')
    } else {
      console.log('不支持,请优雅降级!')
    }
    
  • 一起交流学习

    加群交流看沸点