干货 | 提升前端开发效率,携程机票定制代码生成器实践 - 携程技术

作者简介

Zilin Wang,资深前端开发工程师,擅长前端打杂,专注于Remix、Radix UI、Haskell等领域。

Sheila Dai,资深前端开发工程师,关注前端性能优化、前端智能化。

在日常的研发工作中,编写前端界面结构占据了一部分工作量。很多UI组件都存在共性,如何减少编写UI界面的开发时间,以及提取公用的前端组件,从而达到提升研发效能的目的,是我们的重要课题。

《前端智能化探索,骨架屏低代码自动生成方案实践》中,我们曾经探索过一种自动生成骨架屏代码的方案,在此基础上,我们设计了一套代码生成器的定制流程,到达可以定制任意目的代码的效果。本文将围绕视觉稿生成任意代码,探讨代码生成器的原理与细节,最后是落地的效果展示。

一、背景

骨架屏代码自动生成,作为一个最小 MVP (Minimum Viable Product),验证了视觉稿自动生成代码的可行性与实用性。

那么,除了高度重复性质的骨架屏以外,还能够实现哪些通用组件?甚至能不能实现应用的业务组件?

如图是视觉稿生成代码的粗略步骤,我们可以看到,在提取视觉稿信息后使用通用算法,得到中间代码后,可以生成我们的目的代码。在这个步骤中,通过编辑中间代码,我们可以自动生成不同的目的代码。

我们的预期则是把这一步骤,交付给业务研发自己来实现,通过在 D2C 平台上插入不同的代码生成器,实时自动生成需要的目的代码。

在整个页面的转换结果上,页面上的每个组件都可以通过选择不同的代码生成器来得到相应的结果。

二、问题分析


本方案是在骨架屏探索成果上的拓展设计,我们面临的问题主要有以下三个:

1)平台角度:如何让生成器可高度定制?

我们首先需要解决的问题,是让业务研发可以自行定义代码生成器。那么我们需要把中间代码层从封闭的平台中剥离出来,变成一个开放的插件入口。

2)生成器作者角度:如何快速上手编写?

虽然针对一个指定目标代码结果的生成器,只需要一次中间代码的编写,即可作为一个公开插件在平台上提供给其他研发进行使用。但这个中间代码的编写过程依然存在一定的门槛,会让想要使用的人望而却步。我们需要降低这个门槛,让业务研发可以随时发布或者调整自己的代码生成器。

3)普通使用者角度:如何零成本使用已有生成器?

如果平台上提供的生成器,已经满足使用者的需求了,那么他可以在不学习相关知识的前提下进行一键生成代码使用。除此之外,我们需要在平台上新增一些功能,以便让使用者在这个过程中可以更加顺畅地进行预期的自动代码生成。

三、解决方案


为了解决上文提到的三个问题,我们从三个方向去解决这些问题:

  • 中间代码插件化
  • 生成器编写模版化
  • D2C 平台优化

3.1 中间代码插件化

在自动生成代码的流程中,我们需要把生成器这部分从封闭的平台中剥离出来,提供给业务研发进行自研。我们主要借助了 UIDL (Universal Interface Definition Language)结构,依据这个 UI 层面的数据结构解析得到我们需要的目的代码内容。

1 ) UIDL 简介

UIDL,即通用界面定义语言,一种用于描述 Web、移动和桌面应用程序用户界面的通用语言,是 teleport 对于 UI 描述的一种定义规范。UIDL是 DSL(Domain Specific Language,解决特定领域问题的语言) 的子集。它与平台无关,可以应用于任何平台或者应用程序。

使用 UIDL 的主要目的就是将用户界面描述成一种机器可读的格式,以帮助开发人员更加高效地构建、测试和维护用户界面。UIDL具体定义内容可以参考官方文档,这里给出一个简单的示例:


{ 
  "name": "Badge", 
  "propDefinitions": { 
    "count": { 
      "type": "number" 
    } 
  }, 
  "node": { 
    "type": "element", 
    "content": { 
      "elementType": "container", 
      "attrs": { 
        "class": "badge" 
      }, 
      "children": [ 
        { 
          "type": "element", 
          "content": { 
            "elementType": "text", 
            "attrs": { 
              "class": "badge-text" 
            }, 
            "children": [ 
              { 
                "type": "dynamic", 
                "content": { 
                  "referenceType": "prop", 
                  "id": "count" 
                } 
              } 
            ] 
          } 
        } 
      ] 
    } 
  } 

上面的UIDL描述了一个 Badge 组件,它接受一个 number 类型的属性 count,输出我们常见的 Badge 组件 UI 结构。通过使用teleport 提供的 React 代码生成器,我们可以得到下面的结果:


import React from 'react' 
import PropTypes from 'prop-types' 
const Badge = (props) => { 
  return ( 
    <div> 
      <span>{props.count}</span> 
    </div> 
  ) 
} 

Badge.propTypes = { 
  count: PropTypes.number, 
} 
export default Badge 

UIDL 会给出一个通用的 UI 界面节点描述,通过生成器可以渲染得到我们需要的指定框架代码。

2 ) 生成器解析

在已有生成器满足业务研发的需求前提下,可以直接进行使用;不满足则业务研发通过编辑模版可以得到新的生成器,再通过平台研发审核发布到已有生成器列表中,开放给所有研发进行代码的自动生成。

在把中间代码层插件化的过程中,我们主要借助了 UIDL 的定义结构,使用 teleport 定义的生成器结构来进行代码转换,内部包含了 Mappings(UI 组件映射)、Plugins(插件处理逻辑)、PostProcessors(后处理器)。

  • Mappings:给出组件映射关系;
  • Plugins:处理复杂对应逻辑;
  • PostProcessors:美化代码格式等等。

代码生成器实际上就是对中间代码进行解析,不同的解析得到不同的结果。

3 ) 生成器插槽

我们在平台上提供了选择不同生成器的入口,根据即时选择的类型,实时自动生成对应的代码。

业务研发完成新生成器的编写后,发布成npm包,通知平台研发进行审核。审核成功后,相应的生成器版本会展示在平台的生成器入口处。

其他业务研发可以在这个公开的生成器列表中,选择适合自己业务场景的生成器,进行代码的一键自动生成。

3.2 生成器编写模版化

为了降低新生成器编写者的学习成本,我们提供了可本地预览的代码模版,并且封装了常用前端框架生成器及一些通用方法。

1 ) 开发流程

业务研发clone模版仓库到本地,模版编写主要有两个方向:

a. 编写某种前端框架下的通用组件:视觉稿 DSL 转换为需要的 UIDL 结构(调整层级、组件名称等),再调用对应的框架生成器,生成代码;

b. 编写特定的数据结构:获取 DSL 中的节点数据,构建为新的数据结构。

再在本地进行效果预览,最后发布成为一个独立的npm包,通知平台研发审核后插入到插槽中。

2 ) 本地开发与预览

业务研发可以根据我们提供的模版来进行定制化输出,也可以参考其他公开的生成器进行编写。

以下为提供的模版结构:


custom-generator-template 
  |----package.json         // 发布的 npm 包信息 
  |----README.md         
  |----src                             
  |      |----index.ts      // 编写的自定义 DSL 
  |----test                 // 本地测试输入与输出 
  |      |----files        // 输出的文件结构 
  |      |----dsl.json     // 输入的dsl结构 
  |      |----index.ts 
  |----tsconfig.json       // 默认配置源文件为 ts 
  |---- dist               // 输出的 dist 目录 

模版中内置了输入与输出的本地便捷测试:在编辑src/index.ts文件后,执行npm run test即可在test/目录下看到输出的内容。

下图为本地预览示例,左为 DSL 结构,右为经过生成器转换的代码:

本地测试使用的 DSL 结构可以在平台上快速复制得到。

3 ) 常用 API 封装

为了降低生成器编写者的学习成本,我们会处理前序与后序步骤,在实际使用中都是可选的:

  • 前序:原始视觉稿 DSL 转换为 UIDL 结构;
  • 后序:调用 API 生成相应框架代码。

普通的 DSL 代码生成模型如下:


export type GenerateCustomFiles = (dsl: DSL) => Promise 
// CompiledComponent 是定义好的输出结构,包含代码依赖和代码文件 

对于页面级别的 DSL,引进了 Design Tokens 和共享样式的概念,会在普通的 DSL 上添加一些属性:


export type GenerateCustomFiles = (rootDSL: RootDSL, options: GenerateCustomOptions) => Promise; 
// options 是传入的生成文件参数,当前有样式参数(CSS Module、Inline Style等)、布局参数(Pixel、Rem)等 

如果调用平台开发的API,就可以很方便的生成业务定制化的代码:


const generateFiles: GenerateCustomFiles = async (rootDSL, options) => { 

const rootUIDL = rootDSLToRootUIDL(rootDSL, { 
  layoutUnit: options.layoutUnit, 
  uidlAdjust, 
})  // 前序:使用uidlAdjust进行DSL转成UIDL过程中属性的修改 

    // 核心逻辑:UIDL 结构修改 
    // ... 

  const cc = await generateReactFiles(rootUIDL, { layoutUnit: options.layoutUnit, style: options.style }) 
    // 后序:调用前端框架生成API 

  return cc 
} 

在后序步骤中,携程 Design To Code 支持的框架 API 如下:

  • generateHtmlFiles
  • generateVueFiles
  • generateReactFiles
  • generateReactNativeFiles

这样处理是为了让编写者无需关心复杂的前端框架自动生成过程,仅通过修改 DSL 结构层次或名称即可到达自己定制的目的。

3.3 D2C 平台优化

为了让普通使用者零成本地在 D2C 平台上进行代码的一键自动生成,我们对平台进行了一些优化。接下来介绍其中三个重要功能:

  • 节点标签
  • 局部自动生成
  • 沙盒预览模式

1) 节点标签

点击界面左侧的 DSL 节点的标签 icon,弹出浮层,可以手动给当前节点添加语义化标签名称。

手动打标签,是在 DSL 节点中新增了一个属性。在生成器中可以根据该属性实现不同的效果。

例如在落地方案 Flybirds 测试用例的自动生成中,通过给视觉稿中不同的文案部分做了三个特定标签识别:render、exist、click,分别对应了以下的执行语句:

  • render: 页面渲染完成出现元素 [机票]
  • exist: 存在[机票]的文案
  • click: 点击[text=机票]

从视觉稿信息中来说,这些结构都是文字节点,没有什么区别。通过手动打标可以在自动生成时补充额外的信息。

2 ) 局部自动生成

下图中,左边是 DSL 节点,右边是视觉稿标注。点击左侧节点会突出显示视觉稿中内容,同样地,点击右侧视觉稿局部也会在左侧节点中同步突出对应节点。

同时,会在下端实时展示当前选择的局部画板通过生成器自动生成的代码内容。图示为点选“仅看直飞”按钮局部后,生成的 React Native 代码。

在生成器满足业务研发需求的前提下,可以点选局部后,直接复制下方的代码到仓库中进行应用。

在初始进入画板时,或者点选了 DSL 的根节点,下端会通过选择的生成器实时渲染整个页面的代码。

3 ) 沙盒预览模式

为了更加真实地预览自动生成的代码,我们提供了沙盒模式。React / Vue 等代码可以直接在 web 端预览,React Native 我们也通过<span style="font-size: 15px">react-native-web</span>转为web端代码,可以进行实时编辑并查看对应效果。

此外,沙盒模式还提供了一键打包下载的功能,使项目可以进行快速部署发布。

四、落地效果


落地效果均为机票实现的生成器,可在平台中进行一键自动生成。我们从三个不同的维度来进行自定义生成器效果展示。

4.1 自动生成指定框架代码

1 ) 效果演示

图例为 React Native 代码自动生成。

2 ) 内部实现

语言框架的应用,是作为自动化转码的一个基础底层代码内容。在此基础上,我们可以得到组件化的自动代码生成。但语言框架转码比组件化更为复杂,可以说语言框架转码是组件化转码的一个超集。

在这个过程中,借助了前文提到的 teleport 定义的 generator 生成器结构,其中需要调整mappings、plugins、postprocessors,来得到我们预期的框架代码结构。需要处理的具体内容如下:

  • 识别抽象 DSL 节点:建立我们自定义的语言框架的首要步骤,需要把抽象的 DSL 映射为基础的 HTML 节点,识别 elementType 为正确的对应组件名;
  • 映射组件:支持 React Native 组件映射及对应引用 package,读取资源文件生成 React Native组件。在这里最终映射预览的文件建立在 react-native-web 的基础上;

  • 处理依赖:处理文件之间的依赖关系,加载组件,以便输出正确文件;
  • 样式表风格化:第一步,将 CSS 风格的样式表转换为 React Native StyleSheet;第二步,处理屏幕适配;
  • 调整 DSL 结构:处理中间 DSL,减少冗余以及修正转化错误;在有了大体的转化前后结构内容,依然需要进一步修复转化过程中的一些冗余,以减少代码 size,或者降低生成的样式在不同机型上出错的可能性。

以 React Native 为例,我们主要需要做到:

(i). 删除冗余:删除不需要的样式节点,例如 fontFamily、letterSpacing、text 节点的 width / height;
(ii). 合并代码:合并节点,例如 paddingTop 与 paddingBottom 需要合并为 paddingVertical;
(iii). 兼容样式:删除或者修改会引起不同机型样式兼容问题的节点,例如 lineHeight、fontWeight。

  • 美化代码:需要格式化生成的 typescript 代码;
  • 支持在线预览自动生成的 React Native 代码:我们需要在 web 页面进行实时编辑预览,因此引入了 <span style="font-size: 15px">react-native-web</span>,以便使用者能实时调试。

4.2 自动生成指定组件代码

1) 效果演示

图例为 React Native Label 组件代码自动生成。

2) 内部实现

我们可以通过编辑中间代码,来得到预期的业务组件功能代码,包含动画效果、交互逻辑等。目标是生成一套在生产环境高可用性、复用性的组件。以标签组件为例,示范如何生成预期的组件代码。

在这个过程中,需要使用多个真实场景视觉稿进行代码渲染,在线预览效果,进行代码调试与可用性测试。经过该场景的多次测试,我们需要覆盖两个结构不同的转换场景:

  • 带有渐变背景的标签处理
  • 常规文案处理。

转换核心代码如下(抓取对应节点信息,构建为我们预期的代码结构:

根据当前节点的类型,做不同的层级处理。原始 DSL 与目的代码前后预览如下:

4.3 自动生成测试用例代码

1 ) 演示效果

图例为 Flybirds 测试用例代码自动生成。

2) 内部实现

我们使用携程机票开源的跨端跨框架的BDD UI 自动化测试方案——Flybirds 的用例结构来进行构建该生成器的目的内容。

因为文本节点的结构是一致的,我们需要手动给不同的文本节点赋上不同的语句含义,在此处我们通过在平台上给视觉稿图层打标签进行实现。

在实现上,通过递归 DSL 结构中的文本节点进行文案内容的查找与输出对比。转换代码如下:

执行这段代码,本地运行<span style="font-size: 15px">npm run test</span>,前后输出内容对比:

测试用例生成作为一项实验性实践,充分说明了自动转码的自由性——有了视觉稿的原始信息结构,你可以从中解析出你需要的数据节点,并且修改为需要的目的内容。

五、未来规划


在后续的开发规划上,我们侧重于完善流程中的细节,以及通过我们官方的示例,增强 D2C 自动代码内容的实用性,继续推出更多语言框架,以及适配更多应用平台,例如小程序等等。

除了平台细节的完善,在这里主要提出未来的两个方向上的规划:

5.1 人工智能:与 copilot 结合的未来

目前的 D2C 方案,存在一个很明显的劣势,那就是无法处理复杂的逻辑反馈。我们提供的自定义生成器功能虽然可以高度定制化生成的组件,但是开发使用的内容依然没有相关的逻辑代码内容。

人工智能的出现可以弥补这一缺憾。目前人工智能还未推出完善的编写代码功能,但能够根据输入的自然语言描述或者是环境来推断开发需要使用的代码片段。copilot 可以辅助开发人员在开发过程中自动生成一些复杂的代码,减少开发人员的负担,提高效率与质量。

5.2 深度定制化:一键换肤,Design Tokens + custom DSL

Design Tokens 是一种用于描述设计系统中的基本视觉和品牌属性的集合。这些属性包括颜色、字体、间距、边角半径等。通过设计标记,我们可以将视觉属性抽象出来,从而实现统一的设计语言。

实际上目前平台已经支持了 tokens 的抽象抽取,与对应的主题映射表。除了一些设计元素上的调整,我们也可以在不同环境下使用不同的组件来进行兼容展示,例如在 React Native 中,通过修改 mapping 得到需要的交互组件。

这种深度定制化的方案,不仅可以为用户提供更好的体验,同时可以大大提高开发效率。

六、结语


携程机票开放了视觉稿生成代码流程中的生成器入口,通过让业务研发参与生成器的发布与更新,抽象出更多适合业务场景的组件/数据结构。

同时,机票在三个维度上进行了生成器落地示例,多次验证了该方案的可行性与实用性。在提高项目生产效率与设计稿还原质量的同时,确保了代码的一致性与可维护性。

视觉稿自动生成代码,可以极大地提高团队的效率,减少人为错误和重复劳动,从而更加专注于创意和创新。我们相信,在未来的发展中将会为我们带来更加高效和便捷的工作体验。