如果你曾参与过航空公司、机场或旅游分销平台的数字化项目,大概率遇到过同一个尴尬:业务系统多,界面风格却各自为战,同一个“航班查询”功能在不同系统中长着完全不同的脸。前端团队每接手一个新项目,都要重新设计一遍基础组件,既浪费时间,又让用户学得头疼。
IATA(国际航空运输协会)这几年在航空零售化和标准统一上做了不少事情,其中一个面向行业前端的开源设计系统,就是 Rave。简单说,Rave 让航空行业的开发者可以基于一套统一的 UI 组件和设计规范去构建 Web 应用。本文围绕“成为 Rave 开发者”这个主题展开,适合想进入航空业做前端或全栈的开发者,也适合已经在航空 IT 项目中摸爬滚打、想提升组件化效率的团队。读完你会理解 Rave 解决了什么问题、它的技术栈长什么样,并动手搭建第一个 Rave 风格的应用和自定义航班业务组件。
正文中的代码属于工程示例,Rave 的包名、组件前缀和 API 在不同版本中可能调整,请以你当前使用的官方文档为准。
1. 为什么航空行业需要 Rave 开发者
1.1 航空 IT 的 UI 碎片化问题
航空公司的数字化体系远比普通人想象中复杂。一套面向旅客的购票官网背后,往往连接着 PSS 旅客服务系统、订单管理系统、支付网关、常旅客计划、机场离港系统等多个平台。这些系统来自不同供应商,有些是几年前的旧系统,有些是新上的云原生应用。消费者前端可以被设计得很漂亮,但内部运营人员每天使用的后台工具,仍然充满了表格式界面、密集表单和风格不一致的按钮。
这种碎片化带来的不只是视觉问题,还有真实的开发和维护成本。一个机场地服系统要增加一个“登机口变更”功能,前端团队可能要从零写一套组件;另一家供应商开发的同类系统,又因为内部封装完全不同,几乎所有代码都要重写。行业缺少的是一种可共享、可扩展的 UI 资产。
Rave 的定位,就是航空运输行业的开源设计系统和前端组件生态。它由 IATA 推动,目标不是做一套“谁都通用的普通组件库”,而是专注航空业务场景,让航空公司、机场、技术供应商在同一个生态里协作。
1.2 什么是 IATA Rave
从技术角度看,Rave 是一套基于现代 Web 标准构建的组件库与设计规范;从行业角度看,它是航空业数字化转型过程中的公共基础设施。Rave 组件覆盖了表单、表格、按钮、弹窗、日期选择等常用能力,同时把颜色、字体、圆角、间距等视觉要素抽象成设计令牌,方便团队在统一规范下做品牌化定制。
Rave 与航空业务标准关系紧密。航空公司正在从传统“航班为中心”的销售模式,转向“订单为中心”的零售模式,这个过程中涉及 NDC 标准、One Order 标准等。Rave 的价值在于,把这类航空业务模型在界面上做出一致的表达。比如乘客信息录入、行李选项、辅营产品选择、航班变更等场景,都可以用同一套组件语言实现。
所以,“Rave 开发者”并不是只会用组件库的页面拼接工人,而是既懂现代前端工程化,又能把航空业务标准落地成高质量界面的一类角色。
2. Rave 的技术架构与核心概念
2.1 基于 Web Components
Rave 最值得关注的技术选择是 Web Components。它由浏览器原生支持的自定义元素、Shadow DOM、HTML 模板等标准组成,核心优势是框架无关。
你不需要担心项目用的是 React 还是 Vue,只要浏览器支持自定义元素,就能直接使用 Rave 组件。这对航空行业尤其重要:同一套组件库要被十几个不同技术栈的团队使用,如果只提供 React 版本,Vue 和 Angular 团队就难以复用;但如果基于 Web Components 发布,所有业务系统都能以最小成本接入。
用户在页面中看到的是一个自定义元素标签,例如一个按钮可能被渲染成<rave-button>。组件内部的 DOM 结构和样式被隔离在 Shadow DOM 中,不轻易受到外部全局样式污染。这种封装特性在大型后台系统中非常实用。
如果你刚开始接触这个概念,可以把 Web Components 理解为“浏览器原生支持的组件化能力”。它不依赖某个框架的运行时,定义一次,随处使用。
2.2 设计令牌与主题
Rave 组件在样式层大量使用 CSS 变量,也就是设计令牌。设计令牌并不是某个库的专属概念,而是一种将颜色、字体、间距、圆角等设计决策集中管理的方式。
通常 Rave 会在:root或特定主题作用域下定义一组变量,例如主色、边框色、常用间距。组件内部通过var(--rv-color-primary)这类写法引用。这样,业务系统可以基于一套变量定制品牌色,而不需要重写组件源码。
这种设计给航空企业带来的直接好处是:下属子品牌或不同地区站点需要调整视觉时,只需要覆盖主题变量,不需要修改业务组件逻辑。后续升级组件库时,也不会因为本地改动过多产生合并冲突。
2.3 组件分层
一个成熟的航空业务前端项目,组件通常会分三个层次:
- 基础组件:按钮、输入框、下拉菜单、表格、弹窗等。
- 业务组件:航班卡片、乘客信息表单、行李选项、优惠券选择器等。
- 页面模板:查询页、订单页、乘客管理页等完整界面。
Rave 设计系统覆盖的范围,越往上层越需要业务团队自己沉淀。基础组件可以直接复用 Rave 的能力,业务组件则要结合航空领域的数据模型做封装。作为 Rave 开发者,不能只满足于调用基础组件,还要具备业务组件的抽象能力。
3. 环境准备与开发工具
3.1 开发环境
在动手写代码之前,先把基础环境准备好。
- Node.js 环境,建议 16 或以上版本,npm 或 yarn 任选。
- 代码编辑器,推荐 VS Code,并安装 ESLint、Prettier 等前端工程化插件。
- 现代浏览器,推荐 Chrome 或 Edge,用于调试自定义元素和 Shadow DOM。
- Git,用于项目管理和提交代码。
如果你是想用 Rave 官方组件开始,需要先确认你的项目能访问 npm registry 并安装依赖。国内网络环境下,如果安装慢,可以配置 npm 镜像,这里不做展开。
3.2 项目脚手架
本文的示例使用 Vite 构建一个原生 Web Components 项目。Vite 开发服务器启动快、配置简单,适合用来做组件原型验证。
# 创建项目 npm create vite@latest rave-demo -- --template vanilla # 进入目录 cd rave-demo # 安装依赖 npm install执行完后,项目里会看到一个标准的index.html、main.js和style.css文件。后续我们会在这些文件上修改。
3.3 关于版本与组件前缀
Rave 组件库的发布形式和命名在不同阶段可能有变化。有些版本使用<rv-*>前缀,有些版本使用<rave-*>前缀;安装包名也可能从单包变为按模块拆分。所以,尽量不要把“某个固定前缀”背死,而是在项目中统一查文档,或者在浏览器开发者工具的 Elements 面板里,确认自定义元素的实际标签名。
下面的示例中,我会统一用rave-作为标签前缀,目的是让结构更直观。你实际开发时,请以官方文档暴露出来的标签名为准。
4. 第一个 Rave 风格应用:从零搭建
4.1 引入组件库
假设你已经通过 npm 安装了 Rave 的相关组件包。如果组件库是用 Stencil 这类工具构建的,通常需要调用它的 loader 方法,把自定义元素注册到浏览器中。
在src/main.js中写入:
// 文件路径:src/main.js import { defineCustomElements } from 'rave-components/dist/loader'; defineCustomElements();这里的rave-components/dist/loader是示例写法。实际项目中,请根据你安装的包名和导出路径调整。
如果你是直接在一个普通 HTML 页面中使用,也可以通过<script>标签引入组件库的构建产物。哪种方式适合你,取决于项目架构和组件库官方给出的接入方式。
4.2 在页面中使用组件
修改index.html,加入一个简单的航空订单创建界面:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Rave Demo</title> </head> <body> <main class="page"> <h1>航班订单管理</h1> <rave-button>npm run dev浏览器打开终端提示的本地地址,正常情况下页面会显示标题、按钮和输入框。打开开发者工具的 Elements 面板,可以观察到这些标签内部存在 Shadow DOM 结构。这就是 Web Components 的典型表现。
如果页面中组件没有渲染,优先检查两个方面:一是defineCustomElements是否在入口执行成功;二是组件标签名称是否和官方文档一致,浏览器不认识的标签会被当成普通行内元素,无法看到组件效果。
4.4 与主流框架集成
Rave 的 Web Components 特性让集成变得简单。用 React 举例,你仍然需要处理一个细节:React 的 JSX 语法对自定义元素支持不算特别原生,官方推荐通过createElement或 ref 结构使用。
下面是一个在 React 组件中包装flight-card自定义元素的示例:
// 文件路径:src/components/FlightCardWrapper.jsx import { useEffect, useRef, createElement } from 'react'; export default function FlightCardWrapper(props) { const ref = useRef(null); useEffect(() => { const el = ref.current; const handler = (event) => { console.log('用户选择航班:', event.detail); }; el?.addEventListener('flight-select', handler); return () => el?.removeEventListener('flight-select', handler); }, []); return createElement('flight-card', { ...props, ref }); }这里的关键点是:React 的合成事件只覆盖标准事件,自定义事件flight-select需要拿到原生 DOM 元素后,通过addEventListener手动监听,并在组件卸载时移除监听器,避免内存泄漏。
如果你使用 TypeScript,还需要给自定义元素声明类型,否则 JSX 和createElement的类型校验会报“未知元素”的错。
// 文件路径:src/types/global.d.ts declare namespace JSX { interface IntrinsicElements { 'flight-card': React.DetailedHTMLProps< React.HTMLAttributes<HTMLElement>, HTMLElement > & { from?: string; to?: string; 'deps-time'?: string; 'arrs-time'?: string; price?: string; }; } }5. 实战:开发一个航空航班组件
5.1 需求分析
进入具体业务场景。假设我们要开发一个“航班卡片”组件,用于在搜索结果列表中展示一条航班信息。需求包括:
- 展示出发地、目的地机场三字码。
- 展示出发和到达时间。
- 展示价格。
- 点击卡片时触发一个自定义事件,方便外部业务逻辑监听。
这个组件既要符合 Rave 的设计语言,又要保持业务上的可复用性。我们采用原生 Web Components 实现,不依赖任何框架。
5.2 设计令牌定义
为了贴合 Rave 的设计系统,我们先用一组 CSS 变量定义主题。
/* 文件路径:src/styles/tokens.css */ :root { --rv-color-primary: #005fc5; --rv-color-primary-hover: #004a9a; --rv-color-bg: #ffffff; --rv-color-border: #e5e7eb; --rv-color-text: #1f2937; --rv-color-text-secondary: #6b7280; --rv-font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; --rv-radius: 8px; --rv-space-sm: 8px; --rv-space-md: 16px; --rv-space-lg: 24px; }这些变量命名体现了 Rave 生态里常见的--rv-前缀。具体变量名以你使用的设计系统为准,但思路是一致的:把颜色、空间、字体这些可复用的设计决策沉淀成变量。
5.3 航班卡片组件实现
创建FlightCard组件类。
// 文件路径:src/components/FlightCard.js class FlightCard extends HTMLElement { static get observedAttributes() { return ['from', 'to', 'deps-time', 'arrs-time', 'price']; } constructor() { super(); // 使用阴影 DOM 隔离内部样式 this.attachShadow({ mode: 'open' }); } connectedCallback() { this.render(); this.bindEvents(); } attributeChangedCallback() { this.render(); } formatPrice(value) { const amount = Number(value); if (Number.isNaN(amount)) { return '--'; } return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount); } bindEvents() { const card = this.shadowRoot.querySelector('.card'); card?.addEventListener('click', () => { this.dispatchEvent( new CustomEvent('flight-select', { detail: { from: this.getAttribute('from'), to: this.getAttribute('to'), depsTime: this.getAttribute('deps-time'), arrsTime: this.getAttribute('arrs-time'), price: this.getAttribute('price') }, bubbles: true, composed: true }) ); }); } render() { const from = this.getAttribute('from') || '---'; const to = this.getAttribute('to') || '---'; const depsTime = this.getAttribute('deps-time') || ''; const arrsTime = this.getAttribute('arrs-time') || ''; const price = this.formatPrice(this.getAttribute('price')); this.shadowRoot.innerHTML = ` <style> :host { display: block; font-family: var(--rv-font-family, sans-serif); } .card { border: 1px solid var(--rv-color-border, #e5e7eb); border-radius: var(--rv-radius, 8px); padding: var(--rv-space-md, 16px); display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: box-shadow 0.2s ease; background: var(--rv-color-bg, #fff); } .card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } .airport { font-size: 20px; font-weight: 700; color: var(--rv-color-text, #1f2937); } .time { color: var(--rv-color-text-secondary, #6b7280); font-size: 14px; margin-top: 4px; } .price { color: var(--rv-color-primary, #005fc5); font-size: 18px; font-weight: 700; } </style> <div class="card" role="button" tabindex="0"> <div> <div class="airport">${from} → ${to}</div> <div class="time">${depsTime} — ${arrsTime}</div> </div> <div class="price">${price}</div> </div> `; } } customElements.define('flight-card', FlightCard);代码中有几个细节值得注意:
observedAttributes声明了组件监听哪些属性变化,属性变化时会自动重新渲染。attachShadow({ mode: 'open' })创建了隔离的样式作用域。render()方法是简单的字符串模板渲染,适合组件内部状态不复杂的场景。formatPrice使用Intl.NumberFormat做货币格式化,航空业务中涉及多语言、多币种时非常实用。
composed: true的设置很关键:它让自定义事件可以穿透 Shadow DOM,被外层业务代码监听。如果你忘记设置composed,事件可能无法正确向上冒泡。
5.4 页面使用航班卡片
在index.html中加入组件引用。
<flight-card from="PEK" to="SIN" deps-time="09:30" arrs-time="15:45" price="3680" ></flight-card>然后在主入口中注册组件。
// 文件路径:src/main.js import './components/FlightCard.js'; import './styles/tokens.css';启动开发服务器后,页面上应该能看到一张包含航班信息和价格的卡片。点击卡片,控制台会打印出flight-select事件以及对应的航班明细。
注意,这里字段中的from、to在完整的航空业务中可能需要搭配城市名称,而不仅仅是三字码。真实项目中,你可以增加from-city、to-city等属性,或者从后台接口直接返回完整的展示对象。
5.5 组件测试思路
这个组件是纯前端实现,建议至少覆盖以下测试点:
- 属性不传时,能否正常渲染且不报错。
- 价格字段传入非法字符串时,是否显示
--。 - 点击组件是否触发
flight-select事件。 - 重复更新属性时,内容是否正确刷新。
对于更复杂的业务组件,建议引入 Playwright 做端到端测试,在真实浏览器中验证自定义元素行为。
6. 常见问题与排查思路
Web Components 和 Rave 相关开发中,最容易遇到下面几类问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 页面中自定义元素没有渲染 | loader 未调用、组件 JS 未加载、标签名拼写错误 | 检查入口文件是否调用了defineCustomElements,并用浏览器的 Elements 面板确认标签名 |
| Shadow DOM 内部样式异常 | 写入了组件外部的全局样式,或 CSS 变量没有被正确继承 | 将样式定义在组件内部,并有意识地使用 CSS 变量控制主题 |
| React 中自定义元素事件不生效 | JSX 合成事件无法覆盖自定义事件 | 通过ref获取原生 DOM 元素,用addEventListener手动绑定,并在卸载时移除 |
服务端渲染或构建时报window is not defined | Web Components 依赖浏览器 API,Node 环境没有window | 在客户端动态导入组件包,或使用dynamic import隔离 |
| 组件之间有样式冲突 | 全局使用非隔离性 CSS 类名 | 推荐使用 Shadow DOM 或规范化类名,遵循 Rave 的命名约定 |
| 主题变量不生效 | 变量定义在选择器优先级较低的位置,或变量名不一致 | 统一在:root定义,并通过开发者工具检查元素的 Computed 样式 |
这类问题的排查思路通常有一个共性:先确认元素是否真正渲染,再确认事件和样式层级。使用 Chrome DevTools 的 Elements 面板可以快速定位自定义元素的边界和 Shadow DOM 内部结构,这是日常调试的推荐起点。
7. 成为 Rave 开发者的最佳实践与学习路线
7.1 从业务标准出发
Rave 不只是一个 UI 组件库,它背后是航空业对“零售化系统体验一致性”的追求。学习 Rave 时,最好先理解几个航空业务概念:
- NDC:航空零售领域的 API 通信标准,让航空公司能够动态地销售辅营产品。
- One Order:用“订单”统一替代传统的“PNR + E-Ticket + EMD”多记录结构。
- 机场三字码与航段时间:航空界的基础数据概念。
界面上的航班卡片、订单表单,本质上都是这些业务模型的视觉表达。只有理解了字段含义,才能在组件设计时做出合理的抽象。
7.2 打好前端技术基础
Rave 开发者能力的核心,仍然是扎实的前端功底。建议按这个顺序学习:
- HTML5 自定义元素、Shadow DOM、
<template>与<slot>。 - TypeScript,特别是泛型与声明文件编写。
- 至少一门主流框架,并理解框架与 Web Components 相互操作时的优缺点。
- 设计系统基础,包括设计令牌、主题化、可访问性。
7.3 组件开发规范
实际项目中,我建议团队内部形成一套组件开发约定:
- 组件命名统一,比如以
flight-、order-、passenger-作为业务组件前缀。 - 所有可配置的视觉内容尽量通过属性或 CSS 变量暴露,而不是让使用方直接修改内部样式。
- 每个组件必须考虑无属性和异常属性的渲染状态。
- 事件名与组件名强关联,例如
flight-select、order-submit。 - 在组件文档中记录可用的属性、事件、CSS 变量,方便团队其他人接入。
7.4 无障碍与国际化
航空软件的用户来自全球各地,无障碍和国际化不是加分项,而是基础要求。
- 颜色对比度需要满足 WCAG 标准。
- 按钮、卡片等可交互元素要支持键盘操作。
- 日期、时间要区分 UTC 与当地时间,避免直接拼接字符串。
- 货币金额尽量使用
Intl.NumberFormat按地区格式化。
7.5 参与开源与社区
Rave 是开源设计系统,意味着你可以通过阅读源码理解航空级组件的设计思路,也可以通过提交 Issue、修复文档、贡献新组件来进入这个生态。参与开源的好处不仅仅是代码能力提升,还能帮助你建立航空领域的人脉和行业认知。
7.6 学习资源建议
学习 Rave 和航空前端开发,优先看这几类资源:
- IATA 官方关于零售化标准和 Rave 的文档。
- Rave 设计系统官方组件示例与使用说明。
- Web Components 标准文档和 MDN 相关教程。
- GitHub 上航空领域开源项目,特别是 Rave 相关仓库。
如果所在公司已经有航空类项目,争取在真实业务中沉淀一个业务组件,远比只看文档有效。
8. 写在最后
Rave 的价值,在于让航空行业的界面开发从“重复造轮子”走向“共享组件资产”。作为 Rave 开发者,你不需要等整个行业标准完全成熟才开始行动。可以先从今天示例中的航班卡片组件入手,把 Web Components、设计令牌、事件封装这几点练熟,再逐步接触 IATA 的行业标准和更复杂的航空业务场景。
航空业务系统的需求往往比普通企业应用更复杂,数据和状态也更严肃。但正因如此,一个懂得组件化、标准化和业务建模的前端开发者,在这个行业里会非常有价值。
如果你也想尝试,建议第一步就是把环境搭好,跑通一个最简单的 Rave 风格页面,再往里面加业务组件。动手遇到问题的时候,欢迎在评论区把报错信息发出来,我们一起讨论排查思路。