百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-08-31 03:01 6 浏览 0 评论

在上个月举行的 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';
{1}
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 相关的事情,让你能够以更有意义的方式连接这些实用程序。其中一些用例(如类型的代码生成)是通用的,并且最终成为整个基础设施的一部分。

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码