前言
UI(User Interface),即用户界面,是软件和用户之间进行交互和信息交换的媒介,实现信息的内部形式与人类可接受形式间的转换。UI开发一般需要经过UI设计、UI实现两个过程。UI设计是对软件的交互、操作逻辑、界面的设计,通常由UI设计师和交互设计师按照用户对软件的需求完成一套UI界面的设计,并最终以UI设计稿的形式呈现(psd、png、jpeg文件等)。UI实现是对UI设计阶段产生的UI设计稿进行编码实现,这部分是前端工程师的任务。
随着互联网的快速发展,从最早只有简单的超文本文档内容,逐渐发展成丰富多彩的灵活动态体验平台,各种手机App,PC端应用和网站更是多得迎接不暇。用户从最早只注重软件功能的实现,到如今不仅需要软件功能实现,还对软件整体UI界面非常挑剔。目前软件为满足用户的审美,软件UI被设计的越来越复杂,无论是布局还是元素样式,前端开发起来越来越费劲,开发成本越来越高,并且对于大量需要快速上线的页面,没有足够的人力物力去开发。
在字节跳动直播活动中台-前端的业务中,经常需要开发多个平台的活动页面。而活动页面通常布局、逻辑相似、需求频率高且需要快速迭代。如果使用常规的开发方式去开发一个活动页面,需要产品、前端、服务端、测试等多方参与,并且每一个活动页面上线周期长,无法快速响应产品的需求。对于活动页面开发, 较优的流程是使用页面可视化搭建平台来实现,即直播活动中台的魔方平台。平台基于DOM实现了一个组件化的UI编辑器,并且提供封装良好的UI组件供运营同学使用,以此完成一个活动页面。从以前需要4人天完成活动页面的开发,到2小时就能拖拽出一个活动页面并且上线,极大的提高了页面开发效率。
但魔方平台也有一定的局限性,由于只需要针对活动相关业务,因此平台只能适用于活动页面的生成。通过拓展JSON来定义schema的形式描述一个编辑的UI页面,而基于JSON的schema描述能力有限,只能通过对应的client端去解析schema来还原UI页面,并且不能适用到其他平台。
因此基于魔方平台提出了更通用的UI编辑App,将拖拽出来的页面使用更加通用的DSL来描述,并能将DSL代码编译到各平台代码。类似于阿里Imgcook,基于WebGL实现UI编辑器,基于DSL编译到多端代码,提升UI开发效率。
运行效果展示&所用技术
运行效果展示
主页面:左侧提供基础组件,中间则是使用WebGL实现的UI编辑器,右侧实现对选中的UI组件的属性修改
代码编译:将当前UI页面生成到目标代码,并导出相应的代码文件
DSL编辑页面:提供DSL代码的编辑,并生成到UI页面
所用技术
一般的拖拽式UI生成平台会做成一个网站,本文则是尝试将其实现为一个Electron App。
-
Electron: Electron是使用Web前端技术(HTML/CSS/JavaScript/React等)来创建原生跨平台桌面应用程序的框架。可以使用
electron-react-boilerplate
模版快速使用React去开发,但本文则是使用手动搭建React环境,使用Webpack、Electron-builder完成资源打包和App构建,参考文章:使用Webpack/React去打包构建Electron应用。 -
Node.js:Node.js是一个开源、跨平台、基于Chrome V8引擎的JavaScript运行时,可以让JavaScript运行在服务端环境下。Node.js采用单线程、异步非阻塞IO、事件驱动架构,使得Node.js在处理IO密集型任务时效率极高。
-
React:React是一个用于构建Web UI的JavaScript库,允许开发者以数据驱动、组件化、声明式的方式编写UI。
-
WebGL:是一种在Web端运行的3D绘图协议,这种绘图协议把JavaScript和OpenGL ES2.0结合起来,提供硬件加速3D渲染并借助显卡来在浏览器里渲染3D场景和模型。WebGL技术的诞生解决了现有的Web 3D渲染的两个关键问题:1.跨平台,使用原生的canvas标签即可实现3D渲染。2.渲染效率高,图形的渲染基于底层的硬件加速实现。
-
Konva:一个基于Canvas开发的2D JavaScript库,可以轻松的用于实现桌面应用和移动应用的图形交互效果,可以高效实现动画、变换、节点嵌套、局部操作、滤镜、缓存、事件等功能。Konva最大的特点是图形可交互,Konva的所有的图形都可以监听事件,实现类似于原生DOM的交互方式。事件监听是在层(
Konva.Layer
)的基础上实现的,每一个层有一个用于显示图形的前台渲染器和用于监听事件的后台渲染器,通过在后台渲染器中注册全局事件来判断当前触发事件的图形,并调用处理事件的回调。Konva很大程度上借鉴了浏览器的DOM,比如Konva通过定义舞台(Konva.Stage
)来存储所有图形,类似于html
标签,定义层来显示图形,类似于body
标签。其中的节点嵌套、事件监听、节点查找等等也借鉴了DOM操作,这使得前端开发者可以很快速的上手Konva框架。
应用设计
需求分析
App核心功能包括WebGL UI编辑器和DSL代码编辑器以及DSL代码编译器,系统功能需求如下图。
-
基础功能:系统需要实现基础的登录注册功能、登出功能、全局快捷键绑定等功能。
-
UI编辑器:可视化WebGL UI编辑器,提供基础的通用UI组件库,允许用户通过拖拽基础的通用UI组件库的组件来绘制一个UI页面;提供组件工具栏,允许用户对画布上的组件进行复制、删除、粘贴、重做等操作。提供组件的属性面板,允许用户对组件的背景、边框、位置、大小等属性进行修改;提供DSL代码构建工具栏,允许用户将画布上的UI页面生成到DSL代码,进而编译DSL代码到目标平台代码。
-
DSL代码编辑器:提供一个编写DSL代码的编辑器,支持代码高亮、复制、粘贴、保存等功能。提供文件系统,允许用户新建、删除一个DSL代码文件;提供代码运行工具,将DSL代码生成到UI页面或者生成到目标代码。
-
帮助中心:DSL代码语法帮助、UI编辑器使用帮助。
整体架构设计
系统采用Client/Server模式进行架构,前后端分离方式开发,Client端为Electron App,服务端则使用Express实现。
-
Client端,采用Electron、React、Node.js来实现一个跨平台的PC端App。
-
Server端,基于Node.js Express编写的服务端,并暴露出相应的API供Client端调用。集成WebSocket服务,独立运行在Node.js侧,共享相应的数据库连接等公共类和函数,提供Socket支持。并基于Niginx搭建一个静态资源服务器,提供图片等文件的存储服务。
-
数据库使用MySQL/MongoDB数据库,MongoDB存储UI页面信息,比如UI元素位置、大小、样式等信息,以及其他类JSON形式的信息。MySQL存储用户信息、组件信息等一些基础信息。
Client端架构设计
Client端是一个PC端应用,采用Electron技术进行开发。Electron虽然是使用前端技术来创建跨平台应用的框架,但又与传统的网站开发方式不一样。Electron基于主从进程模型,即由一个主进程和多个渲染进程组成,进程之间使用IPC进行通信。基于这种进程模型,对系统进程进行功能划分:
- 主进程负责进程间通信、窗口管理、服务端请求和native C++插件加载
- 渲染进程只负责Web页面的渲染和具体的业务逻辑
渲染进程使用Typescript/React/Redux开发,借助React Hooks可以更好的将通用UI逻辑抽离,提高代码复用率。主进程使用Typescript/C++开发,其中C++开发Node.js插件并打包成.node
文件,主进程加载.node
文件从而调用到C++代码。借助Webpack编译工具,将渲染进程所有代码编译为index.html
、renderer.js
、style.css
并进行代码压缩和代码分割优化,提高代码运行效率。主进程所有代码编译只编译为一个main.js
,并在main.js
中加载渲染进程的index.html
完成整个系统的运行。最后再利用electron-builder
将编译后的主进程代码和渲染进程代码以及其他资源文件打包成一个.dmg
应用文件,完成整个系统的构建。
主进程设计
Client端主进程可分为三部分模块:widget模块、services模块、compile模块。
-
Widget模块负责窗口创建和管理,比如创建login窗口,实现最小化、关闭login窗口等IPC调用。
-
Services模块负责提供系统基础服务,包括IPC调用服务,用于渲染进程与主进程之间的通信;fetch服务,提供后端接口调用能力;session服务,存储用户session,记录登录等信息;socket服务,提供后端socket连接;fileSave服务,提供文件保存功能。
-
Compile模块负责执行DSL代码编译,通过实现多种编译器来实现多平台代码构建。
渲染进程设计
在渲染进程打包过程中,采用多页面打包设计,将部分UI页面从一个渲染进程中分离,设计成多个独立的新窗口(渲染进程),开发时在每个渲染进程中都注入模块热更新代码实现开发环境页面热更新。在Webpack的entry字段中添加多个页面入口实现独立打包,并且每个打包页面使用HtmlWebpackPlugin插件生成对应的HTML文件。主进程实例化一个独立窗口加载对应页面打包后的index.html
完成一个新窗口的创建。
在多个窗口中,主窗口是系统最核心的窗口,实现的模块和功能相对复杂,使用React Hooks开发的组件避免不了相互通信,故使用采用Redux进行全局状态管理,优化组件间的通信流程。
在Redux的工作流中,将state提取到Redux状态树store中存储,通过dispatch
action进入reducer
去更新state
,更新完state后触发一次React render去更新视图。设计Redux状态树的关键点在于抽离组件状态,将多个组件依赖的状态抽离到Redux状态树中,并在组件使用useSelector
Hooks订阅状态树中的某个状态,使用useDispatch
获取dispatch
去更新Redux状态树中的某个状态。
在主窗口渲染进程中,包括Redux模块、Page模块、Components模块、WebGL模块。