JavaScript >> Javascript 文檔 >  >> JavaScript

如何在 JavaScript 中編寫和組織 GraphQL Schema

如何使用文件夾和文件結構編寫 GraphQL 架構,以減少理解和維護的負擔。

在一個使用 GraphQL 作為其數據層的應用程序中——也就是你的應用程序用來檢索和操作數據的東西——架構是客戶端和服務器之間的關鍵。

雖然 GraphQL 中的模式確實有關於如何編寫它們的規則,但沒有關於如何組織的任何規則 他們。在大型項目中,組織是保持事情順利進行的關鍵。

開始使用

對於本教程,我們將使用 CheatCode Node.js 樣板作為起點。這將使我們能夠訪問已附加模式的正常運行的 GraphQL 服務器。我們將修改該架構並討論其組織結構,以幫助您了解自己的 GraphQL 架構的組織結構。

首先,讓我們從 Github 克隆一份樣板:

終端

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

接下來,cd 進入樣板文件並安裝其依賴項:

終端

cd nodejs-server-boilerplate && npm install

安裝好依賴後,現在我們可以啟動開發服務器了:

終端

npm run dev

有了這個,我們就可以開始了。

設置基本文件夾結構

在使用 GraphQL 的應用程序中,有兩個核心部分:您的 GraphQL 架構和您的 GraphQL 服務器(獨立於您的 HTTP 服務器)。架構已附加 到服務器,以便當請求進來時,服務器知道如何處理它。

因為這兩個部分協同工作,所以最好將它們放在一起。在我們剛剛克隆的示例項目中,這些都放在/api/graphql中 目錄。這裡,/api 目錄包含描述我們應用程序中不同類型數據的文件夾。組合後,我們的架構和服務器代表 GraphQL API 為我們的應用程序(因此位置)。

在那個文件夾裡面——/api/graphql — 我們將架構和服務器聲明分成兩個文件:/api/graphql/schema.js/api/graphql/server.js .我們前進的重點將放在 schema 這個等式的一部分,但如果您想了解有關設置 GraphQL 服務器的更多信息,我們建議您閱讀有關設置 GraphQL 服務器的其他 CheatCode 教程。在結束之前,我們將討論如何將我們寫入的模式附加到 GraphQL 服務器。

組織類型、查詢解析器和變異解析器

接下來,我們組織模式的核心部分將是我們如何在 GraphQL API 中分離不同的類型、查詢解析器和突變解析器。在我們的示例項目中,建議的結構是將所有內容組織在 /api 我們之前了解的目錄。在該文件夾中,每個數據“主題”都應該有自己的文件夾。 “主題”描述數據庫中的集合或表,第三方 API(例如,/api/google ),或應用中任何其他不同類型的數據。

├── /api
│   ├── /documents
│   │   ├── /graphql
│   │   │   ├── mutations.js
│   │   │   ├── queries.js
│   │   │   └── types.js

對於 GraphQL,在主題文件夾中,我們添加了一個 graphql 文件夾來組織該主題的所有與 GraphQL 相關的文件。在上面的示例結構中,我們的主題是 documents .對於這個主題,在 GraphQL 的上下文中,我們有一些自定義類型(types.js )、查詢解析器 (queries.js ) 和變異解析器 (mutations.js )。

/api/documents/graphql/types.js

const DocumentFields = `
  title: String
  status: DocumentStatus
  createdAt: String
  updatedAt: String
  content: String
`;

export default `
  type Document {
    _id: ID
    userId: ID
    ${DocumentFields}
  }

  enum DocumentStatus {
    draft
    published
  }

  input DocumentInput {
    ${DocumentFields}
  }
`;

在我們的 types.js 文件,我們導出一個字符串,使用 backtics `` 定義 這樣我們就可以利用 JavaScript(從標準的 ES6 版本開始)的字符串插值(允許我們在字符串中包含和解釋 JavaScript 表達式)。在這裡,作為一種組織技術,當我們有一組跨多種類型使用的屬性時,我們將這些字段提取到一個字符串中(使用反引號定義,以防我們需要進行任何插值)並將它們存儲在頂部的變量中我們的文件(這裡,DocumentFields )。

然後,利用該插值,我們連接我們的 DocumentFields 在導出字符串中返回的類型中使用它們的地方。這使得當我們的類型最終被導出時,“共享”字段被添加到我們定義的類型中(例如,這裡的 type Document 將具有 DocumentFields 中的所有屬性 上定義)。

/api/documents/graphql/queries.js

import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";

export default {
  documents: async (parent, args, context) => {
    return Documents.find({ userId: context.user._id }).toArray();
  },
  document: async (parent, args, context) => {
    await isDocumentOwner(args.documentId, context.user._id);

    return Documents.findOne({
      _id: args.documentId,
      userId: context.user._id,
    });
  },
};

查看我們的 queries.js 下一個文件,在這裡我們存儲與文檔主題相關的查詢的所有解析器函數。為了幫助組織,我們將所有解析器函數組合在一個對像中(在 JavaScript 中,定義在對像上的函數稱為 方法 ) 並從文件中導出該父對象。稍後當我們將類型和解析器導入架構時,我們會看到為什麼這很重要。

/api/documents/graphql/mutations.js

import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";

export default {
  documents: async (parent, args, context) => {
    return Documents.find({ userId: context.user._id }).toArray();
  },
  document: async (parent, args, context) => {
    await isDocumentOwner(args.documentId, context.user._id);

    return Documents.findOne({
      _id: args.documentId,
      userId: context.user._id,
    });
  },
};

關於結構,mutations.js 等同於 queries.js .這裡唯一的區別是 這些 解析器函數負責解析突變而不是查詢。雖然我們可以 將我們的查詢和變異解析器組合成一個 resolvers.js 文件,將它們分開使維護更容易一些,因為解析器功能之間沒有固有的區別。

接下來,準備好這些文件,為了使用它們,我們需要將它們的內容導入並添加到我們的架構中。

將類型、查詢解析器和突變解析器導入並添加到架構中

現在我們了解瞭如何組織構成我們模式的各個部分,讓我們將它們組合在一起,這樣我們就有了一個功能模式。讓我們看一下示例項目中的架構,看看它是如何映射回我們上面創建的文件的。

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: {
    Query: {
      ...DocumentQueries,
    },
    Mutation: {
      ...DocumentMutations,
    },
  },
};

export default makeExecutableSchema(schema);

希望這開始有意義。您在上面看到的內容與您在此代碼塊頂部的文件路徑中找到的內容略有不同。不同之處在於,在這裡,我們提取了與用戶相關的模式部分,以使我們之前創建的部分如何組合在一起(這些部分包含在我們從 Github 克隆的項目中)。

從文件頂部開始,為了創建我們的模式,我們導入 gql graphql-tag 中的標記 包(已經作為我們之前克隆的項目中依賴項的一部分安裝)。 gql 表示一個函數,它接收一個字符串,該字符串包含用 GraphQL DSL(領域特定語言)編寫的代碼。這是 GraphQL 獨有的特殊語法。因為我們在 JavaScript 中使用 GraphQL,所以我們需要一種在 JavaScript 中解釋該 DSL 的方法。

gql 此處的函數將我們傳遞給它的字符串轉換為 AST 或抽象語法樹。這是一個大型 JavaScript 對象,表示我們傳遞給 gql 的字符串內容的技術映射 .稍後,當我們將架構附加到我們的 GraphQL 服務器時,那個 服務器實現將預測並了解如何解析該 AST。

如果我們看一下 gql 在上面的文件中使用,我們看到它分配給了 typeDefs 我們存儲在 schema 中的對象的屬性 多變的。在架構中,typeDefs 描述服務器的查詢和變異解析器返回的數據的形狀,並定義可以執行的查詢和變異。

類型有兩種變體:描述應用中數據的自定義類型和 root 類型。根類型是 GraphQL 保留用於描述 字段 的內置類型 可用於查詢和突變。更具體地說,如果我們查看上面的代碼,type Querytype Mutation 塊是可用的三種根類型中的兩種(第三種是 type Subscription 用於向 GraphQL 服務器添加實時數據)。

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    [...]
  `,
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

利用我們之前編寫的自定義類型(在 /api/documents/graphql/types.js 文件),在我們的 schema.js 頂部 文件在這裡,我們將類型導入為 DocumentTypes .接下來,在我們調用 gql 之後的反引號內 (我們分配給 typeDefs 的值 ),我們使用 JavaScript 字符串插值將我們的類型連接成我們傳遞給 typeDefs 的值 .這樣做的目的是將我們的自定義類型“加載”到我們的 GraphQL 架構中。

接下來,為了定義我們可以運行哪些查詢和突變,我們需要在根 type Query 內定義查詢字段和突變字段 和 type Mutation 類型。兩者的定義方式相同。我們指定我們希望映射到模式中解析器函數的字段的名稱。可選地,我們還描述了可以從客戶端傳遞給該字段的參數或參數。

/api/graphql/schema.js

[...]

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

在這裡,在 type Query 下 , document(documentId: ID!): Document 正在說“定義一個將由名為 document 的解析器函數解析的字段 這需要 documentId 作為標量類型傳遞 ID 並期望它以 type Document 的形式返回數據 類型(作為 ${DocumentTypes} 的一部分添加到我們的架構中 我們連接到 typeDefs 的行 就在對 gql 的調用中 )。我們對希望在 type Query 下查詢的每個字段重複此操作 .

我們在 type Mutation 下使用相同的規則重複相同的模式 .就像我們之前討論的那樣,這裡唯一的區別是這些字段描述了 mutations 我們可以運行,而不是查詢。

添加查詢和變異解析器

現在我們已經在根 type Query 中指定了自定義類型和字段 和根 type Mutation ,接下來,我們需要添加 resolve 的解析器函數 我們在那裡定義的查詢和突變。為此,在文件頂部,我們導入單獨的 queries.jsmutations.js 文件(記住,這些是導出 JavaScript 對象)為 DocumentQueriesDocumentMutations .

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: {
    Query: {
      ...DocumentQueries,
    },
    Mutation: {
      ...DocumentMutations,
    },
  },
};

export default makeExecutableSchema(schema);

接下來,在 resolvers 我們分配給 schema 的對象的屬性 變量,我們嵌套兩個屬性:QueryMutation .這些名稱對應於我們在 typeDefs 中定義的根類型 堵塞。這裡,與根 type Query 關聯的解析器 在 resolvers.Query 中設置 與根 type Mutation 關聯的對象和解析器 在 resolvers.Mutation 中設置 目的。因為我們導出了我們的 DocumentQueriesDocumentMutations 作為對象,我們可以使用 ... 在此處“解包”這些對象 在 JavaScript 中傳播語法。

顧名思義,這會將這些對象的內容“展開”到父對像上。一旦被 JavaScript 解釋,這段代碼將有效地實現:

{
  typeDefs: [...],
  resolvers: {
    Query: {
      documents: async (parent, args, context) => {
        return Documents.find({ userId: context.user._id }).toArray();
      },
      document: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        return Documents.findOne({
          _id: args.documentId,
          userId: context.user._id,
        });
      },
    },
    Mutation: {
      createDocument: async (parent, args, context) => {
        const _id = generateId();

        await Documents.insertOne({
          _id,
          userId: context.user._id,
          ...args.document,
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        });

        return {
          _id,
        };
      },
      updateDocument: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        await Documents.updateOne(
          { _id: args.documentId },
          {
            $set: {
              ...args.document,
              updatedAt: new Date().toISOString(),
            },
          }
        );

        return {
          _id: args.documentId,
        };
      },
      deleteDocument: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        await Documents.removeOne({ _id: args.documentId });
      },
    },
  }
}

雖然我們當然可以做到這一點,但將我們的查詢和解析器拆分為主題和它們自己的文件會使維護變得更加容易(並且不那麼繁重)。

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

[...]

const schema = {
  typeDefs: [...],
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

最後,在我們文件的底部,我們導出我們的 schema 變量,但首先包含對 makeExecutableSchema 的調用 .類似於 gql 函數,當我們這樣做時,它將整個模式轉換為 AST(抽象語法樹),可以被 GraphQL 服務器和其他 GraphQL 庫(例如,有助於身份驗證、速率限製或錯誤處理的 GraphQL 中間件函數)理解)。

從技術上講,有了所有這些,我們就有了 GraphQL 模式!總結一下,讓我們看看我們的模式是如何加載到 GraphQL 服務器中的。

將架構添加到 GraphQL 服務器

幸運的是,將模式添加到服務器(一旦定義了服務器)只需要兩行:schema 的導入 來自我們的 /api/graphql/schema.js 文件,然後將其分配給我們服務器的選項。

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({
    schema,
    [...]
  });

  [...]
};

而已!請記住,我們在這里傳遞模式的方式是特定於 Apollo Server 庫的,不一定全部 GraphQL 服務器實現(Apollo 是少數 GraphQL 服務器庫之一)。

總結

在本教程中,我們學習瞭如何組織 GraphQL 模式以簡化維護。我們學習瞭如何將 GraphQL 架構的不同部分解析為單獨的文件,並將這些文件分成與我們的數據直接相關的主題。我們還學習瞭如何將這些單獨的文件組合成一個模式,然後將該模式加載到 GraphQL 服務器中。


Tutorial JavaScript 教程
  1. This or That:使用三元運算符的示例(第 5 部分)

  2. 擴展組件庫及其文檔以實現快速設計系統

  3. 13 個有趣的網絡發現——2013 年 4 月

  4. React Redux 和回顧

  5. JavaScript 面試問題 #23:Array.splice

  6. JavaScript 延遲函數 |簡單的示例代碼

  7. 這是我用來構建我的投資組合網站的所有技術堆棧

  1. 如何使用 Easybase 在 React 和 React Native 中部署動態雲功能

  2. 2個圓圈javascript之間的交集

  3. JavaScript 桌面自動化

  4. 3,000 多字關於為什麼應該使用 Next.js

  5. [第 23 部分] 使用 GraphQL、Typescript 和 React 創建 Twitter 克隆(關注者建議)

  6. 在 Vue.js 中創建生物識別登錄頁面

  7. 使用 RXJS 和 Angular 過濾列表

  1. JS 是什麼解釋型語言?

  2. 10 個帶有 1K UI 組件的 React 包

  3. 如何隱藏 iPhone 上的地址欄?

  4. 面向前端開發人員的 Docker