百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

将你的 Virtual dom 渲染成 Canvas

liuian 2024-12-18 15:36 21 浏览

来源:https://zhuanlan.zhihu.com/p/39886896

项目概述

一个基于 vue 的 virtual dom 插件库,按照Vue render 函数的写法,直接将 Vue 生成的 Vnode 渲染到 canvas 中。支持常规的滚动操作和一些基础的元素事件绑定。

demo 地址:https://muwoo.github.io/vnode2canvas/

背景

从一个小的需求说起:某一天,产品提了一个这样的需求,需要制作一个微信活动页,活动页可以分享包含用户相关信息的图片。这些信息是需要从接口取的,而且每个人都不一样。第一次碰到这种需求的时候,基本上都会去手撸 canvasAPI 去做渲染功能,这种情况的步骤大致如下:

  1. 写一大串 dom template 标签
  2. 渲染 template 成 dom 标签
  3. 开始捕捉 dom 元素,绘制 canvas
  4. 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 元素(举个 ):

  1. view/scrollView/scrollItem --> fillRect
  2. text --> fillText
  3. 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的渲染的组合。

更多干货文章,可关注小智的公众号:大迁世界 (打开微信,搜索公众号),里面有很多类似的文章,有需要啥资料的也可以在评论区跟我说,我会尽量解答。

相关推荐

GANs为何引爆机器学习?这篇基于TensorFlow的实例教程为你解惑!

「机器人圈导览」:生成对抗网络无疑是机器学习领域近三年来最火爆的研究领域,相关论文层出不求,各种领域的应用层出不穷。那么,GAN到底如何实践?本文编译自Medium,该文作者以一朵玫瑰花为例,详细阐...

高丽大学等机构联合发布StarGAN:可自定义表情和面部特征

原文来源:arXiv、GitHub作者:YunjeyChoi、MinjeChoi、MunyoungKim、Jung-WooHa、SungKim、JaegulChoo「雷克世界」编译:嗯~...

TensorFlow和PyTorch相继发布最新版,有何变化

原文来源:GitHub「机器人圈」编译:嗯~阿童木呀、多啦A亮Tensorflow主要特征和改进在Tensorflow库中添加封装评估量。所添加的评估量列表如下:1.深度神经网络分类器(DNNCl...

「2022 年」崔庆才 Python3 爬虫教程 - 深度学习识别滑动验证码缺口

上一节我们使用OpenCV识别了图形验证码躯壳欧。这时候就有朋友可能会说了,现在深度学习不是对图像识别很准吗?那深度学习可以用在识别滑动验证码缺口位置吗?当然也是可以的,本节我们就来了解下使用深度...

20K star!搞定 LLM 微调的开源利器

LLM(大语言模型)微调一直都是老大难问题,不仅因为微调需要大量的计算资源,而且微调的方法也很多,要去尝试每种方法的效果,需要安装大量的第三方库和依赖,甚至要接入一些框架,可能在还没开始微调就已经因为...

大模型DeepSeek本地部署后如何进行自定义调整?

1.理解模型架构a)查看深度求索官方文档或提供的源代码文件,了解模型的结构、输入输出格式以及支持的功能。模型是否为预训练权重?如果是,可以在预训练的基础上进行微调(Fine-tuning)。是否需要...

因配置不当,约5000个AI模型与数据集在公网暴露

除了可访问机器学习模型外,暴露的数据还可能包括训练数据集、超参数,甚至是用于构建模型的原始数据。前情回顾·人工智能安全动态向ChatGPT植入恶意“长期记忆”,持续窃取用户输入数据多模态大语言模型的致...

基于pytorch的深度学习人员重识别

基于pytorch的深度学习人员重识别Torchreid是一个库。基于pytorch的深度学习人员重识别。特点:支持多GPU训练支持图像的人员重识别与视频的人员重识别端到端的训练与评估简单的re...

DeepSeek本地部署:轻松训练你的AI模型

引言:为什么选择本地部署?在AI技术飞速发展的今天,越来越多的企业和个人希望将AI技术应用于实际场景中。然而,对于一些对数据隐私和计算资源有特殊需求的用户来说,云端部署可能并不是最佳选择。此时,本地部...

谷歌今天又开源了,这次是Sketch-RNN

前不久,谷歌公布了一项最新技术,可以教机器画画。今天,谷歌开源了代码。在我们研究其代码之前,首先先按要求设置Magenta环境。(https://github.com/tensorflow/magen...

Tensorflow 使用预训练模型训练的完整流程

前面已经介绍了深度学习框架Tensorflow的图像的标注和训练数据的准备工作,本文介绍一下使用预训练模型完成训练并导出训练的模型。1.选择预训练模型1.1下载预训练模型首先需要在Tensorf...

30天大模型调优学习计划(30分钟训练大模型)

30天大模型调优学习计划,结合Unsloth和Lora进行大模型微调,掌握大模型基础知识和调优方法,熟练应用。第1周:基础入门目标:了解大模型基础并熟悉Unsloth等工具的基本使用。Day1:大模...

python爬取喜马拉雅音频,json参数解析

一.抓包分析json,获取加密方式1.抓包获取音频界面f12打开抓包工具,播放一个(非vip)视频,点击“媒体”单击打开可以复制URL,发现就是我们要的音频。复制“CKwRIJEEXn-cABa0Tg...

五、JSONPath使用(Python)(json数据python)

1.安装方法pipinstalljsonpath2.jsonpath与Xpath下面表格是jsonpath语法与Xpath的完整概述和比较。Xpathjsonpath概述/$根节点.@当前节点...

Python网络爬虫的时候json=就是让你少写个json.dumps()

大家好,我是皮皮。一、前言前几天在Python白银交流群【空翼】问了一个Python网络爬虫的问题,提问截图如下:登录请求地址是这个:二、实现过程这里【甯同学】给了一个提示,如下所示:估计很多小伙伴和...