这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode
生成二维码,然后在用html2canvas
导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob
就可以了,非常的easy啊。
开始动手
思路没啥问题,但第一步就犯了难,用过react
框架或者其他MVVM
框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。
但是问题是,html2canvas
也是必须需要获取真实dom的快照然后转换成canvas对象。
听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas
来生成图片不就完事了嘛!但是想归想,却不能这么做。
原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能“啪”点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)
另一个原因呢,主要是跟html2canvas
这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom
的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom
对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas
对象存在当前页面。
而canvas
标签是会占用内存的,那么当同时存在过多的canvas
时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。
那么这篇文章主要是解决第一个原因所带来的问题的。
编程!启动!
第一步
那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。
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
树的结构。
思路就说到这了,接下来直接抛出代码:
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>) }
这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成
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
。
第三步
那么地基打好了,我们该怎么用这两个东西呢
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> ) }
首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blob
和canvas
对象,另一个用来卸载节点。
至于useEffect就很容易理解了,挂载后用html2canvas
处理组件顶层div
获取截图,然后返回数据,并卸载节点。
组件改造完毕了,那我们接下来把这两个组合一下
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的方式返回给用户。
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> )) }) }
本文转载于:
https://juejin.cn/post/7278512641781334051
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
1.本站内容仅供参考,不作为任何法律依据。用户在使用本站内容时,应自行判断其真实性、准确性和完整性,并承担相应风险。
2.本站部分内容来源于互联网,仅用于交流学习研究知识,若侵犯了您的合法权益,请及时邮件或站内私信与本站联系,我们将尽快予以处理。
3.本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
4.根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
5.本站是非经营性个人站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途
暂无评论内容