【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

开发速度快 10 倍!Airbnb 用 GraphQL+Apollo 做到了

  • 2019-01-04
  • 本文字数:8250 字

    阅读完需:约 27 分钟

开发速度快10倍!Airbnb用GraphQL+Apollo做到了

在上个月举行的 GraphQL 峰会上,我做了一场演讲,其中涉及很多实时编码演示,可以看一下视频回顾:


https://youtu.be/JsvElHDuqoA


从参会者的反馈来看,人们非常惊讶我们的开发速度为什么会如此之快,但因为我没有太多时间解释其中的原理,很多人认为这是因为 Airbnb 投入了数年的工程师时间构建了可以支持 GraphQL 的基础设施。但实际上,演示中有 90%的繁重工作都是由 Apollo 的 CLI 工具提供支持的。


在这篇文章中,我将通过部分代码介绍这种快速的开发体验。

将 GraphQL 用于后端驱动的 UI

在演讲中,我们假定开发了一个系统,这个系统有一个动态页面,这个页面基于一个可以返回一系列“section”的查询,这些 section 是响应式的,用于定义页面 UI。


主文件是一个生成文件(稍后我们将介绍如何生成它),如下所示:


import SECTION_TYPES from '../../apps/PdpFramework/constants/SectionTypes';import TripDesignerBio from './sections/TripDesignerBio';import SingleMedia from './sections/SingleMedia';import TwoMediaWithLinkButton from './sections/TwoMediaWithLinkButton';// …many other imports…
const SECTION_MAPPING = { [SECTION_TYPES.TRIP_DESIGNER_BIO]: TripDesignerBio, [SECTION_TYPES.SINGLE_MEDIA]: SingleMedia, [SECTION_TYPES.TWO_PARAGRAPH_TWO_MEDIA]: TwoParagraphTwoMedia, // …many other items…
};const fragments = { sections: gql` fragment JourneyEditorialContent on Journey { editorialContent { ...TripDesignerBioFields ...SingleMediaFields ...TwoMediaWithLinkButtonFields # …many other fragments… } } ${TripDesignerBio.fragments.fields} ${SingleMedia.fragments.fields} ${TwoMediaWithLinkButton.fragments.fields} # …many other fragment fields…`,};
export default function Sections({ editorialContent }: $TSFixMe) { if (editorialContent === null) { return null; }
return ( <React.Fragment> {editorialContent.map((section: $TSFixMe, i: $TSFixMe) => { if (section === null) { return null; }
const Component = SECTION_MAPPING[section.__typename]; if (!Component) { return null; }
return <Component key={i} {...section} />; })} </React.Fragment> );}
Sections.fragments = fragments;
复制代码


因为 section 可能会有很多(现在用于搜索的 section 大概有 50 个),所以我们没有需要事先将所有可能的 section 都打包。


每个 section 组件都定义了自己的查询片段,与 section 的组件代码放在一起:


import { TripDesignerBioFields } from './__generated__/TripDesignerBioFields';
const AVATAR_SIZE_PX = 107;
const fragments = { fields: gql` fragment TripDesignerBioFields on TripDesignerBio { avatar name bio } `,};
type Props = TripDesignerBioFields & WithStylesProps;
function TripDesignerBio({ avatar, name, bio, css, styles }: Props) { return ( <SectionWrapper> <div {...css(styles.contentWrapper)}> <Spacing bottom={4}> <UserAvatar name={name} size={AVATAR_SIZE_PX} src={avatar} /> </Spacing> <Text light>{bio}</Text> </div> </SectionWrapper> );}
TripDesignerBio.fragments = fragments;
export default withStyles(({ responsive }) => ({ contentWrapper: { maxWidth: 632, marginLeft: 'auto', marginRight: 'auto',
[responsive.mediumAndAbove]: { textAlign: 'center', }, },}))(TripDesignerBio);
复制代码


这就是 Airbnb 后端驱动 UI 的一般性概念。它被用在很多地方,包括搜索、旅行计划、主机工具和各种登陆页面中。我们以此作为出发点,然后演示如何更新已有 section 和添加新 section。

使用 GraphQL Playground 探索 schema

在开发产品时,你希望能够基于开发数据探索 schema、发现字段并测试潜在的查询。我们借助GraphQL Playground实现了这一目标,这个工具是由 Prisma 提供的。


在我们的例子中,后端服务主要是使用 Java 开发的,我们的 Apollo 服务器(Niobe)负责拼接这些服务的 schema。目前,由于 Apollo Gateway 和 Schema Composition 还没有上线,我们所有的后端服务都是按服务名称进行划分的。这就是为什么在使用 Playground 时需要提供一系列服务名。下一级是服务方法,比如 getJourney()。

通过 VS Code 的 Apollo 插件查看 schema

在开发产品时有这么多工具可用真的是太好了,比如在 VS Code 中访问 Git,VS Code 还提供了用于运行常用命令的集成终端和任务。


当然,除此之外,还有其他一些与 GraphQL 和 Apollo 有关的东西!大多数人可能还不知道新的Apollo GraphQL VS Code插件。它提供的很多功能我在这里就不一一累述了,我只想介绍其中的一个:Schema Tag。


如果你打算基于正在使用的 schema 来 lint 你的查询,需要先决定是“哪个 schema”。默认情况下可能是生产 schema(按照惯例,就是“current”),但如果你需要进行迭代并探索新的想法,可能需要灵活地切换不同的 schema。


因为我们使用的是 Apollo Engine,所以使用标签发布多个 schema 可以实现这种灵活性,并且多个工程师可以在单个 schema 上进行协作。一个服务的 schema 变更被上游合并后,它们会被纳入当前的生产 schema 中,我们就可以在 VS Code 中切换回“current”。

自动生成类型

代码生成的目标是在不手动创建 TypeScript 类型或 React PropType 的情况下利用强大的类型安全。这个很重要,因为我们的查询片段分布在各种组件中,同一个片段会在查询层次结构的多个位置出现,这就是为什么对查询片段做出 1 行修改就会导致 6、7 个文件被更新。


这主要是 Apollo CLI 的功劳。我们正在开发一个文件监控器(名字叫作“Sauron”),不过现在如果有需要,可以先运行:apollo client:codegen --target=typescript --watch --queries=frontend/luxury-guest/**/*.{ts,tsx}。


因为我们将片段和组件放在一起,所以当我们向上移动组件层次结构时,更改单个文件会导致查询中的很多文件被更新。这意味着在与路由组件越接近的位置(也就是树的更上层),我们可以看到合并查询以及所有相关的各种类型的数据。

使用 Storybook 隔离 UI 变更

我们使用Storybook来编辑 UI,它为我们提供了快速的热模块重新加载功能和一些用于启用或禁用浏览器功能(如 Flexbox)的复选框。


我使用来自 API 的模拟数据来加载 story。如果你的模拟数据可以涵盖 UI 的各种可能状态,那么这么做就对了。除此之外,如果还有其他可能的状态(比如加载或错误状态),可以手动添加它们。


import alpsResponse from '../../../src/apps/PdpFramework/containers/__mocks__/alps';import getSectionsFromJourney from '../../getSectionsFromJourney';
const alpsSections = getSectionsFromJourney(alpsResponse, 'TripDesignerBio');
export default function TripDesignerBioDescriptor({ 'PdpFramework/sections/': { TripDesignerBio },}) { return { component: TripDesignerBio, variations: alpsSections.map((item, i) => ({ title: `Alps ${i + 1}`, render: () => ( <div> <div style={{ height: 40, backgroundColor: '#484848' }} /> <TripDesignerBio {...item} /> <div style={{ height: 40, backgroundColor: '#484848' }} /> </div> ), })), };}
复制代码


这个文件完全由 Yeoman(下面会介绍)生成,默认情况下,它提供了来自 Alps Journey 的示例。getSectionsFromJourney()过滤了部分 section。


另外,我添加了一对 div,因为 Storybook 会在组件周围渲染空格。对于按钮或带有边框的 UI 来说这没什么问题,但很难准确分辨出组件的开始和结束位置,所以我在这里添加了 div。


把所有这些神奇的工具放在一起,可以帮你提高工作效率。如果结合 Zeplin 或 Figma 使用 Storybook,你的生活变得更加愉快。

自动获取模拟数据

为了在 Storybook 和单元测试中使用逼真的模拟数据,我们直接从共享开发环境中获取模拟数据。与代码生成一样,即使查询片段中的一个小变化也会导致模拟数据发生很多变化。这里最困难的部分完全由 Apollo CLI 负责处理,你只需要将生成的代码与自己的代码拼接在一起即可。


第一步只要简单地运行 apollo client:extract frontend/luxury-guest/apollo-manifest.json,你将得到一个清单文件,其中包含了所有的查询。需要注意的是,这个命令指定了“luxury guest”项目,因为我不想刷新所有团队的所有可能的模拟数据。


我的查询分布在很多 TypeScript 文件中,这个命令将负责组合所有的导入。我不需要在 babel/webpack 的输出基础上运行它。


然后,我们只需要添加一小部分代码:


const apolloManifest = require('../../../apollo-manifest.json');
const JOURNEY_IDS = [ { file: 'barbados', variables: { id: 112358 } }, { file: 'alps', variables: { id: 271828 } }, { file: 'london', variables: { id: 314159 } },];
function getQueryFromManifest(manifest) { return manifest.operations.find(item => item.document.includes("JourneyRequest")).document;}
JOURNEY_IDS.forEach(({ file, variables }) => { axios({ method: 'post', url: 'http://niobe.localhost.musta.ch/graphql', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ variables, query: getQueryFromManifest(apolloManifest), }), }) .catch((err) => { throw new Error(err); }) .then(({ data }) => { fs.writeFile( `frontend/luxury-guest/src/apps/PdpFramework/containers/__mocks__/${file}.json`, JSON.stringify(data), (err) => { if (err) { console.error('Error writing mock data file', err); } else { console.log(`Mock data successfully extracted for ${file}.`); } }, ); });});
复制代码


我们目前正与 Apollo 团队合作,准备将这个逻辑提取到 Apollo CLI 中。我期待着将来我们只需要指定示例数组,并将它们和查询放在同一个文件夹中,然后根据需要自动生成模拟数据。想象一下我们只需要像这样指定模拟数据:


export default {  JourneyRequest: [    { file: 'barbados', variables: { id: 112358 } },    { file: 'alps', variables: { id: 271828 } },    { file: 'london', variables: { id: 314159 } },  ],};
复制代码

借助 Happo 将屏幕截图测试纳入代码评审

Happo是我用过的唯一的一个屏幕截图测试工具,所以无法将它与其他工具(如果有的话)进行比较。它基本原理是这样的:你推送代码,它渲染 PR 的组件,将其与 master 上的版本进行比较。


如果你在编辑< Input/>之类的组件,它会显示你做的修改影响到了哪些依赖 Input 的组件。


不过,最近我们发现 Happo 唯一的不足是屏幕截图测试过程的输入并不总能充分反映出数据的可靠性。不过因为 Storybook 使用了 API 数据,我们会更加有信心。另外,它是自动化的,如果你向查询和组件中添加了一个字段,Happo 会自动将差异包含到 PR 中,让其他工程师、设计师和产品经理看到变更后的视觉后果。

使用 Yeoman 生成新文件

如果你需要多次搭建脚手架,那么应该先构建一个生成器,它可以帮你完成很多工作。除了 AST 转换(我将在下面介绍),这里是三个模板文件:


const COMPONENT_TEMPLATE = 'component.tsx.template';const STORY_TEMPLATE = 'story.jsx.template';const TEST_TEMPLATE = 'test.jsx.template';
const SECTION_TYPES = 'frontend/luxury-guest/src/apps/PdpFramework/constants/SectionTypes.js';const SECTION_MAPPING = 'frontend/luxury-guest/src/components/PdpFramework/Sections.tsx';
const COMPONENT_DIR = 'frontend/luxury-guest/src/components/PdpFramework/sections';const STORY_DIR = 'frontend/luxury-guest/stories/PdpFramework/sections';const TEST_DIR = 'frontend/luxury-guest/tests/components/PdpFramework/sections';
module.exports = class ComponentGenerator extends Generator { _writeFile(templatePath, destinationPath, params) { if (!this.fs.exists(destinationPath)) { this.fs.copyTpl(templatePath, destinationPath, params); } }
prompting() { return this.prompt([ { type: 'input', name: 'componentName', required: true, message: 'Yo! What is the section component name? (e.g. SuperFlyFullBleed or ThreeImagesWithFries)', }, ]).then(data => { this.data = data; }); }
writing() { const { componentName, componentPath } = this.data; const componentConst = _.snakeCase(componentName).toUpperCase();
this._writeFile( this.templatePath(COMPONENT_TEMPLATE), this.destinationPath(COMPONENT_DIR, `${componentName}.tsx`), { componentConst, componentName } );
this._writeFile( this.templatePath(STORY_TEMPLATE), this.destinationPath(STORY_DIR, `${componentName}VariationProvider.jsx`), { componentName, componentPath } );
this._writeFile( this.templatePath(TEST_TEMPLATE), this.destinationPath(TEST_DIR, `${componentName}.test.jsx`), { componentName } );
this._addToSectionTypes(); this._addToSectionMapping(); }};
复制代码


你可以想象一下,原先需要一个下午才能完成的工作现在只需要 2 到 3 分钟就可以完成。

使用 AST Explorer 了解如何编辑现有文件

Yeoman 生成器最困难的部分是如何编辑现有文件,不过,借助抽象语法树(AST)转换,这个任务变得更加容易。


以下是我们如何实现 Sections.tsx 的转换:


const babylon = require('babylon');const traverse = require('babel-traverse').default;const t = require('babel-types');const generate = require('babel-generator').default;
module.exports = class ComponentGenerator extends Generator { _updateFile(filePath, transformObject) { const source = this.fs.read(filePath); const ast = babylon.parse(source, { sourceType: 'module' }); traverse(ast, transformObject); const { code } = generate(ast, {}, source); this.fs.write(this.destinationPath(filePath), prettier.format(code, PRETTER_CONFIG)); } _addToSectionMapping() { const { componentName } = this.data; const newKey = `[SECTION_TYPES.${_.snakeCase(componentName).toUpperCase()}]`; this._updateFile(SECTION_MAPPING, { Program({ node} ) { const newImport = t.importDeclaration( [t.importDefaultSpecifier(t.identifier(componentName))], t.stringLiteral(`./sections/${componentName}`) ); node.body.splice(6,0,newImport); }, ObjectExpression({ node }) { // ignore the tagged template literal if(node.properties.length > 1){ node.properties.push(t.objectTypeProperty( t.identifier(newKey), t.identifier(componentName) )); } }, TaggedTemplateExpression({node}) { const newMemberExpression = t.memberExpression( t.memberExpression( t.identifier(componentName), t.identifier('fragments') ), t.identifier('fields') ); node.quasi.expressions.splice(2,0,newMemberExpression);
const newFragmentLine = ` ...${componentName}Fields`; const fragmentQuasi = node.quasi.quasis[0]; const fragmentValue = fragmentQuasi.value.raw.split('\n'); fragmentValue.splice(3,0,newFragmentLine); const newFragmentValue = fragmentValue.join('\n'); fragmentQuasi.value = {raw: newFragmentValue, cooked: newFragmentValue}; const newLinesQuasi = node.quasi.quasis[3]; node.quasi.quasis.splice(3,0,newLinesQuasi); } }); }};
复制代码


_updateFile 是使用 Babel 进行 AST 转换的样板代码。这里最关键的是_addToSectionMapping,并且你可以看到:


  • 它在程序层面插入了一个新的导入声明。

  • 在两个对象表达式中,具有多个属性的那个是 section 映射,我们将在那里插入一个键值对。

  • gql 片段是标记模板字面量,我们想在那里插入 2 行,第一行是成员表达式,第二行是“quasi”表达式中的一个。


如果执行转换的代码看起来令人生畏,我只能说,这对我来说也是如此。在写这些转换代码之前,我也还没用过 quasi。


好在 AST Explorer 可以很容易地解决这类问题。这是同一个转换在 Explorer 中的显示。在四个窗格中,左上角包含源文件,右上角包含已解析的树,左下角包含建议的变换,右下角包含变换后的结果。


通过查看解析后的树,你就知道如何应用转换和测试它们了。

从 Zeplin 或 Figma 中提取模拟内容

Zeplin 和 Figma 的出现都是为了让工程师能够直接提取内容来提升产品开发效率。



如上所示,要提取整个段落的副本,只要在 Zeplin 中选择内容,并单击侧栏中的“复制”图标。



在 Zeplin 中,可以先选择图像,并单击侧栏“Assets”里的“下载”图标来提取图像。

自动化照片处理

照片处理管道肯定是 Airbnb 特有的。我想要强调的是 Brie 创建的用来包装现有 API 端点的“Media Squirrel”。如果没有 Media Squirrel,我们就没有这么好的方法可以将我们机器上的原始图像转换为 JSON 对象,更不用说可以使用静态 URL 作为图像的源。

在 Apollo Server 中拦截 schema 和数据

这部分工作仍在进行中,还不能作为最终的 API。我们想要做的是拦截和修改远程 schema 和远程响应。因为虽然远程服务是事实的来源,但我们希望能够在规范化上游服务 schema 变更之前对产品进行迭代。


因为 Apollo 近期路线图中包含了 Schema Composition 和 Distributed Execution,所以我们没有详细地解释所有细节,只是提出了基本概念。


实际上,Schema Composition 允许我们定义类型,并像下面这样执行某些操作:


type SingleMedia {  captions: [String]  media: [LuxuryMedia]  fullBleed: Boolean}  extend type EditorialContent {  SingleMedia}
复制代码


在这种情况下,schema 知道 EditorialContent 是一个联合,因此通过扩展它,它真的可以知道另一种可能的类型。


将 Berzerker 响应代码修改如下:


import { alpsPool, alpsChopper, alpsDessert, alpsCloser } from './data/sections/SingleMediaMock';
const mocks: { [key: string]: (o: any) => any } = { Journey: (journey: any) => ({ ...journey, editorialContent: [ ...journey.editorialContent.slice(0, 3), alpsPool, ...journey.editorialContent.slice(3, 9), alpsChopper, ...journey.editorialContent.slice(9, 10), alpsDessert, ...journey.editorialContent.slice(10, 12), alpsCloser, ...journey.editorialContent.slice(12, 13), ], }),};
export default mocks;
复制代码


这里并没有使用 mock 填补 API 的空白,而是让它们保持原样,并根据你提供的东西对内容进行覆盖。

结论

Apollo CLI 负责处理所有与 Apollo 相关的事情,让你能够以更有意义的方式连接这些实用程序。其中一些用例(如类型的代码生成)是通用的,并且最终成为整个基础设施的一部分。


更多内容,请关注前端之巅(ID:frontshow)



英文原文:https://medium.com/airbnb-engineering/how-airbnb-is-moving-10x-faster-at-scale-with-graphql-and-apollo-aa4ec92d69e2


公众号推荐:

2024 年 1 月,InfoQ 研究中心重磅发布《大语言模型综合能力测评报告 2024》,揭示了 10 个大模型在语义理解、文学创作、知识问答等领域的卓越表现。ChatGPT-4、文心一言等领先模型在编程、逻辑推理等方面展现出惊人的进步,预示着大模型将在 2024 年迎来更广泛的应用和创新。关注公众号「AI 前线」,回复「大模型报告」免费获取电子版研究报告。

AI 前线公众号
2019-01-04 09:4811396
用户头像

发布了 731 篇内容, 共 433.3 次阅读, 收获喜欢 1997 次。

关注

评论

发布
暂无评论
发现更多内容

手撸二叉树之递增顺序搜索树

HelloWorld杰少

数据结构与算法 8月日更

Seata TCC模式原理与实战

码农参上

分布式事务 seata SpringCloud Alibaba 8月日更

netty系列之:自定义编码解码器

程序那些事

Java Netty 程序那些事

Python入门:ChainMap 有效管理多个上下文

华为云开发者联盟

Python 字典 上下文 映射 ChainMap

能源区块链研究 | 加密行业碳抵消有助于大众接纳比特币吗?

CECBC

LeetCode题解:220. 存在重复元素 III,暴力法,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

用Java仿一个低配版的Everything软件

Regan Yue

Java 8月日更 Everything

Excelize 发布 2.4.1 版本,新增并发安全支持

xuri

Excel Go 语言 Excelize #Github

趣说开源|学生如何参与开源社区?

SphereEx

数据库 开源

区块链技术:为什么说波卡能加速区块链行业的发展?

CECBC

OpenYurt 联手 eKuiper,解决 IoT 场景下边缘流数据处理难题

阿里巴巴云原生

云计算 阿里云 开源 云原生 中间件

Fastdata for TSDB: SQL使时序数据可扩展

数据库 大数据 时序数据库 tsdb 数据智能

百亿级分布式文件系统之元数据设计

焱融科技

云计算 技术 分布式 高性能 文件存储

“遇见”未来“编程”语言,面向组件编程,送给在校学生

清风

Java 小程序 毕业设计

高并发中,那些不得不说的线程池与ThreadPoolExecutor类

华为云开发者联盟

Java 线程 高并发 线程池 ThreadPoolExecutor类

为什么区块链是互联网的100倍?

CECBC

导播上云,把 “虚拟演播厅” 搬到奥运村

阿里云视频云

阿里云 视频处理 视频直播 视频云 云导播

零代码以“王者荣耀”为例解析设计七原则

华为云开发者联盟

软件 设计原则 王者荣耀 单一职责

【LeetCode】有效的字母异位词Java题解

Albert

算法 LeetCode 8月日更

架构实战营-模块二作业

俞立夫

架构实战营

基于java springboot体育馆预约微信小程序源码(毕设)设计开发

清风

Java 小程序 源码 毕业设计

Go语言:如何通过Go来更好的开发并发程序 ?

微客鸟窝

Go 语言

Compose 中的主题

Changing Lin

8月日更

docker的使用

Rubble

8月日更

FastApi-15-文件上传-3

Python研究所

FastApi 8月日更

数据加密和BCrypt哈希算法应用 | StartDT Tech Lab 15

奇点云

出现吧,Python Web 菜谱系统的首页,不会前端技术,也能做

梦想橡皮擦

8月日更

【Vue2.x 源码学习】第三十七篇 - 组件部分 - 组件的合并

Brave

源码 vue2 8月日更

智能时代的信任口诀:让计算远离算计

白洞计划

如何将知识引入机器学习模型提升泛化能力?

华为云开发者联盟

机器学习 算法 数据 模型 物理学

Spark RDD模型

布兰特

spark

开发速度快10倍!Airbnb用GraphQL+Apollo做到了_语言 & 开发_Adam Neary_InfoQ精选文章