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

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

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

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

背景

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

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

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

后记

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

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

相关推荐

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网络爬虫的问题,提问截图如下:登录请求地址是这个:二、实现过程这里【甯同学】给了一个提示,如下所示:估计很多小伙伴和...