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

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

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

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

背景

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

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

我估摸着就是画点文字到画布上这么简单吧,结果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模型
  • 树数据结构:构建、遍历

后记

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

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

相关推荐

如何修改文件(如何修改文件创建时间)

工具/原料电脑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无线路由器设置

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...

电脑软件下载网址(电脑软件下载网址排行)
  • 电脑软件下载网址(电脑软件下载网址排行)
  • 电脑软件下载网址(电脑软件下载网址排行)
  • 电脑软件下载网址(电脑软件下载网址排行)
  • 电脑软件下载网址(电脑软件下载网址排行)