使用 GraphQL 和 Postgres 構建實時圖表

已發表: 2022-03-10
快速總結↬沒有比用圖表和圖表可視化數據更好的方法來理解數據了。 JS 社區有一些很棒的開源項目可以使數據可視化變得更容易,但是,還沒有一個可以構建實時後端的解決方案來支持這些圖表並使它們成為實時的。 使用 GraphQL(它有一個明確的實時訂閱規範),我們可以在幾秒鐘內獲得一個實時後端運行,並用它來支持實時圖表。

圖表是任何處理數據的行業不可或缺的一部分。 圖表在投票和民意調查行業中非常有用,它們還非常有助於我們更好地了解與我們合作的用戶和客戶的不同行為和特徵。

為什麼實時圖表如此重要? 好吧,它們在不斷產生新數據的情況下很有用; 例如,當使用實時序列來可視化股票價格時,實時圖表非常有用。 在本教程中,我將解釋如何使用適合此特定任務的開源技術構建實時圖表。

注意本教程需要 React 和 GraphQL 的基本知識。

  1. PostgreSQL
    使用圖表背後的關鍵是可視化“大量”數據。 因此,我們需要一個能夠有效處理大數據並提供直觀 API 來重構數據的數據庫。 SQL 數據庫允許我們為我們創建抽象和聚合數據的視圖。 我們將使用 Postgres,這是一個經過時間考驗的高效數據庫。 它還具有花哨的開源擴展,例如 Timescale 和 PostGIS,它們允許我們分別構建基於地理位置和基於時間序列的圖表。 我們將使用 Timescale 來構建我們的時間序列圖表。
  2. GraphQL 引擎
    這篇文章是關於構建實時圖表的,GraphQL 帶有一個定義明確的實時訂閱規範。 Hasura GraphQL Engine 是一個開源 GraphQL 服務器,它採用 Postgres 連接並允許您通過實時 GraphQL 查詢 Postgres 數據。 它還帶有訪問控制層,可幫助您根據自定義訪問控制規則限制數據。
  3. ChartJS
    ChartJS 是一個流行且維護良好的開源庫,用於使用 JavaScript 構建圖表。 我們將使用chart.js及其 ReactJS 抽象react-chartjs-2 。 關於為什麼使用 React,是因為 React 為開發人員提供了直觀的事件驅動 API。 此外,React 的單向數據流非常適合構建數據驅動的圖表。
跳躍後更多! 繼續往下看↓

要求

對於本教程,您的系統上將需要以下內容:

  1. 碼頭工人 CE
    Docker 是一種軟件,可讓您將應用程序容器化。 docker 鏡像是一個獨立的數據包,其中包含軟件及其依賴項和簡約的操作系統。 這樣的 docker 鏡像在技術上可以在任何安裝了 docker 的機器上運行。 本教程將需要 docker。
    • 閱讀有關 Docker 的更多信息
    • 安裝 Docker
  2. npm:npm 是 JavaScript 的包管理器。

演示

我們將構建以下實時時間序列圖表,顯示從現在開始的過去 20 分鐘內以 5 秒為間隔的位置的最高溫度。

實時圖表的 GIF 演示
實時圖表的 GIF 演示

設置後端

運行服務

後端由 Postgres 數據庫、其時間尺度擴展和 Hasura GraphQL 引擎組成。 讓我們通過運行各自的 docker 鏡像來運行數據庫和我們的 GraphQL 服務器。 創建一個名為docker-compose.yaml的文件並將此內容粘貼到其中。

注意docker-compose是一個以聲明方式運行多個 docker 鏡像的實用程序。

 version: '2' services: timescale: image: timescale/timescaledb:latest-pg10 restart: always environment: POSTGRES_PASSWORD: postgrespassword volumes: - db_data:/var/lib/postgresql/data graphql-engine: image: hasura/graphql-engine:v1.0.0-alpha38 ports: - "8080:8080" depends_on: - "timescale" restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@timescale:5432/postgres HASURA_GRAPHQL_ACCESS_KEY: mylongsecretkey command: - graphql-engine - serve - --enable-console volumes: db_data:

這個docker-compose.yaml包含兩個服務的規範:

  1. timescale
    這是我們安裝了 Timescale 擴展的 Postgres 數據庫。 它被配置為在端口 5432 上運行。
  2. graphql-engine
    這是我們的 Hasura GraphQL Engine 實例,即指向數據庫並在其上提供 GraphQL API 的 GraphQL 服務器。 它被配置為運行在 8080 端口,並且 8080 端口映射到運行這個 docker 容器的機器的 8080 端口。 這意味著你可以通過機器的localhost:8080訪問這個 GraphQL 服務器。

讓我們通過在放置docker-compose.yaml

 docker-compose up -d

此命令從雲中提取 docker 映像並按給定順序運行它們。 根據您的網速,可能需要幾秒鐘。 完成後,您可以通過https://localhost:8080/console訪問您的 GraphQL 引擎控制台。

Hasura GraphQL 引擎控制台
Hasura GraphQL 引擎控制台(大預覽)

設置數據庫

接下來,讓我們創建一個名為 temperature 的表,用於存儲不同時間的溫度值。 轉到控制台中的數據選項卡,然後轉到SQL部分。 通過運行這個 SQL 塊來創建我們的temperature

 CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );

這將在數據庫中創建一個簡單的 Postgres 表。 但我們希望利用 Timescale 擴展的時間間隔分區。 為此,我們必須通過運行 SQL 命令將此表轉換為 timescale 的超表:

 SELECT create_hypertable('temperature', 'recorded_at');

此命令在recorded_at的字段中創建一個按時間分區的超表。

現在,由於創建了該表,我們可以直接開始對其進行 GraphQL 查詢。 您可以通過單擊頂部的GraphiQL選項卡來試用它們。 先嘗試做一個突變:

 mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }

上面的 GraphQL 突變在temperature中插入一行。 現在嘗試進行 GraphQL 查詢以檢查數據是否已插入。

然後嘗試進行查詢:

 query { temperature { recorded_at temperature location } }

希望它有效:)

現在,我們手頭的任務是創建一個實時時間序列圖表,顯示從現在開始的過去 20 分鐘內以 5 秒為間隔的某個位置的最高溫度。 讓我們創建一個視圖來準確地為我們提供這些數據。

 CREATE VIEW last_20_min_temp AS ( SELECT time_bucket('5 seconds', recorded_at) AS five_sec_interval, location, MAX(temperature) AS max_temp FROM temperature WHERE recorded_at > NOW() - interval '20 minutes' GROUP BY five_sec_interval, location ORDER BY five_sec_interval ASC );

此視圖將溫度表中的數據與其最高temperature ( max_temp)分組在 5 秒窗口中。 二級分組是使用location字段完成的。 所有這些數據都來自現在的過去二十分鐘。

而已。 我們的後台設置好了。 現在讓我們構建一個漂亮的實時圖表。

前端

你好 GraphQL 訂閱

GraphQL 訂閱本質上是“實時”的 GraphQL 查詢。 它們通過 WebSocket 進行操作,並且具有與 GraphQL 查詢完全相同的響應結構。 返回https://localhost:8080/console並嘗試對我們創建的視圖進行 GraphQL 訂閱。

 subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }

此訂閱訂閱位置為London的視圖中的數據, five_second_intervals的升序排列。

自然,視圖的響應將是一個空數組,因為在過去的 20 分鐘內我們沒有在數據庫中插入任何內容。 (如果您在 20 分鐘內到達此部分,您可能會看到我們之前插入的條目。)

 { "data": { "last_20_min_temp": [] } }

保持此訂閱狀態,打開另一個選項卡並嘗試使用我們之前執行的相同突變在temperatures中插入另一個值。 插入後,如果您返回訂閱所在的選項卡,您會看到響應已自動更新。 這就是 GraphQL 引擎提供的實時魔法。 讓我們使用此訂閱來支持我們的實時圖表。

Create-React-App 入門

讓我們使用 create react app 快速開始使用 React app starter。 運行命令:

 npx create-react-app time-series-chart

這將創建一個空的啟動項目。 cd進入它並安裝 GraphQL 和圖表庫。 此外,安裝將時間戳轉換為人類可讀格式的時刻。

 cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment

最後,使用npm start運行應用程序,一個基本的 React 應用程序將在https://localhost:3000打開。

原始創建反應應用程序
原始 creat-react-app(大預覽)

為客戶端 GraphQL 設置 Apollo 客戶端

Apollo 客戶端是目前最好的 GraphQL 客戶端,可以與任何 GraphQL 兼容的服務器一起使用。 Relay Modern 也很好,但服務器必須支持 Relay 規範才能利用 Relay Modern 的所有優勢。 在本教程中,我們將使用 Apollo 客戶端作為客戶端 GraphQL。 讓我們執行設置以向應用程序提供 Apollo 客戶端。

我沒有深入了解此設置的細微之處,因為以下代碼片段直接取自文檔。 前往 React 應用程序目錄中的src/index.js並實例化 Apollo 客戶端並將此代碼片段添加到ReactDOM.render上方。

 import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache });

最後,將App包裝在ApolloProvider中,以便我們可以在子組件中使用 Apollo 客戶端。 您的App.js最終應該如下所示:

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache }); ReactDOM.render( ( <ApolloProvider client={client}> <App /> </ApolloProvider> ), document.getElementById('root') );

Apollo 客戶端已設置完畢。 我們現在可以輕鬆地從我們的應用程序中使用實時 GraphQL。 前往src/App.js

建立圖表

ChartJS 為構建圖表提供了一個非常簡潔的 API。 我們將建立一個折線圖; 所以折線圖需要以下形式的數據:

 { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }

如果上面的數據集用於渲染折線圖,它看起來像這樣:

示例折線圖
示例折線圖(大預覽)

讓我們首先嘗試構建這個示例圖表。 從react-chartjs-2導入Line並將其作為數據 prop 傳遞給上述對象。 渲染方法看起來像:

 render() { const data = { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], } return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Line data={data} /> </div> ); }

接下來,我們將訂閱視圖中的數據並將其提供給折線圖。 但是我們如何在客戶端執行訂閱呢?

Apollo 的<Subscription>組件使用 render prop 模式工作,其中組件的子級使用訂閱數據的上下文進行渲染。

 <Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>

讓我們使用一個這樣的Subscription組件來訂閱我們的視圖,然後將訂閱數據轉換為 ChartJS 期望的結構。 轉換邏輯如下所示:

 let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); })

注意您還可以使用開源庫 graphq2chartjs 將數據從 GraphQL 響應轉換為 ChartJS 期望的形式。

在 Subscription 組件中使用它之後,我們的App.js看起來像:

 import React, { Component } from 'react'; import { Line } from 'react-chartjs-2'; import { Subscription } from 'react-apollo'; import gql from 'graphql-tag'; import moment from 'moment'; const TWENTY_MIN_TEMP_SUBSCRIPTION= gql' subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } } ' class App extends Component { render() { return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Subscription subscription={TWENTY_MIN_TEMP_SUBSCRIPTION}> { ({data, error, loading}) => { if (error) { console.error(error); return "Error"; } if (loading) { return "Loading"; } let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); }) return ( <Line data={chartJSData} options={{ animation: {duration: 0}, scales: { yAxes: [{ticks: { min: 5, max: 20 }}]} }} /> ); } } </Subscription> </div> ); } } export default App;

您將在https://localhost:3000準備好一個完整工作的實時圖表。 然而,它會是空的,所以讓我們填充一些樣本數據,這樣我們就可以實際看到一些神奇的事情發生了。

注意我在折線圖中添加了更多選項,因為我不喜歡 ChartJS 中那些花哨的動畫。 簡單的時間序列看起來很漂亮,但是,如果您願意,可以刪除 options 屬性。

插入樣本數據

讓我們編寫一個腳本,用虛擬數據填充我們的數據庫。 創建一個單獨的目錄(在這個應用程序之外)並創建一個名為script.js的文件,其中包含以下內容,

 const fetch = require('node-fetch'); setInterval( () => { const randomTemp = (Math.random() * 5) + 10; fetch( `https://localhost:8080/v1alpha1/graphql`, { method: 'POST', body: JSON.stringify({ query: ` mutation ($temp: numeric) { insert_temperature ( objects: [{ temperature: $temp location: "London" }] ) { returning { recorded_at temperature } } } `, variables: { temp: randomTemp } }) } ).then((resp) => resp.json().then((respObj) => console.log(JSON.stringify(respObj, null, 2)))); }, 2000 );

現在運行這兩​​個命令:

 npm install --save node-fetch node script.js

您可以返回https://localhost:3000並查看圖表更新。

整理起來

您可以使用我們上面討論的想法構建大多數實時圖表。 算法是:

  1. 使用 Postgres 部署 GraphQL 引擎;
  2. 創建要存儲數據的表;
  3. 從你的 React 應用訂閱這些表;
  4. 渲染圖表。

你可以在這裡找到源代碼。