将你的 Virtual dom 渲染成 Canvas
liuian 2024-12-18 15:36 58 浏览
来源:https://zhuanlan.zhihu.com/p/39886896
项目概述
一个基于 vue 的 virtual dom 插件库,按照Vue render 函数的写法,直接将 Vue 生成的 Vnode 渲染到 canvas 中。支持常规的滚动操作和一些基础的元素事件绑定。
demo 地址:https://muwoo.github.io/vnode2canvas/
背景
从一个小的需求说起:某一天,产品提了一个这样的需求,需要制作一个微信活动页,活动页可以分享包含用户相关信息的图片。这些信息是需要从接口取的,而且每个人都不一样。第一次碰到这种需求的时候,基本上都会去手撸 canvasAPI 去做渲染功能,这种情况的步骤大致如下:
- 写一大串 dom template 标签
- 渲染 template 成 dom 标签
- 开始捕捉 dom 元素,绘制 canvas
- canvas 渲染图片
面临的主要问题是复用性太差,其次是性能上也有问题,用户看到的界面不一定和正式渲染出的界面一致,可能存在渲染差异。作为一个有追求的前端,当然得想想看有没有更好的法子。于是乎了解到了一个 html2canvas 这样一个库。但是总是感觉还是要转成 dom 再去绘制,而且感觉性能和稳定性也不是很好。
我们知道 vue 通过 vnode 实现了对不同端的渲染工作,那有没有可能通过 vnode 实现对 canvas 的渲染呢?也就是说,没有 vnode -> html -> canvas 而是直接vnode -> canvas。同时利用 vue 的数据驱动,来达到绘制的数据驱动。想法有了,下面开始实施。
调研
这篇文章对此有详细的介绍:60 FPS on the mobile web 这里简单的概括一下:
canvas 是一种立即模式的渲染方式,不会存储额外的渲染信息。Canvas 受益于立即模式,允许直接发送绘图命令到 GPU。但若用它来构建用户界面,需要进行一个更高层次的抽象。例如一些简单的处理,比如当绘制一个异步加载的资源到一个元素上时会出现问题,如在图片上绘制文本。
在 HTML 中,由于元素存在顺序,以及 CSS 中存在 z-index,因此是很容易实现的。dom 渲染是一种保留模式,保留模式是一种声明性 API,用于维护绘制到其中的对象的层次结构。保留模式 API 的优点是,对于你的应用程序,他们通常更容易构建复杂的场景,例如 DOM。通常这都会带来性能成本,需要额外的内存来保存场景和更新场景,这可能会很慢。
看来 canvas 绘制页面的研究,很久之前就已经有人付出过研究了。而且性能还是很不错的。那我们更要试试看,到底我们的想法能不能实现了!越来越期待....
开始
canvas 的渲染其实也是一种尝试,既然前人以及做了充分的实践,那么我们便站在巨人的肩膀上去基于 vue 来实现一个数据驱动的canvas渲染。说做就做!(我们这里只提供思路,不做具体实现细节的讨论,因为实现起来有点复杂,如果有兴趣可以参考我的项目实现,或者一起交流探讨 )
处理 vnode
熟悉 Vue 源码的应该都知道,Vue 通过 render 函数,传入 createElement 方法来构造出一个 vnode,通过发布--订阅模式来实现对数据的监听,重新生成 vnode。vnode 最后被转成各平台所需的视图。而我们要做的就是在 vnode 这一层开始。所以,我们基于 Vue 源码的方式,实现一个监听函数,并混入 Vue 实例中:
Vue.mixin({
// ...
created() {
if (this.$options.renderCanvas) {
// ...
// 监听vnode中引用的变化,重新渲染
this.$watch(this.updateCanvas, this.noop)
// ...
}
},
methods: {
updateCanvas() {
// 模拟Vue render 函数
// 寻找实例中定义的 renderCanvas 方法,并传入createElement方法
let vnode = this.$options.renderCanvas.call(this._renderProxy, this.$createElement)
}
})
这样我们就可以愉快的在组件内部使用:
renderCanvas (h) {
return h(...)
}
canvas 元素处理
render 的 vnode 我们需要做额外的一些约束,也就是说我们需要怎么样的渲染标签,来渲染对应的 canvas 元素(举个 ):
- view/scrollView/scrollItem --> fillRect
- text --> fillText
- image --> drawImage
其中这些元素类分别都继承于一个 Super 类,并且由于它们各有不同的展示方式,因此它们分别实现自己的 draw 方法,做定制化的展示。
绘制对象的布局机制实现
绘制 canvas 布局最基础的写法是为 canvas 元素传入一系列坐标点和相关的基础宽高,这样写到实际项目中可能是这样的:
renderCanvas(h) {
return h('view', {
style: {
left: 10,
top: 10,
width: 100,
height: 100
}
})
}
这样写确实有点不方便维护,目前有好几种解决方案,一种是使用 css-layout去做管理。css-layout 支持的转换属性如下:
这样也只是做了一层转换,帮我们更好的用 css 思维去写 canvas,但是如果我们很不爽 css in js 的写法,其实我们还可以写一个webpack loader 来加载外部 css:
const css = require('css')
module.exports = function (source, other) {
let cssAST = css.parse(source)
let parseCss = new ParseCss(cssAST)
parseCss.parse()
this.cacheable();
this.callback(null, parseCss.declareStyle(), other);
};
class ParseCss {
constructor(cssAST) {
this.rules = cssAST.stylesheet.rules
this.targetStyle = {}
}
parse () {
this.rules.forEach((rule) => {
let selector = rule.selectors[0]
this.targetStyle[selector] = {}
rule.declarations.forEach((dec) => {
this.targetStyle[selector][dec.property] = this.formatValue(dec.value)
})
})
}
formatValue (string) {
string = string.replace(/"/g, '').replace(/'/g, '')
return string.indexOf('px') !== -1 ? parseInt(string) : string
}
declareStyle (property) {
return `window.${property || 'vStyle'} = ${JSON.stringify(this.targetStyle)}`
}
}
简单的来说:主要也就是将 css 文件转成 AST 语法树,之后再对语法树做转换,转成 canvas 需要的定义形式,并以变量的形式注入到组件中。
实现列表滚动
如果我们的元素很多,需要滚动时,我们必须解决 canvas 内部元素滚动的问题。这里我选择了使用Zynga Scroller 来模拟用户滚动方法,通过他返回的滚动坐标点,来对 canvas 进行重绘。有兴趣的可以参考这里我的实现:
https://github.com/muwoo/vnode2canvas/blob/master/src/core/shape/scrollView.js
事件模拟
对于 click,touch 等 dom 事件的模拟,我们采用的方案是根据点击区域进行检测,并找出最底层的元素,递归寻找父元素并触发对应事件处理程序,从而模拟事件冒泡。详细的实现可以参考这里:
https://github.com/muwoo/vnode2canvas/blob/master/src/core/event.js
最后
canvas 绘制页面也是一种创新的尝试,希望这里的研究对你有启发,也欢迎你的 PR。这里也做了很多性能优化,限于篇幅不在赘述了,有兴趣也可以一起探讨。
最后:它并不意味着完全取代基于DOM的渲染,这仍然需要文本输入,复制/粘贴,可访问性和SEO。出于这些原因,我们可以使用canvas和基于DOM的渲染的组合。
更多干货文章,可关注小智的公众号:大迁世界 (打开微信,搜索公众号),里面有很多类似的文章,有需要啥资料的也可以在评论区跟我说,我会尽量解答。
相关推荐
- 如何修改文件(如何修改文件创建时间)
-
工具/原料电脑windows系统方法/步骤1、新建一个文档文件。2、在文件名后面输入“.exe”按下enter键。3、文件的名字改变了,但格式没变。4、点击“菜单”点击“工具”,点击“文件夹选项...
- win7剪贴板怎么调出来(windows7的剪贴板在哪里)
-
要开启Win7剪贴板,首先需要打开“运行”窗口,方法是按下“Win+R”快捷键。在弹出的窗口中输入“clipbrd”并点击“确定”按钮。这样就会打开剪贴板窗口。在窗口中可以看到最近复制或剪切的内容。如...
- cmd一键清除垃圾命令chkdsk(cmd一键清理)
-
这个就是自检命令,在一些轻微的文件损坏可以用这个命令回复楼主你打的/F是修复磁盘上的错误意思/R是查找不正确的扇区并恢复可读信息。chkdsk的全称是checkdisk,就是磁盘检查的意思。这个东...
- 无备份彻底删除照片找回(苹果手机无备份彻底删除照片找回)
-
如果您的手机照片没有备份,但是误删了照片,可以尝试以下几个方法恢复:1.使用Android手机自带的垃圾桶功能:如果您使用的是安卓手机,最新版的Android系统中提供了“回收站”功能。您可以在相册...
- qq怎么改实名认证(qq怎么改实名认证吗)
-
要先将原来的实名认证注销掉,才可修改QQ的实名认证,具体方法如下,打开手机【QQ】,点击左上角的【头像】,然后选择【我的QQ钱包】,点击右上角的【设置】,在设置界面选择【实名认证】,进入到实名认证界面...
- win10启用网络发现自动关闭(win10启用网络发现自动关闭了)
-
因为在Win10系统中,网络发现是一个网络共享和连接的设置选项,如果关闭了网络发现,那么其他计算机就无法找到你的计算机并进行资源共享,这样能够提高安全性。同时关闭网络发现能够减少广播包,降低网络负载,...
- 看图软件cad手机版下载(看图软件cad手机版下载安装)
-
你可以在应用商店或者CAD官方网站上搜索"CAD快速识图"并下载安装。在下载前,建议先确认你的手机是否兼容这个应用程序,以及查看是否有最新版本可供下载。下载完成后,打开应用并按照提示完...
-
- 怎么进入tp link无线路由器设置
-
tp-link路由器的设置登录入口进入方法如下1.打开tplogin.cn页面,点击右上角的“登录”菜单。2.输入用户名和密码,点击登录按钮,进入登录页面。3.如果你忘记了用户名或密码,可点击忘记密码,并输入注册邮箱或者手机号,点击确认,系...
-
2025-12-31 08:05 liuian
- 电脑莫名重启怎么回事(电脑莫名奇妙的重启)
-
电源的大电容漏电,供电不足造成的,这个就要更换电源2、主板上的内存插槽和内存之间接触不良出现问题,或者内存的显存集成块出现虚焊也会出现老是重启3、CPU风扇出问题,或者散热器的卡子松了。当CPU的风扇...
- 如何一键还原电脑系统win7(一键还原win7系统按那个键)
-
方法如下: 1、下载“一键GHOST硬盘版”用压缩工具软件解压,解压后选“setup.exe”文件,即自动把一键还原安装到硬盘中。安装完成后,在桌面和开始菜单将建立程序的快捷方式: Win7系统...
- 笔记本键盘无法使用(dell笔记本电脑键盘失灵一键修复)
-
个别键因为脏了接触不好或者是弹簧失去了弹性,可以自行打开键盘,用无水酒精清洗一下键盘内部。修改笔记本键盘的驱动:通过“我的电脑”打开系统属性,选择硬件标签,打开设备管理器,我们发现中文Windows...
- u启宝装机工具(u启宝装系统)
-
1、将下载好的ghostwin7系统镜像文件拷贝到u盘内,重启电脑,在看到开机画面时按下相应的启动快捷键(大家可以到u启动官网查找相应的快捷键)即可进入u启动的主菜单界面,随后选择usb选项并按回车...
- 找回wifi密码的方法(找回wifi密码怎么找)
-
1、在已经连接WiFi的手机上操作:在手机桌面找到设定,进入到手机设置页面。2、在设置中,找到WLAN也就是无线局域网,点击进入无线网络的查看或配置页面。3、进入到WLAN页面后,我们会看见周围的Wi...
- 一周热门
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
