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

实现一个类Web的布局引擎 实现一个类web的布局引擎有哪些

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

一看标题,懂的人都懂,确实挺唬人的,没办法,这是个人的习惯,喜欢小小的吹一下,如果大佬们对内容有什么看法可以共同探讨,如果有错误的地方还请多多指教。闲言少叙(少逼逼,大家烦得慌),进入正题。

背景

背景很重要,需要介绍一下为什么要做这件事情,这样对大家才有借鉴的意义。

最近公司的产品要实现一个功能,就是把系统产生的一些文字的对话信息生成图片,至于作用就不说了,反正是这么一个需求吧。我估计应该不少人做过类似的需求吧。

我估摸着就是画点文字到画布上这么简单吧,结果UI抛过来一张图,嗯,就要按图上的效果来,图上有啥呢?

  • 银灰色的背景
  • 大字号的居中标题
  • 左对齐的深灰色的说明文字
  • 紧接着下面是许多项目,每个项目里面有两段文字,两段文字的颜色不一样,分为黑色和灰色,每个项目都是白色的背景,有圆角,还有阴影,为了排版好看,当然还有边距行距。
  • 这些显示项目的高度是要根据显示的内容自动适应
  • 项目的数量当然不是固定的

嗯,基本上就是这样的需求吧!不对,还差一点:需要输出多页图片[捂脸]!

各位大佬觉得这件事情难不难做呢?前端大佬表示毫无压力:HTML做好布局,然后用html2canvas库这么捣腾一下,图片妥妥的就出来了,JPEG/PNG格式任你选,好简单哦!后端大佬:不行,我们后端要用这些图,你前端弄了我们这边不好搞,不容易在业务流程中控制,得我们后端自己来弄。于是这个活成功的被揽下来了,对了,我们的后端用的是NodeJS。

哎~,我知道你是搞Java的,那个写Go的,还有搞Rust都,沾点后端的,都别忙着跑,还是有点东西,说不定哪天老板也让你们画图的,以我的经验,这种活似乎还不少。

结构设计

活儿被接下来了,接下来要做事了。既然要把文字画得好看,就需要更好地控制,这就需要我们来做排版布局了。排版布局对于前端来说,可能再熟悉不过了,但是对于从没有接触过前端开发的后端来说,可能真的会没头绪,当然对于全栈的大佬,就另说了,小小的秀一下,我就是那个全栈[看]。

我们好好地理一下怎么来做。

为了完成这个需求的功能,考虑到后面的一些扩展和通用性(说不定这个模块在公司发扬光大大家都要用呢),我把这个需求做成了一个独立的模块,由三个部分组成:

  • 文档及元素结构
  • 样式及布局
  • 渲染引擎(嗯,直白点就是画图)

我们接下来进行逐个的分析。

文档及元素结构

我们在进行画图的时候,首先需要知道我们要画什么样的内容,然后才能进行画图,为了更方便地管理和使用,我们需要将这些内容组织到一起,这就形成了一个文档,这个文档是概念性,并不一定是存在硬盘的文件。文档包含了我们要处理的各种内容。

那么我们该如何组织这些文字内容呢?最简单的,我们把要绘制的文字分为多行,每行都是纯文字,我们日常见到的TXT文件就是这样的,所以你看到记事本的显示文字就很单一,从这点来说这并不能符合我们的需求。根据我们的需求,我们可以看到其实我们需要一个结构化的方式来管理我们的文字内容。

为此我引入了元素的概念(其实我在实现的时候是借用HTML/XML的DOM的概念了),我们屏幕上或者说我们要画的部分都由一个元素来进行组织和描述。比如:我们要画一段文字,就需要一个文字元素来描述;我们要画一张图片就由一个图片元素来描述,我们只要知道是什么类型的元素,就知道应该如何做。

于是就定义了一个如下的数据结构(Typescript伪代码,没有测试,如果有问题请拍砖):

interface Element {
  tag: string;  // 元素的标签名称
  parent?: Element;  // 父级标签
  children: Element[];  // 子标签
  previousElementSibling?: Element;  // 前一个节点
  nextElementSibling?: Element;   // 后一个节点
  // ...
}

前端大佬看过来,是不是有点DOM的意思;没错,有点儿数据结构基础知识的人就能看出来,这最终组成的结构必然是一棵树。有了这棵树,我们就可以玩出花来了,比如一个方框里套一个方框,方框里放一个图片,放一段文字都可以,我们就很容易的把我们要绘制内容的层次表达清楚了。因为实现了一个DOM的子集,还可以像DOM一样操作元素了。

为了更好的处理要绘制的内容,我又把元素给具体化了三个标签出来:分别是ViewElement,TextElement,ImageElement(暂时未用,所以并没有实现这个标签)。

interface ViewElement extends Element {
	tag: "view";
}

interface TextElement extends Element {
  tag: "text";
  text: string;    // 新增了一个text属性,标识需要绘制文字的内容
}

interface ImageElement extends Element {
  tag: "image";
  src: string;    // 新增src属性,标识图片的原地址
}

样式及布局

有了前面的文档结构,就可以很清晰地知道要画什么内容了,要画文字,要画图片都知道,但是有一些问题,比如前面需求上要求的文字是黑色的,字体的大小是多少等等格式或者样式都不知道,虽然有了这个文档结构,但并不足以让你画出来满足需求的画面,所以我们需要引入一样新的东西:它可以描述元素在绘制的时候应该是什么样子的,这就是样式

例如,我们有以下的场景:

  • 画一个白色的方块,它的宽度是10个像素,高度是是个像素
  • 画一个文字,字是红色的,背景是绿色的(嗯,你可能猜到了,我写红色和绿色的时候内心想的就是红配绿赛狗屁),字体是楷体

我是用下面的样式结构描述,来满足上述的两种要求:

// 白色方框
{
  "width": "10px",   // 宽度10像素
  "height": "10px",  // 高度10像素
  "backgroundColor": "#FFFFFF"  // 背景颜色:白色
}

// 文字
{
  "color": "#FF0000",  // 红色
  "backgroundColor": "#00FF00", // 绿色
  "fontFamily": "楷体"  // 字体
}

有了这些内容是否就可以知道如何绘制了呢,显然是可以的,但是还是不够,我们还需要更详细地描述我们要画的内容,具体的可以参考CSS样式表,我只实现了很少的一部分:

interface IStyle {
  display: "block" | "inline-block" | "inline";  //  元素的布局方式(屏幕上就是显示方式了)
  position: "static" | "relative" | "absolute";   // 显示的位置
  left?: string;  // 左边的位置,position为relative和absolute时有效
  top?: string; // 上边位置,position为relative和absolute时有效
  width?: string; // 元素的宽度
  height?: string; // 元素的高度
  boder: string; // 元素的边框描述,如:1px soild #ff0000
  // border-left, border-top, border-right, border-bottom,四个边界
  margin: string; // 外边距
  // margin-left, margin-top, margin-right, margin-bottom,四个外边距
  padding: string; // 内边距
  // padding-left, padding-top, padding-right, padding-bottom, 四个内边距
  color: string; // 前景颜色
  backgroundColor: string; // 背景颜色
  fontFamily: string; // 字体
  fontSize: string; // 字号大小
  // 等等其他
}

如果你没有前端经验,可能你会比较好奇,为什么类似于left、top这样的属性为什么不使用number(数字类型)而是使用string(字符串)呢?因为我考虑兼容CSS(嗯,没错,就是抄作业),CSS在设置这些数值的时候一般是带有单位的比如10px代表是像素,宽度10%使用就是父元素宽度的10%,等等以此类推,它有很多单位的,有兴趣的不了解的可以去查看下CSS相关的资料。

有了这些样式的描述,基本上就可以知道所画的内容是什么样子的了,所以我们可以为我们的Element添加上Style样式了。

如果你仔细看过上面的left和top属性,有个特别的说明了,这两个属性要在position属性为relative和absolute上有效,如果我们position是static怎么办?那这个元素画在哪里呢?还有一点,如果我每个元素上都写这么多东西不要累死呀,那该怎么办呢?其实这属于两个问题,后面一个问题就好办了,我们给每个元素添加一个默认的预设好的属性就可以了,如果没有设置,用默认的就可以了;至于前面一个问题,我们要引入布局功能。

何为布局?简单地说就是元素该在什么位置出现,它的大小是多少。这些是需要我们进行计算的。在计算之前我们先引入一个东西,盒子模型,示例如下:

我们要画的元素是一个嵌套层次的盒子,从里到外依次是显示内容、内边距、边框和外边距,对于盒子模型,前端大佬内心呵呵一笑,咱们后端也就理解理解吧,查查资料都是可以的。

这个盒子模型是我们计算元素大小要注意的,比如这个元素到底占用多少尺寸呢,边框算不算元素的尺寸这个有点烦,我们就简化一下,元素的尺寸包含了边框的尺寸。外边框很好理解,就是元素离它的父元素的边界距离。

为了快速完成目标,我还是做了很多的简化计算了,比如display的模式我只支持了一个block,这个block是什么意思呢?它指明了当前这个元素是占据一整行的,也就意味着,它的下一个元素不能紧挨着它的右边,而是在它的下边,那么我们计算一个元素的垂直位置就好计算了:

当前元素顶部位置 = 前一个元素的顶部位置 + 前一个元素的高度

太简单了有没有。如果这个元素就是第一个元素,那么它的位置就是父级元素的顶部位置。很容易就能计算出元素的位置了。

对于元素的高度,一种是样式中设置好的高度,如果没有设置高度,那么元素的初始高度值就是0,需要根据情况计算高度了:

元素高度 = 元素的子节点元素高度的累加;

对于文字元素,如果我们没有指定它的高度,则需要对文字占用的高度进行计算,简单地来说文字元素的高度计算如下:

// 伪代码吧,如果逻辑有问题,请拍砖
// 简化了,计算的时候要考虑到盒子模型等各方面的影响
function calcTextElementHeight(element) {
  let height = 0;
  let tmpText = ""
  for (let ch of element.text) {
    // 测量文本的宽度
    let tWidth = measureText(tmpText + ch);
    // 如果文本的宽度大于元素的宽度,说明文字要换行了,那么其占用的高度要要增加
    if (tWidth > element.style.height) {
      height += element.style.lineHeight;  // 行高,默认是字体的大小
      tmpText = ch;
    } else {
      tmpText += ch;
    }
  }
}

通过上述一系列的过程,就可以计算出每个元素该绘制成什么样,在什么位置绘制等信息;这是一个递归过程的计算,有兴趣可以自己思考怎么实现,对大佬们来说肯定都不是事。

把计算出来的结果生成一个数据结构,我称之为渲染节点,这个节点就挂在我们前面Element节点中。具体计算的出来的内容这里就不列出来了,和Style属性大同小异,只不过全部是通过计算出来的具体数值,是拿过来就可以用的。

渲染引擎

没啥说的,就是绘制呗,前面说了,我是用NodeJS的,有啥图形库呢?NodeCanvas就很好,当然安装的时候是个坑,不过好处也显而易见,和H5的Canvas基本上做到了兼容,就用它就好了,事实上我是先在H5上写的差不多了,才拿到NodeJS测试运行的。

前面已经生成出渲染节点,接下来要做的就是怎么绘制了,很简单,对文档的Element节点做个遍历(广度还是深度自己思考吧),拿出渲染节点的内容,按照渲染节点的指示画就对了。

在处理渲染节点的时候,我做了一个特别的处理,我根据渲染节点计算出来的样式先生成出一个渲染命令序列,然后再真正渲染的时候将命令应用到目标Canvas的2D Context上。这个设计的初衷是让计算时候用的Canvas 2D Context和真正渲染的目标Canvas是隔离开的,如果将渲染节点和Canvas耦合到一起就会有很大的限制,比如当时需求里要输出多页的就会有问题。输出多页的有兴趣可以自己思考一下怎么做。

知识点

  • 基本布局和排版
  • CSS样式
  • DOM模型
  • 树数据结构:构建、遍历

后记

其实这篇文章酝酿了很久,里面涉及的知识点说多不多,也有一定的实用参考价值,所以就写下来了。

力图用生动的代入式的方式向大家介绍清楚内容,无奈细节实在有点多,比较难以表达,所以需要大家有一定的基础,如果感兴趣,我们可以一起讨论。如果有错误请拍砖指正。

相关推荐

驱动网卡(怎么从新驱动网卡)
驱动网卡(怎么从新驱动网卡)

网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...

2026-01-30 00:37 liuian

win10更新助手装系统(微软win10更新助手)

1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...

windows11专业版密钥最新(windows11专业版激活码永久)

 Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...

手机删过的软件恢复(手机删除过的软件怎么恢复)
手机删过的软件恢复(手机删除过的软件怎么恢复)

操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...

2026-01-29 23:55 liuian

一键ghost手动备份系统步骤(一键ghost 备份)

  步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。  步骤...

怎么创建局域网(怎么创建局域网打游戏)

  1、购买路由器一台。进入路由器把dhcp功能打开  2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。  3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...

精灵驱动器官方下载(精灵驱动手机版下载)

是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...

一键还原系统步骤(一键还原系统有哪些)

1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。  2、在弹出的“准备安装”窗口中,可...

电脑加速器哪个好(电脑加速器哪款好)

我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...

任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)

是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...

u盘怎么恢复文件(u盘文件恢复的方法)

开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...

系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)

1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...

剪贴板权限设置方法(剪贴板访问权限)
剪贴板权限设置方法(剪贴板访问权限)

1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...

2026-01-29 21:37 liuian

平板系统重装大师(平板重装win系统)

如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...

联想官网售后服务网点(联想官网售后服务热线)

联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...