NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

开发速度快 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


公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2019-01-04 09:4811402
用户头像

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

关注

评论

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

情感语音识别:技术发展与未来趋势

来自四九城儿

赋能自然语言处理的强大模型

百度开发者中心

nlp 大模型

人工智能测试新篇章:有限状态机与知识图谱的融合

测吧(北京)科技有限公司

测试

Android和iOS应用程序加固方法详解:混淆、加壳、数据加密、动态加载和数字签名实现

软件测试/测试开发/人工智能丨多维度的测试场景覆盖

测试人

人工智能 软件测试

Curve v2.7 发布:支持 Hadoop SDK,助力大数据存储降本提效背景

OpenCurve

开源 文件存储 分布式存储 块存储

BatchOutput PDF for Mac(PDF批量处理软件) 3.0.6永久激活版

mac

苹果mac Windows软件 BatchOutput PDF PDF批量处理软件

汇总 | 一文了解常用的 NFT 数据分析平台

NFT Research

NFT 数据分析平台 NFT\ NFTScan

糟糕!试用期被裁了

王磊

Java

Authing 入选《 2023 年央国企信创应用与实践研究报告》优秀服务商

Authing

Idaas Authing 第一新声

ITSS会议周丨新华三参编运维国标发布,探索技术服务发展新路径

Geek_2d6073

人工智能 | 知识图谱引领精准测试:人工智能在软件测试的新风向

测吧(北京)科技有限公司

测试

人工智能测试演进:测试覆盖度分析技术的巅峰

测吧(北京)科技有限公司

测试

以微模块+液冷重塑绿色智算中心,新华三亮相CDCC数据中心标准大会

Geek_2d6073

引领Transformer时代的新型大模型架构

百度开发者中心

大模型 LLM

HarmonyOS属性动画开发示例(ArkTS)

HarmonyOS开发者

HarmonyOS

OpenHarmony之NAPI框架介绍

OpenHarmony开发者

Open Harmony

“数字创新产品课程” 2024年1月20-21日 · CSPO认证周末班【提前报名特惠】CST导师亲授

ShineScrum捷行

软件测试 |人工智能在软件测试中的崭新应用

测吧(北京)科技有限公司

测试

戳穿人工智能的六个谎言:辨别真伪

这我可不懂

人工智能 AI

软件测试/测试开发/人工智能丨从面试屡遭失败,到年薪28w

测试人

人工智能 软件测试

使用 Pinia 的五个技巧

高端章鱼哥

Pinia

情感语音识别:挑战与未来发展方向

来自四九城儿

大宗商品贸易集团数据治理实践,夯实数字基座 | 数字化标杆

袋鼠云数栈

数字化转型 数据治理 数据资产 数据开发 大宗贸易

全国独家线下面授 | 上海大规模敏捷LeSS认证2024年3月14-16日开班

ShineScrum捷行

ArcGraph 缓存的设计与应用实践丨技术解读

Fabarta

AI 图数据库 数据库缓存 AI基础设施

如何使用 CSS columns 布局来实现自动分组布局?

伤感汤姆布利柏

CSS 前端

选择云服务器的五大考虑因素:性能、成本与安全性

一只扑棱蛾子

云服务器

WonderPen妙笔 for Mac:优雅高效,写作利器一触即发

晴雯哥

大模型时代的智能运维与部署

百度开发者中心

人工智能 nlp 大模型 LLM

基于 Flink SQL 和 Paimon 构建流式湖仓新方案

阿里云大数据AI技术

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