前端如何解决较大资源生成问题

163 阅读2分钟

工作场景中,可能会遇到一下场景

  • 用户分享使用canvas生成图片
  • 用户提交过往数据,生成pdf等文件,比如生成几十兆的文件

以上场景,第一种还好,直接canvas绘制即可,基本上是零延迟,有延迟也是在可以接受的范围内,但如果对资源进行高斯模糊,就会出现用户有效反馈时间变长(TTI),表现为点击了事件却很久才有响应

但对于第二种,若是放在客户端阶段生成怕是要炸掉,一是等待时间长不说,二是因为客户端的不确定性怕是要炸掉,比如说对于低端机型内存不够直接闪退等


如何解决

一:使用webworker,解决大量计算占用线程,交互事件无法及时执行的问题。

基础知识可参见阮一峰老师的Web Worker 使用教程
兼容性如下表,对所有的内核机型都是适用的

image.png

既然canvas绘制了占用了主线程,那我们就把他放到另一个线程去执行。以Vue为例,让页面一进来就去执行高斯模糊操作。 关键代码如下
main.js


// 使用tti-polyfill包来反映Time to Interactive
import Vue from 'vue'
import App from './App.vue'

import ttiPolyfill from 'tti-polyfill';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  console.log(tti)
})
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <div class="btn" @click="doClick">点击事件</div>
    <div class="img-wrapper">
      <div class="left-img">
        <img src="./assets/render.jpg" ref="testImg" class="test-img" alt="" />
      </div>
      <div class="right-img">
        <!-- width="1200" height="800"  -->
        <!-- width="4480" height="6620"  -->
        <canvas width="4480" height="6620" id="_poster-canvas"></canvas>
      </div>
    </div>
  </div>
</template>

<script>
import toGauss from './gauss'
import Worker from './gauss.worker'

export default {
  name: 'App',
  components: {
  },
  methods: {
    doClick() {
      console.log('我是点击事件')
    }
  },
  mounted() {
    const imgEle = this.$refs.testImg
    imgEle.onload = () => {
      const imgWidth = imgEle.naturalWidth
      const imgHeight = imgEle.naturalHeight
      const canvas = document.getElementById('_poster-canvas')
      const ctx = canvas.getContext('2d')
      ctx.drawImage(imgEle, 0, 0, imgWidth, imgHeight, 0, 0, imgWidth, imgHeight);
      const data = ctx.getImageData(0, 0, imgWidth, imgHeight);
      // const emptyData = ctx.createImageData(imgWidth, imgHeight);
      // emptyData = toGauss(data)
      // ctx.putImageData(emptyData,0,0)
      const worker = new Worker()
      worker.postMessage(data)
      worker.onmessage = function (e) {
        ctx.putImageData(e.data.msg, 0, 0)
      }
    }
  }

}
</script>

<style lang="scss">
* {
  margin: 0;
  padding: 0;
}
.btn {
  color: #ffff;
  padding: 15px 30px;
  font-size: 30px;
  text-align: center;
  background-color: black;
}
.img-wrapper {
  display: flex;
  width: 100vw;
  overflow: hidden;
  img {
    width: 100%;
  }
  .left-img {
    flex: 1;
  }
  .right-img {
    flex: 1;
    canvas {
      width: 100%;
    }
  }
}
</style>

结果

属性使用webworker没有使用webwoker
TTI1261.3000000044703ms5709.80000000447ms

二:使用 selenium起一个渲染服务

什么是selenium: selenium作为一个是一个应用web应用测试工具,Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。

利用这一特性,我们就可以把客户端的任务交给selenium去做。

  • 下载selenium-webdriver插件
  • 起一个消息队列服务,用来处理用户的提交生成的消息
const webdriver = require('selenium-webdriver')
const chrome = require('selenium-webdriver/chrome')
const io = require('selenium-webdriver/io')

const CHROMEDRIVER_EXE = process.platform === 'win32' ? 'lib/chromedriver.exe' : (process.platform === 'darwin' ? 'lib/chromedriver_mac' : 'lib/chromedriver_linux')
class Render {
  constructor () {
    this._driver = null
    this.failedResult = { success: false, message: '生成失败'}
    this.running = false
  }
  
  get driver () {
    if (!this._driver) {
      const service = new chrome.ServiceBuilder(io.findInPath(CHROMEDRIVER_EXE, true))
      this._driver = new webdriver.Builder()
      .setChromeService(service)
      .forBrowser('chrome')
      .build()
    }
    return this._driver
  }

  set driver (value) {
    this._driver = value
  }
  async runTask (params, callback) {
    this.running = true
    const driver = this.driver
    await driver.get(params.url)
    await callback({ success: true, message: '生成成功'})
    this.running = false
  }
  print (options, callback) {
    return this.runTask(options, callback)
  }
}

module.exports = new Render()

demo gif图

Jul-26-2022 11-46-25.gif 这个只是提供一个思路,当然中间还有很多case场景要考虑。
以上的demo地址:github.com/lecheng-lc/…