立即领取|华润集团、宁德核电、东风岚图等 20+ 标杆企业数字化人才培养实践案例 了解详情
写点什么

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


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

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

关注

评论

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

2023百度教育再出发,探索经营增长新空间

彭飞

GLTF-pipeline

3D建模设计

gltf编辑器

如何将 OBJ 模型转换和压缩为 GLTF 以与 AWS IoT TwinMaker 配合使用

3D建模设计

GLTF

如何在Blender中压缩/减小GLTF模型的大小

3D建模设计

blender GLTF

HarmonyOS应用侧与前端页面数据通道建立

HarmonyOS开发者

HarmonyOS

5 个适合出海离岸公司的地区推荐

出海的猹

出海企业

文字图像转换的创新技术

百度开发者中心

#人工智能 生成式AI 千帆大模型平台

Red Giant Magic Bullet Suite for Mac(红巨人调色降噪插件合集下载) v2024.0.0永久激活版

mac

苹果mac Windows软件 Red Giant Magic Bullet 视频后期处理软件

“敏捷教练必修课程”10月14-15日 ·A-CSM认证在线周末班【提前报名特惠】CST导师亲授

ShineScrum捷行

莆田市属于哪个省?有正规等保测评机构吗?

行云管家

等保 等保测评 等保等级保护

预告!网络安全红队GPT大模型训练直播来袭

云起无垠

GLTF文件格式解析与预览、编辑

3D建模设计

GLTF

OpenCloudOS + 英特尔第四代至强处理器:完美适配,加速未来

OpenCloudOS

Linux intel

基于YOLOv2和传感器的多功能门禁系统

timerring

YOLOv2

北京大上科技发布全球首款25.3英寸彩色墨水屏显示器

硬科技星球

创新生产力的新引擎

百度开发者中心

#人工智能 生成式AI 文心一言

解决方案| anyRTC远程检修应用场景

anyRTC开发者

人工智能 音视频 视频会议 远程协助 电话会议

创新力量重塑生产力

百度开发者中心

文学 #人工智能 生成式AI 文心一言

反驳来了!放弃TypeScript?说明你无知!

树上有只程序猿

typescript 代码质量 js

华为云CodeArts Check代码检查服务用户声音反馈集锦(1)

华为云PaaS服务小智

云计算 软件开发 华为云

glTF 中基于物理的渲染(PBR)

3D建模设计

9.19-21,openEuler与您相约2023欧洲开源峰会

openEuler

Linux 开源 操作系统 openEuler

国产化操作系统有哪些?适配国产化操作系统堡垒机哪款好用?

行云管家

信创 堡垒机 国产化 国产操作系统 国产

区块链集成:加密货币网站开发的必备条件

区块链软件开发推广运营

数字藏品开发 dapp开发 区块链开发 链游开发 NFT开发

文心一言 VS 讯飞星火 VS chatgpt (93)-- 算法导论9.2 1题

福大大架构师每日一题

福大大架构师每日一题

运行程序提示路径错误?

矩视智能

深度学习 机器视觉

基于Web的智慧污水厂2D组态系统

2D3D前端可视化开发

组态软件 智慧水务 智慧污水处理 污水厂组态图 污水厂监控系统

HarmonyOS NEXT带来的DevEco Profiler助您轻松分析应用性能问题

Geek_2d6073

生成式AI的发展与内容质量及安全性的挑战

百度开发者中心

#人工智能 生成式AI 文心一言 千帆大模型平台

医院如何实现安全又稳定的跨网文件数据交换呢?

镭速

跨网文件数据交换

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