## 聯合服務
[<span style="color:red">Apollo 聯合服務</span>](https://www.apollographql.com/docs/federation/)提供了一種將單體式 GraphQL 服務器拆分為獨立微服務的手段。它有兩個組成部分:一個網關和一或多個聯合微服務。每個微服務都持有部分 schema,網關將這些 schema 合并為一個可以被客戶端使用的 schema。
引用[<span style="color:red">Apollo 文檔</span>](https://www.apollographql.com/blog/announcement/apollo-federation-f260cf525d21/),聯合服務的設計遵循以下核心原則:
- 構建圖表應該是**聲明式**的。使用聯合服務,你可以在 schema 內部聲明式地組合圖表,而不是編寫命令式 schema 拼接代碼。
- 代碼應該按**關注點**分割,而不是按類型。通常沒有一個團隊能控制像 User 或 Product 這種重要類型的各個方面,因此這些類型的定義應該分布在團隊和代碼庫中,而不是寫在一起。
- 圖表應盡可能簡單,以讓客戶端使用。同時,聯合服務可以形成一個完整的、以產品為中心的圖表,準確地反映它在客戶端的使用情況。
- 它只是 GraphQL,僅使用符合規范的語言特性。任何語言,不僅僅是 JavaScript,都可以實現聯合服務。
> Apollo 聯合服務到目前為止還不支持訂閱。
在接下來的例子中,我們將設置一個帶有網關和兩個聯合端點的演示程序:一個 Users 服務和一個 Posts 服務,
### 聯合示例:Users
首先,安裝聯合服務的依賴包:
```bash
npm install --save @apollo/federation @apollo/subgraph
```
### 模式優先
Users 服務有一個簡單的 schema。注意 `@key` 這個指令:它告訴 Apollo 查詢規劃器,如果你有它的 `id`,則可以獲取特定的 User 實例。另外,請注意我們也要繼承這個 `Query` 類型。
```graphql
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
```
我們的解析器有一個額外的方法:`resolveReference()`。每當相關資源需要 User 實例時,它就會被 Apollo 網關調用。我們在后面的 Posts 服務中也會看到這個例子。請注意 `@ResolveReference()` 這個裝飾器。
```typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Resolver('User')
export class UsersResolvers {
constructor(private usersService: UsersService) {}
@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}
```
最后,我們在模塊中使用 `GraphQLFederationModule` 將所有東西連接起來。此模塊接收與常規的 `GraphQLModule` 相同的配置。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { UsersResolvers } from './users.resolvers';
@Module({
imports: [
GraphQLFederationModule.forRoot({
typePaths: ['**/*.graphql'],
}),
],
providers: [UsersResolvers],
})
export class AppModule {}
```
### 代碼優先
代碼優先聯合服務與常規的代碼優先 GraphQL 很像。我們只需添加一些額外的裝飾器到 `User` 實體即可。
```typescript
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;
@Field()
name: string;
}
```
我們的解析器有一個額外的方法:`resolveReference()`。每當相關資源需要 User 實例時,它就會被 Apollo 網關調用。我們在后面的 Posts 服務中也會看到這個例子。請注意 `@ResolveReference()` 這個裝飾器。
```typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsersResolvers {
constructor(private usersService: UsersService) {}
@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}
```
最后,我們在模塊中使用 `GraphQLFederationModule` 將所有東西連接起來。此模塊接收與常規的 `GraphQLModule` 相同的配置。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { UsersResolvers } from './users.resolvers';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLFederationModule.forRoot({
autoSchemaFile: true,
}),
],
providers: [UsersResolvers, UsersService],
})
export class AppModule {}
```
### 聯合示例:Posts
我們的 Post 服務通過 `getPosts` 查詢提供文章聚合,同時也使用 `user.posts` 來擴展我們的 `User` 類型。
### 模式優先
Posts 服務在它的 schema 中通過用 `extend` 關鍵字標記來引用 User 類型。它還向 User 類型添加了一個屬性。請注意用于匹配 User 實例的 `@key` 指令,以及指示 `id` 字段在別處管理的 `@external` 指令。
```graphql
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}
extend type Query {
getPosts: [Post]
}
```
在我們的解析器這里有一個有趣的方法:`getUser()`。它返回一個引用,其中包含 `__typename` 和應用程序解析引用所需的任何其他屬性,在這個例子中僅是一個屬性 `id`。`__typename`被 GraphQL 網關用來精確定位負責 User 類型和請求實例的微服務。上面討論的 Users 服務將在 `resolveReference()` 方法上被調用。
```typescript
import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';
@Resolver('Post')
export class PostsResolvers {
constructor(private postsService: PostsService) {}
@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}
@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}
```
Posts 服務幾乎具有和 Users 相同的模塊,但為了完整起見,我們在下面將它包含進來:
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { PostsResolvers } from './posts.resolvers';
@Module({
imports: [
GraphQLFederationModule.forRoot({
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}
```
### 代碼優先
我們需要創建一個代表我們的 User 實體的類。即使它存在于其他服務中,我們也將使用和繼承它。注意 `@extends` 和 `@external` 指令。
```typescript
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [Post])
posts?: Post[];
}
```
我們在 `User` 實體上為我們的擴展創建解析器,如下所示:
```typescript
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => User)
export class UsersResolvers {
constructor(private readonly postsService: PostsService) {}
@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
```
我們還需要創建我們的 `Post` 實體:
```typescript
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field((type) => Int)
authorId: number;
@Field((type) => User)
user?: User;
}
```
還有它的解析器:
```typescript
import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => Post)
export class PostsResolvers {
constructor(private readonly postsService: PostsService) {}
@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
```
最后,在模塊中把它們串聯起來。注意 schema 構建配置,在這里我們指定 `User` 為外部類型。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example
@Module({
imports: [
GraphQLFederationModule.forRoot({
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolvers, UsersResolvers, PostsService],
})
export class AppModule {}
```
### 聯合示例:網關
首先,安裝網關的依賴包:
```bash
$ npm install --save @apollo/gateway
```
我們的網關只需要一個端點列表,它會從那里自動發現所有的 schemas。因為代碼和模式優先是一樣的,所以網關的代碼很短:
```typescript
import { Module } from '@nestjs/common';
import { GraphQLGatewayModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLGatewayModule.forRoot({
server: {
// ... Apollo server options
cors: true,
},
gateway: {
serviceList: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
},
}),
],
})
export class AppModule {}
```
代碼優先模式和架構優先模式在此處提供了[一個](https://github.com/nestjs/nest/tree/master/sample/32-graphql-federation-schema-first/gateway)[工作](https://github.com/nestjs/nest/tree/master/sample/31-graphql-federation-code-first/gateway)示例。[](https://github.com/nestjs/nest/tree/master/sample/32-graphql-federation-schema-first/gateway)
> Apollo 建議你不要依賴生產環境中的服務發現,而是使用它們的[圖表管理器](https://www.apollographql.com/docs/federation/managed-federation/overview/)
### 與 ` Mercurius`聯合
首先安裝所需的依賴項:
~~~bash
$ npm install --save @apollo/subgraph @nestjs/mercurius
~~~
> **筆記**:`@apollo/subgraph` 包是構建子圖模式(`buildSubgraphSchema`、`printSubgraphSchema` 函數)所必需的。
#### 架構優先
“用戶服務”提供了一個簡單的模式。請注意`@key` 指令:它指示 Mercurius 查詢計劃器,如果您指定其 id,則可以獲取特定的 User 實例。另外,請注意我們擴展了 `Query` 類型。
~~~graphql
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
~~~
`Resolver` 提供了另一種名為 `resolveReference()` 的方法。 每當相關資源需要用戶實例時,此方法由 Mercurius 網關觸發。 稍后我們將在 `Posts` 服務中看到一個這樣的例子。 請注意,該方法必須使用 `@ResolveReference()` 裝飾器進行注釋。
~~~typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Resolver('User')
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}
~~~
最后,我們通過在配置對象中注冊傳遞 `MercuriusFederationDriver` 驅動程序的 `GraphQLModule` 來連接所有內容:
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
typePaths: ['**/*.graphql'],
federationMetadata: true,
}),
],
providers: [UsersResolver],
})
export class AppModule {}
~~~
### 聯合示例:Posts
我們的 Post 服務通過 `getPosts` 查詢提供文章聚合,同時也使用 `user.posts` 來擴展我們的 `User` 類型。
#### 架構優先
`“Posts service”`通過用 extend 關鍵字標記它來引用其架構中的用戶類型。 它還在用戶類型(帖子)上聲明了一個附加屬性。 請注意用于匹配 `User `實例的 `@key` 指令,以及指示 id 字段在其他地方管理的 `@external `指令。
~~~graphql
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}
extend type Query {
getPosts: [Post]
}
~~~
在下面的示例中,PostsResolver 提供了 getUser() 方法,該方法返回包含 __typename 的引用和您的應用程序可能需要解析引用的一些附加屬性,在本例中為 id。 __typename 被 GraphQL 網關用來查明負責用戶類型的微服務并檢索相應的實例。 執行 resolveReference() 方法時將請求上述“用戶服務”。
~~~typescript
import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';
@Resolver('Post')
export class PostsResolver {
constructor(private postsService: PostsService) {}
@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}
@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}
~~~
最后,我們必須注冊 `GraphQLModule`,類似于我們在“用戶服務”部分中所做的。
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
federationMetadata: true,
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}
~~~
#### 代碼優先
首先,我們必須聲明一個代表用戶實體的類。 盡管實體本身存在于另一個服務中,但我們將在此處使用它(擴展其定義)。 注意@extends 和@external 指令。
~~~ts
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [Post])
posts?: Post[];
}
~~~
現在讓我們在 User 實體上為我們的擴展創建相應的解析器,如下所示:
~~~ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}
@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
~~~
我們還必須定義 Post 實體類:
~~~ts
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field((type) => Int)
authorId: number;
@Field((type) => User)
user?: User;
}
~~~
及其解析器:
~~~ts
import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
~~~
最后,將其捆綁在一個模塊中。 請注意架構構建選項,我們在其中指定 User 是孤立(外部)類型。
~~~ts
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}
~~~
### 聯合示例:網關
網關需要指定一個端點列表,它將自動發現相應的模式。因此,對于代碼優先和架構優先方法,網關服務的實現將保持不變。
~~~typescript
import {
MercuriusGatewayDriver,
MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusGatewayDriverConfig>({
driver: MercuriusGatewayDriver,
gateway: {
services: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
},
}),
],
})
export class AppModule {}
~~~
### 共享上下文
你可以通過一個構建服務來自定義網關和聯合服務之間的請求。這讓你能夠共享有關請求的上下文。你能輕松繼承默認的 `RemoteGraphQLDataSource` 并實現其中一個鉤子。有關可能性的更多信息,請參閱 [Apollo 文檔](https://www.apollographql.com/docs/federation/api/apollo-gateway/#class-remotegraphqldatasource)中的 `RemoteGraphQLDataSource` 章節.
```typescript
import { Module } from '@nestjs/common';
import { GATEWAY_BUILD_SERVICE, GraphQLGatewayModule } from '@nestjs/graphql';
import { RemoteGraphQLDataSource } from '@apollo/gateway';
import { decode } from 'jsonwebtoken';
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
async willSendRequest({ request, context }) {
const { userId } = await decode(context.jwt);
request.http.headers.set('x-user-id', userId);
}
}
@Module({
providers: [
{
provide: AuthenticatedDataSource,
useValue: AuthenticatedDataSource,
},
{
provide: GATEWAY_BUILD_SERVICE,
useFactory: (AuthenticatedDataSource) => {
return ({ name, url }) => new AuthenticatedDataSource({ url });
},
inject: [AuthenticatedDataSource],
},
],
exports: [GATEWAY_BUILD_SERVICE],
})
class BuildServiceModule {}
@Module({
imports: [
GraphQLGatewayModule.forRootAsync({
useFactory: async () => ({
gateway: {
serviceList: [
/* services */
],
},
server: {
context: ({ req }) => ({
jwt: req.headers.authorization,
}),
},
}),
imports: [BuildServiceModule],
inject: [GATEWAY_BUILD_SERVICE],
}),
],
})
export class AppModule {}
```
#### 代碼優先
首先向 User 實體添加一些額外的裝飾器。
~~~ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;
@Field()
name: string;
}
~~~
`Resolver` 提供了另一種名為 `resolveReference()` 的方法。 每當相關資源需要用戶實例時,此方法由 `Mercurius` 網關觸發。 稍后我們將在 `Posts` 服務中看到一個這樣的例子。 請注意,該方法必須使用 `@ResolveReference()` 裝飾器進行注釋。
~~~ts
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}
~~~
最后,我們通過在配置對象中注冊傳遞 MercuriusFederationDriver 驅動程序的 GraphQLModule 來連接所有內容:
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
~~~
### 異步配置
聯合服務和網關模塊都支持使用同樣的 `forRootAsync` 異步初始化,相關文檔詳見[快速開始](/8/graphql?id=async-配置)。
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?