GraphQL + Apollo + Vue 牛刀小試

前言

GraphQL 這門新技術在去年就開始火熱起來,今年也在不少技術週刊、論壇上看到關於這門新技術的研究和討論。所以做爲一名前端開發,緊跟技術潮流是必須的 🤣,週末便花了點時間對 GraphQL 進行了相關學習,學習過程當中寫了一些簡單的 demo,在此過程當中發現這玩意是真的香啊,因此決定要開篇博客來記錄下這個過程。html

什麼是 GraphQL ?

當學習一本新技術時,首先就要去了解這門新技術的定位和一些相關概念,而後就要去研究它的誕生可以解決哪些痛點和帶來哪些爽點。我我的習慣是直接去官方文檔找到這些疑問的答案。打開官網,你就看到了下面有很醒目的一句話:前端

一種用於 API 的查詢語言vue

還附加了幾句頗有意思的話:java

  • 請求你所要的數據,很少很多
  • 獲取多個資源,只用一個請求
  • 描述全部的可能的類型系統
  • 支持多種語言 (js、java、go)

說實話我看到這些第一反應就是感受很酷,但也給我帶來了不少疑惑,單從紙面上看仍是比較難理解的,沒有實際操做的話就很難體會上面說的那些特徵。因此我開始了一波學習和 demo 的嘗試 🤔node

GraphQL 一些關鍵概念包含 TypeSchemaQuery, Mutation等,下面會分別作一下簡單的說明,具體仍是要結合實際代碼進行分析。react

查詢 (Query)

順着文檔看,首先就看到到了關於查詢的相關使用,所謂的查詢就是向服務端獲取你要想的數據,好比我要查全部的用戶列表,webpack

// 先定義 User 數據結構
type User {
  id: Int!
  name: String!
  age: Int!
}

// query 查詢
query {
  // 返回一個User類型的集合
  userList() : [User!]
  // 能夠傳參查詢
  // 根據id查詢用戶信息
  orderUser(id: Int!) : User
}
複製代碼

REST 風格接口應該是這樣子的git

GET /api/v1/userList
GET /api/V1/userList/:id/
複製代碼

這時候,你會對GraphQL的解析---查詢語言會有點小小的感觸了github

還有,querytype的使用息息相關,因此在使用query的時候,必定要先去了解type類型系統,關於type類型系統在官網的文檔 上已經寫得很是詳細,這裏我就不具體說了web

變動 (Mutation)

GraphQL 中的 Mutation 是用來改變服務器上的數據的。對應着 REST 風格中的 PUT,DELETE,POSTMutation的語法風格和Query很相似。關鍵在解析Mutation過程當中會有所不一樣。還有值得注意的是,查詢字段時,是並行執行,而變動字段時,是線性執行,一個接着一個。

好比說,我要變動一個用戶user的名字

mutation {
  // 經過參數 id 去查到對應的用戶信息
  MutationUserName(id: Int!, name: String!) : User!
}
複製代碼

解析 MutationUserName

Mutation: {
  MutationUserName(_, { id, name }) {
    const user = userList.find(val => val.id === id);

      if (!user) {
        throw new Error(`找不到id爲 ${id} 的user`);
      }

      user.name = name

      return user;
    }
  }
複製代碼

Schema

在 GraphQL 中,Schema 主要是用來描述數據的形態,哪些數據能被查詢到,因此在 Schema 中主要定義可用數據的數據類型。這麼說吧,你想要查到的數據都必需要在 Schema 中進行定義,因此這裏是須要寫不少 type 的,這裏還須要統必定義 QueryMutation,也就是要把上面那些定位所有放到這裏來

🌰:

type User {
  id: Int!
  name: String!
  age: Int!
}

type Query {
  userList() : [User]
  orderUser(id: Int!) : User
}

mutation {
  MutationUserName(id: Int!, name: String!) : User
}
複製代碼

很很基礎的內容大概就是醬紫,下面應該要開始來一波實戰操做了

Apollo-GraphQL

Apollo-GraphQL 是基於 GraphQL 封裝的一種實現,它能夠在服務上進行分層,包括 REST api 和 數據庫,它包含客戶端和服務端,還有 GUI 開發工具,讓開發人員能夠快速上手進行開發。

架構圖

我在 google 關於 GraphQL 的時候,就發現了這個平臺,在官方文檔上看到了一些講解和入門練習。發現其入口門檻其實並不高,安裝幾個依賴即可快速啓動服務端,客戶端也支持了 vuereact/rnng等熱門的前端框架。我這裏的 demo 也是根據文檔教學一步步作出來的,下面就來說講個人 demo 實現過程和一些思路。

開始實戰

具體想法

  • 以搭建博客網站爲例,有博客列表、分類、博客信息 (query)
  • 點擊某個博客,跳轉到具體文章內容,返回時有已讀標註 (mutation)
  • 服務端使用 apollo-server-express
  • 客戶端使用 vue-apollo
  • 數據爲 mock 的靜態 json

搭建服務端

這邊採用 apollo-server-express 快速搭建服務端

首先安裝依賴

yarn add apollo-server-express express graphql
複製代碼

好,這個 demo 只須要上面三個工具

對於apollo-server,比較基本的就是要搞清楚 schemaresolvers 應該如何定義,這些理解起來仍是比較容易,但若是想玩得很溜,確定須要投入大量的時間去研究。

其實就是

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

定義好 typeDefs(schema) 和 resolvers,即可快速啓動

首先,就拿本人博客文章爲例,把 mock 的數據源造好

[
  {
    "id": 1,
    "title": "記一次系統前端底層升級總結",
    "date": "2018-11-11",
    "introduction": "最近參與了一個比較大的類後臺管理系統的前端開發(vue技術棧),並負責了該系統的底層升級,升級過程期間,遇到了很多問題,在解決問題的過程當中學到了不少,趁着今天雙11沒啥事作,那麼就花點時間總結下升級系統的過程吧",
    "category": "vue",
    "isRead": false
  },
  {
    "id": 2,
    "title": "Vue實踐小結(長期更新)",
    "date": "2018-11-04",
    "introduction": "近期都在用 Vue 全家桶進行項目開發,過程當中不免會遇到很多問題,這篇博客主要就是記錄開發過程當中遇到的問題,和每一個問題對應的解決方案。此外,Vue 框架和周邊生態會一直更新,以及發佈新功能,在實踐過程當中總會遇到一些所謂的「坑」,我也會把填坑過程記錄於此。坑是填不完的,這篇博客也是寫不完的。",
    "category": "vue",
    "isRead": false
  },
  {
    "id": 3,
    "title": "如何用Koa2返回文本和圖片流以及解決亂碼事件",
    "date": "2018-04-05",
    "introduction": "前兩天作項目的時候,碰到了一個要在客戶端(瀏覽器)中實現預覽 txt 文檔和圖片的小需求,在開發過程當中遇到了一些有趣的小插曲---客戶端讀取 txt 時出現各類奇怪亂碼,圖片就沒問題。一時半會還沒找到好的解決方法,由於那天又恰好週五,因此在週末的時候,我決定宅在家裏好好研究如何解決這個有趣的現象。",
    "category": "koa2",
    "isRead": false
  }
][
  ({
    "id": 1,
    "html": "<h1>記一次系統前端底層升級總結</h1>"
  },
  {
    "id": 2,
    "html": "<h1>Vue實踐小結(長期更新)</h1>"
  },
  {
    "id": 3,
    "html": "<h1>如何用Koa2返回文本和圖片流以及解決亂碼事件</h1>"
  })
]
複製代碼

而後,要開始思考如何定義 Schema 中的 type

我這邊一共定義瞭如下這些 type

type Article {
  id: Int!
  title: String!
  date: String!
  introduction: String!
  category: String
  isRead: Boolean!
}

type ArticleContent {
  id: Int!
  html: String!
}

type Category {
  num: Int!,
  name: String!
}

type Query {
  fetchArticles: [Article]
  getAllCategories: [Category]
  getArticleContent(id: Int!): ArticleContent
}

type Mutation {
  articleIsRead(id: Int!): Article
}
複製代碼

把 這些 schema 轉換爲 typeDefs, 須要用到

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

module.exports = gql`上面定義的type`;
複製代碼

定義 resolvers

resolvers 實際上是 querymutation 的實現過程。也就是說這裏會進行數據庫的查詢或者是 api 的調用等等,最終放回的結果在這裏出來。

我這邊的實現大概是這樣的

const articles = require("../data/articles.json");
const articleContent = require("../data/articleContent.json");

const resolvers = {
  Query: {
    fetchArticles() {
      return articles;
    },
    getAllCategories() {
      return articles.reduce((pre, cur) => {
        const cate = pre.find(_ => _.name === cur.category);
        if (cate) {
          cate.num++;
        } else {
          const obj = {
            name: cur.category,
            num: 1
          };
          pre.push(obj);
        }
        return pre;
      }, []);
    },
    getArticleContent(_, { id }) {
      return articleContent.find(val => val.id === id);
    }
  },

  Mutation: {
    // 標記已讀
    articleIsRead(_, { id }) {
      const article = articles.find(val => val.id === id);

      if (!article) {
        throw new Error(`找不到id爲 ${id} 的文章`);
      }

      if (article.isRead) {
        return article;
      }

      article.isRead = true;

      return article;
    }
  }
};

module.exports = resolvers;
複製代碼

好了,typeDefs 和 resolvers 搞定了,寫點服務器啓動腳本

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

const typeDefs = require("./schema");
const resolvers = require("./resolvers");

const PORT = 4000;

const app = express();

const server = new ApolloServer({
  typeDefs,
  resolvers,
  playground: {
    endpoint: `/graphql`,
    settings: {
      "editor.theme": "light"
    }
  }
});

server.applyMiddleware({ app });

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

而後 node server.js 一下看看

啓動不報錯的話,在瀏覽器http://localhost:4000/graphql能夠看到圖形化頁面

gql-ui

試試 query 查詢

gql-query

服務端搭建完成 🎉

客戶端搭建

最近在用 vue 比較多,因此就直接用 vue create xxx 進行一頓操做來弄了,主要也是基於 vue-apollo 文檔 進行學習和編寫

安裝?

yarn add vue-apollo graphql apollo-boost

import Vue from "vue";
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";

Vue.use(VueApollo);

const apolloClient = new ApolloClient({
  // 你須要在這裏使用絕對路徑
  uri: "http://localhost:4000/graphql"
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});

new Vue({
  el: "#app",
  apolloProvider,
  render: h => h(App)
});
複製代碼

配置支持 .gql || .graphql 文件後綴的 webpack loader

vue.config.js

module.exports = {
  // 支持 gql 文件
  chainWebpack: config => {
    config.module
      .rule("graphql")
      .test(/\.(graphql|gql)$/)
      .use("graphql-tag/loader")
      .loader("graphql-tag/loader")
      .end();
  }
};
複製代碼

這樣就能夠導入xxx.gql文件了,不必定非要在.vue文件或者.js文件裏面寫查詢語句了

就是這麼簡單 😁

so,使用?

接下來就試一下查詢多個數據,好比說一塊兒查詢博客文章列表和分類信息。驗證一下是否是官方說的獲取多個資源,只用一個請求那麼神奇

import gql from "graphql-tag";

const fetchDataGql = gql` { fetchArticles { id title date introduction category isRead } getAllCategories { num name } } `;

export default {
  data() {
    return {
      articles: [],
      categories: []
    };
  },
  apollo: {
    fetchData() {
      const vm = this;
      return {
        query: fetchDataGql,
        update(data) {
          vm.articles = data.fetchArticles;
          vm.categories = data.getAllCategories;
        }
      };
    }
  }
};
複製代碼

打開瀏覽器控制檯看看

vue-gql-query

確實是只有一次請求就能獲取到一個頁面上全部的資源!最近在作項目的時候,遇到一個頁面要請求 5 個REST接口,有些接口常常返回了不少頁面上用不到的,簡單來講就是多餘的數據,這樣不只浪費了服務器資源,先後端對接起來也不方便,因此 GraphQL 能夠很好地解決這個痛點,真是香啊!

ui 界面上隨便寫點

vue-apollo-article

好了,查詢搞定了,下面試試 變動mutation

需求是點擊某篇文章,讓這篇文章的 title 有個已讀標識(opacity: 0.4)

服務端那邊已經定義好,客戶端代碼核心實現

const mArticleISRead = gql` mutation articleIsRead($id: Int!) { articleIsRead(id: $id) { id title date introduction category isRead } } `
methods: {
  mutationIsRead(id) {
    this.$apollo.mutate({
      mutation: mArticleISRead,
      variables: {
        id
      },
      update: (store, { data: { articleIsRead } }) => {
        console.log(articleIsRead);
      }
    });
  }
},

複製代碼

模板層增長一些判斷

<router-link :class="{isRead: item.isRead}" @click.native="mutationIsRead(item.id)" :to="{ name: 'content', params: { id: item.id } }" >
  {{ item.title }}
</router-link>
複製代碼

測試

vue-apollo-mutation

done!

寫到這裏,讓我感受好奇的是,我從新跳轉回 article 頁面,並無從新觸發 ajax 請求,後來看了文檔才發現這是因爲 Apollo-GraphQL 自帶了運行時的 cache,變動數據的某個字段是不須要從新去獲取最新的列表的,Apollo 會智能去識別而後自動觸發視圖的 render。這樣的話,我在想,這樣既然了 Apollo 的緩存機制,是否是就不須要像 vuex、redux、mobX 這些狀態管理工具了呢?畢竟都是運行時的數據,若是 Apollo 的 cache 機制能解決狀態管理層面上的問題,那麼會減小不少項目層級的複雜度吧。憑空想是想不出的,只有在實際項目中才能找到真正的答案。

好了,GraphQL 的服務端和客戶端均已搭建,一些自身的想法也獲得了相關印證,不斷探索、不斷驗證想法正是學習的一種樂趣 😁

上面代碼均已上傳到 github

總結

GraphQL 牛刀小試結束。關於 GraphQL 之後能不能取代 RESTapi 仍是一個很富有爭議的話題。REST 已經發展多年,它解決了之前的很多問題但也留下了很多問題,GraphQL 仍是比較新的,相信還有不少人都不知道有這個技術,即便它已誕生了好多年...仍是那句話,每一個新的技術的誕生,必然是爲了解決某些問題,以及提升生產效率,這樣纔有它們存在的價值和意義。

相關文章
相關標籤/搜索