Webpack中使用Web Worker

3,294 阅读3分钟

最近老大说计算消息的函数耗时很长,导致后续渲染直接卡住,让我调研下能否使用web worker来处理处理消息的函数看看是否能解决渲染阻塞的问题,然后我就开始动手实践。

什么是Web Worker

现代浏览器为JavaScript创造的多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可独立运行,互不干扰,可以通过自带的信息机制相互通信。

const worker = new Worker('work.js') //参数必须是网络获取的脚本文件,不能是本地资源
worker.postMessage('message from 主线程') //主线程向worker传递信息
worker.terminate() //主线程关闭worker
//worker中
self.postMessage('message from worker') //worker向主线程传递信息,self是worker中的全局对象
self.close() //worker线程关闭自己

限制:

  • 同源限制
  • 无法使用document/window/alert/confirm
  • 无法加载本地资源

Web Worker的改造

Web Worker的交互类似与iframe之间的交互,因为postMessage是异步任务,如果直接使用,没法确保信息回来的时机,因此我们可以对Web Worker进行一些处理,让其内部返回一个promise以达到返回数据时机可控,类似下方:

myWorker.postMessage('message').then(res => console.log(res))

然后去git上面找果然找到了想要的库 promise-worker,看了下其内部的实现,大致分为两个文件,一个是主文件中创建的,一个是worker文件中使用的,因为设计场景很少,所以就稍作修改,然后就是我自己的了

主文件中创建Worker

let messageIds = 0 // 为了区分每一条消息 因此需要一个id
export class PromiseWorker {
  constructor(worker) {
    this.worker = worker
    this.callbacks = {}
  }
  postMessage(userMessage) {
    let messageId = messageIds++
    let messageToSend = [messageId, userMessage]
    // 通过返回一个promise达成可以使用.then来获取返回数据的能力
    return new Promise((resolve, reject) => {
      // 每次都创建一个新messageId的回调储存在内部,内部传入promise的reject resolve方便调用回调之后直接返回数据给外部
      this.callbacks[messageId] = (error, result) => {
        if (error) {
          reject(new Error(error.message))
        }
        resolve(result)
      }
      // 以下是创建一个传递消息的通道 
      // 因为在实际使用中 发现window.addEventListener('message') 监听不到 又因为worker中也支持channel 所以直接使用了MessageChannel
      // MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据
      const channel = new MessageChannel()
      // 外部使用port1监听worker发回来的信息
      channel.port1.onmessage = e => {
        onMessage(this, e)
      }
      // worker.postMessage(aMessage, transferList)
      // 第二个参数transferList类型 就是一个Transferable对象的数组,用于传递所有权
      // MessagePort的实例对象 就是Transferable对象 因此我们就可以将port2发送给worker内部用来传递消息
      this.worker.postMessage(messageToSend, [channel.port2])
    })
  }
}
// 外部用来处理worker返回消息的函数
function onMessage(self, e) {
  let message = e.data
  if (!Array.isArray(message) || message.length < 2) return
  let [messageId, error, result] = message
  // 通过data中的messageId找到对应的回调函数
  let callback = self.callbacks[messageId]
  if (!callback) return
  delete self.callbacks[messageId]
  // 执行回调函数 并且传入worker内部返回的error以及result
  callback(error, result)
}

Worker文件内部处理函数

// worker内部的注册函数传入一个函数callback,它就是用来处理大数据的函数
export function registerPromiseWorker(callback) {
  // 最终发送消息的地方 e.ports[0]就是主线程发送过来的port2 通过其上面的postMessage把消息发送给port1
  function postOutgoingMessage(e, messageId, error, result) {
    // 如果有错误就在发送一个[messageId, error]
    if (error) {
      e.ports[0].postMessage([
        messageId,
        {
          message: error.message
        }
      ])
    } else {
      // 否则就发送[messageId, null, result]
      e.ports[0].postMessage([messageId, null, result])
    }
  }
  
  function tryCatchFunc(callback, message) {
    try {
      return { res: callback(message) }
    } catch (e) {
      return { err: e }
    }
  }
  // 处理主线程发送的数据
  function handleIncomingMessage(e, callback, messageId, message) {
    // 捕获可能出现的错误
    let result = tryCatchFunc(callback, message)
    if (result.err) {
      postOutgoingMessage(e, messageId, result.err)
    } else if (!isPromise(result.res)) { // 如果callback不是promise直接发送处理后的结果
      postOutgoingMessage(e, messageId, null, result.res)
    } else {
     // 如果callback是promise则通过then去处理两种情况fulfillment rejection
      result.res.then(
        finalResult => {
          postOutgoingMessage(e, messageId, null, finalResult)
        },
        finalError => {
          postOutgoingMessage(e, messageId, finalError)
        }
      )
    }
  }
  // 处理主线程发过来的函数
  function onIncomingMessage(e) {
    let payload = e.data
    if (!Array.isArray(payload) || payload.length !== 2) return
    let [messageId, message] = payload
    // 判断callback是否可以执行 如果不可以则直接返回错误 否则就去处理message中的信息
    if (typeof callback !== 'function') {
      postOutgoingMessage(e, messageId, new Error('Please pass a function init register().'))
    } else {
      handleIncomingMessage(e, callback, messageId, message)
    }
  }
  // worker内部通过self.addEventListener('message')去监听主线程发过来的消息进行处理
  self.addEventListener('message', onIncomingMessage)
}

最终的使用

由于Worker不能使用本地文件,所以我们直接new Worker('./test.worker.js')是不行的,因此需要稍作处理,在webpack中使用需要加一个worker-loader去处理worker.js文件

// 首先安装一下worker-loader
npm i worker-loader -D
// 另外有一个坑点 worker-loader在2.0.0之后不再支持webpack3 因此如果webpack3需要安装1版本
npm i worker-loader@1.1.1 -D

// 安装好之后在webpack.config.js中配置
module: {
  // ...
  rules: [
    // ...
    {
      test: /.worker.js$/,  // 匹配所有的xxx.worker.js
      loader: 'worker-loader'
    }
    // ...
  ]
  // ...
}
// test.worker.js
import { registerPromiseWorker } from '../webworker/index'

registerPromiseWorker(m => {
  return  `${m} pong`
})

// index.js webpack5以下
import Worker from './test.worker.js' // loader处理过后可以直接import引入
import { PromiseWorker } from '../webworker/index'
const worker = new Worker() // 直接创建实例
const pw = new PromiseWorker(worker)

pw.postMessage('ping').then(result => {
  console.log('返回内容', result)
})

// index.js webpack5之后可以不需要使用worker-loader 直接生成一个url文件地址引入
const workerURL = new URL('./test.worker.js', import.meta.url)
const worker = new Worker(workerURL)
const pw = new PromiseWorker(worker)

pw.postMessage('ping').then(result => {
  console.log('返回内容', result)
})