序言
iVX是一个高度灵活的应用开发工具,实现同一个功能,有各种不同的实现方法。为了提高项目在一个团队内部的可读性与可维护性,并确保项目运行时的性能,我们总结了30+个移动端项目,提出了一套最佳实践方案,帮助大家以最优化的结构开始搭建项目。
一、 框架
一个项目的推荐架构,详见视频:框架说明
项目框架的核心要点如下:
1.使用页面作为项目构成的基本单元,即每个独立的UI场景构成一个页面。注意,尽管使用if条件容器也可以实现UI场景的切换,但为了能够让项目结构更加清晰,我们还是推荐使用页面作为基本场景容器,而if条件容器,可用于页面内小部分UI内容切换。
2. 页面内的核心UI模块,即负责承载内容的顶层组件与小模块,不超过7个,否则,使用小模块来对UI模块进行封装。比如,可将多个输入组件合并成一个多属性的输入小模块。减少顶层UI模块的数量,有助于每个页面的结构更容易理解与维护,让页面结构“一目了然”。
3. UI模块之间的交互,使用逻辑处理器(即动作组)进行封装。逻辑处理器的包括页面内逻辑处理器,以及跨页逻辑处理器,我们使用以下命名规则对逻辑处理器进行命名:
a) 页面内逻辑器:【前】A->B/C 切换详情,其中:
1) 【前】/【后】代表该逻辑处理器是否仅涉及前端逻辑,或涉及到后台数据(包括后台服务,api以及socket连接);
2) A代表触发逻辑处理器的UI模块,B与C代表对逻辑处理器做出响应的UI模块,即逻辑处理过程中涉及的目标对象;
3) “获取详情”,代表该逻辑处理器的功能简介。
b)跨页逻辑处理器:【后】A页->B页提交成功跳转,其中:
1) 【前】/【后】代表该逻辑处理器是否仅涉及前端逻辑,或涉及到后台数据(包括后台服务,api以及socket连接);
2) A页代表发起逻辑处理器的UI模块所在页面,B页代表逻辑处理完毕后跳转至的页面;
3) “提交成功跳转”,代表该页面跳转的功能简介。
4. 尽量将前端状态变量封装至小模块中,减少全局变量与页面级变量的使用。通常情况下,仅以下几种类型的数据或变量,我们建议以全局或页面变量的形式进行存储,而不是存储在小模块的属性中。
1) 全案例通用的数据,比如当前用户信息,当前系统时间等,以及案例通用的静态配置字典,比如状态-颜色字典等,才作为全局数据变量进行存储。
2) 页面内UI模块使用到的后台数据,即从服务或API获取到的原始数据,使用页面级变量进行存储,并在小模块中的相应属性中进行绑定。
3) 需要提交至后台的数据,比如一个申请表单信息,使用页面级或全局变量进行存储(仅当一个申请表单涉及多个页面时才使用全局变量)。
二、 静态UI
搭建静态UI时,我们主要秉承以下两个原则:
1. 尽量减少容器组件使用数量与层级
2. 尽量使用绝对定位容器来替代相对定位容器(即行、列)
以上两点,根本目的是为了提高项目运行性能,并让UI布局更简单易懂。
我们先来看第1点。我们在对象树中添加的任意一个UI对象,包括行、列、绝对定位容器等布局容器,在项目运行时都会被创建为一个实体的元素(即网页中的div),每个实体元素的创建与维护,都需要占用客户端的cpu与内存资源,如果实体元素过多,则会影响到项目性能。
因此,我们切记不要将布局容器作为规划UI界面的“参考线”,任意一个不必要的布局容器都会增加页面渲染的时间,尽管对于大多数页面,添加几个布局容器的渲染时间极短而不会被感知到,但在项目结构复杂,或列表数量庞大时,就会明显影响到项目运行性能了。同时,使用过多的布局容器,会造成对象树层级过深,必须通过层层展开,才能完全理解项目结构,提高了项目的维护难度。
再来看第2点。行列等相对定位布局容器,使用到了flex系列的css属性,属于高级的css属性,在对象渲染时相对其他基础属性会消耗更多性能。
另外,相对定位布局容器,其主要作用是为了制作响应式的UI,即在PC端不同浏览器窗口大小时,提供自动适配的UI布局。在手机端,由于用户无法随意拖拽改变浏览器,而ivx的项目会自动在各个手机设备上进行全屏宽适配,相对布局容器在手机端的作用便远不如在pc端那么明显了。
因此,在大多数情况下,我们其实可以使用绝对定位容器作为手机端布局的基础容器,而仅在需要使用自动高度,或自动宽度布局时,才借助相对定位容器。
根据我们总结的移动端项目,以下两种情况是最常见的可使用相对布局容器的场景:
最后,我们举一个简单的例子来说明移动端布局的优化:
三、 小模块
小模块是构建项目的基础UI元素,其基本概念请见:小模块的使用
如上图所示,小模块通过属性、方法以及事件来与外部进行交互,分别对应于小模块的公共数据、公共方法以及自定义事件,这些配置也成为了小模块功能的核心。针对以上小模块对外的的接口配置,我们提出以下建议:
1. 尽最大可能减少公共数据的数量,即仅需要通过外部设置的属性,才设为公共数据。公共数据与小模块内部变量,在小模块内部引用时并没有区别,但公共数据会自动绑定小模块的相应属性,一旦设置相应属性,该公共数据就无法通过内部的函数来进行赋值了,除非通过双向绑定数据来更新相应属性(在第3点中我们会说明,双向绑定数据也是需要尽量避免的)。因此,公共数据在使用上相比内部变量,会有很大的局限性,且每个公共数据都会形成一个新的小模块属性,导致小模块属性列表过长,因此,在不必要的情况下,我们尽量不要使用公共数据。
比如,一个多选列表小模块,其列表内容需要通过外部数据来设置,即将外部数据列表设为公共数据,但用户选择的选项信息(一个选项数组),是通过用户选择设置的,并非通过小模块外部数据来设置,因此,我们不要把用户选择的选项数组设为公共数据,而是作为自定义事件的参数抛出来。
2. 使用对象数组来作为列表类小模块的列表内容数据来源,并添加“数据ID”列,作为这个对象数组的第一列。对象数组相比二维数组,可以根据列的名称来绑定数据,无需规定数据来源的顺序,因此用户也能够更加容易设置这个列表内容的属性。另外,由于列表内容通常来自于数据库,每行数据都有自带的“数据ID”字段,这个字段可以作为该列表项的独立唯一标识,在列表项被点击或选中的时候作为一个天然的ID标识抛出来。
3. 尽量避免使用双向绑定的公共数据。双向绑定的公共数据,即小模块的某个属性,可以通过外部来设置,也可以通过小模块内部检测到的用户行为来改变。
比如,一个输入类的小模块,外部可以设置输入的默认值,而用户也可以在小模块中任意输入值。此时,我们可以将“输入值”作为小模块的一个公共数据,将内部输入的值绑定为该公共数据,并在用户输入改变时,将当前输入值设置至该公共数据,同时“更新当前数据至小模块”。此时,小模块的“输入值”属性,即成为了双向绑定的数据,类似普通输入框组件的“值”。但双向绑定的数据,会造成小模块的二次渲染,即用户输入值,会让小模块第一次渲染,而将输入值“更新至小模块”,即改变了小模块的“输入值”属性,又会造成小模块第二次渲染。
二次渲染会影响项目性能,因此,通常情况下,仅当我们制作输入类小模块(表单填写,选择模块,开关模块等),且该小模块需要通过外部属性来设置其默认值时,才使用双向绑定的公共数据。
4. 尽量避免使用公共方法。一个小模块,可以通过属性和方法来改变其UI状态,但属性和方法的适配范围是不同的。属性可以在任意时刻设置,因为属性是作为小模块的外部变量存储在项目中的,因此即使小模块尚未渲染出来,设置属性也是生效的。比如,我们可以先设置一个小模块的属性,然后再跳转至该小模块所在的页面。相反,方法必须要在小模块渲染完成之后才可以调用,小模块的渲染需要时间,因此,我们先跳转至小模块所在页面,然后立刻调用其方法也是不生效的,比如以下情况:
由于公共方法的这些局限性,我们尽量减少使用公共方法来设置 小模块内部的一些变量,而是直接使用设置属性来完成。(注意,这里属性与方法的区别,对普通组件也是适用的。)
四、 后台服务
后台服务的构建包括两部分,一是后台服务本身的设计,二是前端调用后台服务的方式。
我们先来看后台服务本身的设计。在ivx后台中定义的服务,其本质上就是我们为自己的项目制作的API,或者REST接口。我们在定义服务时,需要尽量考虑到服务整体架构的清晰整洁,以及前台使用服务时的便利性,为此,我们提出以下建议:
1. 不要设计“万能服务”,也不要为每一个前端调用场景来专门设计一个服务。
所谓的“万能服务”,即通过服务的参数来区分服务的功能,前端可以通过调用一个服务来完成所有的功能需求,比如,创建一个“用户服务”,将注册,删除,屏蔽,修改用户信息等,通过一个“服务类型”的入参来指定。尽管这样的设计可以减少后台服务的数量,但会使服务的定义过于冗长,不易理解,且由于参数过多而增加前端调用的难度。
“万能服务”的另一个极端,是每个前端场景都调用一个服务,比如,前端用户可以点击自己的头像来替换头像,也可以在用户信息页,通过修改修改用户信息来整体更新包括头像在内的用户信息。我们不需要为这两个场景做两个服务,即“更新头像服务”与“整体更新信息服务”,而是统一的使用“更新用户信息”这个服务,前端可以通过不同的更新参数来满足以上两个场景的需求。
因此,我们在设计服务时,需要在以上两个极端间找到一个平衡点,以下原则可以作为参考:
a) 针对不同数据库表的操作,做成不同的服务;
b) 针对同一个或同一组数据库表的读取类和写入类的操作,做成不同的服务,即“获取用户列表”与“更新用户信息”应该是两个独立的服务;
c) 针对同一个或同一组数据库表的不同读取类操作,可以做成一个服务,通过参数来区分读取的方式,比如不同的筛选条件,比如分页读取等等
d) 针对同一个或同一组数据库表的写入类操作,根据场景来判断是否做成一个或多个服务,比如,注册用户与更新用户信息,都是对用户表的写入操作,但使用场景非常不同,可以做成是两个服务,但添加或更新标签,两者都是在前端更新数据库的标签列表,调用的场景是一致的,可以做成是一个服务,然后根据当前数据库中是否有用户指定的标签,来决定是插入一条数据,还是更新原有数据。
2. 将针对同一资源的服务通过对象容器归类,所谓同一资源即同一个或一组数据库表,比如所有针对用户表的服务,包括注册、更新用户信息,禁用/解禁当前用户,可以归类至“用户服务”容器。
3. 将管理员使用的服务与普通用户使用的服务隔离开。许多应用会涉及用户端和管理端,即管理员有一个专门的界面来管理用户,设置网站banner,添加优惠信息,管理商品等等。尽管客户端和管理端使用的数据库是一套的,但由于客户端和管理端的权限非常不同,处于安全考虑,我们需要严格避免使用一个服务的不同参数来区分管理员端与客户端的请求,比如,判断管理员参数是否为1,来决定是否返回某些用户敏感信息。
接下来我们来看前端对后台服务的调用。这里我们只要遵守两个原则:
1. 一次性不要请求超过30条数据,特别是针对前端列表内容的请求,如果超过30条,则使用分页,否则会延长后台服务请求时间,并影响前端渲染性能。
2. 一次事件触发,尽量只触发一个服务。如果有特殊情况,比如,前台初始化时,需要从多个服务获取项目各个部分的初始化数据,一次性请求服务的数量也不要超过3个。
3. 除非是一次性的数据库操作,比如数据库输出,提交,或电商组件的购买等服务,不要直接在前端对后台数据库(包括普通数据库,用户库,电商库等)进行操作。
和3.0相比,4.0的后台数据库相关操作,全部通过服务进行,这是为了提供给我们自定义后台逻辑的能力,并保证了更好的安全性。但4.0我们依然可以直接在前端调用数据库,如下图:
但请注意,这种直接在前端调用数据库的操作,其实是工具提供的一种捷径,当我们在前端调用数据库时,工具会自动生成一个相应的服务,因此本质上和我们在后台自定义服务的操作是一致的。这种捷径操作,可以在我们一次性的数据库操作时使用,但如果我们需要对操作的结果进行判断,然后再对另一个数据库进行操作,比如,判断投票流水数据库里是否有当前用户的投票记录,如果有,就返回已投票,如果没有,就插入一条记录,并更新投票总表数据库。以上判断逻辑,要绝对避免在前端直接操作,因为这样会造成客户端过多的请求(记住,每次前端对数据库的操作都是一次服务的调用),同时,会由于我们把处理逻辑放在前端,而降低案例的安全性。
因此,非一次性的数据库操作,全部使用后台的自定义服务进行封装,不要在前端直接对数据库进行访问。
五、 数据库结构设计
iVX中的数据库服务,使用经典mysql数据库,我们在后台创建的每个db表格,都对应于mysql库中的一个表格。对于经典mysql数据库的结构设计,已有相当成熟的规范,详见:数据库结构设计优化