我們談論了一個具有很多要素的頁面組件,並支持了 GraphQL 的片段共存
首先
你好,初次见面。
我是稻葉,在Nijibox株式会社担任前端工程师。
在之前的工作中,我主要负责WordPress主题的制作,有着想要参与现代化的团队开发的愿望,所以决定转职到Nijibox。
我的这个小小愿望实现了,我被分派到了某个公司的内部系统的新开发项目中。
那个产品的技术架构真的超现代!它让我兴奋到了极致。
-
- フロント: React(Next.js) + TypeScript
-
- ライブラリ: Storybook, Chakra UI, Jest, MSW などなど
-
- バックエンド: Go
-
- API: GraphQL(Apollo)
- インフラ: AWS
然后,我被指派负责这个产品中至关重要的”文件编辑和预览”的巨大页面。
预览界面规格简要介绍

由于这是公司内部系统,所以我们对其进行了马赛克处理。但这个界面是用于预览显示此类文档的图片和表格的界面。(此界面非常长)
在此屏幕上的源代码结构(Fragment Colocation引入之前)
GraphQL 的模式
以下是一个与实际代码不同的示例,但其模式如下所示。
# ドキュメント
type Document {
id: ID
# 情報01
info01: Info01
# 情報02
info02: Info02
# 情報03
info03: Info03
}
# 情報01
type Info01 {
# 色々なデータ
}
# 情報02
type Info02 {
# 色々なデータ
}
# 情報03
type Info03 {
# 色々なデータ
}
实际上,有大量的模式定义一字排开。
客户端
在引入碎片分放前的目录结构
/src/
├── /components/
│ └── /Document/
│ ├── Document.tsx // ページコンポーネント
│ ├── /Info01/
│ │ └── Info01.tsx // 子コンポーネント
│ ├── /Info02/
│ │ └── Info02.tsx // 子コンポーネント
│ └── /Info03/
│ └── Info03.tsx // 子コンポーネント
└── /graphql/
└── getDocument.graphql
GraphQL查詢(Fragment Colocation導入前)
客户端执行的信息查询是记录在/src/graphql/getDocument.graphql文件中的。
# ドキュメント情報を取得するquery
query getDocument($documentId: ID!) {
document(documentId: $documentId) {
info01 {
# 色々なデータ
}
info02 {
# 色々なデータ
}
info03 {
# 色々なデータ
}
}
}
在实际的代码中,getDocument.graphql 是一个超过1000行的巨大查询。
React 组件(在 Fragment Colocation 引入之前)
如您查看目录结构,就会发现这是一个巨大的屏幕,因此页面组件下嵌套了大量的子组件。
事实上,有大约30个子组件排列着…
在引入Fragment Colocation之前的页面组件内容。
export function Document() {
// query でデータを取得している
const { data, error } =
useGetDocumentQuery({
variables: {
documentId: documentId,
},
});
return (
<>
<Info01 savedData={data.info01}/> {/* <Info01 />で使うデータのみをpropsで渡しています */}
<Info02 savedData={data.info02}/> {/* <Info02 />で使うデータのみをpropsで渡しています */}
<Info03 savedData={data.info03}/> {/* <Info03 />で使うデータのみをpropsで渡しています */}
</>
);
}
为了减少查询次数,我们采取在页面组件层级上执行查询,并将获取的数据进行分割,通过props中继传递给子组件的策略。
在引入Fragment Colocation之前的子组件内容
type Info01Props = GetDocumentQuery["info01"] // ← Code Generator が生成するQueryの型からコンポーネントで使うデータの型までネストして指定する必要がある
export function Info01({savedData}: Info01Props) {
return (
<>
<p>{savedData.title}</p>
<p>{savedData.description01}</p>
<p>{savedData.description02}</p>
</>
);
}
关于在客户端使用的类型
我正在使用GraphQL Code Generator来从schema和查询生成TypeScript类型。
我将生成的类型应用到React组件的props中进行开发。
在引入片段定位之前所面临的问题
-
- 由于API的数据被一次性获取到页面组件层而不是被显示数据的子组件获取,所以查询的代码非常长!!!
在子组件的props中使用的类型(由代码生成器生成的类型)形式为Document[“Info01”][“xxxx”][“xxxx”],导致可读性较低!!!
在庞大的查询代码中找到组件中使用的哪个数据需要花费很长时间!!!
“Fragment Colocation是什么?(终于进入正题)”
我不太擅长也不喜欢英语,但“fragment”的意思似乎是“碎片”。
另外,据说co-location的意思是“一起放置”。
在GraphQL中,片段(fragment)是可以将查询分割和共享的功能。
碎片共置(Fragment Colocation)是一种源代码管理思想,即将查询(query)分割为碎片(fragment),然后将使用该碎片获取数据的React组件放置在相同的目录中。
为了完全解决上述问题1到3,我们在这个产品中引入了Fragment Colocation。
顺便提一句,Fragment Colocation这个概念是通过阅读Quramy先生的文章而学到的。
片段配置导入后的源代码结构
文件夾結構(引入碎片共存後)
/src/
└── /components/
└── /Document/
├── Document.tsx // ページコンポーネント
├── getDocument.graphql // queryファイル
├── /Info01/
│ ├── Info01.graphql // Info01 コンポーネントで使うデータを取得するfragmentファイル
│ └── Info01.tsx // 子コンポーネント
├── /Info02/
│ ├── Info02.graphql // Info02 コンポーネントで使うデータを取得するfragmentファイル
│ └── Info02.tsx // 子コンポーネント
└── /Info03/
├── Info03.graphql // Info03 コンポーネントで使うデータを取得するfragmentファイル
└── Info03.tsx // 子コンポーネント
从前,在/graphql/目录下有一个巨大的查询文件,但是引入后,用于组件的查询和片段文件与组件目录一起存放,非常清晰明了。很容易发现过度获取和欠获取的问题!
接下来我们来看一下每个graphql文件的内容。
GraphQL查询+片段(在Fragment Colocation引入后)
query getDocument($documentId: ID!) {
document(documentId: $documentId) {
...info01 # fragmentはJSのスプレッド構文みたいにドットを3つ「...」並べて読み込みます
...info02
...info03
}
}
fragment info01 on Document {
info01 {
# 色々なデータ
}
}
fragment info02 on Document {
info02 {
# 色々なデータ
}
}
fragment info03 on Document {
info03 {
# 色々なデータ
}
}
通过在”on”后指定要作为分割依据的”Type”,可以将查询”query”按照”fragment”进行分割。
此外,您还可以在片段中创建其他片段。
无论对片段进行文件分割与否,原始的查询都将被正确执行。
(请求次数将保持为1次!← 重要)
因此,可以将海量代码量的查询通过分割成片段并分割到不同文件中!
引入 Fragment Colocation 后,子组件的内容。
type Info01Props = Info01Fragment // ← Fragment Colocation導入前の型にあった["info01"]などのネストが無くなっています!!
export function Info01({savedData}: Info01Props) {
return (
<>
<p>{savedData.title}</p>
<p>{savedData.description01}</p>
<p>{savedData.description02}</p>
</>
);
}
在分割后,使用代码生成器可以生成片段的类型(在示例代码中是Info01Fragment),从而可以解决前面提到的问题2。
在组件的目录中,可以存储所使用数据的片段文件,并且由于组件使用了片段的类型,所以变得更加易于理解了。
“Fragment” 是一种可以兼容组件分割思想和在页面组件层执行 API 的方法。
额外的东西
我正在对已经开发了一定程度的巨大屏幕的Fragment Colocation进行适配。
这个重构工作相当不容易,我已经写了大约一个月的片段。
(但只要源代码变得稍微整洁一点,我就会感到非常满足,所以我喜欢这项工作。)
如果大家以后在新产品开发中采用类似此文章所述的技术堆栈和运营策略,我推荐尽早引入Fragment Colocation!