JavaScript >> Javascript 文檔 >  >> JavaScript

如何使用 Apollo Server 和 Express 設置 GraphQL 服務器

如何使用 Apollo Server 庫和現有的 Express.js 服務器正確配置和處理對 GraphQL 服務器的請求。

開始使用

首先,我們將依賴 CheatCode Node.js Boilerplate。這將為我們提供一個已經設置好的 GraphQL 服務器來使用,並為下面的解釋添加上下文。首先,通過 Github 克隆樣板:

終端

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

接下來,cd 進入克隆的 nodejs-server-boilerplate 目錄並安裝依賴:

終端

cd nodejs-server-boilerplate && npm install

接下來,讓我們手動添加 apollo-server 依賴(這與 apollo-server-express 不同 樣板文件中已經包含的依賴項——我們稍後會看):

終端

npm i apollo-server

完成後,將安裝本教程其餘部分所需的所有依賴項。現在,開始,讓我們看看如何使用 Apollo Server 設置一個基本的 GraphQL 服務器。

設置基礎服務器

首先,我們需要從 apollo-server 導入兩個命名為導出的東西 , ApolloServer 構造函數和 gql 功能。

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

// We'll set up our server here.

為了創建一個服務器,接下來,我們創建一個 ApolloServer 的新實例 與 new ApolloServer()

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`
    type Example {
      message: String
    }

    type Query {
      queryExample: Example
    }

    type Mutation {
      mutationExample: Example
    }
  `,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

我們在這裡添加了很多內容,所以讓我們逐步介紹一下。首先,我們創建一個變量server 並將其設置為調用new ApolloServer()的返回值 .這是我們的 Apollo Server 實例。作為該構造函數的參數來配置我們的服務器,我們傳遞一個具有三個屬性的對象:playground , typeDefs , 和 resolvers .

這裡,playground 被分配一個布爾值 true 告訴 Apollo Server 在 /graphql 啟用 GraphQL Playground GUI 的值 服務器運行時。這是一個方便的工具,用於測試和調試 GraphQL API,而無需編寫一堆前端代碼。通常,最好將 Playground 的使用限制為僅限您的開發 NODE_ENV .為此,您可以設置 playgroundprocess.env.NODE_ENV === 'development' .

接下來,typeDefsresolvers 這裡的屬性一起描述了 schema 用於您的 GraphQL 服務器。前者,typeDefs 是架構的一部分,您可以在其中定義服務器可以處理的可能類型、查詢和突變。在 GraphQL 中,有兩個 root 類型 QueryMutation 可以與您的自定義類型(描述查詢和突變返回的數據的形狀)一起定義,例如 type Pizza {} .

上面,我們已經指定了一個完整的示例模式。首先,請注意我們已經分配了 typeDefs 值等於 gql`` gql() 是一個函數,它需要單個參數作為字符串。這裡的語法(gql 後面沒有括號 ) 是 JavaScript 的一個內置功能,它允許您同時調用一個函數並同時向它傳遞一個字符串值。明確一點,上面等價於gql(´´) .使用此語法要求傳遞的字符串值作為模板文字完成(意思是使用反引號而不是單引號或雙引號定義的字符串)。

gql´´ 函數本身負責獲取包含用 GraphQL DSL(特定領域語言)編寫的代碼的字符串。 DSL 在這裡指的是 GraphQL 語言的獨特語法。在定義我們的模式時,我們可以選擇在 GraphQL DSL 中編寫它。 gql`` 函數接收該字符串並將其從 DSL 轉換為抽象語法樹 (AST),它作為一個對象,以 GraphQL 可以理解的格式描述模式。

在我們傳遞給 gql() 的字符串中 ,首先,我們將數據類型包含為 type Example 它定義了一個自定義 type (不是內置的 QueryMutation types) 描述了一個包含 message 的對象 值應為 String 的字段 .接下來,我們定義根 Query 鍵入和 Mutation 類型。在根 Query 類型,我們定義一個字段 queryExample (我們希望接下來與解析器函數配對)我們希望以 type Example 的形式返回數據 我們剛剛定義。接下來,我們對根 Mutation 執行相同的操作 鍵入,通過添加 mutationExample 並且還期望 type Example 形式的返回值 .

為了讓它工作,我們需要在 resolvers 中實現解析器函數 對象(傳遞給我們的 ApolloServer 構造函數)。請注意,這裡的 resolvers 我們定義了一個 Query 屬性和 Mutation 財產。這些故意模仿 type Query 的結構 和 type Mutation 以上。這裡的想法是函數 resolvers.Query.queryExample 每當在 queryExample 上運行查詢時都會調用 來自客戶端(瀏覽器或本機應用程序)的字段,完成或解析 查詢。

同樣的事情發生在 resolvers.Mutation.mutationExample ,但是在這裡,我們定義了一個 mutation (意思是,我們希望這段代碼改變我們數據源中的一些數據,而不僅僅是從我們的數據源返回一些數據)。請注意,從 queryExample 返回的對象的形狀 解析器和 mutationExample 解析器匹配 type Example 的形狀 我們之前定義的。這樣做是因為,在我們的根 Query 和根 Mutation ,我們已經指定從這些解析器返回的值將採用 type Example 的形式 .

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

最後,使用我們的 typeDefsresolvers 定義好了,我們就可以使用我們的服務器了。為此,我們採用 server 我們之前存儲 Apollo Server 的變量並將其稱為 listen() 返回 JavaScript Promise 的方法(因此 .then() 語法最後被鏈接)。傳遞給 listen() ,我們提供一個帶有單個屬性 port 的選項對象 等於 3000 .這指示 Apollo Server 在 localhost:3000 處監聽入站連接 .

有了這個,我們應該有一個正常運行的 Apollo 服務器。值得注意的是,因為我們正在覆蓋包含的 /api/graphql/server.js 在我們開始的 Node.js 樣板文件中,我們添加了一個 export default () => {} ,導出一個空函數來滿足現有 Express.js 服務器的期望(我們將在教程後面學習如何連接 Apollo 服務器和這個 Express 服務器)。

要對此進行測試,請從樣板的根目錄運行 npm run dev 啟動服務器。公平的警告,因為我們使用這個命令啟動了兩個獨立的服務器(我們剛剛在上面實現的 Apollo 服務器和样板中包含的現有 Express 服務器),你會看到兩條記錄的語句告訴你服務器在不同的端口上運行:

終端

Server running at http://localhost:5001
Server running at http://localhost:3000/

在我們繼續在樣板文件中將這個新的 Apollo 服務器與現有的 Express 服務器結合起來之前,讓我們看看如何為解析器設置自定義上下文。

設置解析器上下文

雖然我們現在在技術上擁有一個正常運行的 GraphQL 服務器(您可以通過訪問 http://localhost:3000/graphql 來驗證這一點 在您的瀏覽器中),最好了解如何設置自定義解析器上下文,因為這在使用 GraphQL 作為主要數據層時會影響用戶身份驗證。

/api/graphql/server.js

import { ApolloServer, gql } from "apollo-server";

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    const token = req?.cookies["jwt_token"];

    const context = {
      req,
      res,
      user: {},
    };

    const user = token ? await authenticationMethod({ token }) : null;

    if (!user?.error) {
      context.user = user;
    }

    return context;
  },
  typeDefs: gql`...`,
  resolvers: { ... },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

export default () => {};

在 GraphQL 中,無論您是執行查詢還是突變,您的解析器函數都會傳遞一個 context 對像作為他們的最終論點。此對象包含向 GraphQL 服務器發出的請求的當前“上下文”。例如,如果用戶登錄到您的應用並執行 GraphQL 請求,我們可能希望在上下文中包含用戶的帳戶信息以幫助我們解決查詢或突變(例如,驗證登錄用戶是否具有正確的訪問該查詢或突變的權限)。

在這裡,除了 playground , typeDefs , 和 resolvers 我們之前添加的屬性,我們添加了 context 設置為一個函數。每當請求進入服務器時,Apollo 服務器會自動調用此函數。它傳遞了一個選項對像作為包含服務器請求 req 的參數 並響應 res 對象(Apollo Server 在內部使用什麼來響應向 GraphQL 服務器發出的 HTTP 請求)。

從該函數中,我們希望返回一個表示 context 的對象 我們希望在所有解析器中都可用的參數。上面,我們提出了一個假設的例子,我們預計 HTTP cookie 被傳遞到服務器(連同 GraphQL 請求)並使用它來驗證用戶。 注意 :這是偽代碼,不會 返回當前狀態的用戶。

為了將用戶分配給上下文對象,我們定義了一個基本的 context 對象優先,其中包含 reqres 從通過 Apollo Server 傳遞給 context 函數的選項對象,並將其與代表我們用戶的空對象結合起來。接下來,我們嘗試使用假設的 jwt_token 來驗證我們的用戶 曲奇餅。同樣,假設這個函數存在,我們希望我們返回一個用戶對象(例如,包含電子郵件地址、用戶名和其他用戶識別數據)。

最後,從 context: () => {} 函數,我們返回 context 對象 我們定義(使用 req , res , 和 user ) 值。

/api/graphql/server.js

import * as apolloServer from "apollo-server";
const { ApolloServer, gql } = apolloServer.default;

const server = new ApolloServer({
  playground: true,
  context: async ({ req, res }) => {
    [...]

    return context;
  },
  typeDefs: gql`...`,
  resolvers: {
    Query: {
      queryExample: (parent, args, context) => {
        console.log(context.user);
        return {
          message: "This is the message from the query resolver.",
        };
      },
    },
    Mutation: {
      mutationExample: (parent, args, context) => {
        console.log(context.user);
        console.log("Perform mutation here before responding.");

        return {
          message: "This is the message from the mutation resolver.",
        };
      },
    },
  },
});

server.listen({ port: 3000 }).then(({ url }) => {
  console.log(`Server running at ${url}`);
});

展示如何在我們的 queryExample 中使用上下文 和 mutationExample 解析器,我們已經註銷了 context.user 我們在上面設置的值。

將 GraphQL 服務器附加到現有的 Express 服務器

到目前為止,我們一直在將 Apollo 服務器設置為獨立 GraphQL 服務器(意思是,我們沒有將它附加到現有服務器)。雖然這可行,但它限制我們的服務器只有一個 /graphql 端點。為了解決這個問題,我們可以選擇將 Apollo 服務器“附加”到現有的 HTTP 服務器。

我們現在要做的是粘貼回 /api/graphql/server.js 的原始源代碼 我們用獨立的 GraphQL 服務器覆蓋了上面的文件:

/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,
    introspection: isDevelopment,
    playground: isDevelopment,
    context: async ({ req, res }) => {
      const token = req?.cookies["app_login_token"];

      const context = {
        req,
        res,
        user: {},
      };

      const user = token ? await loginWithToken({ token }) : null;

      if (!user?.error) {
        context.user = user;
      }

      return context;
    },
  });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

其中一些應該看起來很熟悉。首先,注意不是調用 new ApolloServer() 直接在我們的 /api/graphql/server.js 的正文中 文件,我們已經將該調用包裝在一個期望 app 的函數中 作為論據。這裡,app 表示在 /index.js 處設置的現有 Express.js 服務器 在本教程中我們一直使用的 Node.js 樣板中。

在函數內部(請注意,我們將此函數導出為文件的默認導出),就像我們在上面所做的那樣設置我們的 Apollo 服務器。不過,請注意 typeDefsresolvers 缺少屬性。這些包含在 schema 中 從 ./schema.js 導入的值 /api/graphql/schema.js 的同一目錄中的文件 .

該文件的內容與我們在上面看到的幾乎相同。出於組織目的,它在樣板文件中被分開——這不是 服務於任何技術目的。為了利用該文件,我們使用 JavaScript 擴展運算符 ... 說“解壓導入的schema中包含的對象的內容 值到我們傳遞給 new ApolloServer() 的對像上 ." 作為解包的一部分,typeDefsresolvers 已導入的屬性 對象將被分配回我們傳遞給 new ApolloServer() 的選項 .

在此下方,我們還可以看到添加了一個新屬性 introspection .這與現有的 playground 我們之前看到的屬性——設置為 isDevelopment 的值 , 通過 .app/environment.js 導入的值 來自項目根目錄的文件,並告訴我們我們的 process.env.NODE_ENV 是否 值等於 development (意味著我們在我們的開發環境中運行這段代碼)。

introspection 屬性告訴 Apollo Server 是否允許 GraphQL 客戶端“自省”或發現 GraphQL 服務器提供的類型、查詢、突變等。雖然這對於調試和使用 GraphQL 構建的公共 API 很有幫助,但對於使用 GraphQL 構建的私有 API 來說,這是一個安全風險。

/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({ [...] });

  server.applyMiddleware({
    cors: corsConfiguration,
    app,
    path: "/api/graphql",
  });
};

最後,將我們的 Apollo 服務器插入現有 Express.js 服務器的部分是 server.applyMiddleware() 我們導出函數底部的方法。這需要三個屬性:

  • cors 它描述了 what 的 CORS 配置和權限 允許域訪問 GraphQL 服務器。
  • app 代表我們的現有 Express.js 服務器。
  • path 它描述了我們現有中的哪個URL Express.js 服務器將可訪問 GraphQL 服務器。

對於 cors 屬性,我們使用我們正在使用的 Node.js 樣板中包含的 CORS 中間件(我們將在下一節中詳細介紹)。對於 path ,我們指定我們的 GraphQL 服務器將連接到我們正在運行的服務器(在端口 5001 上啟動 通過運行 npm run dev 從項目的根目錄)在路徑 /api/graphql .換句話說,而不是 http://localhost:3000/graphql 我們之前看到的路徑,現在,我們在現有的 Express.js 服務器上“捎帶”並讓我們的 GraphQL 服務器可以在 that 上訪問 服務器的端口 (5001) 在 http://localhost:5001/api/graphql .

最終結果實際上是相同的——我們通過 Apollo Server 獲得了一個正在運行的 GraphQL 服務器——但我們這樣做不是 在新端口上啟動另一個 HTTP 服務器。

通過外部客戶端連接時處理 CORS 問題

最後,我們需要介紹的最後一個細節是 CORS 配置。就像我們在上一節中看到的那樣,我們依賴於 cors 我們在本教程中使用的 Node.js 樣板中包含的中間件。讓我們在樣板文件中打開該文件並解釋它如何影響我們的 GraphQL 服務器:

/middleware/cors.js

import cors from "cors";
import settings from "../lib/settings";

const urlsAllowedToAccess =
  Object.entries(settings.urls || {}).map(([key, value]) => value) || [];

export const configuration = {
  credentials: true,
  origin: function (origin, callback) {
    if (!origin || urlsAllowedToAccess.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`${origin} not permitted by CORS policy.`));
    }
  },
};

export default (req, res, next) => {
  return cors(configuration)(req, res, next);
};

這看起來比實際更具威脅性。切入正題,這裡的最終目標是告訴瀏覽器的 CORS 檢查(CORS 代表跨源資源共享,並定義哪些 URL 可以訪問服務器)是否發出其請求的 URL(例如,應用我們在 http://myapp.com 運行 ) 可以訪問我們的 GraphQL 服務器。

設置-development.json

{
  [...]
  "urls": {
    "api": "http://localhost:5001",
    "app": "http://localhost:5000"
  }
}

該請求的訪問由 urls 控制 settings-<env>.json 中包含的列表 項目根目錄下的文件。該設置包含一組允許訪問服務器的 URL。在此示例中,我們希望允許訪問現有 Express.js 服務器的相同 URL 訪問我們的 GraphQL 服務器。

這裡,http://localhost:5001 是服務器本身(這意味著它可以在必要時向自身發出請求)和 http://localhost:5000 是我們面向客戶的前端應用程序(我們使用 localhost:5000 因為這是 CheatCode 的 Next.js Boilerplate 運行的默認端口)。

總結

在本教程中,我們學習瞭如何使用 apollo-server 設置 GraphQL 服務器 使用兩種方法打包:將服務器定義為獨立 GraphQL 服務器並將 GraphQL 服務器附加到現有 HTTP 服務器(在本例中為 Express.js 服務器)。

我們還學習瞭如何設置基本的 GraphQL 架構並附加 那個 到我們的服務器,以及如何為我們的解析器定義一個自定義上下文,以處理我們 GraphQL 服務器中的身份驗證等事情。

最後,我們查看了 CORS 配置,並了解了在將 GraphQL 服務器附加到現有服務器時如何控制對它的訪問。


Tutorial JavaScript 教程
  1. 開始了解一些以太坊概念並將主網與 React 應用程序連接起來

  2. 如何保護您的網站免受 Cookie 盜竊和跨站腳本攻擊

  3. WebAssembly 運行時比較

  4. 訂車 (React-Redux)

  5. 使用 Prisma2 和 Photon 製作 GraphQL API

  6. 學習 Redux - 最佳課程概述

  7. Javascript 解釋 |速寫筆記 PART1

  1. 面向未來的 Firefox 的 JavaScript 調試器實現

  2. 讓我們通過 nodejs 製作一個小型 Web 應用程序。

  3. 從 url 中刪除所有空值

  4. JavaScript 中的 !=和 !==運算符有什麼區別?

  5. 通過 Node.js 和 Lighthouse 優化用戶體驗和性能

  6. 了解 React Children 類型

  7. 使用 Typescript 將圖像添加到 React 項目

  1. [第 20 部分] 使用 GraphQL、Typescript 和 React 創建 Twitter 克隆(轉推)

  2. 如何將 dev.to 與 Gatsby 集成

  3. 在 React (CRA) 中使用 Redux Thunk 和 Axios 服務

  4. JavaScript map() 函數 |基本