Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript (P2)

Trong phần đầu tiên của loạt bài này, chúng ta đã thảo luận về các bước để xây dựng một máy chủ GraphQL sử dụng ASP.NET Core WebAPI. Vì chúng ta đã có một máy chủ GraphQL đang chạy, bây giờ chúng ta sẽ xây dựng một ứng dụng client làm việc với API mà chúng ta vừa tạo.

Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng GraphQL với ASP.NET Core, Entity Framework Core và TypeScript.

Chúng ta sẽ xây dựng một ứng dụng client tối thiểu bằng cách sử dụng TypeScript không phụ thuộc vào các framework hoặc thư viện như Angular và React.

Ứng dụng client của chúng ta sẽ chấp nhận một query hoặc mutation từ đối số dòng lệnh và gửi yêu cầu đến API GraphQL. Ứng dụng client sẽ chuyển phản hồi mà nó nhận được từ API thành một chuỗi và in nó ra console.

Để xây dựng ứng dụng client này, chúng ta sẽ sử dụng thư viện Ứng dụng khách Apollo phổ biến hoạt động với cả TypeScript và Javascript.

Mã nguồn

Mã nguồn hoàn chỉnh của ứng dụng bao gồm các thành phần máy khách và máy chủ có sẵn để tải xuống từ GitHub.

Mã nguồn bao gồm hai thư mục:

  1. api: Máy chủ GraphQL dựa trên ASP.Net Core WebAPI.
  2. web: Máy khách GraphQL dựa trên TypeScript.

Chạy ứng dụng mẫu

Tải xuống ứng dụng mẫu và mở thư mục chứa các ứng dụng của bạn bằng VSCode. Tôi đã thêm task trong launch.json, và tasks.json file cho VSCode (trong thư mục .vscode) mà sẽ giúp bạn build và debug các ứng dụng liên quan. Trong cửa sổ lệnh của bạn (Ctrl + `), điều hướng đến thư mục api và thực hiện lệnh sau để khởi động máy chủ GraphQL.

dotnet build
dotnet run

Đảm bảo rằng API có thể truy cập được tại địa chỉ: http://localhost:5000/graphql. Bây giờ, khởi chạy một thiết bị đầu cuối khác và điều hướng đến thư mục web. Thực hiện lệnh sau để cài đặt các gói NodeJS cần thiết.

npm install

Để xây dựng dự án client, bạn có thể thực hiện lệnh sau trong thiết bị đầu cuối hoặc sử dụng phím tắt Ctrl + Shift + B và chọn tác vụ build-watch web từ trình đơn thả xuống.

npm run build:watch

Để thực hiện các bài kiểm tra Jasmine đính kèm, hãy thực hiện lệnh sau trong thiết bị đầu cuối.

npm run test

Sau khi tất cả các thử nghiệm thành công, dự án của bạn bây giờ đã sẵn sàng để thử nghiệm. Ứng dụng hỗ trợ các lệnh ở định dạng sau.

node index.js query\mutation [-m] [-a [argument dictionary]]

Lệnh có ba phần, chuỗi thao tác query hoặc mutation, tham số cờ (-m) cho phép bạn chỉ định xem chuỗi được nhập làm đối số là query hay mutation (bạn có thể dễ dàng xác định điều này bằng cách đọc từ đầu tiên của chuỗi thao tác cũng như vậy), và một dictionary các đối số được chỉ định thông qua tham số -a.

Để giúp bạn tiết kiệm công sức và cũng để cung cấp cho bạn một số ví dụ, tôi đã thêm hai tập lệnh npm đơn giản vào ứng dụng sẽ giúp bạn kiểm tra ứng dụng client. Thực thi lệnh sau để thực hiện thao tác mutation trên API.

npm run run:testQuery

Lệnh này sẽ thực hiện truy vấn sau trên API GraphQL và in phản hồi dưới dạng chuỗi vào bảng điều khiển console.

query AuthorQuery($id: ID) {
  author(id: $id) {
    name
    quotes {
      text
      category
    }
  }
}

Lệnh cũng gửi dictionary đối số sau đến query để thay thế  tham số $id.

{ "id": 1 }

Đầu ra của lệnh này sẽ liệt kê tất cả các trích dẫn từ tác giả có giá trị Id 1. Thực hiện lệnh sau sẽ thêm trích dẫn cho tác giả có giá trị Id 2.

npm run run:testMutation

Thao tác này sẽ thực hiện thao tác mutation sau trên API GraphQL.

mutation QuoteMutation($authorId: Int!, $text: String!, $category: String!) 
{
    createQuote(quote: {authorId: $authorId, text: $text, category: $category}) 
    {
        name
        quotes 
        {
            text
            category
        }
    }
}

Thao tác này cũng gửi dictionary các đối số sau vào thao tác mutation.

{"authorId":2,"text":"FamousQuote1","category":"fun"}

Đầu ra của lệnh trước đó sẽ liệt kê tên và trích dẫn từ tác giả có bản ghi mà chúng ta vừa thay đổi.

Kết quả

Để so sánh kết quả, sau đây là ảnh chụp màn hình kết quả của thao tác mutation.

Kết quả của mutation
Kết quả của mutation

Tiếp theo, sau đây là đầu ra của thao tác query.

Kết quả của query
Kết quả của query

Bây giờ chúng ta hãy tìm hiểu sâu hơn về đoạn mã mà tôi đã viết để xây dựng ứng dụng này.

Theo dõi trình gỡ lỗi

Ứng dụng bắt đầu thực thi từ tệp index.ts\js chứa đoạn mã sau.

import * as OperationParser from "./operationParser";
import { gqlOperations } from "./options";

var opts = OperationParser.fromArgv(process.argv.slice(2));
console.log("Input:", opts.input);
console.log("Is Mutation:", opts.mutation);
console.log("Args:", opts.arguments);
var executeOperation = async () =>
  opts.mutation
    ? await gqlOperations["mutation"].operation(opts.input, opts.arguments)
    : await gqlOperations["query"].operation(opts.input, opts.arguments);
executeOperation().then((result) => console.log("OUTPUT:", result));

Mã ở trên sử dụng các đối số mà bạn đã truyền cho lệnh và gửi chúng đến hàm formArgv được xác định trong tệp optionsParser. Hàm trả về một thể hiện của lớp Options trong phản hồi, nó hiển thị các thuộc tính mà chúng ta đã sử dụng để hiển thị các đối số và cũng gọi hoạt động thích hợp bằng cách sử dụng đầu vào.

Điều hướng đến tệp optionsParser.ts ngay bây giờ. Hàm fromArgv sử dụng một hàm helper là minimistAs để phân chia các đầu vào thành chuỗi hoạt động, cờ hoạt động, và giá trị tham số đối số.

import * as minimist from "minimist";

import { Options, Arguments } from "./options";

function minimistAs<T>(
  args?: string[],
  opts?: minimist.Opts
): T & minimist.ParsedArgs {
  return <T & minimist.ParsedArgs>minimist(args, opts);
}

export function fromArgv(argv: string[]): Options {
  var parsedArgs = minimistAs<Arguments>(argv, {
    alias: { mutation: "m", arguments: "a" },
  });

  return new Options(parsedArgs._.join(" "), parsedArgs);
}

Hàm minimistAs sử dụng một dòng lệnh gói phân tích cú pháp đơn giản minimist để phân tích các đối số và sử dụng các tính năng kiểu giao của TypeScript để tạo ra một kiểu mà bao gồm các thuộc tính trong các kiểu Arguments và kiểu ParsedArgs. Chúng ta đã sử dụng thêm điều này để tạo một thể hiện của lớp Options.

Bây giờ chúng ta hãy chuyển sang khám phá lớp Options trong tệp options.ts.

import { Mutation } from "./mutation";
import { Query } from "./query";

function exitIfUndefined(value: any, message: string) {
  if (typeof value === "undefined" || value.trim() === "") {
    console.error(message);
    throw new Error(`${value} is not valid input.`);
  }
}

export const gqlOperations = {
  ["mutation"]: Mutation,
  ["query"]: Query,
};

export interface Arguments {
  readonly mutation: boolean;
  readonly arguments: string;
}

export class Options implements Arguments {
  readonly mutation: boolean;
  readonly arguments: string;

  constructor(public readonly input: string, args: Arguments) {
    exitIfUndefined(input, "Please pass an input string.");

    this.mutation = args.mutation === undefined ? false : args.mutation;
    this.arguments = args.arguments;
  }
}

Hầu hết mã trong tệp này là đơn giản và không cần giải thích. Dictionary gqlOperations chứa tham chiếu đến các biến MutationQuery sử dụng thư viện ứng dụng khách Apollo để gửi yêu cầu đến API GraphQL. Mã trong tệp index.ts gọi hàm operation trong một trong các biến này bằng cách truy xuất tham chiếu của chúng từ dictionary gqlOperations theo tên.

Bây giờ chúng ta hãy xem lại hàm Query có trong tệp query.ts.

import { IOperation } from "./IOperation";
import { default as ApolloClient, ApolloQueryResult } from "apollo-boost";
import { default as gql } from "graphql-tag";
import { Config } from "./config";
import "cross-fetch/polyfill";

export var Query: IOperation = {
  async operation(input: string, argument: string): Promise<string> {
    let query = async (): Promise<ApolloQueryResult<any>> => {
      let client = new ApolloClient({ uri: Config.graphQl });
      return await client.query({
        query: gql(input),
        variables: JSON.parse(argument),
      });
    };

    let result = await query();
    return JSON.stringify(result.data);
  },
};

Cả biến QueryMutation đều triển khai interface IOperation và sử dụng lớp ứng dụng khách Apollo có sẵn trong gói apollo-client để gửi yêu cầu đến API GraphQL.

Phương thức query của lớp ApolloClient cũng hỗ trợ gửi đối số của truy vấn trong yêu cầu. Chúng tôi đã sử dụng tính năng này của phương thức query để gửi các đối số của truy vấn tới API.

Tìm hiểu về mã của Mutation trong tập tin mutation.ts sẽ dễ dàng cho bạn vì nó gần giống với việc triển khai bạn đã trải qua trong tập tin query.ts.

import { IOperation } from "./IOperation";
import { default as ApolloClient, FetchResult } from "apollo-boost";
import { default as gql } from "graphql-tag";
import { Config } from "./config";
import "cross-fetch/polyfill";

export var Mutation: IOperation = {
  async operation(input: string, argument: string): Promise<string> {
    let query = async (): Promise<FetchResult<any>> => {
      let client = new ApolloClient({ uri: Config.graphQl });
      return await client.mutate({
        mutation: gql(input),
        variables: JSON.parse(argument),
      });
    };

    let result = await query();
    return JSON.stringify(result.data);
  },
};

Tôi muốn mang đến cho bạn một vài tính năng nổi bật khác của giải pháp này. Như bạn đã thấy, hàm operation của interface IOperation là không đồng bộ. Promise này được giải quyết trong tập tin index.ts.

Tệp cấu hình config.js chứa liên kết đến API GraphQL. Nếu API của bạn đang chạy trên một số cổng khác, thì bạn có thể thay đổi liên kết đến API trong tệp này.

Tôi đã viết các bài kiểm tra cho một số thành phần của ứng dụng này bằng cách sử dụng Jasmine, đây là framework kiểm tra BDD phổ biến nhất cho Javascript. Để hỗ trợ ánh xạ biểu tượng để trình gỡ lỗi VSCode có thể hoạt động với các thử nghiệm, tôi đã sử dụng gói hỗ trợ bản đồ nguồn.

Tôi tham chiếu gói này và gọi install ở đầu mỗi tệp thử nghiệm. Đối với các ứng dụng lớn hơn, bạn có thể nối mã này với tất cả các tệp thử nghiệm trên bản dựng. Để xuất kết quả thử nghiệm ở định dạng trực quan hấp dẫn, tôi đã sử dụng gói jasmine-console-report.

Định nghĩa kiểu

Một vấn đề mà chúng ta vẫn chưa thảo luận là tính năng định kiểu mạnh. Trong các ứng dụng thế giới thực, bạn sẽ cần các định nghĩa kiểu (tệp * .d.ts) cho TypeScript để giúp bạn phát triển các query và mutation được định kiểu mạnh.

Vì máy chủ GraphQL xuất bản lược đồ các query và mutation có sẵn cho máy khách, các công cụ như apollo-codegen có thể giúp tạo các định nghĩa TypeScript một cách tự động.

GraphQLASP.NET Core Web APIASP.NET CoreEntity Framework CoreTypeScript
Bài Viết Liên Quan:
Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core
Trung Nguyen 01/05/2021
Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core

Hướng dẫn bạn các bước để tạo một API GraphQL trên ASP.NET Core bằng GraphQL, Entity Framework Core, Autofac và mẫu Repository.

Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript
Trung Nguyen 26/04/2021
Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript

Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng GraphQL với ASP.NET Core, Entity Framework Core và TypeScript.

GraphQL + NodeJS: Giới thiệu
Trung Nguyen 16/05/2020
GraphQL + NodeJS: Giới thiệu

Giới thiệu hướng dẫn xây dựng API sử dụng GraphQL và NodeJS. Các mục tiêu của hướng dẫn và mã nguồn.

Các kiến trúc sử dụng GraphQL
Trung Nguyen 15/05/2020
Các kiến trúc sử dụng GraphQL

Hướng dẫn này sẽ trình bày qua 3 loại kiến ​​trúc khác nhau sử dụng máy chủ GraphQL.