<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      GraqphQL 學(xué)習(xí)

        GraphQL是Graph+QL。Graph是圖,用圖數(shù)據(jù)結(jié)構(gòu)來描述數(shù)據(jù),來組織數(shù)據(jù)。QL是query language,寫請求就像寫query。GraphQL就是用圖來組織數(shù)據(jù),寫query的方式來請求。怎樣用圖的方式來組織數(shù)據(jù)?定義Schema(類型), type 類型名稱 {}, 

      type Library {
        branch: String!
        books: [Book!]
      }
      
      type Book {
        title: String!
        author: Author!
      }
      
      type Author {
        name: String!
      }

        大括號中是鍵值對,鍵表示該類型有哪些字段可供查詢,值表示查詢該字段會(huì)返回什么類型的數(shù)據(jù)。查詢library可以查到book,查到book可以查到author,數(shù)據(jù)形成一個(gè)圖。String,Int,F(xiàn)loat,Boolean,ID是GraphQL提供的基本數(shù)據(jù)類型,ID類型比較特別,表示唯一性,它會(huì)被序列化成字符串。Library,Book是基于基本類型,自定義的對象類型。定義對象類型,不能嵌套定義,每一個(gè)對象類型必須單獨(dú)定義,再進(jìn)行組合。!表示不能返回null,如果沒有!,就可以返回null, null可以賦值給任意類型。[]表示返回的是列表(list)。Graphql沒有時(shí)間類型,不過可以用字符串表示,new Date().toISOString()。為了能夠讓客戶端查詢數(shù)據(jù),還要定義一個(gè)特殊的類型Query。Query是客戶端和服務(wù)端的約定,客戶端能夠查詢哪些字段,是查詢的入口點(diǎn)。

      type Query {
          library: Library
      }
      

        客戶端能夠查詢library了,但怎么請求?首先是一個(gè){},然后是{libary}, 因?yàn)閘ibrary返回的是一個(gè)對象類型,還要繼續(xù){},query需要的某個(gè)或某些屬性,括號里面就是想要的屬性,

      {
          library {
              branch
              books {
                  title
              }
          }   
      }

        這就是GraphQL提出的,query要明確表明想要什么。query都是query對象的某個(gè)屬性或某些屬性,對象類型{屬性} 就是query對象類型的某個(gè)屬性。 但剛開始時(shí),libarary并不是某個(gè)對象的屬性,它是根,所以寫一個(gè){},表示根對象,所有的query 都是以{}開始,以對象的某個(gè)屬性結(jié)束,這也是scalar 類型的概念,query最終都會(huì)落腳到scalar類型。scalar類型就是類型再也沒有子字段,通常是基本數(shù)據(jù)類型。query寫好了,后端怎么返回?cái)?shù)據(jù)呢?函數(shù),為類型中的每一個(gè)字段提供函數(shù),進(jìn)行數(shù)據(jù)的返回,這些函數(shù)也稱為resolver函數(shù),query發(fā)過去,調(diào)用函數(shù),返回?cái)?shù)據(jù)

      Query {
          library: () => {
              return {
                  branch: '1',
                  books: ['js', 'react']
              }
          }
      }
      
      Library {
          branch: () => '11',
          books: () => ['html', 'css']
      }

        schema中定義返回什么類型,resolver函數(shù)一定返回什么類型,請求和返回值都是JSON數(shù)據(jù)。GraphQL的核心是schema(類型)。前端根據(jù)定義的schema來query,服務(wù)端根據(jù)schema來提供resolver函數(shù)。增刪改也是一樣,定義特殊的類型Mutation,字段表明可以進(jìn)行哪些增刪改,值是增刪改返回什么數(shù)據(jù)類型,類型的定義沒有什么區(qū)別。

        以上說的都是理論,GraphQL只是定義了一套理論,如果要實(shí)踐,就要寫一個(gè)GraphQL實(shí)現(xiàn)。先看一下,寫一個(gè)GraphQL實(shí)現(xiàn)需要什么?首先,要提供一個(gè)請求入口(endpoint),供客戶端訪問,比如http://localhost:4000/graphql。其次,解析Query請求, 看是否符合GraphQL語法規(guī)范(有效的語法),是否符合Schema的定義。然后調(diào)用Resolver函數(shù),query就查詢數(shù)據(jù),mutation就存儲(chǔ)數(shù)據(jù),subscription,服務(wù)端還要開一個(gè)chanel來進(jìn)行數(shù)據(jù)通信。最后,收集所有resolver返回的數(shù)據(jù),把它序列化為JSON格式,返回給請求者。

        解析請求,驗(yàn)證schema,調(diào)用Resolver函數(shù),收集并返回?cái)?shù)據(jù),都是通用的,于是出現(xiàn)了Apollo Server等第三方框架。有了,它們寫一個(gè)graphql服務(wù),就只需要提供schema和resolvers。假設(shè)使用Apollo Server,mkdir graphql-server && cd graphql-server,再npm init -y  && npm pkg set type="module",npm install @apollo/server graphql安裝依賴,最后touch index.js, 創(chuàng)建schema, resolvers 
      import { ApolloServer } from '@apollo/server';
      import { startStandaloneServer } from '@apollo/server/standalone';
      
      //創(chuàng)建Schema, 在模板字符串前面加#graphql指令,字符串被認(rèn)定為graphql語法,可能被語法高亮。
      const typeDefs = `#graphql
        type Query {
          hi: String
        }
      `;
      
      // 創(chuàng)建resolvers, 它是一個(gè)對象
      const resolvers = {
        Query: {
          hi: () => "world"
        }
      };
      
      // 把 schema 和resolvers 傳遞給Apollo Server, 創(chuàng)建服務(wù)器
      const server = new ApolloServer({
        typeDefs,
        resolvers,
      });
      
      const { url } = await startStandaloneServer(server, {
          listen: { port: 4000 },
      });
      
      console.log(`??  Server ready at: ${url}`);
      

        node index.js,啟動(dòng)服務(wù),瀏覽器輸入localhost:4000,出現(xiàn)一個(gè) sandbox,左邊列出了Schema,中間Operation寫query,比如{hi}, 右邊的response返回結(jié)果。再寫個(gè)復(fù)雜一點(diǎn)的的schema,帶參數(shù)并返回對象類型。比如查詢某個(gè)用戶信息,需要傳遞id。schema改成如下

      const typeDefs = `
        type Query {
          employee(id: ID!): Employee
        }
        type Employee {
          id: ID
          name: String
          email: String
        }
      `;

        query如下

      {
          employee(id: 42) {
              name
              email
          }
      }

        那怎么寫resovler呢?請求Query類型的employee,employee需要一個(gè)resolver函數(shù),接受參數(shù),但它返回的是Employee類型,query又請求了Employee類型的name和email, Employee類型的name和email也需要resolver函數(shù)返回?cái)?shù)據(jù),但name和email返回的數(shù)據(jù)是employee resolver返回那個(gè)employee對象的name 和email,它們r(jià)esolver函數(shù)依賴employee的resolver函數(shù)的返回值,從employee resolver的返回值中取數(shù)據(jù)。怎么建立resolver之間的關(guān)系呢?參數(shù)。resolver函數(shù)本身接受4個(gè)參數(shù)(parent, args, context, info)。parent 用來建立聯(lián)系,args接受客戶端傳遞過來的數(shù)據(jù)。先調(diào)用employee的resolver,返回employee(對象),再調(diào)用name和email的resolver,從employee中查詢name和email, 有先有后,先的如果為父,那么后的就為子,employee的resolver的返回值可以看作是name和email的resolver的父級。name和email中resolver的parent參數(shù),就是employee resolver的返回值。employee由于是頂級查詢,一般沒有parent,所以參數(shù)用 _ 表示。整個(gè)resolver 如下

      const resolvers = {
          Query: {
              employee: (_, args) => {
                  console.log(args.id);
                  return { "id": 42, "name": "sam", "email": "123@qq.com" }
              }
          },
          Employee: {
              name: parent => parent.name,
              email: parent => parent.email
          }
      };

        當(dāng)resolver返回一個(gè)對象類型時(shí),服務(wù)器會(huì)遍歷該類型的字段,依次調(diào)用每一個(gè)字段的resolver 函數(shù),然后把這些函數(shù)的返回值收集起來,形成一個(gè)響應(yīng)。在這個(gè)例子中,GraphQL服務(wù)繼續(xù)一個(gè)接一個(gè)的遍歷Employee對象的字段(name和email),調(diào)用每個(gè)字段的resolver函數(shù),每個(gè)resolver函數(shù)都會(huì)接受到employee的resolver函數(shù)的返回值作為第一個(gè)參數(shù),GraphQL服務(wù)使用這3個(gè)resovler函數(shù)的返回值,組合到一起,形成query的響應(yīng)(As each field is resolved, the resulting value is placed into a key-value map with the field name(or alias) as the key and the resolved value as the value. This continues from the bottom leaf fields of the query all the way back up to the original field on the root Query type. Collectively these produce a structure that mirrors the origianl query which can then be send(typically as JSON) to the client which requested it )

      {
          data: {
              employee: {
                  name: "sam",
                  email: "123@qq.com"
              }
          }
      }

        但是像email這種resolver(parent => parent.email),只是parent對象的某個(gè)屬性值,太簡單了,于是許多grahpql實(shí)現(xiàn)都提供了一個(gè)這樣的默認(rèn)resolver。如果某個(gè)字段沒有resolver 函數(shù),直接讀取parent對象上和這個(gè)字段名相同的key的值。email字段名匹配parent對象的email屬性。resovler通常這么寫

      const resolvers = {
          Query: {
              employee: (_, args) => ({
                  "id": 42, "name": "sam", "email": "123@qq.com"
              })
          }
      };

        當(dāng)看到一個(gè)schema返回了對象類型,但該對象類型并沒有為每個(gè)字段提供resolver時(shí),不是不用寫resolver函數(shù),而是使用了默認(rèn)的resolver。由于schema定義employee(id: ID!): Employee,employee的resolver可以返回null,表示沒有找到id對應(yīng)的employee。

      const resolvers = {
          Query: {
              employee: (_, args) => null
          }
      };

        query請求沒有變化,服務(wù)器直接返回了employee: null,并不會(huì)調(diào)用Employee類型上面的resolver

        請求一個(gè)可能返回null的字段,客戶端雖然請求了它的屬性,但它可以直接返回null,沒有子屬性。同時(shí)請求id為42和43的employee的信息呢?

      {
        employee(id: 42) {
          name
          email
        }
        employee(id: 43) {
          name
          email
        }
      }

        報(bào)錯(cuò)了,不同的參數(shù),卻query相同的字段,使用alias,給字段起別名,格式為別名: schema名

      {
        employee42: employee(id: 42) {
          name
          email
        }
        employee43: employee(id: 43) {
          name
          email
        }
      }

        可以對任何查詢的字段進(jìn)行alias,name也可以寫成 nickname: name。只要GraphQL中定義的schema和自己要求的字段不一致,都可以alias。但還有一個(gè)問題,name,email字段重復(fù)了,可以把這些字段抽取出來,形成一個(gè)fragement。 由于字段都是定義在Schema類型中的,所以fragement也是基于Schema類型的。 在query中使用fragement,在它前面加...,query如下

      {
        employee42: employee(id: 42) {
         ...NameEmail
        }
        employee43: employee(id: 43) {
         ...NameEmail
        }
      }
      
      fragment NameEmail on Employee {
        nickname: name
        email
      }

        再進(jìn)一步,參數(shù)能不能是個(gè)變量, 想查詢哪個(gè)id就傳哪個(gè)id?有點(diǎn)復(fù)雜,首先給query起個(gè)名字,在最外層的{前面加query關(guān)鍵字和名稱,比如 query getEmployeeById {...},然后再在名字后面加(),括號中定義變量(變量名:變量類型)。變量名必須以$開始,變量的類型必須是scalars, enum,或input 類型。定義好變量后,query 語句中就可以使用變量。

      query getEmployeeById($employeeId: ID!) { #聲明變量 $employeeId
          employee(id: $employeeId) { #使用變量
              name
              email
          }
      }
      # 或者
      query getEmployeeById($employeeId: ID!) {
          # 如果query中使用framement
          employee(id: $employeeId) {
              ...NameEmail
          }
      }
      fragment NameEmail on Employee {
         # name(id: $employeeId) fragement也能夠獲取到變量
          name
          email
      }

        在sandbox,中間Operation下面有一個(gè)Variables, 可以給query中的變量傳值,要傳一個(gè)對象,屬性是在query中定義的變量,不過沒有$符號,值就是query中變量要接受的值,比如{employeeId: 42}。說一下enum類型,有些變量的取值是有特定范圍的,比如性別,就男和女。聲明類型時(shí)把可能取值范圍都列出來,這個(gè)類型用enum來聲明,

      enum Gender {
          MAN
          WOMAN
      }

        由于enum類型的變量取值只能是它定義的列表中的一個(gè),Apollo Server會(huì)把值序列化為字符串,enum類型可以和scalar 類型一樣使用。

      const typeDefs = `
        type Query {
          employee(id: ID!, gender: Gender): Employee
        }
        type Employee {
          id: ID
          name: String
          email: String
        }
        enum Gender {
          MAN
          WOMAN
        }
      `

        然后query如下

      query getEmployeeById($employeeId: ID!, $gender: Gender) {
        employee(id: $employeeId, gender: $gender) {
          name
        }
      }

        給變量傳值時(shí),gender的取值為 “MAN” 和“WOMEN” 兩種,傳的是字符串,就是因?yàn)锳pollo Server 把enum 序列化成了字符串

      {
       "employeeId": 42,
       "gender": "MAN" // 傳的是字符串
      }

        input類型,和type定義類型一樣,不過它僅用于定義參數(shù)類型。當(dāng)Schema要求傳遞的參數(shù)越來越多時(shí),一個(gè)一個(gè)列出就有點(diǎn)麻煩,可以聲明一個(gè)對象類型,Schema中,參數(shù)是對象類型。比如新建一個(gè)employee,需要姓名,年齡,性別,地址等。input  CreateEmployeeInput {name: String, gender: Gender, age: int} ,更改數(shù)據(jù)用mutation,type Mutation { createEmployee(input: CreateEmployeeInput): Employee } ,typeDefs 添加如下內(nèi)容,

      const typeDefs = `
        # ....
        type Mutation {
          createEmployee(input: CreateEmployeeInput): Employee
        }
        input CreateEmployeeInput {
          name: String
          age: Int
          gender: Gender
        }
      `;

        resolver 中添加

      const resolvers = {
          // ...
          // 添加muation
          Mutation: {
              createEmployee(_, args) {
                  const {name, age, gender} = args.input;
                  return {
                      name, age, gender
                  }
              }
          }
      };

         可以直接寫Muation的請求,

      mutation {
        # 如果在語句中直接傳參,enum類型變量的值直接寫類型中的任意一個(gè)值,如MAN
        createEmployee(input: {name: "sam", age: 30, gender: MAN}) {
          name
        }
      }

         可以聲明變量的方式,來發(fā)送請求

      mutation CreateEmployee ($input: CreateEmployeeInput) {
        createEmployee(input: $input) {
          name
        }
      }
      
      # 傳遞的參數(shù)
      {
        "input" : {
          "name": "sam",
          "age": 30,
          "gender": "MAN"
        }
      }

         GraphQL還有幾個(gè)不太常用的語法:指令,interface,union。指令是用來改變query結(jié)構(gòu)的,在某些情況下,query這個(gè)字段,在某些情況下,又不query這個(gè)字段,@include(if Boolean) 和@skip(if Boolean) 如要Boolean是true,返回值包含或跳過,false則相反

      query getEmployeeById($employeeId: ID!, $gender: Gender, $withName: Boolean!) {
        employee(id: $employeeId, gender: $gender) {
          name @include(if: $withName)
          email
        }
      }

        union是幾個(gè)類型聯(lián)合在一起形成一個(gè)新類型,union Person = Student | Employee,當(dāng)Schema中返回Person時(shí),可能返回Student類型,也可能返回Employee類型。這時(shí)resolver和query都有點(diǎn)復(fù)雜,由于resolver中不能返回抽象類型,person的resolver它必須返回Student或Employee類型的對象,同時(shí)在Person類型下寫__resolveType確定返回的真正類型。

      const typeDefs = `
        type Query {
          person: Person
        }
        union Person = Student | Employee
      
        type Employee {
          name: String
          email: String
        }
      
        type Student {
          name: String
          studentNumber: Int
        }
      `;
      
      const resolvers = {
        Query: {
          person: () => ({
            // 返回 Employee 類
             "name": "sam", "email": "123@qq.com"
          })
        },
      
        Person: { 
          __resolveType(parent) {
            if(parent.email) {
              return "Employee"
            }
      
            if(parent.studentNumber) {
              return "Student"
            }
      
            return null // 拋出錯(cuò)誤
          }
        }
      };

        由于返回的類型不確定,query時(shí),也要query某個(gè)類型字段,使用inline fragement。此時(shí),還可以query 一個(gè)__typename字段,看是返回什么具體類型, __typename不用定義,Apollo Server 自動(dòng)加的, 每一個(gè)對象類型都自動(dòng)獲得一個(gè)__typename字段

      {
        person {
          __typename,
          ... on Student {
            studentNumber
          }
          ... on Employee {
            email
          }
        }
      }

        interface定義了多個(gè)類型共有的字段,子類型(具體類型)必須實(shí)現(xiàn)它, 當(dāng)shema中返回interface類型時(shí), resolver中要必須返回具體類型,當(dāng)query interfacei 中的字段時(shí),可以正常 query,如果要query特定的類型,必須使用inline framgemnet.

      const typeDefs = `
        type Query {
          person: Person
        }
        interface Person {
          name: String
        }
      
        type Employee implements Person {
          name: String
          email: String
        }
      
        type Student implements Person {
          name: String
          studentNumber: Int
        }
      `;
      
      const resolvers = {
        Query: {
          person: () => ({
            // 返回 Employee 類
             "name": "sam", "email": "123@qq.com"
          })
        },
      
        Person: { 
          __resolveType(parent) {
            if(parent.email) {
              return "Employee"
            }
      
            if(parent.studentNumber) {
              return "Student"
            }
      
            return null // 拋出錯(cuò)誤
          }
        }
      };

        query

      {
        person {
          __typename,
          name # 請求interface中的字段
          ... on Student { #請求具體類型的字段
            studentNumber
          }
          ... on Employee {
            email
          }
        }
      }

        現(xiàn)在resolver都是簡單的同步函數(shù),如果resolver中有異步操作,比如數(shù)據(jù)庫查詢,它就要返回promise,Apollo Server會(huì)等待Promise的resolve 或reject ,然后返回?cái)?shù)據(jù)。用pg庫演示一下,在默認(rèn)數(shù)據(jù)庫postgres下,創(chuàng)建users表,

      CREATE TABLE users(
          id INT PRIMARY KEY, 
          name VARCHAR(10),
          email VARCHAR(50)
      )
      
      INSERT INTO users (id, name, email)
      VALUES (1, 'sam', '123@qq.com'), (2, 'jason', '456@qq.com');

        在代碼中,npm i pg,  連接數(shù)據(jù)庫。

      import pg from 'pg'
      // ...
      
      const pgClient = new pg.Client({
          connectionString: 'postgres://postgres:123456@localhost:5432/postgres'
      });
      await pgClient.connect();
      
      const { url } = await startStandaloneServer(server, {
          listen: { port: 4000 },
      });

        pgClient就是數(shù)據(jù)庫的連接實(shí)例,用它來操作數(shù)據(jù)庫。那resolver中怎么獲取到這個(gè)連接呢?Apollo Server 有一個(gè)context的概念,在配置startStandaloneServer時(shí)設(shè)置,它是一個(gè)函數(shù),返回一個(gè)對象,這個(gè)對象就可以在resover中的第三個(gè)參數(shù)context獲取到。

      const { url } = await startStandaloneServer(server, {
          context: () => ({ pgClient }),
          listen: { port: 4000 },
        });

        然后

      const typeDefs = `
        type Query {
          users: [User] 
        }
      
        type User {
          name: String
          email: String
        }
      `;
      
      const resolvers = {
        Query: {
          users: async (_, args, context) => { // async 函數(shù)自己就是返回Promise
            // 從context中獲取數(shù)據(jù)庫的連接
            const users = await context.pgClient.query('select * from users')
            return users.rows
          }
        },
      };

        對數(shù)據(jù)庫的操作,還可以進(jìn)行一層抽象,添加了一層dataSources 層,data source 層操作數(shù)據(jù)庫,resovler 調(diào)用data sources 層。新建userModel.js, 專門操作users表,

      export const generateUserModel = (pgClient) => ({
          getAll: () => {
             return pgClient.query('select * from users')
          }
      });

        錯(cuò)誤處理:GraphQL執(zhí)行過程中出現(xiàn)錯(cuò)誤,Apollo Server會(huì)捕獲這個(gè)錯(cuò)誤,然后把它返回客戶端,服務(wù)器不會(huì)崩潰,也就是說,拋出錯(cuò)誤是可行的,在resovler函數(shù)中,可以拋出錯(cuò)誤。比如驗(yàn)證不通過,拋出錯(cuò)誤。 如果覺得返回的錯(cuò)誤信息太多,可以定制錯(cuò)誤。ApolloServer 構(gòu)造函數(shù)除了接受typeDefs和resolvers,還接受formatError,格式化GraphQL執(zhí)行產(chǎn)生的錯(cuò)誤。GraphQL產(chǎn)生的每一個(gè)錯(cuò)誤在返回客戶端之前,都會(huì)經(jīng)過formatError。它是一個(gè)函數(shù),接受兩個(gè)參數(shù),第一個(gè)是JSON化后的錯(cuò)誤信息,用來作為響應(yīng)返回給客戶端,第二個(gè)是原始的錯(cuò)誤,如果是resovler中拋出的錯(cuò)誤,error會(huì)被GraphQLError包裹。此時(shí),要想獲取原始錯(cuò)誤,需要unwrapResolverError

      const server = new ApolloServer({
        typeDefs,
        resolvers,
        formatError(formattedError, error) {
          if (formattedError.message.startsWith('Database Error: ')) {
            // 只要返回message 字段就可以了
            return { message: 'Internal server error' };
          }
      
          // 或者
          // if (error instanceof CustomDBError) {
          //   return { message: 'Internal server error' };
          // }
      
          // 再或者
          // import { unwrapResolverError } from '@apollo/server/errors';
          // if (unwrapResolverError(error) instanceof CustomDBError) {
          //   return { message: 'Internal server error' };
          // }
          return formattedError;
        }
      });

        第二種錯(cuò)誤處理方式是構(gòu)建帶有error的類型,返回這個(gè)類型,

      const typeDefs = `
        type Query {
          user(id: Int): UserWithError
        }
      
        type User {
          name: String
          email: String
        }
        type UserError {
          message: String
        }
        type UserWithError {
          userErrors: [UserError!]!
          user: User
      }
      `;
      
      const resolvers = {
        Query: {
          user: async (_, args, context) => {
            if (args.id > 2) {
              return {
                userErrors: [{ message: "沒有user" }],
                user: null
              }
            }
            const users = await context.pgClient.query('select * from users')
            return {
              userErrors: [],
              user: users.rows[args.id]
            }
          }
        },
      };
      
      // query
      {
        user(id: 3) {
          user {
            email
          }
          userErrors {
            message
          }
        }
      }

         GraphQL的resolver 函數(shù), 查詢數(shù)據(jù)庫時(shí),會(huì)造成 N+1 查詢 。比如一個(gè)博客網(wǎng)站, 提供query查詢整個(gè)posts, 由于post 和users 又有關(guān)系, 查詢Post時(shí)也想查詢它的user。

      const typeDefs =`
        type Query {
          posts: [Post!]!
        }
          
        type Post {
          id: ID!
          title: String!
          user: User!
        }
        type User {
          id: ID!
          name: String!
          email: String!
        }
      `

        在pg庫創(chuàng)建posts表,插入幾條數(shù)據(jù) 

      CREATE TABLE posts(
          id INT,
          title VARCHAR(100),
          userId INT
      )
      INSERT INTO posts (id, title, userid)
      VALUES 
      (1, 'learing web development', 1),
      (2, 'learing react', 1),
      (3, 'learing redux', 1),
      (4, 'learing backend development', 1),
      (5, 'learing node.js', 2)
      ;

        resolvers 修改如下

      const resolvers = {
        Query: {
          posts: async (_, args, { pgClient }) => {
            const query = 'select * from posts'
            console.log(query, 'post query')
            const result = await pgClient.query(query)
            return result.rows
          },
        },
      
        Post: {
          user: async (parent, args, { pgClient }) => {
            const query = {
              text: 'SELECT * FROM users WHERE id = $1',
              values: [parent.userid]
            }
            console.log(query, 'user query')
            const result = await pgClient.query(query)
            return result.rows[0]; // 是返回一個(gè)對象
          },
        },
      };

        查詢post對應(yīng)的user時(shí),

      {
        posts {
          title
          user {
            name
          }
        }
      }

        看控制臺的輸出,post的查詢(select * from posts)只執(zhí)行了一次,而user的查詢(SELECT * FROM users WHERE id = $1)卻執(zhí)行了5次。posts resovler返回的posts中每一條post都會(huì)發(fā)送一次請求(SELECT * FROM users WHERE id = $1),返回的posts中有多少條post, 就要執(zhí)行多少次請求,這就是N。

         但是1,2,3,4條post是屬于user 1, 而5屬于user 2,能不能把前4個(gè)變成一個(gè)請求,

         這要用DataLoader庫,npm install dataloader,它會(huì)把所有的user的請求用的id 參數(shù)都收集起來,發(fā)送一個(gè)query, 一個(gè)請求帶著所有的userid,然后返回所有的數(shù)據(jù),整個(gè)user的查詢就只有一次查詢。要先定義一個(gè)批量函數(shù),接受所有的userId, 去請求數(shù)據(jù)

       const batchUsers = async (userIds) => {
          const query = {
            text: 'SELECT * FROM users WHERE id = ANY($1::int[])',
            values: [userIds]
          }
          const result = await pgClient.query(query)
          return result.rows
        }

        怎么使用呢?把它傳給DataLoader的構(gòu)造函數(shù),創(chuàng)建一個(gè)DataLoader的實(shí)例,然后在resovler中調(diào)用實(shí)例的load方法,把id傳遞進(jìn)去。

      import Dataloader from "dataloader";
      
      function userLoader(pgClient) {
        const batchUsers = async (userIds) => {
          const query = {
            text: 'SELECT * FROM users WHERE id = ANY($1::int[])',
            values: [userIds]
          }
          const result = await pgClient.query(query)
          return result.rows
        }
      
        return new Dataloader(batchUsers);
      }
      
      // post resolvers
      Post: {
          user: async (parent, ars, { userBatch }) => {
            return userBatch.load(parent.userid)
          }
        },
      
      // 把 userloader 放到context中,供resolver 使用
      const { url } = await startStandaloneServer(server, {
        context: () => ({ pgClient, userBatch: userLoader(pgClient) }),
        listen: { port: 4000 },
      });

        現(xiàn)在的batchuser方法還有一點(diǎn)小問題,post 中resolver 調(diào)用load方法,調(diào)用了5次,分別傳入userid 是1,1,1,1,2, batchUsers方法的參數(shù)userIds接受到[1, 2],返回user數(shù)組。如果返回的數(shù)組是

        [
          { id: 2, name: 'jason', email: '456@qq.com' },
          { id: 1, name: 'sam', email: '123@qq.com' }
        ]

        就會(huì)有問題,接收的參數(shù)是數(shù)組,返回的結(jié)果也是數(shù)組,要建立某種關(guān)聯(lián)。dataLoader 會(huì)按照傳遞進(jìn)來的參數(shù)的順序從返回的結(jié)果中取值,也就是說,userid 是1, 是參數(shù)數(shù)組的第0個(gè)元素, dataloader就會(huì)從返回的結(jié)果中取第0個(gè)元素,bashUser要求返回的數(shù)據(jù)的id和 傳入的數(shù)據(jù)的 ids 順序保持一致

      function userLoader(pgClient) {
        const batchUsers = async (userIds) => {
          const query = {
            text: 'SELECT * FROM users WHERE id = ANY($1::int[])',
            values: [userIds]
          }
          const result = await pgClient.query(query)
      
          const userMap = {};
          result.rows.forEach(user => {
            userMap[user.id] = user
          }) 
          //  {1: {id: 1, name: 'sam'},  2: {id: 2} }
          return userIds.map(id => userMap[id])
        }
      
        return new Dataloader(batchUsers);
      }

         權(quán)限管理:context可以是異步的函數(shù),并且接受request 和response 做為參數(shù),因此可以從request的header中,取出token, 然后訪問數(shù)據(jù)庫獲取到用戶信息,放到context中,此時(shí),resolver中都能獲取user信息,可以進(jìn)行權(quán)限管理

      const { url } = await startStandaloneServer(server, {
        context: async (request, resonse) => {
          // 假設(shè)有一個(gè)getUserInfo
          const user = await getUserInfo(request.header.token)
      
          return ({
            pgClient,
            userBatch: userLoader(pgClient),
            user
          })
        },
        listen: { port: 4000 },
      });

        GraphQL服務(wù)寫好了,Apollo Studio測試成功,前端頁面怎么調(diào)用?Graphql和Restful API 有幾個(gè)不同, 首先,GraphQL只有一個(gè)endpoint, 并不像restful 有很多endpoint。其次,graphql 并不遵循h(huán)ttp 規(guī)范,前端都是發(fā)送post請求,query只是簡單的字符串,放在請求的body中。后端graphql 報(bào)錯(cuò),有可能返回200,需要查看響應(yīng)的對象,GraphQL的一切都是基于響應(yīng)。GraphQL的API可以用fetch請求,但通常使用第三方GraphQL客戶端,比如 apollo client, 它們幫忙處理http請求和響應(yīng),只需要把query和variables傳遞給它們,apollo client還有cache功能。npx create-react-app graphql-client 創(chuàng)建一個(gè)client項(xiàng)目,npm install @apollo/client graphql安裝依賴,在index.js中初始化apollo client, 就是連接哪個(gè)服務(wù)器,使用什么緩存,默認(rèn)情況下,apollo client 會(huì)把query回來的數(shù)據(jù)進(jìn)行緩存。npm install @apollo/client graphql

      import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
      
      const client = new ApolloClient({
        uri: 'http://localhost:4000/graphql', // 鏈接本地服務(wù)器
        cache: new InMemoryCache(),// 緩存到內(nèi)存中
      });
      
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(
        <ApolloProvider client={client}>
          <App />
        </ApolloProvider>,
      );

        在App.js中請求posts, 可以像普通的fetch請求一樣,在useEffect中調(diào)用client的query,但apollo client 提供了useQuery,它會(huì)返回error, loading 和data,實(shí)現(xiàn)了部分的狀態(tài)管理。

      import { useQuery, gql } from '@apollo/client';
      const GET_POSTS = gql`
        query GetPosts {
          posts {
            id
            title
            user {
              id
              name
            }
          }
        }
      `;
      
      export default function App() {
        const { loading, error, data } = useQuery(GET_POSTS);
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error : {error.message}</p>;
      
        return data.posts.map(({ id, title, user }) => (
          <div key={id}>
            <h3>{title}</h3>
            <p>{user.name}</p>
          </div>
        ));
      }

        graghql的返回值一定包含data或error,這是規(guī)定,當(dāng)成功請求到數(shù)據(jù)時(shí),data存在,error是null,當(dāng)請求失敗,error包含錯(cuò)誤信息,data為null。默認(rèn)情況下,Apollo client會(huì)把query的數(shù)據(jù)緩存起來,安裝瀏覽器插件Apollo Client Devtools能看到緩存數(shù)據(jù),插件中使用的也是程序中創(chuàng)建的client。在Firefox下面

         緩存的數(shù)據(jù)是扁平化的,它不是把整個(gè)數(shù)組進(jìn)行緩存,而是把數(shù)組中的每一項(xiàng)拿出來進(jìn)行緩存,數(shù)組中使用引用進(jìn)行關(guān)聯(lián)。post中關(guān)聯(lián)的user也拿出來進(jìn)行單獨(dú)緩存,post和user使用引用進(jìn)行關(guān)聯(lián),緩存的id是返回的__typenamet和Id,所以graphql 請求一定要返回id。it automatically attempts to identify and store the distinct objects (i.e., those with a __typename and an id property) from a query's data into separate entries within its cache. 

        寫一個(gè)mutation, 更新了一個(gè) post,在服務(wù)器typeDefs加

      type Mutation {
        updatePost(id: ID!, title: String!): Post
      }

        在resolvers中加

      Mutation: {
          updatePost: async (_, args, {pgClient}) => {
            var params = [args.title, args.id]
            // 要使用return * 把更新后的數(shù)據(jù)返回
            var query = 'UPDATE posts SET title = $1 WHERE id = $2 RETURNING *';
            const result = await pgClient.query(query, params);
            return result.rows[0]
          }
        }

        在前端app.js中,加一個(gè)update button,添加onClick事件,調(diào)用updatePost

      const UPDATE_POST = gql`
        mutation UpdatePost($updatePostId: ID!, $title: String!) {
          updatePost(id: $updatePostId, title: $title) {
            id
            title
          }
        }
      `
      
      export default function App() {
        const { loading, error, data } = useQuery(GET_POSTS);
        const [updatePost] = useMutation(UPDATE_POST);
        if (loading || error) return null
      
        return (
          <>
            <div><button>Add post</button></div>
            <div><button>delete post</button></div>
            {
              data.posts.map(({ id, title, user }) => (
                <div key={id} style={{ display: 'flex' }}>
                  <h3>{title}</h3>
                  <p>{user.name}</p>
                  <button
                    onClick={() => {
                      updatePost({
                        variables: {
                          updatePostId: id,
                          title: `${title} ${id}`
                        }
                      })
                    }} >
                    updatePost
                  </button>
                </div>
              ))
            }
          </>
        )
      }

        點(diǎn)擊updatePost 按鈕,頁面實(shí)時(shí)顯示了更改后的數(shù)據(jù)。mutation只是更新一條數(shù)據(jù)時(shí),apollo client會(huì)自動(dòng)更新cache,根據(jù)id進(jìn)行匹配。如果mutation返回的字段比緩存的對象的字段少,剩下的字段保持原樣不變。當(dāng)寫mutation時(shí)候,最好想一下已經(jīng)寫過的query,最好返回的數(shù)據(jù)和query返回的數(shù)據(jù)相同同。

         返回變更后的數(shù)據(jù),只是保持服務(wù)端數(shù)據(jù)和客戶端緩存數(shù)據(jù)同步的第一步,但有時(shí)并不夠,比如增加一條post,也返回增加后的post,但是頁面并沒有顯示出來。在服務(wù)端typeDefs 的mutation下面增加addPost,
      addPost(id: ID!, title: String!, userId: ID!): Post

        resolves對象的Mutation下面增加

      addPost: async (_, args, {pgClient}) => {
        var params = [args.id, args.title, args.userId]
        var query = 'INSERT INTO posts VALUES($1, $2, $3) RETURNING *';
        const result = await pgClient.query(query, params);
        return result.rows[0]
      }

        給前端的Add post, 添加一個(gè)onclick事件

      const ADD_POST = gql`
        mutation AddPost($addPostId: ID!, $title: String!, $userId: ID!) {
          addPost(id: $addPostId, title: $title, userId: $userId) {
            id
            title
            user {
              id
              name
            }
          }
        }
      `
      const [updatePost] = useMutation(UPDATE_POST);
      
      <div><button
              onClick={() => {
                addPost({
                  variables: {
                    addPostId: 10,
                    title: 'Learning Java',
                    userId: 2
                  }
                })
              }}

        添加成功,頁面并沒有顯示添加成功的post,看一下控制臺的cache,Post:10 已經(jīng)緩存起來了,但是頁面上顯示的是posts,在ROOT_QUERY 中,它的cache并沒有發(fā)生變化,還是5個(gè)數(shù)據(jù)。a newly cached object isn't automatically added to any list fields that should now include that object。本地cache的post數(shù)據(jù)和服務(wù)器中的post并不一致(同步)了。執(zhí)行mutation時(shí),修改后端數(shù)據(jù)。通常,希望更新本地緩存的數(shù)據(jù)來反映后端的修改。最直接的辦法是把這次mutation影響到的query再執(zhí)行一遍(refetch the query)。有兩種方式實(shí)現(xiàn),一種是使用useQuery返回的refetch,一種是useMuattion接受第二個(gè)參數(shù)refetchqueries, 把要refetch的query列出來。使用refetch

      import { NetworkStatus } from '@apollo/client';
      
      export default function App() {
        const { loading, error, data, refetch, networkStatus } = useQuery(GET_POSTS, {
          notifyOnNetworkStatusChange: true,
        });
      
        const [updatePost] = useMutation(UPDATE_POST);
        const [addPost] = useMutation(ADD_POST);
        // 檢測networkSatus的狀態(tài),一定要在loading 和error 檢測之前。
        if (networkStatus === NetworkStatus.refetch) return 'Refetching!';
        if (loading || error) return null
      
        return (
          <>
            <div><button
              onClick={async () => {
                await addPost({
                  variables: {
                    addPostId: 10,
                    title: 'Learning Java',
                    userId: 2
                  }
                })
                // refetch the posts query
                refetch()
              }}
            >Add post</button></div>
          ...

        使用refetchQueries

      <div><button
              onClick={async () => {
                await addPost({
                  variables: {
                    addPostId: 11,
                    title: 'Learning spirng',
                    userId: 2
                  },
      
                  refetchQueries: [
                    "GetPosts", //query的名稱
               // 或者{query: GET_POSTS}
      ], }) }} >Add post</button></div>

        但多了一次網(wǎng)絡(luò)請求,如果mutation返回了變更的所有字段,我們可以直接更新緩存,mutation第二個(gè)參數(shù)中,接受一個(gè)update方法, 它有兩個(gè)參數(shù),第一個(gè)參數(shù)是cache,表示Apollo client緩存對象,它提供對緩存 API 方法的訪問,例如 readQuery / writeQuery、readFragment / writeFragment、modify 和 evict。這些方法使您能夠?qū)彺鎴?zhí)行 GraphQL 操作,就像您正在與 GraphQL 服務(wù)器交互一樣。第二個(gè)參數(shù)是mutation的返回值,使用這個(gè)值通過 cache.writeQuery、cache.writeFragment 或 cache.modify 更新緩存。

      <div><button
          onClick={async () => {
            await addPost({
              variables: {
                addPostId: 11,
                title: 'Learning spirng',
                userId: 2
              },
              update(cache, { data: { addPost } }) {
                cache.modify({
                  fields: {
                    posts(existingPostsRefs = []) {
                      const newPostRef = cache.writeFragment({
                        data: addPost,
                        fragment: gql`
                          fragment NewTodo on Post {
                            id
                            title
                          }
                        `
                      });
                      return [...existingPostsRefs, newPostRef];
                    }
                  }
                });
              }
            })
          }}
        >Add post</button></div>

        cache.modify 直接操作緩存,更改某個(gè)緩存字段的值,或刪除某個(gè)緩存字段,所以它有fields字段,修改哪些緩存字段。怎么修改呢?給修改的字段名定義一個(gè)函數(shù),函數(shù)接受現(xiàn)有的字段的值作為參數(shù),然后返回新值作為緩存值。添加一個(gè)post,更改的是緩存中ROOT_QUERY.posts數(shù)組,所以給posts字段添加了一個(gè)函數(shù),把新添加的post的引用添加到posts數(shù)組中。借助 cache.writeFragment,可以獲得對添加的 post 的內(nèi)部引用,然后將該引用附加到 ROOT_QUERY.posts 數(shù)組。在mutation函數(shù)中對緩存數(shù)據(jù)所做的任何更改都會(huì)自動(dòng)廣播到正在監(jiān)聽該數(shù)據(jù)更改的查詢。因此,您的應(yīng)用程序的 UI 將更新以反映這些更新的緩存值。

        更新緩存還有一種情況,

         還有一種情況,就是messages 和chatId 想關(guān)聯(lián),那就不能直接cache.modify了,cache.modidy, 會(huì)更改所有的messages,就要使用readQuery和writeQuery了。

      const useCreateMessage = (chatId: string) => {
        return useMutation(createMessageDocument, {
          // update 
          update(cache, { data }) {
              const messagesQueryOptions = {
                query: getMessagesDocument,
                variables: {
                  chatId,
                },
              };
              const messages: any = cache.readQuery({ ...messagesQueryOptions });
              console.log(messages, 'message')
              if (!messages || !data?.createMessage) {
                return;
              }
              cache.writeQuery({
                ...messagesQueryOptions,
                data: {
                  messages: messages.messages.concat(data?.createMessage),
                },
              });
            },
        });
      };

        和create一樣,當(dāng)mutation修改多個(gè)對象或刪除對象時(shí),Apollo 緩存也不會(huì)自動(dòng)更新。只有當(dāng)mutation更新單個(gè)對象時(shí),它才會(huì)更新。所以刪除或更改多個(gè)對象時(shí),還是需要refetch或update函數(shù)來更新緩存。

        以上就是cache優(yōu)先策略,優(yōu)先使用cache,如果cache中沒有數(shù)據(jù),才請求服務(wù)器。cache 是整個(gè)應(yīng)用的cache,整個(gè)應(yīng)用的所有數(shù)據(jù)都進(jìn)行cache。所以對mutation 來說,要么更新cache, 要么refetch,來更新緩存,以保持?jǐn)?shù)據(jù)一致。可以更改cache策略,給useQuery提供fetchpolicy參數(shù),比如fetchpolicy: 'network-only', 先網(wǎng)絡(luò)請求服務(wù)器,不用檢查cache有沒有數(shù)據(jù)。不過,cache 策略,好像都是針對組件的首次渲染,組件首次出現(xiàn)時(shí),執(zhí)行的cache策略,比如頁面的首次加載,路由時(shí)組件來回切換。如果組件一直存在,組件更新時(shí),都是走cache,從cache中獲取數(shù)據(jù),不管是什么fetchpolicy(除了standby),cache更新了,頁面都會(huì)更新,cache沒有更新,頁面不會(huì)更新。

        DataLoader 詳細(xì)解釋

        DataLoader 在一次事件循環(huán)(during one tick of the event loop)中,收集所有的key(collects an array of keys),然后用這些key請求一次數(shù)據(jù)庫(hits the database once with all those keys), 返回一個(gè)promise,它resolve 一個(gè)數(shù)組(Returns a promise which resolves an array of values). 所以我們需要做的就是讓DataLoader 是一個(gè)批處理的函數(shù),接受數(shù)組keys作為參數(shù),返回一個(gè)promise,resolve 為值的數(shù)組。需要注意的是這兩個(gè)數(shù)組的長度必須相等,因?yàn)閐ataLoader 會(huì)進(jìn)行key/value 鍵值對存儲(chǔ),數(shù)組keys 的第一項(xiàng)是key,數(shù)組value的第一項(xiàng)是value,組成一個(gè)鍵值對,數(shù)組key的第二項(xiàng)是key,數(shù)組value的第二項(xiàng)是value,組成一個(gè)鍵值對,依次類推。

        使用DataLoader,使用它的load方法,每一個(gè)load方法都保存它的參數(shù)key,然后返回promise。在一個(gè)事件循環(huán)tick中,它收集到所有的key,然后把它們傳遞給批處理函數(shù)。批處理函數(shù)返回值存儲(chǔ)到相應(yīng)的key上,最后每一個(gè)load方法的promise都resolve成它的參數(shù)key 所對應(yīng)的值。DataLoader 是使用事件循環(huán)的tick處理來標(biāo)記什么時(shí)候觸發(fā)批處理函數(shù)。之所以這么做是因?yàn)椋粋€(gè)tick結(jié)束,一個(gè)query的所有l(wèi)oad方法都調(diào)用完成,也就意味著,Dataloader知道多少key來請求數(shù)據(jù)庫。

        如果在一個(gè)resolver中就要請求多個(gè)id的數(shù)據(jù),可以使用loadMany,

      userLoader.load([3, 4]).then(res => {
          console.log('return an array of values', res)
      })

        Data loader 是每一個(gè)請求的batch 和cache,當(dāng)server 收到一個(gè)graphql請求后,它就會(huì)創(chuàng)建一個(gè)DataLoader的實(shí)例,來處理請求,當(dāng)返回響應(yīng)時(shí),這個(gè)DataLoader的實(shí)例就會(huì)被垃圾回收了。

        當(dāng)booksId傳入[1,2,3] 時(shí),數(shù)據(jù)庫返回10條數(shù)據(jù),dataLoader的批處理函數(shù)要做一個(gè)映射,哪一個(gè)id和哪些數(shù)據(jù)對應(yīng)起來, 看結(jié)果是二位數(shù)組。

       

      posted @ 2024-06-24 23:00  SamWeb  閱讀(272)  評論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 国模精品视频一区二区三区| 漂亮人妻被黑人久久精品| 国产一区二区不卡精品视频| 人人澡人摸人人添| 小污女小欲女导航| 国产口爆吞精在线视频2020版| 99RE8这里有精品热视频| 孟连| 午夜精品福利亚洲国产| 国产av无码专区亚洲av软件| 天天躁夜夜踩很很踩2022| 中文字幕日韩区二区三区| 亚洲国产在一区二区三区| 一二三四中文字幕日韩乱码| 亚洲精品福利一区二区三区蜜桃 | 加勒比亚洲天堂午夜中文| 色噜噜噜亚洲男人的天堂| 久久亚洲精品情侣| 亚洲色成人网站www永久下载| 国产嫩草精品网亚洲av| 国产精品自在线拍国产手机版| 一个色综合国产色综合| 男人av无码天堂| 乱码中文字幕| 国产精品一区二区久久毛片| 欧美三级中文字幕在线观看| 精品日韩亚洲av无码| 成人区精品一区二区不卡| 国产乱码精品一品二品| av亚洲一区二区在线| 日本精品不卡一二三区| 土默特左旗| 国产精品毛片一区二区| 亚洲+成人+国产| 久久精品国产再热青青青| 国产精品国语对白一区二区| 武装少女在线观看高清完整版免费| 无码天堂亚洲国产av麻豆| 成人网站网址导航| 久久er99热精品一区二区| 麻豆av一区二区三区|