实现一个类Web的布局引擎 实现一个类web的布局引擎有哪些
liuian 2024-12-18 15:36 27 浏览
一看标题,懂的人都懂,确实挺唬人的,没办法,这是个人的习惯,喜欢小小的吹一下,如果大佬们对内容有什么看法可以共同探讨,如果有错误的地方还请多多指教。闲言少叙(少逼逼,大家烦得慌),进入正题。
背景
背景很重要,需要介绍一下为什么要做这件事情,这样对大家才有借鉴的意义。
最近公司的产品要实现一个功能,就是把系统产生的一些文字的对话信息生成图片,至于作用就不说了,反正是这么一个需求吧。我估计应该不少人做过类似的需求吧。
我估摸着就是画点文字到画布上这么简单吧,结果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模型
- 树数据结构:构建、遍历
后记
其实这篇文章酝酿了很久,里面涉及的知识点说多不多,也有一定的实用参考价值,所以就写下来了。
力图用生动的代入式的方式向大家介绍清楚内容,无奈细节实在有点多,比较难以表达,所以需要大家有一定的基础,如果感兴趣,我们可以一起讨论。如果有错误请拍砖指正。
相关推荐
- Docker 47 个常见故障的原因和解决方法
-
【作者】曹如熙,具有超过十年的互联网运维及五年以上团队管理经验,多年容器云的运维,尤其在Docker和kubernetes领域非常精通。Docker是一种相对使用较简单的容器,我们可以通过以下几种方式...
- 电脑30个快问快答,解决常见电脑问题
-
1.强行关机/停电对电脑有影响吗?答:可能损坏硬盘(机械硬盘风险高)、未保存数据丢失,偶尔一次影响小,但频繁操作会缩短硬件寿命。2.C盘满影响速度吗?答:会!系统运行需C盘空间缓存临时数据,空间不...
- 使用Tcpdump包抓取分析数据包的详细用法
-
TcpDump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。tcpdump就是一种...
- 电脑启动不了(BootDevice Not Found Hard Disk-3F0)解决方案
-
HP品牌机,开机启动不了,黑屏,开机取下主板电池恢复BIOS后,开机显示找不到启动盘。一、按F2键进入BIOS,出现硬盘内存检测界面的话,直接退出。就会出现这个界面,光标键向下,选择BIOSSetu...
- 电脑开机黑屏别慌!快码住!起底维修老师傅不能说的秘密
-
按下开机键却只收获黑屏大礼包?那些神秘的英文提示、刺耳的蜂鸣声,其实是电脑在给你发送求救信号!从按下电源到进入桌面的12秒里,你的电脑经历了史诗级的硬件自检与系统加载,今天我们就破译这段“摩斯电码”。...
- 电脑启动故障为何总要先看BIOS?新手必读的关键知识解析
-
最近在帮朋友们解答电脑无法正常开机的问题时,发现大家经常收到一句高频建议:“先检查BIOS”。对不少普通用户而言,BIOS依然是个神秘的存在。那么,BIOS到底是什么?电脑出现哪些故障会与它相关呢?本...
- Windows 11 KB5053598更新:安全补丁还是系统噩梦?
-
2025年3月11日,微软发布了Windows1124H2的强制性更新KB5053598,作为“周二补丁日”(PatchTuesday)的一部分。然而,这款本应提升系统安全性的更新却引发了广泛的...
- 飞牛OS入门安装遇到问题,如何解决?
-
之前小编尝试了用旧电脑装飞牛OS安装之前特意查了一些硬件要求飞牛OS目前支持主流的x86架构硬件主机需能连网线飞牛OS暂时不支持只有无线网卡的安装貌似很多小伙伴在一开始安装就卡住了那今天咱们汇总分...
- 几种常见的电脑开机黑屏显示白色英文字母解决方法
-
当电脑开机出现黑屏并显示白色英文字母时,通常表示系统启动过程中遇到了错误。以下是几种常见原因及对应的解决方法,按照排查顺序整理:一、检查外接设备与硬件连接可能原因:外接U盘、移动硬盘等未拔出,或内部硬...
- 电脑启动出现问题,为什么都要先检查BIOS?
-
【ZOL中关村在线原创技巧应用】最近在回答问题的时候,总会发现很多朋友都在问“电脑无法正常开机怎么办?”这样类似的问题,而许多DIY大佬的回复总会出现一条高频建议“先检查BIOS”。但对于许多普通用户...
- 教你怎么用JavaScript检测当前浏览器是无头浏览器
-
什么是无头浏览器(headlessbrowser)?无头浏览器是指可以在图形界面情况下运行的浏览器。我可以通过编程来控制无头浏览器自动执行各种任务,比如做测试,给网页截屏等。为什么叫“无头”浏览器?...
- 12个高效的Python爬虫框架,你用过几个?
-
实现爬虫技术的编程环境有很多种,Java、Python、C++等都可以用来爬虫。但很多人选择Python来写爬虫,为什么呢?因为Python确实很适合做爬虫,丰富的第三方库十分强大,简单几行代码便可实...
- 运维的报表之路,用 node.js 轻松发送 grafana 报表
-
在运维过程中,无论是监控还是报表,都会有一些通过邮件发送图表的需求,由于开源的zabbix,grafana和kibana等并不完全具有“想发送哪儿就发送哪儿”的图片生成功能,在grafana...
- C#基于浏览器内核的高级爬虫(c#爬取网页内容)
-
基于C#.NET+PhantomJS+Sellenium的高级网络爬虫程序。可执行Javascript代码、触发各类事件、操纵页面Dom结构、甚至可以移除不喜欢的CSS样式。很多网站都用Ajax动态加...
- 如何优化一个秒杀项目?(秒杀实现思路)
-
问题1:使用jmeter性能压测,定位瓶颈代码步骤流程:线程组--->Http请求--->查看结果树--->聚合报告tips:host的文件--->优先调用映射,减少DNS的时...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
python使用fitz模块提取pdf中的图片
-
《人人译客》如何规划你的移动电商网站(2)
-
Jupyterhub安装教程 jupyter怎么安装包
-
- 最近发表
- 标签列表
-
- 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)
- table.render (33)
- uniapp textarea (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- react-admin (33)
- vscode切换git分支 (35)
- vscode美化代码 (33)
- python bytes转16进制 (35)