GraphQL的先後端實踐

GraphQL是這兩年興起的一種查詢語言,國內一些比較潮的公司正在使用,它解決了Rest接口方式的一些問題,同時也帶來了一些新的問題。對於咱們底層程序員來講,學就對了,萬一用上了呢。javascript

框架選擇

graphql在各類語言,各類框架都有對應的實現,能夠查看官網根據狀況選擇適合本身的實現,概念上都是一致的。本文更着重於實際代碼,理論部分請結合官網教程觀看。css

本文使用nodejs做爲開發語言,使用express做爲服務器,展現graphql的簡單搭建過程,並逐步添加mysql,typescript,type-graphql,typeorm的支持。這個過程是漸進的,若是你不喜歡(學不動)某個部分,跳過就好。注意本文攜帶大量私貨,未必是最佳實踐,若是有錯誤,請評論指出,共同窗習,謝謝html

快速實現

首先咱們先快速實現一個graphql的服務器前端

mkdir graphqldemo;
cd graphqldemo;
npm init -yes;
npm i express apollo-server-express;
複製代碼

而後建立一個index.jsjava

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

node index.js
複製代碼

等出現成功提示後就能夠在瀏覽器打開 http://localhost:4000/graphql,就能夠看到graphql提供的playgroundmysql

點擊右側的docs,就能查看到咱們設定的type和對應的數據類型,還有咱們寫的註釋,這實際上是一份很完備的接口文檔了。

代碼分析

剛剛咱們使用express和Apollo Server實現了一個最簡單的graphql服務器(Apollo Server是graphql規範的一個實現)。react

const server = new ApolloServer({
  typeDefs,
  resolvers
});
複製代碼

在new一個ApolloServer的時候,傳入了兩個參數,一個typeDefs和一個resolvers。typeDefs是一個字符串或者字符串數組,裏面的內容是咱們定義的schema,而resolvers是schema的實現,也就是typeDefs裏的Query和Mutation,注意全部的schema都要實現以後程序才能啓動。ios

也能夠只傳入一個schema參數來new ApolloServer,使用buildSchema方法能夠將typeDefs和resolvers生成schema(schema這個概念在graphql中處處出現,不要搞混了)。程序員

resolver的返回值須要符合定義的類型,不然會報錯。在ApolloServer中,也能夠返回對應類型的Promise。

server.applyMiddleware({ app });
複製代碼

這一行將Apollo做爲Express的一箇中間件

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

聲明一個盒子,做爲數據源。graphql並不在乎數據是從哪裏來的,能夠從普通變量,數據庫,redis,甚至http請求中獲取,只要這個數據的結構能符合定義便可。如今咱們向服務器請求一下這個box

graphql一共有三種操做類型,query、mutation 或 subscription,這裏演示一下query、mutation

query

query是graphql中的查詢操做,在playground左側輸入

query {
  getBox {
    width
    height
    color
  }
}
複製代碼

點擊按鈕,能夠在右邊得到返回值

{
  "data": {
    "getBox": {
      "width": 100,
      "height": 200,
      "color": "white"
    }
  }
}
複製代碼

咱們能夠隨意減小getBox裏的字段(至少有一個),好比只要width

query {
  getBox {
    width
  }
}
複製代碼

能夠看到返回值裏只有width屬性了。

{
  "data": {
    "getBox": {
      "width": 100
    }
  }
}
複製代碼

graphql在這裏解決了傳統接口模式中一個問題,就是後端在向前端傳輸數據的過程當中,會傳遞不少無效字段,無效字段過多會影響傳輸效率,前端能夠主動獲取本身所需的字段。

另外一方面,後端的DAO層的一些字段從安全角度也是不該該傳遞給前端的,在上文的這個例子裏,box的weight屬性能夠理解爲一個前端不該可見的字段,由於在graphql中沒有被定義,因此被自動過濾了,前端沒法查詢到。傳統後端解決這個問題的方案是在DAO層之上引入一個DTO層。

mutation

mutation表明對數據源會產生反作用的操做,在playground中輸入

mutation {
  setWidth(width: 108) {
    width
    height
    color
  }
}
複製代碼

獲得結果

{
  "data": {
    "setWidth": {
      "width": 108,
      "height": 200,
      "color": "white"
    }
  }
}
複製代碼

能夠看到box的width已經被更新到108了。注意,query和mutation均可以發起多個,服務器內部會順序執行,可是query和mutation不能同時使用,下面是一個多個mutation的例子,query同理

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

複製代碼

返回值

{
  "data": {
    "m1": {
      "width": 108
    },
    "m2": {
      "width": 99
    }
  }
}
複製代碼

由於setWidth重複使用了兩次,重名了,因此咱們使用m一、m2做爲別名(Aliases),語法如上,很是簡單。

傳參

剛剛的mutation咱們直接在語句裏寫了參數,由於語句自己是字符串不利於組合,同時也不適合傳遞複雜的參數,因此咱們須要定義參數。點擊playground左下的Query Variables,在這裏能夠聲明參數,注意須要是標準json格式

{
  "length": 128
}
複製代碼

同時將語句改成

mutation($length: Int) {
  setWidth(width: $length) {
    width
  }
}
複製代碼

在length前加一個$就能在語句中使用了,能夠查看一下瀏覽器控制檯的請求有什麼變化

稍微複雜一點

咱們再看一點複雜的模型,如今給盒子裏裝點隨機的小球,將數據源修改成以下形式

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中增長一個類型,而且修改box的類型

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的數據一層應該只攜帶本層的信息,想象一下這個需求,我須要box和box裏全部color爲red的球。正確作法以下,先修改box讓他有參數

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

而後在resolvers裏添加一個Box,注意resolver的第一個參數parent指向的是他的父元素也就是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
    }
  }
};
複製代碼

如今可使用查詢查出全部的顏色爲red的球

query {
  getBox {
    width
    balls (color:"red"){
      size
      color
    }
  }
}
複製代碼

若是沒有參數,就是所有的球。graphql這麼設計的好處是,能夠在數據庫查詢中,少寫不少的join,壞處是更多的查詢次數

前端使用

在使用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-client官網。由於提供了錯誤處理,數據緩存,錯誤處理等等,配置項稍顯複雜,官方提供了一個apollo-boost的東西簡化了配置。咱們能夠本身對照官方實現一個簡化版,深刻學習一下。

客戶端實現

私貨警告

如下內容在react16.8+的hooks API和typescript下實現,模仿官方包的api設計,去掉了緩存等功能。緩存能夠說是apollo提供的核心功能了,但爲了緩存增長了巨量的代碼,並不適合學習。 首先咱們建立一個新的react工程

create-react-app graphql-client --typescript
複製代碼

接下來咱們要參考官方包實現如下幾個使用頻率最高的模塊(超級精簡版):ApolloClient、ApolloProvider、useQuery、Query

ApolloClient

入參包括uri,fetchOptions等,實際就是一個http請求庫,這部分省點事直接用axios替代吧。注意官方實例,使用了從graphql-tag導出的gql方法處理graphql字符串,包括server端也有這個方法,它的做用是將字符串轉換成ast,方便檢查編寫schema文件時出現的錯誤,本文中都省略掉了,都直接使用字符串。

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

這個組件看得出是將client做爲一個context提供給下文,使用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的入參固定,因此建立一個hook很容易。這裏使用了一個泛型T去定義預期返回值的類型,官方包在這裏還使用了第二個泛型來肯定variables參數的類型。

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>
  );
}
複製代碼

由於獲取的數據是可預測的,因此在寫出查詢語句的同時完成類型文件。若是到如今編碼正確,你的react項目上已經能夠看到效果了

Query

該組件在建立hook以後就很是容易了,一筆帶過

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並非難點,只要能寫出正確的查詢語句,必然能獲得正確的查詢結果,難點多是融合進現有項目,使用typescript等工具,加速開發效率。改造的難度依然在後端,想到咱們以前的後端太過簡陋,如今來優化一下吧,順便補齊後端很是重要的身份認證等功能

後端目錄結構優化

後端一直以來咱們都在一個文件裏寫,隨着模型變得複雜,代碼開始臃腫了,同時在字符串裏寫schema也挺彆扭,最好能寫到單獨的graphql/gql文件裏去,這樣還能有編輯器提供的格式化功能(我使用的是vscode中的Apollo GraphQL插件)。

我在這裏的處理是將typeDefs拆分紅對應的graphql文件,resolvers也進行文件拆分,而後使用文件掃描器自動依賴。

如今建立一個typeDefs文件夾,而後建立一個index.graphql文件,將原來的typeDefs字符串複製進去。

在同一個目錄建立一個ball.gql文件,將index.graphql文件中Ball相關的定義剪貼進去。

接下來建立一個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
};
複製代碼

這樣index.js裏的typeRefs就能夠改爲這樣

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

用一樣的方式解決resolver,不過要先建立一個dataSource.js,將Ball和box移到這個文件裏,而後建立一個resolvers文件夾,而後建立一個query.js文件,一個mutation.js文件,一個box文件(通常根據功能模塊分文件,這裏是個例子)。好比如今query.js就是這樣

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

其他略過。再在utils.js建立一個resolver掃描器,每一個文件的默認導出都是一個普通對象,因此處理起來並不複雜

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;
}
複製代碼

同理能夠搞定index文件裏的resolvers

const resolvers = requireAllResolvers(path.resolve(__dirname, './resolvers'))
複製代碼

Apollo會幫咱們把數組內的內容進行merge,因此咱們只要保證每一個文件裏的內容符合格式便可。若是一切順利的話,項目仍然能夠正確運行,並無什麼改變,可是卻能夠在這基礎上橫向擴展了。

數據庫

對於一個web服務來講,數據應該儲存在專門的數據庫中,好比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;
複製代碼

回到express項目,由於模型有點細微的變化,加入了主鍵id,因此gql文件中的schema須要加入id,下面列出須要修改的schema

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();
      });
    });
  })
}
複製代碼

在resolver中引入sql前須要知道resolver的四個參數。第一個參數parent,是當前元素的父元素,頂級的schema的父元素稱爲root,大部分教程中用_代替。第二個參數是params,也就是查詢參數。第三個參數是config,其中有一個參數dataSources咱們過會兒須要用到。第四個參數是context,它的入參是express的Request和Response,能夠用來傳入身份信息,進行鑑權等操做。

咱們把封裝好的query函數放進這個dataSources。在index.js中修改

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

接着就能夠修改resolvers,先實現第一個getBox,由於如今不止一個盒子,因此返回的是一個數組,schema已經進行了修改

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

query返回的是一個Promise,Apollo是支持這種寫法的

而後完成Box的balls,咱們須要從parent中拿到父元素的id

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

最後還有一個Mutation須要修改,schema中的定義返回的是被修改後的box,因此須要兩條sql來完成這個部分

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的加持,不然怎麼寫都會被認爲是玩具。可是咱們思考一個問題,typescript的類型和graphql都是對模型的描述,基本一致,只在語法上有一些區別,能不能通用呢。官方提供了相關的API實現這個需求,可是語法並不簡潔,type-graphql也許是更好的選擇。

先導入typescript和type-graphql,還有以前用到的包的描述文件,另外type-graphql掃描註解用到了reflect-metadata這個還未進入標準的特性,因此須要引入這個包

yarn add typescript type-graphql reflect-metadata @types/mysql @types/express
複製代碼

typescript老規矩,先寫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
  }
}
複製代碼

而後把全部require改爲import,後綴名js改爲ts就好了若是有報錯就寫個any

運行項目咱們使用ts-node,全局安裝ts-node以後執行ts-node index.ts便可啓動。正式項目咱們可使用pm2指定解釋器或者將項目編譯成js來運行。而後咱們將type-graphql引入項目。

很是遺憾的是引入type-graphql後代碼結構發生大改,除了數據庫相關的內容基本能夠重寫了,graphql文件也不須要了。先創建一個models文件夾,新增兩個文件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中的屬性。Field的第一個參數是個函數用來表示類型,函數的入參沒有意義,寫type是爲了語義化,返回值是類型(typescript的數字類型是number,但graphql的數字類型分爲Int和Float,若是不指定爲Int,typegraphql默認number爲Float);第二個參數是配置項,nullable默認爲false,這裏能夠改成true,description是註釋

而後修改一下index.ts,引入graphql以前的東西基本都不要了,就保留數據庫的query方法,另外query方法不放到dataSources中了,放到Context中

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兩個參數或一個schema參數。看buildSchema這個函數的入參,就知道咱們還有resolvers沒有改造,在resolvers文件夾下新建一個Box.resolver.ts,把以前的三個resolver改造一下

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表明是這兩個基礎類型下的resolver,參數是個函數,表示預期的返回類型;FieldResolver與類註解Resolver關聯,表明Box這個對象類型下的字段balls的resolver;Arg註解的是參數,第一個參數是入參的參數名,它還有第二個參數,能夠配置nullable;Root註解的參數是父元素,在balls方法中拿到了父盒子的id;Ctx註解的是Context,從中取到了apolloserver中context的query方法。

到這裏,不須要手寫graphql文件,依然完成了一個graphql服務器,並且typeDefs和resolvers結合到了一塊兒,不用擔憂漏寫了。而且咱們的程序已經大變樣,不深刻學習一番已經看不懂了,恭喜你離創建技術護城河更進一步。

type-graphql有個內置的權限管理,有興趣的話能夠看看Authorized註解

typeorm

ORM是否要引入項目,主要仍是看項目需求。這裏使用typeorm,它在寫法上與typegraphql很是契合,由於他能夠直接複用typegraphql中建立的model,是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等多種數據庫。synchronize若是爲true,typeorm會根據模型自動建表,若是模型有修改還會對錶結構進行修改(外鍵等緣由會致使修改失敗項目沒法啓動,須要手動干預或者使用typeorm中的migrations)。logging爲true會在控制檯打印自動生成的sql語句。

先修改index.ts

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

跟typegraphql的buildSchema同樣,createConnection也是個異步的promise,因此爲了防止一些意外狀況,app.listen的操做要在這兩個過程以後。若是已經創建了ormconfig.json文件,createConnection會自動讀取其中的配置,不然須要將其做爲參數填進去。

而後修改models,以後models能夠同時在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[];
}
複製代碼

首先將類繼承自typeorm中的BaseEntity,而且類上增長了一個Entity註解,參數的配置項中加一個name能夠指定表名;Column註解這個屬性是數據庫的一列,參數能夠指定具體的類型和長度;PrimaryGeneratedColumn註解表明這是一個自增主鍵;OneToMany是個特殊的註解,用來描述實體直接的relations,一共包括OneToMany,ManyToOne,ManyToMany三種,具體用法說來話長,請自行摸索。這樣就和以前創建的數據庫對應了,感興趣的可使用typeorm的自動建表功能看看有什麼不一樣。

如今咱們能夠拋棄簡陋封裝的query方法,直接使用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的內容很是多,若是對其餘的orm有經驗,上手仍是很快的。另外若是真的有typeorm寫不出來的sql,該手寫就手寫吧~

總結

這篇文章我很早就開始寫了,可是攤子鋪的太大,因此一直寫不完主要是打怪物獵人冰原。文中選取的模型也很是簡單,可是基本完成了一個graphql服務器的框架,固然也留下不少內容沒有講,好比很是重要的標量類型和輸入類型。由於本人主業是前端,會一點java後端,因此若是文章中出現概念性錯誤,請評論指出,共同進步。

相關文章
相關標籤/搜索