Front-end и back-end практика GraphQL

GraphQL

GraphQL — это язык запросов, появившийся за последние два года и используемый некоторыми модными компаниями в Китае, который решает некоторые проблемы в методе интерфейса Rest, а также приносит некоторые новые проблемы. Для наших низкоуровневых программистов правильно учиться, если это используется.

выбор кадра

У graphql есть соответствующие реализации на разных языках и фреймворках, посмотреть можноОфициальный сайтВыбирайте реализацию, подходящую вам по ситуации, и концепции у всех одинаковые. В этой статье больше внимания уделяется фактическому коду.Пожалуйста, посмотрите теоретическую часть в сочетании с руководством по официальному веб-сайту.

В этой статье используется nodejs в качестве языка разработки и Express в качестве сервера, чтобы показать простой процесс построения graphql и постепенно добавлять поддержку mysql, typescript, type-graphql и typeorm. Процесс постепенный, если не нравится(не могу учиться) определенную часть, просто пропустите ее. Обратите внимание, что в этой статье много личного добра, что может быть не лучшей практикой.Если есть какие-либо ошибки, пожалуйста, прокомментируйте и укажите и учитесь вместе, спасибо

Быстрая реализация

Во-первых, давайте быстро реализуем сервер graphql

mkdir graphqldemo;
cd graphqldemo;
npm init -yes;
npm i express apollo-server-express;

Затем создайте index.js

const express = require("express");
const { ApolloServer } = require("apollo-server-express");

const PORT = 4000;
const app = express();

const box = {
  width: 100,
  height: 200,
  weight: "100g",
  color: "white"
}

const typeDefs = [`
"""
一个盒子模型
"""
type Box{
  """
  这是盒子的宽度
  """
  width:Int,
  height:Int,
  color:String
}

type Query {
  getBox: Box
}

type Mutation{
  setWidth(width:Int):Box
}

schema {
  query: Query,
  mutation: Mutation
}`]

const resolvers = {
  Query: {
    getBox(_) {
      return box;
    }
  },
  Mutation: {
    setWidth(_, { width }) {
      box.width = width;
      return box
    }
  }
};


const server = new ApolloServer({
  typeDefs,
  resolvers
});
server.applyMiddleware({ app });

app.listen(PORT, () =>
  console.log(
    `🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
  )
);

затем беги

node index.js

После появления сообщения об успешном завершении вы можете открыть его в браузере.http://localhost:4000/graphql, вы можете увидеть игровую площадку, предоставляемую graphql

Нажмите на Docs справа, чтобы просмотреть настройки типа, и соответствующий тип данных, а также комментарии, которые мы написали. Это на самом деле очень полный интерфейсный документ.

анализ кода

Только что мы реализовали простейший сервер graphql, используя экспресс и сервер Apollo (сервер Apollo — это реализация спецификации graphql).

const server = new ApolloServer({
  typeDefs,
  resolvers
});

При создании ApolloServer передаются два параметра, один typeDefs и один резолвер. typeDefs — это строка или массив строк, содержимое в ней — это определенная нами схема, а распознаватели — это реализация схемы, то есть Query и Mutation в typeDefs.Обратите внимание, что все схемы должны быть реализованы до того, как программа может быть запущена. начал.

Вы также можете передать только один параметр схемы в новый ApolloServer и использовать метод buildSchema для генерации схем из typeDefs и распознавателей (концепция схемы появляется повсюду в graphql, не путайте ее).

Возвращаемое значение преобразователя должно соответствовать определенному типу, иначе будет сообщено об ошибке. В ApolloServer также может быть возвращен Promise соответствующего типа.

server.applyMiddleware({ app });

Эта линия использует Apollo в качестве промежуточного программного обеспечения для Express.

const box = {
  width: 100,
  height: 200,
  weight: "100g",
  color: "white"
}

Объявите коробку в качестве источника данных. Для graphql не важно, откуда берутся данные, их можно получить из обычных переменных, баз данных, Redis или даже HTTP-запросов, если структура данных соответствует определению. Теперь давайте запросим у сервера этот ящик

В graphql есть три типа операций: запрос, мутация или подписка Здесь мы демонстрируем запрос и мутацию.

query

query — это операция запроса в graphql, ввод с левой стороны игровой площадки.

query {
  getBox {
    width
    height
    color
  }
}

Нажмите кнопку, чтобы получить возвращаемое значение справа

{
  "data": {
    "getBox": {
      "width": 100,
      "height": 200,
      "color": "white"
    }
  }
}

Мы можем произвольно уменьшать поля в getBox (хотя бы одно), например до тех пор, пока ширина

query {
  getBox {
    width
  }
}

Вы можете видеть, что в возвращаемом значении есть только свойство ширины.

{
  "data": {
    "getBox": {
      "width": 100
    }
  }
}

graphql здесь, чтобы решить проблему в традиционном режиме интерфейса, то есть от внутреннего интерфейса к внешнему во время передачи данных, будет доставлено много недопустимого поля, слишком много недопустимого поля повлияет на эффективность передачи, внешний интерфейс может взять на себя инициативу, чтобы получить необходимые поля сами.

С другой стороны, некоторые поля внутреннего уровня DAO не должны передаваться на внешний интерфейс с точки зрения безопасности.В приведенном выше примере атрибут веса коробки можно понимать как поле, которое не должно быть видимым для внешнего интерфейса, потому что он не определен, поэтому он автоматически фильтруется и не может быть запрошен внешним интерфейсом. Традиционное серверное решение этой проблемы состоит в том, чтобы ввести уровень DTO поверх уровня DAO.

mutation

мутация представляет собой операцию, которая будет иметь побочные эффекты на источник данных, вводимый на игровой площадке.

mutation {
  setWidth(width: 108) {
    width
    height
    color
  }
}

получил ответ

{
  "data": {
    "setWidth": {
      "width": 108,
      "height": 200,
      "color": "white"
    }
  }
}

Вы можете видеть, что ширина Box была обновлена ​​до 108. Обратите внимание, что можно инициировать несколько запросов и мутаций, и сервер будет выполнять их последовательно, но запросы и мутации нельзя использовать одновременно.Ниже приведен пример нескольких мутаций, и то же самое верно для запроса

mutation {
  m1:setWidth(width: 108) {
    width
  }
  m2:setWidth(width: 99) {
    width
  }
}

возвращаемое значение

{
  "data": {
    "m1": {
      "width": 108
    },
    "m2": {
      "width": 99
    }
  }
}

Поскольку setWidth использовался дважды и имеет одно и то же имя, мы используем m1 и m2 в качестве псевдонимов Синтаксис такой же, как и выше, что очень просто.

Кампания

Только что в мутации мы прописали параметры прямо в операторе, потому что сам оператор представляет собой строку, которая не подходит для комбинирования и не подходит для передачи сложных параметров, поэтому нам нужно определить параметры. Нажмите «Запросить переменные» в левом нижнем углу игровой площадки, где вы можете объявить параметры, обратите внимание, что они должны быть в стандартном формате json.

{
  "length": 128
}

Также измените утверждение на

mutation($length: Int) {
  setWidth(width: $length) {
    width
  }
}

Вы можете использовать его в операторе, добавив перед длиной $.Вы можете проверить изменения в запросе консоли браузера.

немного сложнее

Давайте рассмотрим более сложную модель.Теперь мы украсим коробку случайными шариками и изменим источник данных к следующему виду

class Ball {
  constructor() {
    this.size = ((Math.random() * 10) | 0) + 5;
    this.color = ["black", "red", "white", "blue"][(Math.random() * 4) | 0];
  }
}
const box = {
  width: 100,
  height: 200,
  weight: "100g",
  color: "white",
  balls: new Array(10).fill().map(n => new Ball())
}

Затем добавьте тип в typeDefs и измените тип поля.

type Box{
  width:Int,
  height:Int,
  color:String,
  balls:[Ball]
}
type Ball{
  size:Int,
  color:String
}

Перезапустите службу и сделайте запрос

query {
  getBox {
    width
    balls {
      size
      color
    }
  }
}

результат

{
  "data": {
    "getBox": {
      "width": 100,
      "balls": [
        {
          "size": 5,
          "color": "black"
        },
        //...
      ]
    }
  }
}

Кажется, что ошибки нет, но эта ситуация не соответствует первоначальному замыслу дизайна graphql. Слой данных graphql должен нести только информацию этого слоя.Представьте это требование.Мне нужны все шары красного цвета в коробке и коробке. Правильный способ заключается в следующем, сначала измените поле, чтобы оно имело параметры

type Box{
  width:Int,
  height:Int,
  color:String,
  balls(color:String):[Ball]
}

Затем добавьте Box в преобразователях. Обратите внимание, что первый родительский параметр преобразователя указывает на его родительский элемент, который является полем. Это очень важно. Если имеется несколько полей, этот параметр необходим для определения того, какое поле вернуть мяч в.

const resolvers = {
  Query: {
    getBox(_) {
      return box
    },
  },
  Mutation: {
    setWidth(_, { width }) {
      box.width = width;
      return box
    }
  },
  Box: {
    balls(parent, { color }) {
      return color ? box.balls.filter(ball => ball.color === color) : box.balls
    }
  }
};

Все шары красного цвета теперь можно найти с помощью запроса

query {
  getBox {
    width
    balls (color:"red"){
      size
      color
    }
  }
}

Если нет параметров, то это все шары. Преимущество этого дизайна graphql заключается в том, что вы можете написать намного меньше объединений в запросах к базе данных, но недостатком является то, что запросов больше.

Интерфейсное использование

При использовании http для запроса к серверу graphql вектор по-прежнему json, поэтому можно общаться с сервером graphql даже без использования какой-либо специальной библиотеки.

axios

Сначала попробуйте более классический axios, создайте html-файл.

  <script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
  <script>
    const query = `query($color:String) {
      getBox {
        width
        balls (color:$color){
          size
          color
        }
      }
    }`;
    const variables = {
      color: "red"
    };
    axios
      .post("http://localhost:4000/graphql", {
        query,
        variables
      })
      .then(res => {
        console.log("res: ", res);
      });
  </script>

Также GET полностью легален

 axios.get("http://localhost:4000/graphql", {
        params: { query, variables }
      })

или посетить непосредственно

http://localhost:4000/graphql?query=query($color:String){getBox{width,balls(color:$color){size,color}}}&variables={"color":"red"}

По сравнению с традиционным методом особенность graphql заключается в том, что возвращаемое значение предсказуемо, а поскольку адрес, метод запроса и имя параметра фиксированы, его легче инкапсулировать.

Теперь давайте посмотрим, что делает профессиональный клиент.Так как сервер использует apollo-server, клиент может видеть, как это делает apollo-client.официальный сайт аполло-клиент. Поскольку он обеспечивает обработку ошибок, кэширование данных, обработку ошибок и т. д., элементы конфигурации немного сложнее.Официальный предоставляет apollo-boost для упрощения конфигурации. Мы можем реализовать упрощенную версию по сравнению с официальной версией и подробно изучить ее.

Реализация клиента

Предупреждение о частных товарах

Следующий контент реализован с помощью API-интерфейсов хуков и машинописного текста реакции 16.8+, имитируя дизайн API официального пакета и удаляя такие функции, как кэширование. Можно сказать, что кэширование является основной функцией, предоставляемой apollo, но для кэширования было добавлено огромное количество кода, который не подходит для изучения. Сначала мы создаем новый реактивный проект

create-react-app graphql-client --typescript

Далее обратимся к официальному пакету для реализации следующих модулей с наибольшей частотой (суперупрощенная версия): ApolloClient, ApolloProvider, useQuery, Query

ApolloClient

Входные параметры включают uri, fetchOptions и т. д., что на самом деле является библиотекой http-запросов. Эта часть может быть заменена непосредственно axios. Обратите внимание на официальный пример. Метод gql, производный от graphql-tag, используется для обработки строк graphql, в том числе на стороне сервера. Если он опущен, строки используются напрямую.

import Axios, { AxiosInstance } from "axios";

type config = {
  uri: string;
};

class Client {
  constructor({ uri }: config) {
    this.uri = uri;
    this.axios = Axios.create();
  }
  private uri: string;
  private axios: AxiosInstance;
  query({ query, variables }: { query: string; variables: any }) {
    return this.axios.post(this.uri, { query, variables });
  }
}

ApolloProvider

Можно увидеть, что этот компонент предоставляет клиенту контекст для следующего и использует createContext для завершения этого компонента.

interface ProviderProps {
  client: Client;
}

const graphqlContext: React.Context<{
  client: Client;
}> = React.createContext(null as any);

const GraphProvider: React.FC<ProviderProps> = ({ client, children }) => {
  return (
    <graphqlContext.Provider value={{ client }}>
      {children}
    </graphqlContext.Provider>
  );
};

useQuery

Поскольку входные параметры graphql фиксированы, создать хук несложно. Универсальный тип T используется здесь для определения типа ожидаемого возвращаемого значения, а официальный пакет также использует здесь второй универсальный тип для определения типа параметра переменной.

import { useState, useContext, useEffect, Dispatch } from "react";

const useQuery = <T = any>(query: string, variables?: any) => {
  const { client } = useContext(graphqlContext);
  const [data, setData]: [T, Dispatch<T>] = useState(null as any);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null as any);
  useEffect(() => {
    setLoading(true);
    setError(null);
    client
      .query({ query, variables })
      .then(res => {
        setData(res.data.data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [query, variables, client]);
  return { data, loading, error };
};

Здесь вы можете использовать упакованные компоненты Во-первых, это App.tsx

const client = new Client({
  uri: "http://localhost:4000/graphql"
});
const App: React.FC = () => {
  return (
    <Provider client={client}>
      <Home></Home>
    </Provider>
  );
};

Затем Home.tsx

interface ResData {
  getBox: {
    width: number;
    balls: { size: number; color: string }[];
  };
}
const query = `
  query {
    getBox {
      width
      balls (color:"red"){
        size
        color
      }
    }
  }`;
const Home: FC = () => {
  const { data, loading, error } = useQuery<ResData>(query);
  if (loading) return <div>loading</div>;
  if (error) return <div>{error}</div>;
  return (
    <div>
      <h2>{data.getBox.width}</h2>
      <ul>
        {data.getBox.balls.map(n => (
          <li>
            size:{n.size} color:{n.color}
          </li>
        ))}
      </ul>
    </div>
  );
}

Поскольку извлекаемые данные предсказуемы, файл типов создается одновременно с написанием оператора запроса. Если код сейчас правильный, вы уже можете увидеть эффект на ваш реактивный проект.

Query

Этот компонент очень прост после создания хука, и он принимается

interface queryProps {
  query: string;
  variables?: any;
  children: React.FC<{ data: any; loading: boolean; error: any }>;
}
const Query: React.FC<queryProps> = ({ query, variables, children }) => {
  const { data, loading, error } = useQuery(query, variables);
  return children({ data, loading, error });
};

Суммировать

Для внешнего интерфейса применить graphql несложно. Пока вы можете писать правильные операторы запроса, вы получите правильные результаты запроса. Трудность может заключаться в интеграции в существующие проекты и использовании таких инструментов, как машинописный текст, для повышения эффективности разработки. Сложность преобразования по-прежнему заключается в серверной части. Думая, что наша предыдущая серверная часть была слишком простой, давайте оптимизируем ее сейчас и компенсируем очень важные функции, такие как аутентификация личности, в серверной части.

Оптимизация внутренней структуры каталогов

Бэкенд всегда писал в файле, по мере усложнения модели код начинает раздуваться, при этом писать Schema в строку тоже очень неудобно, лучше всего писать в отдельный файл graphql/gql, так он тоже имеет форматирование функция, предоставляемая редактором (я использую плагин Apollo Graphql в vscode).

Моя обработка здесь состоит в том, чтобы разделить typeDefs на соответствующие файлы graphql, преобразователи также выполняют разделение файлов, а затем используют сканер файлов, чтобы автоматически зависеть от него.

Теперь создайте папку typeDefs, затем создайте файл index.graphql и скопируйте в него исходную строку typeDefs.

Создайте файл ball.gql в том же каталоге и вставьте в него связанные с Ball определения из файла index.graphql.

Затем создайте util.js и напишите сканер кода, потому что все, что вам нужно, это получить строку, поэтому просто используйте модуль fs для непосредственного чтения файла.

const fs = require("fs");
const path = require("path");

function requireAllGql(dir, parentArray) {
  let arr = [];
  let files = fs.readdirSync(dir);
  for (let f of files) {
    let p = path.join(dir, f);
    let stat = fs.statSync(p);
    if (stat.isDirectory()) {
      requireAllGql(p, arr);
    } else {
      let extname = path.extname(p);
      if (extname === ".gql" || extname === ".graphql") {
        let text = fs.readFileSync(p).toString();
        if (!parentArray) {
          arr.push(text);
        } else {
          parentArray.push(text);
        }
      }
    }
  }
  return arr;
}
module.exports = {
  requireAllGql
};

Таким образом, typeRefs в index.js можно изменить на это

const { requireAllGql } = require('./utils.js')
const path = require("path")
const typeDefs = requireAllGql(path.resolve(__dirname, './typeDefs'))

Решите распознаватель таким же образом, но сначала создайте dataSource.js, переместите мяч и коробку в этот файл, затем создайте папку распознавателей, затем создайте файл query.js, файл мутации.js и файл коробки (обычно , файлы разбиты по функциональным модулям, вот пример). Например, сейчас query.js такой

const { box } = require('../dataSource.js')
exports.default = {
  Query: {
    getBox(_) {
      return box
    }
  }
}

Пропустите остальные. Затем создайте резолвер сканера utils.js, по умолчанию каждый экспортируемый файл представляет собой обычный объект, поэтому разобраться с ними не составит труда.

function requireAllResolvers(dir, parentArray) {
  let arr = [];
  let files = fs.readdirSync(dir);
  for (let f of files) {
    let p = path.join(dir, f);
    let stat = fs.statSync(p);
    if (stat.isDirectory()) {
      requireAllResolvers(p, arr);
    } else {
      let extname = path.extname(p);
      if (extname === ".js" || extname === ".ts") {
        let resolver = require(p).default;
        if (!parentArray) {
          arr.push(resolver);
        } else {
          parentArray.push(resolver);
        }
      }
    }
  }
  return arr;
}

Точно так же вы можете получить резольверы в файле индекса

const resolvers = requireAllResolvers(path.resolve(__dirname, './resolvers'))

Apollo поможет нам объединить содержимое массива, поэтому нам просто нужно убедиться, что содержимое каждого файла соответствует формату. Если все пойдет хорошо, проект по-прежнему будет работать правильно, ничего не изменилось, но вдобавок он будет масштабироваться по горизонтали.

база данных

Для веб-сервиса данные должны храниться в специальной базе данных, такой как mysql, redis и т. д. Здесь в качестве примера возьмем обычно используемый mysql, чтобы увидеть, чем отличается graphql в сочетании с базой данных. Возьмите предыдущий шар в качестве примера для создания базы данных.

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_ball`;
CREATE TABLE `t_ball` (
  `id` int(10) NOT NULL,
  `size` int(255) DEFAULT NULL,
  `color` varchar(255) DEFAULT NULL,
  `boxId` int(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
BEGIN;
INSERT INTO `t_ball` VALUES (1, 5, 'red', 1);
INSERT INTO `t_ball` VALUES (2, 6, 'blue', 1);
INSERT INTO `t_ball` VALUES (3, 7, 'white', 2);
INSERT INTO `t_ball` VALUES (4, 8, 'black', 2);
COMMIT;
DROP TABLE IF EXISTS `t_box`;
CREATE TABLE `t_box` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `width` int(255) DEFAULT NULL,
  `height` int(255) DEFAULT NULL,
  `color` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
BEGIN;
INSERT INTO `t_box` VALUES (1, 100, 100, 'red');
INSERT INTO `t_box` VALUES (2, 200, 200, 'blue');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

Возвращаясь к экспресс-проекту, поскольку модель немного изменена, добавлен идентификатор первичного ключа, поэтому схема в файле gql должна добавить идентификатор, а схема, которую необходимо изменить, указана ниже.

type Box {
  id: Int
  width: Int
  height: Int
  color: String
  balls(color: String): [Ball]
}
type Ball {
  id: Int
  size: Int
  color: String
}
type Query {
  getBox: [Box]
}
type Mutation {
  setWidth(width: Int, id: Int): Box
}

Увеличьте пакеты MySQL в проекте

yarn add mysql

Установить пул соединений, простые пакетные запросы, слой DAO не введен, ведь итого не мало sql

const mysql = require('mysql')

const pool = mysql.createPool({
  host: '127.0.0.1',
  user: 'root',
  password: 'password',
  database: 'graphqldemo',
  port: 3306
})

const query = (sql, params) => {
  return new Promise((res, rej) => {
    pool.getConnection(function (err, connection) {
      connection.query(sql, params, function (err, result) {
        if (err) {
          rej(err);
        } else {
          res(result);
        }
        connection.release();
      });
    });
  })
}

Прежде чем внедрять sql в резолвер, нужно знать четыре параметра резолвера. Первый параметр parent является родительским элементом текущего элемента.Родительский элемент схемы верхнего уровня называется root, который в большинстве руководств заменяется на _. Второй параметр — params, который является параметром запроса. Третий параметр — это config, в котором есть параметр dataSources, который нам понадобится позже. Четвертый параметр — это контекст, а его входные параметры — это Запрос и Ответ экспресса, которые можно использовать для передачи идентификационной информации и выполнения таких операций, как аутентификация.

Мы помещаем инкапсулированную функцию запроса в этот источник данных. Изменить в index.js

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({
    query
  })
});

Затем вы можете изменить распознаватели, сначала реализовать первый getBox, потому что теперь есть более одного блока, поэтому возвращается массив, и схема была изменена

  Query: {
    getBox(_, __, { dataSources: { query } }) {
      return query('select * from t_box')
    }
  }

Запрос возвращает обещание, которое поддерживается Apollo.

Затем, чтобы завершить шары Коробки, нам нужно получить идентификатор родительского элемента из родительского

  Box: {
    balls(parent, { color }, { dataSources: { query } }) {
      return query('select * from t_ball where box_id=? and color=?', [parent.id, color])
    }
  }

Наконец, существует мутация, которая должна быть изменена. Определение в схеме возвращает модифицированное поле, поэтому для завершения этой части необходимы два SQLS.

  Mutation: {
    async setWidth(_, { width, id }, { dataSources: { query } }) {
      await query('update t_box set width=? where id=?', [width, id])
      return query('select * from t_box where id=?', [id]).then(res => res[0])
    }
  }

На данный момент основа проекта graphql в основном завершена, и на этой основе можно выполнить простой проект путем горизонтального расширения. Кроме того, в формальном проекте слой DAO все равно нужен для управления данными, иначе рефакторинг научит вас быть человеком.

typescript & type-graphql

Угождая тренду, нам нужно благословение машинописного текста, иначе он будет расцениваться как игрушка, как бы вы его ни писали. Но давайте задумаемся над вопросом.Тип typescript и graphql это оба описания модели,которые в принципе одинаковы.Есть только некоторые отличия в синтаксисе.Могут ли они быть универсальными? Чиновник предоставил соответствующиеAPIЧтобы выполнить это требование, но синтаксис не является кратким,type-graphqlМожет лучше вариант.

Сначала импортируйте typescript и type-graphql, а также файлы описания ранее использовавшихся пакетов.Кроме того, в аннотации сканирования type-graphql используются Reflect-метаданные, функция, которая еще не вошла в стандарт, поэтому этот пакет необходимо импортированный

yarn add typescript type-graphql reflect-metadata @types/mysql @types/express

Старые правила машинописного текста: сначала напишите tsconfig.json, возможно, со следующим содержимым

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": ["es6", "es7", "esnext", "esnext.asynciterable"],
    "noImplicitAny": false,
    "moduleResolution": "node",
    "baseUrl": ".",
    "esModuleInterop": true,
    "inlineSourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "watch": true
  }
}

Затем измените все, что требуется для импорта, и суффикс имени js на ts.Если есть ошибка, напишите любую

Мы используем ts-node для запуска проекта.После глобальной установки ts-node запустите ts-node index.ts для запуска. Для формальных проектов мы можем использовать pm2, чтобы указать интерпретатор или скомпилировать проект в js для запуска. Затем вносим в проект type-graphql.

Жаль, что после введения type-graphql структура кода сильно изменилась, за исключением того, что контент, относящийся к базе данных, можно в принципе переписать, а файл graphql больше не нужен. Сначала создайте папку моделей, добавьте два новых файла Box.ts и Ball.ts.

import { ObjectType, Field } from "type-graphql";

@ObjectType()
export default class Ball {
  @Field()
  id: number;

  @Field()
  size: number;

  @Field()
  color: string;
  
  boxId: number;
}
import { ObjectType, Field, Int } from "type-graphql";
import Ball from "./Ball";

@ObjectType({ description: "这是盒子模型" })
export default class Box {
  @Field(type => Int)
  id: number;

  @Field(type => Int, { nullable: true, description: "这是宽度" })
  width: number;

  @Field(type => Int)
  height: number;

  @Field()
  color: string;

  @Field(() => [Ball])
  balls: Ball[];
}

Аннотация ObjectType указывает, что этот класс является типом объекта в graphql, а свойства, аннотированные Field, являются свойствами, определенными в graphql. Первый параметр поля — это функция для представления типа. Входной параметр функции не имеет смысла. Тип записывается для семантики, а возвращаемое значение — это тип (числовой тип машинописного текста — числовой, но числовой тип graphql делится на Int и Float, если не указано как Int, номер typegraphql по умолчанию - Float); второй параметр - элемент конфигурации, nullable по умолчанию false, здесь можно изменить на true, описание - комментарий

Затем измените index.ts, в принципе ничего не нужно до введения graphql, сохраните метод запроса базы данных и поместите метод запроса в источники данных вместо контекста.

import "reflect-metadata";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import path from "path";
import query from "./db";
import { buildSchema } from "type-graphql";

const PORT = 4000;
const app = express();
app.use(express.static("public"));

buildSchema({
  resolvers: [path.resolve(__dirname, "./resolvers/*.resolver.ts")]
}).then(schema => {
  const server = new ApolloServer({
    schema,
    context: () => ({
      query
    })
  });
  server.applyMiddleware({ app });

  app.listen(PORT, () =>
    console.log(
      `🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
    )
  );
});

buildSchema является асинхронной, поэтому запуск ApolloServer должен быть после этого.Как упоминалось ранее, ApolloServer должен предоставить два параметра typeDefs и resolvers или параметр схемы. Глядя на входные параметры функции buildSchema, мы знаем, что у нас все еще есть резолверы, которые не были изменены.Создайте новый Box.resolver.ts в папке резолверов и измените предыдущие три резолвера.

import {
  Resolver,
  Query,
  Arg,
  Mutation,
  Ctx,
  FieldResolver,
  Root
} from "type-graphql";
import Box from "../models/Box";
import Ball from "../models/Ball";

@Resolver(Box)
export default class BoxResolver {
  @Query(returns => [Box])
  getBox(@Ctx() { query }) {
    return query("select * from t_box");
  }

  @FieldResolver(returns => [Ball])
  balls(@Root() box: Box, @Ctx() { query }, @Arg("color") color: string) {
    return query("select * from t_ball where boxId=? and color=?", [
      box.id,
      color
    ]);
  }

  @Mutation(returns => Box)
  async setWidth(
    @Arg("width") width: number,
    @Arg("id") id: number,
    @Ctx() { query }
  ) {
    await query("update t_box set width=? where id=?", [width, id]);
    return query("select * from t_box where id=?", [id]).then(res => res[0]);
  }
}

Кратко объясните смысл нескольких комментариев. Query и Mutation представляют распознаватели этих двух основных типов, а параметр представляет собой функцию, указывающую ожидаемый тип возвращаемого значения; FieldResolver связан с аннотацией класса Resolver, представляющей распознаватель шаров поля под типом объекта Box; аннотация Arg является параметр, первый параметр One — это имя параметра входного параметра, и у него есть второй параметр, который можно настроить как обнуляемый; параметр аннотации Root — это родительский элемент, а идентификатор родительского поля получается в метод balls; аннотация Ctx — это Context, полученный из метода query контекста в apolloserver.

На данный момент нет необходимости писать файл graphql, и сервер graphql все еще завершен, а typeDefs и преобразователи объединены вместе, поэтому нет необходимости беспокоиться о пропущенной записи. А наша программа сильно изменилась, и без глубокого изучения мне ее не понять.Поздравляю, вы стали на шаг ближе к строительству технического рва.

type-graphql имеет встроенное управление разрешениями, если интересно, можете глянутьАвторизованная аннотация

typeorm

Хочет ли ORM представить проект, в основном зависит от требований проекта. используется здесьtypeorm, который очень совместим с typegraphql в письменной форме, потому что он может напрямую повторно использовать модель, созданную в typegraphql, что является очень хорошей реализацией typegraphql на уровне данных. Сам Typeorm имеет много контента, вы можете написать отдельную статью, в этой статье представлена ​​только часть, связанная с graphql, сначала познакомьтесь с typeorm

yarn add typeorm

Затем создайте ormconfig.json в корневом каталоге проекта и введите конфигурацию базы данных.

{
  "type": "mysql",
  "host": "127.0.0.1",
  "port": 3306,
  "username": "root",
  "password": "password",
  "database": "graphqldemo",
  "synchronize": false,
  "logging": false,
  "entities": ["./models/*.ts"]
}

Где type — это тип базы данных, typeorm поддерживает MySQL, MariaDB, Postgres, SQLite, Oracle, MongoDB и другие базы данных. Если для синхронизации установлено значение true, typeorm автоматически создаст таблицу на основе модели, а если модель будет изменена, структура таблицы будет изменена (внешние ключи и другие причины приведут к сбою запуска проекта с ошибкой модификации, требующему ручного вмешательства или используя миграции в typeform). Если журналирование истинно, автоматически сгенерированный оператор sql будет напечатан на консоли.

Сначала измените index.ts

import { createConnection } from "typeorm";
//····在app.listen之前添加
  createConnection().then(() => {
    app.listen(PORT, () =>
      console.log(
        `🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
      )
    );
  });
//····

Как и buildSchema для typegraphql, createConnection также является асинхронным обещанием, поэтому для предотвращения некоторых непредвиденных ситуаций операция app.listen должна выполняться после этих двух процессов. Если файл ormconfig.json был создан, createConnection автоматически прочитает конфигурацию, в противном случае его нужно заполнить как параметр.

Затем измените модели.После этого модели могут одновременно представлять типы в type-graphql или использоваться как объекты данных в typeorm.

import { ObjectType, Field, Int } from "type-graphql";
import Box from "./Box";
import {
  Column,
  ManyToOne,
  Entity,
  BaseEntity,
  PrimaryGeneratedColumn
} from "typeorm";

@Entity({ name: "t_ball" })
@ObjectType()
export default class Ball extends BaseEntity {
  @PrimaryGeneratedColumn()
  @Field(type => Int)
  id: number;

  @Column()
  @Field(type => Int)
  size: number;

  @Column({ type: "varchar", length: 255 })
  @Field()
  color: string;

  @Column()
  boxId: number;

  @ManyToOne(type => Box)
  box: Box;
}
import { ObjectType, Field, Int } from "type-graphql";
import Ball from "./Ball";
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToMany,
  BaseEntity
} from "typeorm";

@Entity({ name: "t_box" })
@ObjectType({ description: "这是盒子模型" })
export default class Box extends BaseEntity {
  @PrimaryGeneratedColumn()
  @Field(type => Int)
  id: number;

  @Column()
  @Field(type => Int, { nullable: true, description: "这是宽度" })
  width: number;

  @Column()
  @Field(type => Int)
  height: number;

  @Column({ type: "varchar", length: 255 })
  @Field()
  color: string;

  @OneToMany(type => Ball, ball => ball.box)
  @Field(() => [Ball])
  balls: Ball[];
}

Во-первых, класс наследуется от BaseEntity в typeorm, и к классу добавляется аннотация Entity.Добавление имени в элемент конфигурации параметра может указать имя таблицы;Аннотация столбца.Этот атрибут является столбцом базы данных, а параметр может указывать конкретный тип и длину; аннотация PrimaryGeneratedColumn указывает, что это автоматически увеличивающийся первичный ключ; OneToMany — это специальная аннотация, используемая для описания прямых отношений объекта, включая OneToMany, ManyToOne и ManyToMany. Конкретное использование длинная история, пожалуйста, изучите сами. Это соответствует ранее установленной базе данных.Если вам интересно, вы можете использовать функцию автоматического создания таблицы typeorm, чтобы увидеть, в чем разница.

Теперь мы можем отказаться от простого инкапсулированного метода запроса и напрямую использовать метод сбора данных, предоставляемый typeorm.

import {
  Resolver,
  Query,
  Arg,
  Mutation,
  FieldResolver,
  Root,
  Int
} from "type-graphql";
import Box from "../models/Box";
import Ball from "../models/Ball";

@Resolver(Box)
export default class BoxResolver {
  @Query(returns => [Box])
  getBox() {
    return Box.find();
  }

  @FieldResolver(returns => [Ball])
  balls(@Root() box: Box, @Arg("color", { nullable: true }) color: string) {
    return Ball.find({ boxId: box.id, color });
  }

  @Mutation(returns => Box)
  async setWidth(
    @Arg("width", type => Int) width: number,
    @Arg("id", type => Int) id: number
  ) {
    let box = await Box.findOne({ id });
    box.width = width;
    return box.save();
  }
}

Вся программа очень элегантна и сложна для понимания~. В Typeorm много контента, и если у вас есть опыт работы с другими формами, вы сможете быстро начать работу. Кроме того, если действительно есть sql, который typeorm не может написать, вы должны написать его вручную~

Суммировать

Я начал писать эту статью очень рано, но киоск слишком большой, поэтому я не могу все время ее доделывать.В основном играю в Monster Hunter Icefields. Модель, выбранная в этой статье, также очень проста, но в основном завершает структуру сервера graphql.Конечно, осталось много вещей, таких как очень важные скалярные типы и типы ввода. Поскольку мой основной бизнес — это интерфейс, я немного знаком с бэкендом java, поэтому, если в статье есть концептуальная ошибка, пожалуйста, прокомментируйте и укажите, и двигайтесь вперед вместе.