组件阅后即焚?挂载即卸载!看完你就理解了

前言

上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊。

开始动手

思路没啥问题,但第一步就犯了难,用过react框架或者其他MVVM框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。

但是问题是,html2canvas也是必须需要获取真实dom的快照然后转换成canvas对象。

听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas来生成图片不就完事了嘛!但是想归想,却不能这么做。

原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能"啪"点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)

另一个原因呢,主要是跟html2canvas这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas对象存在当前页面。

canvas标签是会占用内存的,那么当同时存在过多的canvas时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。

那么这篇文章主要是解决第一个原因所带来的问题的。

编程!启动!

第一步

那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。

ts 复制代码
interface IProps {
  qrCode: string
  studentName: string
  className: string
}

const SaveQRCode = (props: IProps) => {
  const divRef = React.useRef<HTMLDivElement>(null)
  // 具体怎么渲染看你们需求了
  return (
    <div ref={divRef}>XXXXXX</div>  
  )
}

看到代码,用过html2canvas的小伙伴应该知道ref是干嘛用的了,html2canvas()这个方法的参数是HTMLElement,传统一点的办法呢,可以通过document.getXXXXX这个方法来获取真实的dom元素。那么Ref就是替代前者的,它可以直接通过react框架获取真实的dom元素。

第二步

那么最简单的组件我们已经写好了,接下来就是如何动态的挂载这个组件,并且在挂载完之后就立刻卸载它。

那么先来理一下思路: 1、动态地挂载这个组件,且不能被用户肉眼观察到 2、挂载动作执行完立刻执行html2canvas获取canvas对象 3、通过canvas对象转换成blob对象并返回,或者直接通过回调函数返回canvas对象 4、组件卸载,清空dom

那么根据上面几点,可以得出:从外部获取的肯定是有组件这个东西,而挂载的位置则有要求,但并不一定需要从外部获取。

为了不被样式影响,我们直接在body标签下,再挂载一个div标签,来进行组件的动态渲染和卸载,同时也避免了影响之前dom树的结构。

思路就说到这了,接下来直接抛出代码:

js 复制代码
const AsyncMountComponent = (
  getElement: (onUnmount: () => void) => ReactNode,
  container: HTMLElement,
) => {
  const root = createRoot(container)
  const element = getElement(() => {
    root.unmount()
    container.remove()
  })
  root.render(<Suspense fallback={null}>{element}</Suspense>)
}

这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成

js 复制代码
const AsyncMountComponent = (getElement: (onUnmount: () => void) => ReactNode) => {
  const div = document.createElement('div')
  div.style.position = 'absolute'
  div.style.left = '2000px'
  document.body.appendChild(div)
  const root = createRoot(div)
  const element = getElement(() => {
    root.unmount()
    container.remove()
  })
  root.render(<Suspense fallback={null}>{element}</Suspense>)
}

这里的隐藏方式看个人喜好,无所谓。但有一点要注意的是,一定要可见,不然的话html2canvas生成不了图片,这里是最简单粗暴的方式,直接偏移left

第三步

那么地基打好了,我们该怎么用这两个东西呢

js 复制代码
interface IProps {
  qrCode: string
  studentName: string
  className: string
  // 这里自然就是获取blob和canvas对象的地方了
  onConfirm?: (data: { canvas: HTMLCanvasElement, blob: Blob }) => void
  // 这里是卸载的地方,由外部决定何时卸载节点,更加自由
  onUnmount?: () => void
}

const SaveQRCode = (props: IProps) => {
  const divRef = React.useRef<HTMLDivElement>(null)
  useEffect(() => {
    if (divRef.current && props.onConfirm) {
      html2canvas(divRef.current).then((canvas) => {
        canvas.toBlob((blob) => {
          props.onConfirm!({canvas, blob: blob!})
          props.onUnmount!()
        })
      })
    }
  }, [])
  // 具体怎么渲染看你们需求了
  return (
    <div ref={divRef}>XXXXXX</div>  
  )
}

首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blobcanvas对象,另一个用来卸载节点。

至于useEffect就很容易理解了,挂载后用html2canvas处理组件顶层div获取截图,然后返回数据,并卸载节点。

组件改造完毕了,那我们接下来把这两个组合一下

js 复制代码
const getQRCodeBlobCanvas = async (props: IProps): Promise<{
  canvas: HTMLCanvasElement, blob: Blob
}> => {
  return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    asyncMountComponent(
      (dispose) => (<SaveQRCode {...props} onConfirm={resolve} onUnmount={dispose}/>),
      div
    )
  })
}

那么一个简单的动态阅后即焚组件就完成了,且可以直接通过方法的形式使用,完美适配批量导出功能,当然也包括单个导出,至于批量导出的细节我就不写了,非常的简单。

升级V2

我只提供了最通用一种方式来做这么个阅后即焚组件,之后我闲着无聊,又把它做了一次业务向升级,获得了V2版本

这个版本呢,你只需要传入一个组件进去,且不用关心何时卸载,它是最真实的阅后即焚。至于数据,会通过Promise的方式返回给用户。

js 复制代码
const Wrapper = ({callback, children}: {  
  callback: (data: { blob: Blob,canvas: HTMLCanvasElement }) => void,  
  children: ReactNode  
}) => {  
  const divRef = useRef<HTMLDivElement>(null)  
  useEffect(() => {  
    if (divRef.current) {  
      html2canvas(divRef.current).then((canvas) => {  
        canvas.toBlob((blob) => {  
          callback({canvas, blob: blob!})  
        })  
      })  
    }  
  }, [])  
  return <div ref={divRef}>  
    {children}  
  </div>
}  
  
const getComponentSnapshotBlobCanvas = (getElement: () => ReactNode): Promise<{canvas:HTMLCanvasElement, blob: Blob}> => {
  return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    root.render((
      <Wrapper
        callback={(values) => {
          root.unmount()
          div.remove()
          resolve(values)
        }}
      >
        {getElement()}
      </Wrapper>
    ))
  })
}

其实也没啥特别的,无非就是把业务层公共的东西封装进了方法里,思路还是上面那个思路。

那么这篇博客就到这里了,感谢阅读!

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax