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

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

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

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

背景

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

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

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

后记

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

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

相关推荐

怎么把电脑c盘格式化(怎么把c盘格式化吗)

1.在进行c盘格式化之前,必须备份重要的数据,以免数据丢失。2.在格式化c盘之前,确保所有的应用程序和系统文件都已经备份。否则,在进行格式化之后,将需要重新安装操作系统和所有的应用程序。3.如果...

中国移动赠送的wifi6路由器(192.168.10.1中国移动路由器)

移动tclwifi6路由器很好的。WiFi6路由器远强于WiFi5路由器,值得购买性价比不错。家庭生活中网速慢最主要的原因并非速率,而是穿墙效果。坐在离路由器很近的地方,手机网速一般不会有问题。...

office2010如何激活免费使用
  • office2010如何激活免费使用
  • office2010如何激活免费使用
  • office2010如何激活免费使用
  • office2010如何激活免费使用
win官网网址(win官方网站)

具体方法如下:打开电脑,在浏览器地址栏输入window,就会进入win中文官网,然后再寻找自己需要的软件,升级包等就可以了。下面是windows官网网址:1http://www.microsoft.c...

电脑的任务栏不见了怎么恢复
  • 电脑的任务栏不见了怎么恢复
  • 电脑的任务栏不见了怎么恢复
  • 电脑的任务栏不见了怎么恢复
  • 电脑的任务栏不见了怎么恢复
wifi网址登录入口(wifi网址登录入口有哪些)

wifi网页登录入口是网关地址192.168.1.1.1.首先检查路由器线路连接(参考前面手机设置路由器部分)2.将电脑Tnternet协议(TCP/IP)属性设置为“自动获取IP地址”和“自...

没有驱动程序怎么安装打印机

(1)启动电脑进入Windows操作系统,在桌面上单击开始-设置-打印机。(2)用鼠标右单击安装好的打印机图标,在弹出的右键菜单中选择属性,然后单击打印测试页按钮。(3)此时打印机会打出一页信息,从这...

u盘怎么取消写保护状态(u盘如何解除写保护状态)

如果您的U盘被设置为写保护,您可以尝试以下方法来关闭写保护:1.检查U盘上的物理开关:一些U盘上可能有一个物理开关,用于启用或禁用写保护。请检查U盘的外壳,看看是否有这样的开关,并将其切换到未锁定的...

压缩文件查看器(压缩文件查看器密码是多少)

1,打开手机上面的文件管理器,找到要压缩的WPS文件。2,长按一下WPS文件,然后选择要压缩的文件。3,点击右下角的【更多】,选择【压缩】。4,对压缩文件进行保存,压缩完成。扩展资料:wps产品特点1...

键盘哪个是截图键(键盘中的截图键是哪一个)

1、按Prtsc键截图这样获取的是整个电脑屏幕的内容,按Prtsc键后,可以直接打开画图工具,接粘贴使用。也可以粘贴在QQ聊天框或者Word文档中,之后再选择保存即可。2、按Ctrl+Prtsc键截图...

flash插件电脑版下载(flash插件下载安装)

可以不安装,不安装对电脑也不会有什么影响。友情提示,最好安装,这个也不会占用你多少内存,它是用来播放网页中的flash文件的。如果你不安装,网页中的flash动画就不能正常播放。浏览器也会提示你安装!...

如何打开注册表管理器(如何打开注册表管理器权限)
  • 如何打开注册表管理器(如何打开注册表管理器权限)
  • 如何打开注册表管理器(如何打开注册表管理器权限)
  • 如何打开注册表管理器(如何打开注册表管理器权限)
  • 如何打开注册表管理器(如何打开注册表管理器权限)
foxmail邮箱怎么设置(foxmail邮箱设置成功后点完成没反应)

操作步骤/方法1.打开新建界面:2.打开foxmail,在上方导航栏处找到“邮箱(B)”点开此功能,会看到一个下拉菜单,在下拉菜单中找到“新建邮箱账户(N)”。3.建立账户信息:4.点击“新建邮箱账...

电脑自动关机解决办法(电脑自动关机,原来是这里出了问题)

电脑自动关机的原因一、系统文件损坏一个完整的系统受到袭击之后,电脑就不能进行初始化,从而引起自动关机,这也是一个常见的原因。可以选择重装系统的方法来解决问题。电脑自动关机的原因二、CPU太热这是电脑自...

m2固态硬盘安装系统教程(m2固态如何装系统)

加装m.2固态硬盘后,重装系统的操作步骤如下:1、下载U盘启动盘制作工具,下载一个GHOST版最新的WIN7,准备一个足够大的U盘(16G足够了),用U盘启动盘制作工具将其制作成启动U盘;2、插入新电...