使用 GraphQL 和 Chart.js 构建实时权力的游戏投票应用程序 TL;DR 🕑 长版本 😉 ⚙️ 我是如何构建它的 ⚙️ 🔧 后端 🔧 ✨ 前端 ✨ 将其放在一起 🤝 Valar Viz

2025-06-07

使用 GraphQL 和 Chart.js 构建实时权力的游戏投票应用程序

TL;DR🕑

长版

⚙️ 我是如何建造它的 ⚙️

🔧 后端 🔧

✨ 前端 ✨

整合起来🤝

维拉·维兹

标题

TL;DR🕑

长版

我一直想做一个投票应用,因为嘿——它们太酷了!
我读了一篇关于如何使用chart.js和 GraphQL在 JavaScript 中构建图表的文章,其中用到了一个很棒的工具,叫做graphql2chartjs。时机真是太棒了,《权力的游戏》临冬城之战还有几天就要开始了,所以我决定去体验一下大家认为谁会在这一集里漫漫长夜里被淘汰。

我发了这条推文并等待⏳

该应用在节目播出前就获得了令人震惊的10,000 张投票

投票统计图像

更不用说,超过 50% 的选票都投给了灰虫子#RIPGreyWorm

灰虫子投票

太吓人了!我重置了投票数,这样你就能体验一下这个应用和它的功能了。

🚀试试看! 📈📈

⚙️ 我是如何建造它的 ⚙️

该应用程序具有:
📊前端的Vue.js + Chartjs
🖥️ 😈后端的Hasura + Apollo GraphQL
⚡ 🚀 部署在Netlify上🔥

🔧 后端 🔧

我使用 Hasura 及其一键式Heroku 部署来设置我的后端。Hasura 通过 PostgreSQL 数据库为我们提供了实时 GraphQL 功能。接下来我们需要定义一个模式,在API 控制台的“数据”部分,我们需要创建一个characters包含以下列的表……

-id保存一个整数值,是主键并且自动递增
-name保存一个文本值
-votes保存一个整数值并将默认值设置为 0

一旦设置了模式,您必须在API 控制台的数据部分手动输入角色名称。

我们现在已经完成了后端工作。

✨ 前端 ✨

正如我上面所说,我的前端是用 Vue.js 做的,我们必须先安装它才能继续下一步,而要做到这一点,我们需要在系统上安装Node.js。安装完 Node.js 后,输入以下命令安装 vue.cli npm i -g @vue/cli。要设置一个新的 Vue 项目,我们输入以下命令vue create myapp,将myapp替换为任何你想给这个应用起的别致名字,然后在提示选择预设时点击“default”。初始化完成后,你的文件夹结构应该类似于下图。

应用初始化完成后,cd <myapp>输入npm run serve运行应用。命令行会显示应用托管的本地地址,打开浏览器并访问该地址。应该就是你看到的内容。

整合起来🤝

此时,我们在前端有一个基本的 Vue 应用程序,并且使用 Hasura 的后端已初始化。我们的目标是创建一个应用程序来可视化《权力的游戏》角色的死亡投票,因此我们继续使用以下命令安装可视化工具 chart.js npm install vue-chartjs chart.js --save。我们还安装了 graphql2chartjs,该工具可帮助我们读取 graphql 数据并在图表中使用,为此,我们运行以下命令npm install --save graphql2chartjs

我们需要将一些文件导入到main.js文件中。导入完成后,你的main.js应该如下所示:

import { ApolloClient } from 'apollo-client'
import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from 'apollo-link-http';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import App from './App'
Vue.config.productionTip = false
const httpLink = new HttpLink({
// You should use an absolute URL here
uri: 'https://<your-app-name>/v1alpha1/graphql'
});
const wsLink = new WebSocketLink({
uri: "wss://<your-app-name>.herokuapp.com/v1alpha1/graphql",
options: {
reconnect: true
}
});
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const apolloClient = new ApolloClient({
link,
cache: new InMemoryCache(),
connectToDevTools: true
})
Vue.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$loadingKey: 'loading'
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
apolloProvider,
render: h => h(App)
})
view raw main.js hosted with ❤ by GitHub
import { ApolloClient } from 'apollo-client'
import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from 'apollo-link-http';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import App from './App'
Vue.config.productionTip = false
const httpLink = new HttpLink({
// You should use an absolute URL here
uri: 'https://<your-app-name>/v1alpha1/graphql'
});
const wsLink = new WebSocketLink({
uri: "wss://<your-app-name>.herokuapp.com/v1alpha1/graphql",
options: {
reconnect: true
}
});
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
const apolloClient = new ApolloClient({
link,
cache: new InMemoryCache(),
connectToDevTools: true
})
Vue.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$loadingKey: 'loading'
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
apolloProvider,
render: h => h(App)
})
view raw main.js hosted with ❤ by GitHub

我在下面关于 GraphQL 中的查询和变异的两篇文章中解释了很多导入的包……


由于图表将实时显示数据,我们将使用订阅功能,现在就来介绍一下。像往常一样,我们需要注意一些事项。在第 16行和第 20行,你需要粘贴应用程序的名称,以便 Apollo 可以帮助你的 Vue 应用程序与 GraphQL 后端进行通信。

注意第19行,我们的订阅实现使用 Web 套接字来保持与服务器的持续连接,并向 UI 提供最新的更新数据。

在修改了main.js文件之后,在src中,我们必须创建一个名为Constants的文件夹,并在其中创建一个名为graphql.js的文件。在该文件中,我们需要gql通过粘贴import gql from graphql-tag;到文件顶部来进行导入。

graphql.js文件让我们拥有一个通用文件来保存所有查询、变更和订阅。这样,当我们需要时,可以轻松地将它们导出到App.vue中。

你的graphql.js文件应该看起来像这样...

import gql from 'graphql-tag'
export const ADD_VOTE_MUTATION = gql`
mutation updateVotes($id: Int!) {
update_characters(where: {id: {_eq: $id}},
_inc: {votes: 1}) {
affected_rows
}
}
`;
export const ALL_CHAR_QUERY = gql`
query characters {
characters(order_by: {id: asc}) {
id
name
}
}
`;
export const ALL_VOTES_SUBSCRIPTION = gql`
subscription allVotes{
CharacterDeathVotes : characters(order_by: {id: asc}) {
label: name
data: votes
}
}
`;
view raw graphql.js hosted with ❤ by GitHub
import gql from 'graphql-tag'
export const ADD_VOTE_MUTATION = gql`
mutation updateVotes($id: Int!) {
update_characters(where: {id: {_eq: $id}},
_inc: {votes: 1}) {
affected_rows
}
}
`;
export const ALL_CHAR_QUERY = gql`
query characters {
characters(order_by: {id: asc}) {
id
name
}
}
`;
export const ALL_VOTES_SUBSCRIPTION = gql`
subscription allVotes{
CharacterDeathVotes : characters(order_by: {id: asc}) {
label: name
data: votes
}
}
`;
view raw graphql.js hosted with ❤ by GitHub

ALL_VOTES_QUERY查询获取字符表中某个条目的nameid。同样,您可以尝试其他操作,并像我一样将它们添加到文件中。同样,

然后,我们创建一个图表组件,稍后会将其导出到App.vue文件中。我们将其命名为BarChart.js。如果想要一个从 API(在本例中是 GraphQL API)获取数据的响应式图表,这是标准格式。vue-chart.js文档对此进行了详细介绍。

// CommitChart.js
import { HorizontalBar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: HorizontalBar,
mixins: [reactiveProp],
props: {
chartData: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
mounted () {
this.renderChart(this.chartData, this.options)
}
}
view raw BarChart.js hosted with ❤ by GitHub
// CommitChart.js
import { HorizontalBar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: HorizontalBar,
mixins: [reactiveProp],
props: {
chartData: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
mounted () {
this.renderChart(this.chartData, this.options)
}
}
view raw BarChart.js hosted with ❤ by GitHub

现在,在您的App.vue文件中,您所做的更改将在以下情况下显示:

<template>
<div id="app">
<div class="container">
<div class="row">
<div class="column">
<h2>Who Might Die ⚔️</h2>
</div>
<div class="column">
<h2
v-if="loading"
>⚖️ Total Votes: {{totalVotes.characters_aggregate.aggregate.sum.votes}}</h2>
</div>
</div>
<div class="button-box">
<div v-for="charName of characters" v-bind:key="charName.id">
<button class="button" @click="updateVotes(charName.id)">{{charName.name}}</button>
</div>
</div>
<div>
<div class="chart">
<bar-chart v-if="loaded" :chartData="chartData" :options="options" :width="200" :height="300"/>
</div>
</div>
</div>
</div>
</template>
<script>
import BarChart from "./components/BarChart.js";
import {
ALL_CHAR_QUERY,
ADD_VOTE_MUTATION,
ALL_VOTES_SUBSCRIPTION,
SUM_VOTES_SUBSCRIPTION
} from "./constants/graphql";
import graphql2chartjs from "graphql2chartjs";
export default {
name: "app",
props: {
msg: String
},
data() {
return {
loaded: false,
loading: false,
characters: "",
totalVotes: 0,
chartData: null,
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
},
gridLines: {
display: false
}
}],
xAxes: [ {
gridLines: {
display: true
}
}]
},
legend: {
display: true
},
responsive: true,
maintainAspectRatio: true
}
};
},
components: {
BarChart
},
methods: {
updateVotes(id) {
this.$apollo.mutate({
mutation: ADD_VOTE_MUTATION,
variables: {
id: id
}
});
}
},
apollo: {
characters: {
query: ALL_CHAR_QUERY
},
$subscribe: {
votes: {
query: ALL_VOTES_SUBSCRIPTION,
result({ data }) {
const g2c = new graphql2chartjs();
g2c.add(data, "bar");
this.chartData = g2c.data;
this.loaded = true;
//console.log(g2c.data)
}
},
sumVotes: {
query: SUM_VOTES_SUBSCRIPTION,
result({ data }) {
if (data) {
this.totalVotes = data;
this.loading = true;
//console.log(data);
}
}
}
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 1%;
}
.container {
margin: 0 auto;
}
.row {
display: flex;
}
.column {
flex: 50%;
text-align: center;
}
.chart {
box-shadow: 0px 0px 20px 2px rgba(0, 0, 0, 0.4);
border-radius: 10px;
padding-top: 15%;
}
button {
background-color:black;
border-radius: 10px;
color: white ;
width: 100px;
height: 40px;
float: left;
margin: 2%;
text-align: center;
}
.button-box {
text-align: center;
}
</style>
view raw App.vue hosted with ❤ by GitHub
<template>
<div id="app">
<div class="container">
<div class="row">
<div class="column">
<h2>Who Might Die ⚔️</h2>
</div>
<div class="column">
<h2
v-if="loading"
>⚖️ Total Votes: {{totalVotes.characters_aggregate.aggregate.sum.votes}}</h2>
</div>
</div>
<div class="button-box">
<div v-for="charName of characters" v-bind:key="charName.id">
<button class="button" @click="updateVotes(charName.id)">{{charName.name}}</button>
</div>
</div>
<div>
<div class="chart">
<bar-chart v-if="loaded" :chartData="chartData" :options="options" :width="200" :height="300"/>
</div>
</div>
</div>
</div>
</template>
<script>
import BarChart from "./components/BarChart.js";
import {
ALL_CHAR_QUERY,
ADD_VOTE_MUTATION,
ALL_VOTES_SUBSCRIPTION,
SUM_VOTES_SUBSCRIPTION
} from "./constants/graphql";
import graphql2chartjs from "graphql2chartjs";
export default {
name: "app",
props: {
msg: String
},
data() {
return {
loaded: false,
loading: false,
characters: "",
totalVotes: 0,
chartData: null,
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
},
gridLines: {
display: false
}
}],
xAxes: [ {
gridLines: {
display: true
}
}]
},
legend: {
display: true
},
responsive: true,
maintainAspectRatio: true
}
};
},
components: {
BarChart
},
methods: {
updateVotes(id) {
this.$apollo.mutate({
mutation: ADD_VOTE_MUTATION,
variables: {
id: id
}
});
}
},
apollo: {
characters: {
query: ALL_CHAR_QUERY
},
$subscribe: {
votes: {
query: ALL_VOTES_SUBSCRIPTION,
result({ data }) {
const g2c = new graphql2chartjs();
g2c.add(data, "bar");
this.chartData = g2c.data;
this.loaded = true;
//console.log(g2c.data)
}
},
sumVotes: {
query: SUM_VOTES_SUBSCRIPTION,
result({ data }) {
if (data) {
this.totalVotes = data;
this.loading = true;
//console.log(data);
}
}
}
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 1%;
}
.container {
margin: 0 auto;
}
.row {
display: flex;
}
.column {
flex: 50%;
text-align: center;
}
.chart {
box-shadow: 0px 0px 20px 2px rgba(0, 0, 0, 0.4);
border-radius: 10px;
padding-top: 15%;
}
button {
background-color:black;
border-radius: 10px;
color: white ;
width: 100px;
height: 40px;
float: left;
margin: 2%;
text-align: center;
}
.button-box {
text-align: center;
}
</style>
view raw App.vue hosted with ❤ by GitHub

App.vue中有三个代码片段需要注意:

1️⃣号



<div v-for="charName of characters" v-bind:key="charName.id">
     <button class="button" @click="updateVotes(charName.id)">
        {{charName.name}} 
     </button>
</div>


Enter fullscreen mode Exit fullscreen mode

该变量characters存储查询结果ALL_CHAR_QUERY。我们使用该v-for指令将结果数组中的每个项目打印为按钮的标题。重要的是,我们使用该v-bind指令绑定角色 ID,并将其作为键来迭代结果数组中的项目(即数据库中的所有角色)。这在将每个投票绑定到特定角色时非常有用。

2️⃣号



<h2 v-if="loading">
⚖️ Total Votes: {{totalVotes.characters_aggregate.aggregate.sum.votes}}
</h2>


Enter fullscreen mode Exit fullscreen mode

我希望能够显示投票总数。这段代码就是为了实现这一点。投票数会随着用户投票实时更新,这意味着我们必须订阅这些数据。为了实现这一点……我将订阅功能从我分享的graphql.js代码中移除了。不过不用担心,Hasura Graphiql 提供了一种非常直观的创建订阅的方式(如下所示),只需勾选方框,它就会自动为您输出文本。

API 控制台 Gif

完成后,复制生成的订阅并将其粘贴到graphql.js文件中以启用它。

我们v-if仅在数据加载完成后才显示数据,否则有时您可能会得到一个未定义的对象,而我们不希望这样,对吗?

3️⃣号



<div class="chart">      
      <bar-chart v-if="loaded" :chartData="chartData" :options="options" :width="200" :height="300"/>
</div>


Enter fullscreen mode Exit fullscreen mode

在这里,我们导入了用BarChart.jsbar-chart创建的组件,并使用和变量传递数据。再次看到我们使用指令仅在数据加载后渲染图表,这样做是为了避免错误。chartDataoptionsv-for

添加完这些之后,你就可以设置应用的样式,并npm run serve看到一些非常酷的条形图了。这个 Web 应用基本上就是这样实现的。值得一提的是,在构建它的过程中,我考虑过添加和省略某些功能。我省略了一些功能,分别是:

  • 我没有限制每个用户只能投一票
  • 我没有允许用户发起自己的投票

该项目已在GitHub上发布,请随意分叉并添加您需要或想要的任何功能!

GitHub 徽标 malgamves / GameOfCharts

一个实时应用程序,用于可视化人们认为谁会在《权力的游戏》第八季第三集中死亡的投票结果。使用 Vue.js、Hasura 和 Chart.js 构建

维拉·维兹

权力的游戏角色死亡投票应用程序。

该应用程序具有:

📊前端使用Vue.js + Chartjs 🖥️

😈后端的Hasura + Apollo GraphQL ⚡

🚀 已在Netlify上部署🔥

项目设置

npm install

编译和热重载以进行开发

npm run serve

编译并缩小以用于生产

npm run build

运行测试

npm run test

Lints 和修复文件

npm run lint

自定义配置

参阅配置参考






如果你有任何问题,请在推特上留言。希望你喜欢这篇文章。下次再见 :)

文章来源:https://dev.to/malgamves/building-a-real-time-game-of-thrones-voting-app-with-graphql-and-chart-js-37ma
PREV
更改 Python 版本 Mac 如何在 MacOS 上将 Python3 设置为默认 Python 版本?
NEXT
为什么 Gatsby 是未来的框架