前言

最近做了一个需求:自定义首页。

用户或运营可以自己修改首页的布局,做到千人千面。

这个需求类似于当年的自定义QQ空间,不过怕是年轻一些的没玩过这个东西。

所以你也可以简单理解为是博客园的皮肤,只是不能写样式和代码,但是可以调整各个组件的布局。

明确需求

这并不是一个低代码页面设计器,不是给程序员用的。

只是一个自定义布局的页面而已,是面向客户或者运营的。

如果遇到类似的需求,大家可以针对具体的业务需求,从而一步步明确用户到底需要改变哪些布局属性。

强调这一点的必要在于,需求每明确和精简一次,都会极大简化后面的设计和开发。

可能用户不需要细致到像素级的宽高,而仅仅就是简单的几个字——对齐和能换位置,这在后续实现上有很大差别。

技术最终还是要用来解决问题,而不是创造问题。

给用户过多的自由度,给他一大堆设置选项,除了增加开发周期,更多地只会让他茫然无措。

不懂代码的用户甚至根本不会也不愿意使用你做的这个东西。

所以这个自定义布局页面最核心的一点在于:理解简单,操作简单。

整体流程图

我们将需求简化一下,做成一个如下的流程图:

我们需要一个设计器,去设计自定义布局的页面,设计完成后会将这个数据保存在服务端。

同时首页再从服务端获取数据,用一个渲染器去渲染页面。

按照这个思路,我们可以独立开发渲染器和设计器,只要保证两者之间的数据一致即可。

后续如果有业务变更,需要替换设计器和渲染器,也可以分开替换,而不需要同时替换。

不好的案例

四五年前我其实做过一个类似的需求。

当时的做法是:设计时记录下各个组件的宽高信息和样式,然后首页渲染时,根据相应的信息直接调整各个组件的位置,宽高和样式。

这种做法无疑是可以实现的,但是业务组件和布局信息耦合在了一起,新增业务组件或者调整UI时工作量很大。

对用户而言,调整布局时,需要自行确认宽高,不断进行适配,碰到需要适配不同分辨率屏幕的情况,不懂代码的用户可能心态直接就崩了。

而且,使用这种方式,在后续的业务迭代中,改动这块代码很容易使用户出现线上样式问题,导致开发根本就不敢动这块代码。

简易渲染器结构

为了解决上述问题,关键就在于将布局组件与业务组件分离。

我将所渲染的首页区域,作为一个容器组件。

然后将里面的各个内容区域组件分为布局组件和业务组件,两者完全分离后再由用户进行绑定。

这样在渲染的时候我们只用关心渲染简单的布局组件就行了,布局组件内部各种复杂的业务组件及其逻辑与我们毫无关系。

为了简单,布局组件推荐直接采用Ant Design的24列栅格设计,使用RowCol进行布局,并不需要设定宽高,只用设定布局组件占几列就行了。

简易设计器结构

设计器分为两个区域:预览区和属性区。

左侧为预览区域:

有且仅有一个容器组件,右侧有一个新增按钮,点击后在下方新增一个布局组件。

布局组件可通过拖拽进行排序,这里我通过react-sortable-hoc来实现,技术细节无需赘述。

右侧为属性区域:

点击容器组件后,右侧属性区域显示容器组件的属性,比如各块之间的间距和整体的背景图。

点击布局组件后,右侧属性区域显示布局组件的属性,比如在栅格系统中占多少列,绑定哪个业务组件。

想象一下我们生成的数据结构,用TypeScript可以简化为:

    // 容器组件属性
    interface IContainerInfo{
        //...各种属性
    }

    // 布局组件属性
    interface ILayoutInfo{
        //...各种属性
    }

    // 最终数据结构
    interface IData{
        container:IContainerInfo
        layouts:ILayoutInfo[]
    }

至于组件的排序,用layouts数组的索引即可。

复杂布局与树

上面的布局设定针对大多数简单布局而言,绰绰有余。

如果你的需求比较简单,比如移动端之类的首页布局,这个完全足够了。

但是对稍微复杂一点的布局而言,上面的方案是有问题的,比如下面这种布局:

使用一个RowCol很难实现上面这个布局。

如果要实现,我们需要使用设计器生成下面的结构:

<Row>
    <Col span={12}>
        <Row>
            <Col span={24}>组件A</Col>
            <Col span={24}>组件B</Col>
        </Row>
    </Col>
    <Col span={12}>
        组件C
    </Col>
</Row>

实际上这就是一个树形结构:

容器组件作为唯一根节点,无需改动。

之前绑定业务组件的布局组件,都可以理解为叶子节点,也无需改动。

唯一需要引入的是树枝节点。

为了渲染器能够渲染出目标结果,我们可以修改一下原先的数据结构为:

    // 容器组件属性
    interface IContainerInfo{
        //...各种属性
    }

    // 布局组件属性
    interface ILayoutInfo{
        //...各种属性
        isLeaf:boolean
        children:ILayoutInfo[]
    }

    // 最终数据结构
    interface IData{
        container:IContainerInfo
        layouts:ILayoutInfo[]
    }

可以看到我们给布局组件引入了两个属性

  • isLeaf 是否为叶子节点
  • children 如果是树枝节点,那么children下面为一个布局组件属性数组

既然数据结构确定,反推到我们的设计器,那么就是加一个是否为叶子节点的复选框属性。

勾选该属性,为叶子节点,那么展示绑定业务组件的属性。

不勾选该属性,为树枝节点,展示子组件数量这个属性。

各位可能很疑惑为什么是子组件数量这个数字类型,而不是children之类的数据格式。

实际上,用户可以简单设定子组件数量,来设定树枝节点下子组件的个数,然后再点击左侧的子组件来进行进一步的设置。

这样在操作上来讲,会简单很多。

当然,实际开发中考虑的也需要更多,比如子组件数字增大,需要插入新的子组件,数字变小,需要删除子组件,设计器保存和加载时子组件数量这个属性与children这个属性的互相转换。

细节实现

上面的方案应该是可以满足绝大多数情况了,但是您可能在实际开发过程中遇到以下细节问题:

设计器中节点定位

在设计器中,点击左侧布局组件,在右侧展示属性这一功能,有些朋友可能会存在一点困惑:如何获取当前组件在树形结构中的位置?

我这里实现时,是给每一个节点给了一个code,这个code是从根节点到当前节点的index数组,用字符分隔后得到的字符串。

在点击布局组件时,可以在当前节点上拿到这个字符串,然后快速还原成index数组,从而在树形结构中存取数据。

设计器中业务组件的预览

组件预览的这块,在绑定业务组件后,不必直接加载业务组件来展示。

因为这里只调整布局,不需要业务组件内部的逻辑。

所以我推荐这里用一张图片,或者一些简化后的组件代替即可。

如非必要,设计器这里的预览业务组件和业务组件应该完全分离,用code关联即可,否则代码的可读性和可维护性都会受到挑战。

渲染器中业务组件隐藏的情况

通常我们可能会遇到一些隐藏业务组件的需求,比如如果是管理员,那么就在首页展示这个组件,否则隐藏。

我们通过上述手段实现的布局中,如果业务组件隐藏,包裹它的布局组件是不会隐藏的。

所呈现的效果就是,这个隐藏的业务组件附近,块之间的间隔会比正常间隔大。

解决这个问题,可以使用下面三种方法:

  • 方法一(不推荐):取消块间隔这一属性,组件的间隔由业务组件实现。这样最简单,但是业务组件承担了过多的布局任务。
  • 方法二(不推荐):在业务组件中判定需要隐藏后,触发布局组件传递进来的回调函数,从而隐藏布局组件。但是这种子组件控制父组件的方式并不好,可读性差。而且业务组件应该与布局组件解耦,不能直接控制布局组件。另外这种控制方法,对于隐藏操作而言只能生效一次。
  • 方法三(推荐):在根节点组件中,设置控制逻辑。传递一个操作信息数组到每个子节点,如果布局组件的业务组件Code在这个操作信息数组中存在,并且操作为隐藏,那么就隐藏该布局组件。

推荐使用上述方法三,可以在渲染器上保持业务逻辑和布局逻辑始终分离开来,并且不仅仅能用于隐藏信息,还能用于其它的组件联动操作。

总结

以上便是我自己对这样一个自定义布局页面的思考和实现。

因为是公司的项目,所以无法开源。

但是如果参照上面的思路,实现起来应该也只是填充一些技术细节就可以了,希望对您起到帮助作用。

如果您有更好的方案,也希望不吝赐教。

posted on 2021-12-30 09:00  韩子卢  阅读(2773)  评论(1编辑  收藏  举报