6 分钟阅读

背景

目前,在互联网公司工作的前端工程师,除了开发面向外部用户(C端)的需求外,还要开发很多的内部系统给内部用户使用,比如支持运营、公共技术、算法等部门日常运作的项目平台。随着产品规模和项目数量的扩大,这样的内部系统也越来越多,几乎每一个小的领域方向都需要一个平台来支撑。本文把这种内部平台叫做 CMS 系统,即 Content Management System,全称为“内容管理系统”。

一般来说,整个研发部门的 CMS 系统,它们的样式风格会保持一致,由专业的交互和视觉设计师定义好设计规范,前端工程师根据设计规范实现好 UI 组件库,一个研发团队只需要一套设计规范,没有精力也没有必要维护多套规范,团队 leader 也不会允许多套规范并存,不然团队资源会有巨大的浪费。有了事先实现好的 UI 组件库,项目就像搭积木一样,绝大多数的功能都可以使用组件拼出来,从而提升了前端团队的研发质量和效率。

不管是什么样的产品,只要是在浏览器中运行的,基本控件都是一样的,比如下拉菜单 Select、按钮 Button、表格 Table等等,它们已经被 Web 组织标准化。除了标准控件外,还有和业务相关的组件,比如电商、金融、音乐等产品有它们特定的业务,通用功能就可以抽象成组件方便复用。

既然基本控件是通用的,所以有些有实力的大公司会投入开发资源来开发这样的组件库,并且会开源给外部公司使用,比如蚂蚁金服的 React 组件库 Ant Design,饿了么的 Vue 组件库 Element,等等。可以说正是有了这样优秀的 UI 组件库,整个前端行业的研发质量和效率都有一定的提升。现在越来越多的前端团队都已经意识到重复造轮子是没有意义的,不应该为了 KPI 而去做一些对团队无用的甚至是拖后退的工作,应该把精力投入到其他关键地方。

上述 UI 组件库中的 Select、Button、Table 组件,并不是浏览器原生的控件,而是模拟实现的控件,没做过前端开发的朋友可能不理解为什么要这么做?这是因为这些原生控件在不同的浏览器中的交互行为和样式都不大一样,前端工程师在日常开发经常被其他人指出浏览器兼容性问题,使用模拟实现的方式,在所有浏览器中尽可能地表现一致。

那么,有了 UI 组件库,前端工程师的开发质量和效率问题就不用担心了吗?

现状

实际情况并非如此,比如我现在所在的团队,要开发和维护的这种 CMS 系统至少有 20个 以上,毫不夸张的说,往团队里面再加 5个人 也会瞬间被消耗完。因为前端工程师的数量在整个研发部门中所占的比例不会很高,是后端工程师数量的五分之一是很常见的,同时还需要支持运营、算法、质量、公共技术等部门的需求,因为这些部门都需要提效的工具平台。

现在整个研发行业似乎有一个非常奇怪的现象,后端工程师几乎都不会页面开发(有也是极个别的),而前端团队自己的内部工具平台,后端代码也由前端编写,不管是用 Node.js 还是其他服务端语言。正由于存在这种怪现象,越发显得前端开发资源的珍贵,得出“不应该在这种内部 CMS 系统”投入大量前端资源的结论是非常自然的。

那如何解决这个问题呢?

方案

很多 CMS 系统有一个共同特点,就是它们的业务逻辑比较简单,就是对一些数据模型的增删改查,比如商品、歌曲、歌单、评论等等,稍微复杂的会有一些图表展示。如果在这些系统中投入过多的开发资源,对整个研发组织来说是无法接受的。

前面已经说过,后端工程师的数量是前端工程师的数倍之多,所以自然会想到应该把部分的页面开发工作交给后端。先不说后端愿不愿意开发页面,就现在的前端开发方式,想不经过培训就上手是非常困难的。有些人可能会不同意我的观点,因为这和现在流行的全栈工程师说法是违背的。不过我至今没听过国内有哪个公司的研发部门在推全栈工程师,也就是一个项目从头至尾由一个人完成。

现在见的比较多的做法是,前端工程师会研发一些工具,使用这些工具,可以减少很多重复工作,比如工程环境搭建、开发工具配置、页面模板等,让使用者只要关注具体的业务逻辑即可。由于有了标准的 UI 组件库,很多功能实现就是在配置一些参数,加上现在事实上已经统治了前端开发方式的数据驱动视图的开发理念,最让后端工程师头疼的 CSS 几乎不用关心,掌握基本的 JavaScript 语法就能把一张页面实现出来,JavaScript 毕竟是一门编程语言,和 Java 等其他后端语言没有本质区别,学起来并不难。而 HTML 的语法也非常简单,本身就是一种 XML 的变体。有了这样的工具,后端开发页面才有可能。

我们来看几个这样的工具:

  • 饿了么 duang,一种基于配置自动生成 CMS 的工具。可以理解为整个项目就是一个组件,可以通过约定的配置开发出来,这些配置是经过高度抽象的。
  • 百度 amis,一种基于特定 JSON 格式生成 MIS 页面的工具,功能和饿了么的类似。(官网并没有介绍 MIS 是什么意思,猜测是 Management Information System 的缩写,也就是管理信息系统)
  • 阿里飞冰,简单而友好的前端研发体系。它的功能相对来说比较丰富,是一个客户端可视化工具(也可以在 Web 页面中操作),还提供了很多项目模板。

上面三个工具都使用 Node.js 开发,这意味着要使用这些工具,在本地电脑上至少要安装 Node.js 运行环境,环境问题是编程开发中一个让人头疼的问题,出了问题后就算是专业人员也需要相当多的排查时间。

其实不光是环境,使用这些工具来开发项目,和普通的前端开发没有本质的区别,它们只是让开发特定形式的项目变得容易,如果需求有变化,更多时候是无能为力的。我们来回顾前端工程师开发项目的完整流程:

  • 创建代码仓库,将代码拉取到本地。
  • 使用脚手架工具(比如上面介绍的三种工具)生成项目的初始目录结构。
  • 开发前端相关的业务功能。
  • 和后端联调接口。
  • 申请测试机器部署代码:
    • 可能需要申请多个权限工单,工单还需要等待上级主管审批。
    • 可能需要申请域名。
    • 可能需要更改 Nginx 配置。

部署可以被工具平台取代,比如云音乐自主研发的静态部署平台,可以节省很多时间。

上面所介绍的工具,只解决了部分环节的效率问题。我们前面一直在回避一个问题,就是这些工具开发出来是给谁用的?它们的官方文档中都没有介绍目标用户是谁,我们先来看看它们的 issue:

从这些 issue 的描述来看,猜测基本上还是前端工程师在使用这些工具,这是一个比较有趣的现象。相信工具的开发者们并不会限制目标用户:

  • 对于前端工程师,可以节省很多重复劳动。
  • 对于后端工程师,只要按照教程按部就班地操作,就能把一个简单的 CMS 系统开发出来。

现在世面上开源的工具平台,做法都比较相似,都是尽可能地将本地开发变得简单,通过配置或者可视化的方法来生成页面。

那有没有其他方案呢?

CloudCMS

既然一个项目可以通过配置生成出来,那其他事情为什么还需要开发者关心呢?配置文件其实就是一个 JSON 字符串,可以是本地文件,也可以通过远程加载。所以我们可以做一个在线平台,在这个平台上用户只需要填写配置,平台在线完成解析并生成项目的访问地址,这就是 CloudCMS 要实现的功能。

近几个月以来,我一直在研发这个 CloudCMS 平台,并且已经取得了一定的成效,已经有了可以运行的 DEMO。顾名思义,它是把 CMS 的实现过程放到了云端,用户不用再关心其他诸如本地开发环境、部署等事情,只要在线填写配置就可以生成项目,就这么简单。

先来看一下已经实现的效果:

dashboard

这是项目的 Dashboard 页面,展示用户的项目列表,可以点击“新建项目”来创建项目。项目卡片中的四个图标分别为预览、管理、编辑和删除。

dashboard_create_project

点击“新建项目”按钮显示创建项目弹窗。

admin

这是第一个项目的管理页面。

admin_com_edit

这是点击第一个组件的“编辑”链接弹出的内容,也就是该组件的配置。

上面演示的所有页面和功能,本身也是通过 JSON 配置生成出来的。下面来讲解一下具体的实现过程。

前面已经讲过,CMS 系统的开发离不开 UI 组件库,CloudCMS 也一样,用户填写的配置其实就是传递给 UI 组件的配置,因为技术栈选择了 React,所以选择了 Ant Design。CMS 页面的布局大致是固定的,用户不用关心:头部是页面导航,左侧是菜单列表,中间是具体内容。

在项目的管理界面中,我们看到左侧的菜单列表有四项:组件、工具函数、页面、菜单。其中页面和菜单就是分别来定义上面所说的头部页面和左侧菜单,一般这两项内容比较少,把它们放在底部。组件使用最频繁,所有的业务功能都是靠配置组件实现的,所以把它放在第一位。下面介绍一下工具函数。

在通过配置生成页面的工具中,有一个比较经典的问题是,如何实现下拉菜单的联动?比如省市区的三级联动菜单,大家可以想像用 JSON 来描述应该如何描述。再比如,在提交表单前,要进行验证,由于实际需求的不可预见性,验证功能也不可能仅仅靠 JSON 就能描述清楚。

CloudCMS 采用的是工具函数的思路,也就是让用户可以自己编写 JavaScript 来实现一些自定义功能。这意味着平台需要执行用户编写的 JavaScript 脚本,关于如何执行脚本请参考这篇文章的讲解:如何安全地运行用户的 JavaScript 脚本,脚本执行是放在页面中的 iframe 里面的 Web Worker 中执行的,这是目前我所知道的在浏览器中最安全的执行脚本的方法,这个执行过程是异步的,这个异步特性也给 CloudCMS 的实现带来了不少麻烦,我也尝试用过一些同步执行的 JavaScript 解析器库,但都不够完美,最终还是放弃同步执行的思路。

前面已经介绍过,目前数据驱动视图的理念在前端开发中占了统治地位,想要改变页面中某个按钮的显示或者隐藏,并不需要去查找这个按钮的 DOM 元素然后设置它的 style 属性,而是使用一个状态变量来控制,比如使用 visible 变量,当它为 true 时显示按钮,为 false 时隐藏按钮,具体的显示隐藏实现交给框架,比如 React。

CloudCMS 也基于数据驱动视图的理念,整个项目的数据都由一个单一的数据源在控制。联动的下拉菜单可以理解为由多个独立的下拉菜单组件组成,每个下拉菜单组件都可以配置一个 change 选项,它的值是一个字符串,是工具函数的名称。在更改菜单选项的时候,会触发 change 事件,在该事件中去执行工具函数,将一些必需的数据传入,在工具函数中,它可以改变项目的任意数据,比如另外一个下拉菜单组件的选项值,然后将改变后的项目数据返回,系统再将新数据和真正的项目数据进行合并,从而改变了另外一个下拉菜单的选项,这就实现了联动功能。

工具函数只需要更改项目的数据,不需要操作 DOM,只要掌握最基本的 JavaScript 语法,不用学习 DOM 和 CSS 知识,给 CMS 系统的开发又降低了难度。

cloudcms_architecture

下面我们看一下配置写法。

CloudCMS 配置写法

比如 CloudCMS 的项目管理界面,有两张页面,分别是:项目资源项目设置,所以需要创建两个页面资源,比如 项目资源 页面的配置写法如下:

admin_create_page

页面的配置有两个字段, url 表示页面的 URL,menus 表示该页面中菜单的 URL,它的值是一个数组,数组项是菜单的 URL,而没有使用名称,这是考虑到菜单的名称有可能是相同的,但 URL 肯定是唯一的。

菜单的配置写法也一样,比如 /resource/components 菜单的配置为:

{
  "url": "/resource/components",
  "icon": "block",
  "Layout": {
    "Content": {
      "Rows": [
        {
          "props": {
            "style": {
              "marginBottom": "12px"
            }
          },
          "Cols": [
            {
              "props": {
                "span": 24,
                "style": {
                  "textAlign": "right"
                }
              },
              "Button": "CreateComponentButton",
              "Modals": [
                "CreateComponentModal",
                "EditComponentModal"
              ]
            }
          ]
        },
        {
          "Cols": [
            {
              "Table": "ComponentTable"
            }
          ]
        }
      ]
    }
  }
}

这里需要声明一下,CloudCMS 中的管理页面,几乎都没有用到工具函数的功能,就已经实现了新建删除编辑列表等最常见的增删改查功能。

url 是菜单 URL,icon 是菜单图标,Layout 表示菜单对应页面的内容布局,背后调用的是 Ant Design 中的布局组件 LayoutContentRow(s)Col(s)ButtonModal(s)Table 都是 Ant Design 组件,它们的值都是传递给组件的配置,如果组件的配置值是字符串,则具体的配置需要到该值对应的组件中去查找,比如 CreateComponentButton,它对应的配置写法如下:

{
  "props": {
    "text": "新建组件",
    "type": "primary"
  },
  "options": {
    "onClick": {
      "state": {
        "CreateComponentModal": {
          "props": {
            "visible": true
          }
        }
      }
    }
  }
}

组件的配置只有两个字段,props 的值会原封不动地传递给 Ant Design 组件,options 的值由 CloudCMS 平台解析。我们没有支持直接在 props 字段编写 onClick 选项,不管是函数代码还是 JSON,配置内容是通过异步请求从服务器加载的,拿到的都是字符串,想要把字符串转换成函数而保持其中的逻辑不变,可控性太差,几乎是不太可能实现的。在 CloudCMS 中,如果组件的配置值是一个函数的话,都不能通过 props 传入,需要写在 options 字段,通过平台的约定写法进行配置。

上面的配置,应该不难猜测,它表示在点击这个按钮的时候,将 CreateComponentModalprops 配置中的 visible 更改为 true,翻译成交互就是:点击“新建组件”按钮,显示“新建组件”模态框:

admin_create_page

关于 CloudCMS 的介绍和配置写法就讲到这里。下面来看一些大家可能会比较关心的问题,它们是我在研究 CloudCMS 期间被问得最多的问题。

CloudCMS 常见问题

CloudCMS 的目标用户是谁,需要掌握什么技能?

  • 目标用户是懂 JSON 语法的任何用户,比如后端开发、QA、前端开发等。
  • 技能要求
    • 懂 JSON 语法。
    • 懂基本的页面布局知识,明白页面是由一个个组件模块组成的。
    • 懂基本的 JavaScript 编程知识,就能完成复杂的交互功能。

CloudCMS 如何使用,有教程吗?

有的,对平台的所有介绍和配置说明,有单独的文档工程,后续也会考虑开发其他形式的教程:

docs

本文只介绍了前端功能的开发,那它是如何跟服务器交互的?

全部使用异步接口。和实际 CMS 项目的服务器交互,有两种方式,一种是前端直接请求服务器,二是 CloudCMS 后端转发请求。直接通过前端转发,需要配置接口跨域,为了简单起见,CloudCMS 选择了第二种方式。值得注意的是,这里面有一个隐含逻辑,就是 CloudCMS 的权限和实际 CMS 项目的权限是打通的,由于是同个团队的项目,做到这一点并不难。

配置如果写错了,该怎么排查错误?

这是个好问题。如果配置写错了,排查起来确实很麻烦,这是 CloudCSM 必须要解决的难题,目前的做法是尽可能地将错误原因清晰地提示给用户。

这是一个在线平台,其他团队或者公司不可能使用,因为 CMS 中都是非常机密的数据。

CloudCMS 在开发完成后会选择开源,在自己的团队或者公司部署一套就可以了。

小结

本文花了很大的篇幅介绍了 CMS 研发中的一些实际问题,不了解这些背景知识就不会明白为什么世面上会有这么多通过配置化生成 CMS 系统的工具,而且还有大量的公司在自主研发这样的工具。随后演示了我最近几个月一直在研究的 CloudCMS 以及已经取得的一些成果,希望可以引起大家的兴趣。

留下评论