你好!
今天我将向您展示如何在Flutter中创建超级服务器驱动 UI 引擎,它是超级 CMS 不可或缺的一部分(它的创建者,也就是我,就是这样定位的)。当然,您可能有不同的看法,我很乐意在评论中讨论。
本文是该系列中两篇(已经是三篇)中的第一篇。在这篇文章中,我们将直接研究 Nui,在下一篇文章中,我们将研究 Nui 与 Nanc CMS 的集成程度,在这篇文章和下一篇文章之间,还会有另一篇文章,其中包含大量有关 Nui 性能的信息。
本文将介绍很多有关服务器驱动 UI、Nui(Nanc 服务器驱动 UI)功能、项目历史、个人兴趣和奇异博士的有趣内容。哦,是的,还会有 GitHub 和 pub.dev 的链接,所以如果您喜欢它并且不介意花 1-2 分钟时间 - 我很高兴得到您的 star和点赞。
我已经写过一篇关于 Nanc 的 文章,但从那时起,已经过去了一年多的时间,该项目在功能和“完整性”方面取得了显着的进展,最重要的是 - 它已发布,并附带完整的文档,并采用 MIT 许可证。
它是一个通用的 CMS,不会拖累后端。同时,它不像 React Admin 那样需要编写大量代码才能创建某些东西。
要开始使用 Nanc,只需执行以下操作即可:
此外,第一点可以完全通过 CMS 本身的界面完成 - 也就是说,您可以通过 UI 管理数据结构。如果满足以下条件,则可以跳过第二点:
因此,在某些情况下,您无需编写一行代码即可获得用于管理任何内容和数据的 CMS。将来,这些场景的数量将会增加,比如说 - 加上 GraphQL 和 RestAPI。如果您对还可以为哪些内容实现 SDK 有什么想法 - 我将很乐意在评论中阅读您的建议。
Nanc 使用实体(又称模型)进行操作,在数据存储层级别,实体可以表示为表(SQL)或文档(No-SQL)。每个实体都有字段 - 表示 SQL 中的列,或表示 No-SQL 中的相同“字段”。
可能的字段类型之一是所谓的“屏幕”类型。也就是说,整篇文章只是 CMS 中一个字段的文本。同时,从架构上看,它看起来像这样 - 有一个完全独立的库(实际上是几个库),它们共同实现了名为 Nui 的服务器驱动 UI 引擎。此功能集成到CMS 中,在此基础上还推出了许多附加功能。
至此,我结束了直接讲述 Nanc 的介绍部分,并开始讲述 Nui 的故事。
免责声明:所有巧合都是偶然的。这个故事是虚构的。这是我梦到的。
我曾在一家大公司工作,同时开发过多个应用程序。它们大体相似,但也有很多不同之处。
但它们中完全相同的是我称之为文章引擎的东西。它由几千行(5-10-15,我记不清具体是多少了)相当杂乱的代码组成,用于处理来自后端的JSON 。这些 JSON 最终必须转换为 UI,或者更确切地说,转换为可在移动应用程序中阅读的文章。
文章是使用管理面板创建和编辑的,添加新元素的过程非常非常痛苦和漫长。看到这种恐怖,我决定提出第一个优化 - 怜悯可怜的内容管理员,并为他们实现在浏览器中、在管理面板中实时预览文章的功能。
说了这么多,也算是完成了。过了一段时间,管理面板中应用程序的一小部分开始旋转,内容管理员在预览更改方面节省了大量时间。如果以前他们必须创建一个深层链接,然后针对每个更改打开开发版本,点击此链接,等待下载,然后重复所有操作,那么现在他们只需创建文章并立即查看即可。
但我的想法并没有停止——我对这个引擎以及其他开发人员感到太恼火了,因为可以确定他们是否需要在其中添加一些东西或者只是清理奥吉亚斯的牛圈。
如果是后者,那么开发人员在会议上总是心情很好——尽管气味……相机无法捕捉到。
如果是前者,那么开发人员经常会生病、经历地震、电脑坏了、头痛、陨石撞击、晚期抑郁症或过度冷漠。
扩展引擎的功能还需要在管理面板中添加许多新字段,以便内容管理员可以使用新功能。
看到这一切,我突然想到了一个不可思议的想法:为什么不为这个问题创建一个通用的解决方案呢?一个可以防止我们不断调整和扩展管理面板和应用程序以适应每个新元素的解决方案。一个可以一劳永逸地解决问题的解决方案!然后就来了……
我想:“我可以解决这个问题。我可以为公司节省数万甚至数十万;但这个想法对公司来说太有价值了,不能简单地把它当作礼物送给别人。”
所谓礼物,是指公司的潜在价值与公司以薪水形式支付给我的金额之比相差几个数量级。这就像你在早期阶段去一家初创公司工作,但薪水比某家大公司提供的薪水要低,而且没有公司的股份。然后这家初创公司变成了独角兽,他们会告诉你——“好吧,伙计,我们付给你薪水了。”他们是对的!
我喜欢类比,但经常有人告诉我这不是我的强项。就像你是一条喜欢在海里游泳的鱼,但你却是一条淡水鱼。
然后 - 我决定在空闲时间做一个概念验证(POC),以免因为提出一些甚至可能无法实现的想法而搞砸。
最初的计划是使用现有的现成库来渲染 Markdown,但扩展其功能,以便它不仅可以渲染 Markdown 列表中的标准元素,还可以渲染更复杂的内容。文章不仅仅是带有图片的文本。还有漂亮的视觉设计、内置音频播放器等等。
从周五晚上到周一早上,我花了 40 个小时来测试这个假设 - 这个库对于新功能的可扩展性如何,总体运行情况如何,最重要的是 - 这个解决方案是否可以推翻这个臭名昭著的引擎。这个假设得到了证实 - 在将库拆解到骨架并进行一些修补后,可以通过关键字或特殊语法结构注册任何 UI 元素,所有这些都可以轻松扩展,最重要的是 - 它真的可以取代文章引擎。我花了 15 个小时才找到答案。剩下的 25 个小时我用来完成 POC。
这个想法不仅仅是用一个引擎替换另一个引擎——不。这个想法是要替换整个流程!管理面板不仅允许您创建文章,还可以管理应用程序中可见的内容。最初的想法是创建一个完整的替代品,它不会与特定项目绑定,但可以管理它。最重要的是——这个替代品还应该为这些文章提供一个方便的编辑器,以便可以创建它们并立即看到结果。
对于 POC,我认为只需制作一个编辑器就足够了。它看起来像这样:
经过 40 小时,我得到了一个可以工作的代码编辑器,它由 markdown 和一堆自定义 XML 标签(例如<container>
)的混乱混合物组成,一个实时显示此代码 UI 的预览,以及这个世界上最大的眼袋。还值得注意的是,使用的“代码编辑器”是另一个能够语法突出显示的库,但问题是它可以突出显示 markdown,也可以突出显示 XML,但大杂烩的突出显示经常中断。因此,在 40 个小时里,您可以再添加几个小时来编写一个嵌合体,它将在一个瓶子中提供两者的突出显示。是时候问了——接下来发生了什么?
接下来是演示。我召集了几位高级经理,向他们解释了我解决问题的愿景,以及我在实践中证实了这一愿景的事实,并展示了哪些方法有效、如何有效以及它有哪些可能性。
这些人喜欢这份工作。他们有利用它的愿望。但也有令人难以忍受的贪婪。我的贪婪。我不能就这样把它交给公司吗?当然不行。但我也没打算这么做。演示是一个大胆计划的一部分,我用我的手艺震惊了他们,他们根本无法抗拒,并准备满足任何条件,只是为了使用这个令人难以置信、独家和惊人的开发。我不会透露这个虚构故事的所有细节(!) ,但我只会说我想要钱。钱和假期。带薪一个月的假期,还有钱。多少钱并不重要,重要的是金额与我的工资和数字 6 相关。
但我并不是一个完全鲁莽的冒失鬼。
Dormammu,我来讨价还价了。交易如下 - 我在我的模式下工作整整两个星期(睡眠 4 小时,工作 20 小时),完成 POC,使其达到“可用于我们的应用程序目的”的状态,与此同时,我在应用程序中实现一个新功能 - 整个屏幕,使用这个超级东西(这两周原本是为此分配的)。两周结束时,我们举行了另一次演示。只是这一次我们聚集了更多的人,甚至是公司的高层管理人员,如果他们看到的东西给他们留下了深刻的印象,并且他们想要使用它 - 交易就完成了,我得到了我的愿望,公司得到了一把超级枪。如果他们不想要这些 - 我准备接受这两个星期我免费工作的事实。
好吧,我原本计划在为期一个月的假期中去乌鲁比奇旅行,但不幸的是,这次旅行从未成行。经理们不敢同意这种大胆的举动。而我,低着头,用“经典方式”去雕刻一块新屏风。但是,没有这样的故事,主角在被命运打败后,不会站起来,再次试图驯服他的野兽。
虽然没有...但好像有: 1 , 2 , 3 , 4 , 5 。
看完这些电影后,我决定这是一个信号!而且这样更好——为了些好东西而卖掉如此有前途的开发项目实在是太可惜了(我在骗谁呢??? ),我将继续进一步开发我的项目。我继续了。但周末不再是 40 个小时,每周只有 15-20 个小时,节奏相对平稳。
打破第四面墙并非易事。就像试图想出有趣的标题,让读者继续阅读并等待公司的故事如何结束一样。我将在第二篇文章中完成这个故事。现在,似乎是时候转向实现、功能能力以及所有这些了,从理论上讲,这些应该会使这篇文章更具技术性,也更能体现HackerNoon 的伟大!
首先要说的是语法,原本的大杂烩式的思路适合 POC,但实践证明 markdown 没那么简单,而且把一些原生的 markdown 元素和纯Flutter元素结合起来也不一定能达到预期的效果。
第一个问题是 - 图像是![Description](Link)
还是<image>
?
如果是第一个-我应该把一堆参数塞到哪里?
如果是第二种——那么,为什么我们有第一种呢?
第二个问题是文本。Flutter 为文本设置样式的可能性是无限的。Markdown 的可能性“一般”。是的,你可以用粗体或斜体标记文本,甚至有人想过使用这些结构**
/ __
来设置样式。然后有人想过在中间塞入<color="red">
text </color>
标签,但这种曲线和蠕动让人眼冒金星。使用某种 HTML 及其自身的边缘语法根本不可取。此外,他们的想法是,即使是没有技术知识的经理也可以编写这些代码。
我一步步去掉了嵌合体的部分,得到了一个 Markdown 超级突变体。也就是说,我们得到了一个用于渲染 Markdown 的修补库,但里面塞满了自定义标签,并且不支持 Markdown。也就是说,我们得到了 XML。
我坐下来思考并试验了还有哪些其他简单的语法。JSON 是渣渣。让一个人在扭曲的 Flutter 编辑器中编写 JSON 会得到一个想要杀死你的疯子。而且不仅仅是这样,在我看来,JSON 不适合一般人输入,尤其是对于 UI 来说——它不断向右增长,一堆强制性的""
,没有注释。YAML?嗯,也许吧。但代码也会横向爬行。有一些有趣的链接,但仅靠它们的帮助你无法取得多大成就。TOML?噗——。
好吧,我最终还是选择了 XML。在我看来,现在看来,这是一种相当“密集”的语法,非常适合 UI。毕竟,HTML 布局设计器仍然存在,而且这里的一切都会比网络上更简单(可能)。
接下来,问题出现了——如果能实现一些高亮/代码补全就好了。还有逻辑结构,有点像{{ user.name }}
。然后我开始尝试 Twig、Liquid,看了一些其他我不记得的模板引擎。但我遇到了另一个问题——在标准引擎(比如 Twig)上实现部分计划是完全有可能的,但实现所有内容肯定行不通。是的,有自动补全和高亮是件好事,但它们只会在你在标准 Twig 语法之上推出自己的新功能时才会干扰,而这对于 Flutter 来说是必需的。结果,使用 XML,一切都很顺利,使用 Twig / Liquid 的实验并没有给出任何出色的结果,在某些时候,我甚至遇到了无法实现某些功能的情况。因此,选择仍然留在 XML 上。我们将更多地讨论这些功能,但现在,让我们专注于自动补全和高亮,这在 Twig/Liquid 中非常诱人。
接下来我想说的是 Flutter 的文本输入很不合理。它们在移动端的表现很好。在桌面端的表现也很好,比如说,最多 5-10 行的高度。但是当谈到一个功能齐全的代码编辑器时,这个编辑器就是在 Flutter 中实现的——你看着它就忍不住流泪。在Trello中,我记录所有任务,并写下笔记和想法,有这样一个“任务”:
事实上,几乎从开始这个项目开始,我就一直想着用更合适的东西替换 Nui 代码编辑器。比如说 - 使用 VS Code 的开源部分嵌入一个 Web 视图。但到目前为止,我还没有做到这一点,此外,我想到了一个解决这个编辑器曲率问题的棘手但仍然有效的解决方案 - 改用您自己的开发环境。
实现方式如下 - 创建一个带有 UI 代码 (XML) 的文件,最好使用扩展名.html
/ .twig
,通过 CMS 打开同一个文件 - Web / 桌面 / 本地 / 部署 - 没关系。并通过任何 IDE 打开同一个文件,甚至通过 VS Code 的 Web 版本。瞧 - 您可以在自己喜欢的工具中编辑此文件,并在浏览器或任何地方进行实时预览。
在这种情况下,您甚至可以使用成熟的自动完成功能。在 VS Code 中,可以通过自定义 HTML 标记实现它。但是,我不使用 VS Code,我的选择是 IntelliJ IDEA,对于这个 IDE,不再有这样简单的解决方案(好吧,至少没有,或者至少我没有找到它)。但是有一个更通用的解决方案可以在那里和那里工作 - XML 模式定义(XSD)。我花了大约 3 个晚上试图弄清楚这个怪物,但从未成功,最后,我放弃了这件事,把它留给更好的时间。
有趣的是,最终,经过多次实验、更新,比如说,负责将 XML 转换为小部件的引擎,我们得到了这样一个解决方案,语言并不是特别重要。作为有关 UI 结构的信息载体,最终的选择落在了 XML 上,但同时,您可以安全地为其提供 JSON,甚至是二进制形式——编译后的 Protobuf。这将我们带到了下一个主题。
就这句话而言,本文的大小为 3218 个字。当我开始写这部分内容时,为了定性地完成所有工作,必须编写大量测试用例来比较 Nui 和常规 Flutter 的渲染性能。因为我已经实现了一个完全在 Nui 上创建的演示屏幕:
必须在本机上创建与屏幕完全匹配的内容(当然是在 Flutter 的背景下)。结果,我花了 3 个多星期的时间,大量重写同一件事,改进测试过程,并获得越来越多有趣的数字。仅此部分就超过了 3500 个字。因此,我想到有必要写一篇单独的文章,完全专门讨论 Nui 的性能(作为特例),以及如果您决定使用服务器驱动 UI 作为方法,您将不得不支付的额外费用。
但我会剧透一下:我考虑了两种主要的评估性能的场景 -初始渲染的时间。如果你决定在服务器驱动的 UI 上实现整个屏幕,并且该屏幕将在你的应用程序的某个地方打开,那么这一点很重要。
所以如果这个屏幕非常重,那么即使是原生的 Flutter 屏幕也需要很长时间才能渲染,因此当切换到这样的屏幕时,特别是如果这种过渡伴随着动画,就会出现明显的滞后。 第二种情况是具有动态 UI 变化的帧时间 (FPS) 。数据发生了变化——你需要重新绘制一些组件。问题是这会对渲染时间产生多大影响,是否会影响到屏幕更新时用户会看到滞后。 这里还有另一个剧透——在大多数情况下,你无法分辨出你看到的屏幕是否完全在 Nui 上实现。如果你将 Nui 小部件嵌入到常规的原生 Flutter 屏幕中(比如,应用程序中应该非常动态地变化的屏幕的某个区域)——你肯定无法识别这一点。当然,性能会有所下降。但它们是如此之大,以至于即使在 120FPS 的帧速率下也不会影响 FPS——也就是说,一帧的时间几乎永远不会超过8ms
。对于第二种情况也是如此。至于第一个 - 这一切都取决于屏幕的复杂程度。但即使在这里,差异也不会影响感知,也不会让您的应用程序成为用户智能手机的基准。
下面是来自 Pixel 7a (Tensor G2) 的三段屏幕录像,屏幕刷新率设置为 90 帧(此设备的最大刷新率),视频录制速率为每秒 60 帧(录制设置的最大刷新率)。每隔 500ms,列表中元素的位置就会随机化,从中构建前 3 张卡片的数据,再过 500ms,顺序状态就会切换到下一个。你能猜出这些屏幕中的哪一个是完全在 Nui 上实现的吗?
PS 图像的加载时间与实现无关,因为在此屏幕上,无论使用哪种实现,都有很多 Svg 图像 - 所有图标以及品牌徽标。所有 svg(以及常规图片)都存储在 GitHub 上作为托管,因此它们的加载速度会非常慢,这在一些实验中有所体现。
YouTube:
在创建 Nui 时,我坚持以下理念——必须创建这样一种工具,首先,Flutter 开发人员会发现它与创建常规 Flutter 应用程序一样易于使用。因此,命名所有组件的方法很简单——以与 Flutter 中命名相同的方式命名它们。
这同样适用于窗口小部件参数 - 标量,如String
, int
, double
, enum
等,作为参数,它们本身并未配置。 Nui 中的这些类型的参数称为参数。 而对于复杂的类参数,如Container
小部件中的decoration
,称为属性。 这个规则并不是绝对的,因为有些属性太冗长,所以它们的名称已经简化。 此外,对于某些窗口小部件,可用参数列表已经扩展。 例如 - 要制作一个正方形的SizedBox
或Container
,您只能传递一个自定义参数size
,而不是两个相同的width
+ height
。
我不会给出已实现的小部件的完整列表,因为它们的数量相当多(目前有 53 个)。简而言之 - 您可以实现几乎任何 UI,原则上可以使用服务器驱动 UI 作为方法。包括与Slivers
相关的复杂滚动效果。
此外,关于组件,值得注意的是您必须将云 XML 代码传递给的入口点或小部件。目前有两个这样的小部件 - NuiListWidget
和NuiStackWidget
。
第一个,按照设计,应该在需要实现整个屏幕时使用。在底层,它是一个CustomScrollView
,包含将从原始标记代码解析的所有小部件。此外,有人可能会说解析是“智能的”:由于CustomScrollView
的内容应该是slivers
,因此可能的解决方案是将流中的每个小部件包装在SliverToBoxAdapter
中,但这会对性能产生极其不利的影响。因此,小部件嵌入到它们的父级中,如下所示 - 从第一个开始,我们沿着列表向下移动,直到遇到真正的sliver
。一旦遇到sliver
- 我们将所有先前的小部件添加到SliverList
,并将其添加到父级CustomScrollView
。因此,渲染整个 UI 的性能将尽可能高,因为slivers
的数量将最少。为什么CustomScrollView
中有很多slivers
不好?答案就在这里。
第二个小部件 - NuiStackWidget
也可以用作全屏 - 在这种情况下,值得记住的是,您创建的所有内容都将以相同的顺序嵌入到Stack
中。并且还需要明确使用slivers
- 也就是说,如果您想要slivers
列表 - 您必须添加CustomScrollView
并在其中实现列表。
第二种情况是实现一个可以嵌入到本机组件中的小部件。比如说,制作一个产品卡,该卡将完全由服务器主动定制。这似乎是一个非常有趣的场景,您可以使用 Nui 实现组件库中的所有组件,并将它们用作常规小部件。同时,总是有机会在不更新应用程序的情况下完全更改它们。
值得注意的是, NuiListWidget
也可以作为局部 Widget 来使用,而不是整个屏幕,但是对于这个 Widget,您将需要应用适当的限制,例如为父 Widget 设置明确的高度。
如果使用 Flutter 创建一个counter app
它将如下所示:
import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Demo App'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <text size="32"> {{ page.counter }} </text> </column> </center> ''', pageData: { 'counter': _counter, }, ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
下面是另一个例子,完全基于 Nui(包括逻辑):
import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } final DataStorage globalDataStorage = DataStorage(data: {'counter': 0}); final EventHandler counterHandler = EventHandler( test: (BuildContext context, Event event) => event.event == 'increment', handler: (BuildContext context, Event event) => globalDataStorage.updateValue( 'counter', (globalDataStorage.getTypedValue<int>(query: 'counter') ?? 0) + 1, ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return DataStorageProvider( dataStorage: globalDataStorage, child: EventDelegate( handlers: [ counterHandler, ], child: MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Counter'), ), ), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="FF000000" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> ''', pageData: {}, ), ), ); } }
分离 UI 代码以便突出显示:
<center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="black" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned>
还有交互式的综合文档,其中显示了每个小部件具有的参数和属性的详细信息,以及它们的所有可能值。对于每个属性(也可以同时具有参数和其他属性),也有文档,其中完整演示了所有可用值。除此之外,每个组件都包含一个交互式示例,您可以在其中实时查看此小部件的实现,并根据需要对其进行更改。
Nui 与 Nanc CMS 紧密集成。您不必使用 Nanc 即可使用 Nui,但使用 Nanc 可以为您带来优势,即 - 相同的交互式文档,以及 Playground,您可以在其中实时查看布局结果,使用将在其中使用的数据。此外,您无需创建自己的 CMS 本地版本,您可以使用已发布的演示进行管理,在其中您可以做您需要做的一切。
您可以通过以下链接执行此操作,然后单击Page Interface / Screen
字段。打开的屏幕可以用作游乐场,通过单击同步按钮,您可以通过包含源的文件将 Nanc 与您的 IDE 同步,并且可以通过单击帮助按钮获取所有文档。
PS 这些复杂性的存在是因为我从来没有找到时间制作一个明确的单独页面来记录 Nanc 中的组件,以及无法插入到此页面的直接链接。
创建一个普通的从 XML 到小部件的映射器是毫无意义的。当然,这也很有用,但用例会少得多。不是一回事 - 完全交互式的组件和屏幕,您可以与之交互,您可以对其进行细粒度更新(也就是说,不是一次性更新 - 而是分部分更新)。此外,此 UI 需要数据。考虑到短语 Server-Driven UI 中字母S的存在,可以直接将其替换为服务器上的布局,但您也可以做得更漂亮。并且不必为 UI 中的每次更改从后端拖动布局的新部分(Nui 不是将 jQuery 的最佳实践带到 Flutter 的时光机)。
让我们从逻辑开始:变量和计算表达式可以替换到布局中。假设一个小部件定义为<container color="{{ page.background }}">
将直接从传递给存储在background
变量中的“父上下文”的数据中提取其颜色。而<aspectRatio ratio="{{ 3 / 4}}">
将为其后代设置相应的纵横比值。有内置函数、比较等可用于构建具有某些逻辑的 UI。
第二点是模板化。您可以使用<template id="your_component_name"/>
标记直接在 UI 代码中定义自己的小部件。同时,此模板的所有内部组件都可以访问传递给此模板的参数,这将允许灵活地参数化自定义组件,然后使用<component id="your_component_name"/>
/> 标记重用它们。在模板内部,您不仅可以传递属性,还可以传递其他标记/小部件,这使得创建任何复杂程度的可重用组件成为可能。
第三点 - “for 循环”。在 Nui 中,有一个内置的<for>
标记,允许您使用迭代多次渲染相同(或多个)组件。当您需要从一组数据中创建小部件的列表/行/列时,这很方便。
第四,条件渲染。在布局级别,实现了<show>
标签(有人将其称为<if>
),它允许您绘制嵌套组件,或者在各种条件下根本不将它们嵌入树中。
要点五 - 动作。一些用户可以与之交互的组件可以发送事件。您可以完全按照自己的意愿控制这些事件。假设<inkWell onPressed="something">
- 通过这样的声明,此小部件将变为可点击的,并且您的应用程序或更确切地说是某个EventHandler
将能够处理此事件并执行某些操作。这个想法是,与逻辑相关的所有内容都应该直接在应用程序中实现,但您可以实现任何事情。制作一些可以处理动作组的通用处理程序,例如“转到屏幕”/“调用方法”/“发送分析事件”。也有计划实现动态代码,但这里有细微差别。对于 Dart,有方法可以执行任意代码,但这会影响性能,此外,此代码与应用程序代码的互操作性几乎不是 100%。也就是说,通过在此动态代码中创建逻辑,您将不断遇到一些限制。因此,需要非常仔细地制定这种机制,才能真正适用和有用。
第六点是本地 UI 更新。这要归功于<dataBuilder>
标签。此标签(底层是 Bloc)可以“查看”特定字段,当该字段发生变化时,它将重新绘制其子树。
最初,我遵循了两种数据存储方式——上面提到的“父上下文”。以及“数据”——可以使用<data>
标签直接在 UI 中定义的数据。说实话,我现在记不清为什么需要实现两种存储和将数据传输到 UI 的方式,但我无法因为这样的决定而严厉批评自己。
它们的工作方式如下 - “父上下文”是Map<String, dynamic>
类型的对象,直接传递给NuiListWidget
/ NuiStackWidget
小部件。可以通过前缀page
访问此数据:
<someWidget value="{{ page.your.field }}"/>
您可以引用任何内容,任何深度,包括数组 - {{ page.some.array.0.users.35.age }}
。如果没有这样的键/值,您将得到null
。可以使用<for>
迭代列表。
第二种方式 - “数据” 是全局数据存储。实际上,这是位于树中比NuiListWidget
/ NuiStackWidget
更高的某个Bloc
。同时,没有什么可以阻止以本地风格组织它们的使用,通过DataStorageProvider
传递您自己的DataStorage
实例。
同时,第一种方法不是反应式的 - 也就是说,当page
中的数据发生变化时,UI 不会自行更新。因为这实际上只是StatelessWidget
的参数。如果page
的数据源是您自己的 Bloc,它将为Nui...Widget
提供一组值 - 那么,与常规StatelessWidget
一样,它将使用新数据完全重新绘制。
处理数据的第二种方式是响应式的。如果你使用此类的 API( updateValue
方法)更改DataStorage
中的数据,那么这将调用 Bloc 类的emit
方法,并且如果你的 UI 中有此数据的活动侦听器( <dataBuilder>
标签),那么它们的内容将相应地更改,但 UI 的其余部分将不受影响。
这样,我们得到了两个潜在的数据源——一个非常简单的page
,和一个反应性data
。除了这些源中更新数据的逻辑和 UI 对这些更新的反应之外,它们之间没有任何区别。
我故意没有描述这项工作的所有细节和方面,因为这样它就会变成现有文档的副本。因此,如果您有兴趣尝试或只是了解更多信息 - 欢迎来这里。如果工作的任何方面不清楚或文档未涵盖某些内容,那么我会很荣幸收到您指出问题的消息:
我将简要列出一些本文未涉及但您可以使用的功能:
创建您自己的标签/组件,并能够为它们创建完全相同的交互式文档,就像为它们的参数和属性创建实时预览一样。例如,这就是渲染SVG 图像的组件的实现方式。没有必要将其推入引擎的核心,因为不是每个人都需要它,但作为扩展,只需传递一个变量即可使用 - 简单易行。直接 -实现的示例。
内置的庞大图标库,可以通过添加自己的图标进行扩展(这里我表现得前后矛盾,而且“推诿”,逻辑是让尽可能多的图标可以立即使用,无需更新应用程序即可使用新图标)。开箱即用: fluentui_system_icons 、 material_design_icons_flutter和remixicon 。您可以使用Nanc 、 Page Interface / Screen -> Icons
查看所有可用图标
自定义字体,包括现成的 Google 字体
将 XML 转换为 JSON/protobuf 并将其用作 UI 的“源”
所有这些以及更多内容都可以在文档中进行研究。
最主要的是要研究出动态执行带有逻辑的代码的可能性。这是一个非常酷的功能,它将允许您非常认真地扩展 Nui 的功能。此外,您可以(并且应该)从标准 Flutter 库中添加其余很少使用但有时非常重要的小部件。掌握 XSD,以便 IDE 中出现所有标签的自动完成(有一个想法是直接从标签文档生成此方案,然后很容易为自定义小部件创建它并且它将始终是最新的,还有一个想法是在 Dart 中生成 DSL,然后可以将其转换为 XML/Json/Protobuf)。好吧,还有额外的性能优化——现在还不错,非常不错,但它可以更好,甚至更接近原生 Flutter。
我说的就这么多,下一篇文章我会很详细的讲述Nui的性能,我是如何创建测试用例的,在这个过程中我走过了多少个rake,在哪些场景下能得到什么样的数字。
如果您有兴趣尝试 Nui 或更好地了解它 - 请前往文档台。另外,如果它不难,请在GitHub上加星标并在pub.dev上点赞 - 这对你来说并不难,但对我这个在这艘大船上孤独的划船者来说 - 它非常有用。