От автора: в последнее время данные стали очень важной частью нашей жизни, и понимание того, что данные одинаково важны. Нет смысла иметь данные, если вы не можете отслеживать или анализировать их, особенно если эти данные имеют какое-либо отношение к финансам.
Вот почему мы будем строить график отслеживания расходов и доходов, используя функции реального времени с помощью Pusher. На нашей интерактивной информационной панели будет отображаться линейная диаграмма, отображающая ваши доходы и расходы на каждый день. Вы сможете добавить новые расходы и доход и посмотреть обновление диаграммы для Vue js в режиме реального времени.
На диаграмме приборной панели будет работать Node.js + Express в качестве внутреннего сервера и Vue + vue-chartjs для интерфейса, загруженного vue-cli.
Создание приложения с помощью vue-cli
vue-cli — это простой CLI для создания проектов Vue.js. Мы установим vue-cli, а затем используем его для загрузки приложения с помощью шаблона webpack со следующими командами:
1 2 |
npm install -g vue-cli vue init webpack-simple realtime-chart-pusher |
Совет. шаблон webpack-simple — это простая настройка webpack + vue-loader для быстрого прототипирования.
Настройка сервера Node.js
Следующее, что нужно сделать, это настроить сервер, который поможет нам общаться с Pusher. Я собираюсь предположить, что и Node и npm установлены в вашей системе. Затем мы установим зависимости, которые мы будем использовать для сервера Node.
1 |
npm install body-parser express nodemon pusher |
Совет. Nodemon будет просматривать файлы в каталоге, в котором запускался nodemon, и если какие-либо файлы будут изменены, nodemon автоматически перезапустит ваше node-приложение.
Еще одна вещь, нам понадобится точка входа / файл для нашего Node-сервера. Мы можем сделать это, создав файл server.js в корне приложения.
Настройка Pusher
Чтобы реализовать функциональность в реальном времени, нам понадобится мощность Pusher. Если вы еще этого не сделали, зарегистрируйте учетную запись Pusher и создайте новое приложение. Когда ваше новое приложение будет создано, получите свой app_id, ключи и кластер из панели инструментов Pusher.
Настройка приложения
Теперь, когда у нас есть учетная запись Pusher, и мы установили зависимости, необходимые для бэкэнд Node.js, давайте создавать. Давайте напишем код для файла server.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const express = require('express'); const path = require('path'); const bodyParser = require("body-parser"); const app = express(); const Pusher = require('pusher'); const pusher = new Pusher({ appId: 'YOUR_APP_ID', key: 'YOUR_APP_KEY', secret: 'YOUR_APP_SECRET', cluster: 'eu', encrypted: true }); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname + '/app'))); app.set('port', (process.env.PORT || 5000)); app.listen(app.get('port'), function() { console.log('Node app is running on port', app.get('port')); }); |
Давайте посмотрим, что здесь происходит. Мы требуем Express, path, body-parser и Pusher, и мы инициализировали express () с приложением.
Мы используем body-parser для извлечения всей части входящего потока запросов и выставляем его на req.body.
Pusher также инициализируется учетными данными приложения и кластером из панели управления. Обязательно обновите это, иначе сервер node не будет подключен к панели управления. Наконец, сервер Node будет работать на 5000 порте.
Следующее, что нужно сделать, это определить маршрут нашего приложения, а также добавить макет данных для диаграммы расходов и доходов. Обновите файл server.js следующим образом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
let expensesList = { data: [ { date: "April 15th 2017", expense: 100, income: 4000 }, { date: "April 22nd 2017", expense: 500, income: 2000 }, { date: "April 24th 2017", expense: 1000, income: 2300 }, { date: "April 29th 2017", expense: 2000, income: 1234 }, { date: "May 1st 2017", expense: 500, income: 4180 }, { date: "May 5th 2017", expense: 4000, income: 5000 }, ] } |
Во-первых, у нас есть объект costList с данными, содержащими расходы и доходы за определенные дни.
1 2 3 |
app.get('/finances', (req,res) => { res.send(expensesList); }); |
Этот маршрут просто отправляет объект costList как JSON. Мы используем этот маршрут для получения данных и отображения на интерфейсе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
app.post('/expense/add', (req, res) => { let expense = Number(req.body.expense) let income = Number(req.body.income) let date = req.body.date; let newExpense = { date: date, expense: expense, income: income }; expensesList.data.push(newExpense); pusher.trigger('finance', 'new-expense', { newExpense: expensesList }); res.send({ success : true, income: income, expense: expense, date: date, data: expensesList }) }); |
Маршрут /expense/add делает очень много. Это POST-маршрут, что означает, что мы будем ожидать некоторых входящих данных (в данном случае, суммы расходов и суммы дохода).
Затем мы подталкиваем этот новый доход и расходы к существующему, после чего мы также подталкиваем обновленный costList к Pusher.
Наконец, мы отправляем JSON в ответ на маршрут, содержащий последние доходы, расходы, дату и обновленные расходыList. Ваш окончательный server.js должен выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
const express = require('express'); const path = require('path'); const bodyParser = require("body-parser"); const app = express(); const Pusher = require('pusher'); const pusher = new Pusher({ appId: 'APP_ID', key: 'YOUR_KEY', secret: 'YOUR_SECRET', cluster: 'YOUR_CLUSTER', encrypted: true }); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname + '/app'))); app.set('port', (process.env.PORT || 5000)); let expensesList = { data: [ { date: "April 15th 2017", expense: 100, income: 4000 }, { date: "April 22nd 2017", expense: 500, income: 2000 }, { date: "April 24th 2017", expense: 1000, income: 2300 }, { date: "April 29th 2017", expense: 2000, income: 1234 }, { date: "May 1st 2017", expense: 500, income: 4180 }, { date: "May 5th 2017", expense: 4000, income: 5000 }, ] } app.get('/finances', (req,res) => { res.send(expensesList); }); app.post('/expense/add', (req, res) => { let expense = Number(req.body.expense) let income = Number(req.body.income) let date = req.body.date; let newExpense = { date: date, expense: expense, income: income }; expensesList.data.push(newExpense); pusher.trigger('finance', 'new-expense', { newExpense: expensesList }); res.send({ success : true, income: income, expense: expense, date: date, data: expensesList }) }); app.listen(app.get('port'), function() { console.log('Node app is running on port', app.get('port')); }); |
Построение Frontend (Vue + vue-chartjs)
Большая часть работы с интерфейсом будет выполнена внутри папки src/components. Перейдите в этот каталог, и вы увидите файл Hello.vue. Вы можете либо удалить этот файл, либо переименовать его в Home.vue поскольку нам понадобится файл Home.vue внутри папки компонентов.
Прежде чем мы начнем с построения диаграммы и отображения ее, мы должны сделать пару вещей. Откройте файл App.vue в папке src и замените следующим кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<template> <div id="app"> <home></home> </div> </template> <script> import Home from './components/Home' //We are importing the Home component export default { name: 'app', components: { Home } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> |
Затем мы установим vue-chartjs, momentjs, pusher-js (библиотека Javascript Pusher) и axios (мы будем использовать axios для создания запросов API). А затем добавьте их в приложение Vue.js.
1 |
npm install axios vue-chartjs pusher-js moment |
Как только это будет сделано, мы импортируем axios и зарегистрируем его глобально в нашем приложении. Мы можем это сделать, открыв файл main.js в папке src.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/main.js import Vue from 'vue' import App from './App' import axios from 'axios' // we import axios from installed dependencies Vue.config.productionTip = false Vue.use(axios) // we register axios globally /* eslint-disable no-new */ new Vue({ el: '#app', template: '<App/>', components: { App } }) |
Затем создадим компонент Vue.js, который поможет отобразить нашу диаграмму. Мы собираемся использовать это, чтобы указать, какой тип диаграммы мы хотим, настроить его внешний вид и как он себя ведет.
Затем мы импортируем этот компонент в компонент Home.vue и используем его там. Это одно из преимуществ vue-chartjs, оно работает путем импорта базового класса диаграммы, который мы затем можем расширить. Давайте продолжим и создадим этот компонент. Создайте новый файл с именем LineChart.vue внутри папки src/components и отредактируйте с помощью приведенного ниже кода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<script> import {Line, mixins} from 'vue-chartjs' // We specify what type of chart we want from vue-chartjs and the mixins module const { reactiveProp } = mixins export default Line.extend({ //We are extending the base chart class as mentioned above mixins: [reactiveProp], data () { return { options: { //Chart.js options scales: { yAxes: [{ ticks: { beginAtZero: true }, gridLines: { display: true } }], xAxes: [ { gridLines: { display: false } }] }, legend: { display: true }, responsive: true, maintainAspectRatio: false } } }, mounted () { // this.chartData is created in the mixin this.renderChart(this.chartData, this.options) } }) </script> |
В приведенном выше блоке кода мы импортировали линейную диаграмму из vue-chartjs и модуля mixins. Chart.js обычно не предоставляет возможность автоматического обновления всякий раз, когда набор данных изменяется, но это можно сделать в vue-chartjs с помощью следующих миксинов: reactiveProp, reactiveData.
Эти миксины автоматически создают chartData в качестве опоры или данных и добавляют наблюдателя. Если данные изменились, диаграмма обновится.
Кроме того, this.renderChart() внутри mounted функции отвечает за отображение диаграммы. this.chartData — это объект, содержащий набор данных, необходимый для диаграммы, и мы получим его, включив его как опору в шаблон Home.vue this.options содержит объект опций, который определяет внешний вид и конфигурацию диаграммы.
Теперь у нас есть компонент LineChart, но как мы можем увидеть нашу диаграмму и проверить ее функциональность в реальном времени? Мы делаем это, добавляя LineChart к нашему компоненту Home.vue а также подписываясь на наш канал Pusher через pusher-js.
Откройте файл Home.vue и измените его следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
<template> <div class="hello"> <div class="container"> <div class="row"> <h2 class="title">Realtime Chart with Vue and Pusher</h2> <h3 class="subtitle">Expense and Income Tracker</h3> <!--We are using the LineChart component imported below in the script and also setting the chart-data prop to the datacollection object--> <line-chart :chart-data="datacollection"></line-chart> </div> </div> <div class="container"> <div class="row"> <form class="form" @submit.prevent="addExpenses"> <h4>Add New Entry</h4> <div class="form-group"> <label>Expenses</label> <input class="form-control" placeholder="How much did you spend?" type="number" v-model="expenseamount" required> </div> <div class="form-group"> <label>Income</label> <input class="form-control" placeholder="How much did you earn?" type="number" v-model="incomeamount" required> </div> <div class="form-group"> <label>Date</label> <input class="form-control" placeholder="Date" type="date" v-model="entrydate" required> </div> <div class="form-group"> <button class="btn btn-primary">Add New Entry</button> </div> </form> </div> </div> </div> </template> <script> import axios from 'axios' import moment from 'moment' import Pusher from 'pusher-js' import LineChart from '@/components/LineChart' const socket = new Pusher('APP_KEY', { cluster: 'eu', encrypted: true }) const channel = socket.subscribe('finance') export default { name: 'home', components: {LineChart}, data () { return { expense: null, income: null, date: null, expenseamount: null, incomeamount: null, datacollection: null, entrydate: null } }, created () { this.fetchData() this.fillData() }, mounted () { this.fillData() }, methods: { fillData () { }, addExpenses () { }, fetchData () { } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .title { text-align: center; margin-top: 40px; } .subtitle { text-align: center; } .form { max-width: 600px; width: 100%; margin: 20px auto 0 auto; } .form h4 { text-align: center; margin-bottom: 30px; } h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style> |
fillData
Эта функция вызывается сразу после того, как приложение смонтировано, и в основном делает запрос API к бэкэнду узла (/ finances) и получает costList.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
fillData () { axios.get('/finances') .then(response => { let results = response.data.data let dateresult = results.map(a => a.date) let expenseresult = results.map(a => a.expense) let incomeresult = results.map(a => a.income) this.expense = expenseresult this.income = incomeresult this.date = dateresult this.datacollection = { labels: this.date, datasets: [ { label: 'Expense', backgroundColor: '#f87979', data: this.expense }, { label: 'Income', backgroundColor: '#5bf8bf', data: this.income } ] } }) .catch(error => { console.log(error) }) } |
Запрос GET делается для маршрута /finances Node.js, который, в свою очередь, возвращает последние expensesList и затем мы обрабатываем эти данные с помощью .map Javascript и присваиваем его различным переменным.
addExpenses
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
addExpenses () { //We first get the new entries via the v-model we defined on the income and expense input tag let expense = this.expenseamount let income = this.incomeamount let today = moment(this.entrydate).format('MMMM Do YYYY') //Formats the date via momentJS //Sends a POST request to /expense/new along with the expense, income and date. axios.post('/expense/add', { expense: expense, income: income, date: today }) .then(response => { this.expenseamount = '' this.incomeamount = '' //We are bound to new-expense on Pusher and once it detects a change via the new entry we just submitted, we use it to build the Line Chart again. channel.bind('new-expense', function(data) { let results = data.newExpense.data let dateresult = results.map(a => a.date); let expenseresult = results.map(a => a.expense); let incomeresult = results.map(a => a.income); //The instance data are updated here with the latest data gotten from Pusher this.expense = expenseresult this.income = incomeresult this.date = dateresult //The Chart's dataset is updated with the latest data gotten from Pusher this.datacollection = { labels: this.date, datasets: [ { label: 'Expense Charts', backgroundColor: '#f87979', data: this.expense }, { label: 'Income Charts', backgroundColor: '#5bf8bf', data: this.income } ] } }); }) } |
В приведенном выше блоке кода используется метод POST-роута для /expense/add для обновления expensesList («не забывайте /expense/add маршрут на сервере Node» отправляет обновленный список expensesList на панель управления Pusher) вместе с данными о доходах, расходах и дате.
Затем он использует данные, полученные от Pusher через channel.bind чтобы снова построить линейную диаграмму и автоматически добавляет новую запись в диаграмму.
fetchData
Эта функция вызывается после создания экземпляра Vue, а также прослушивает изменения в наборе данных диаграммы через Pusher и автоматически обновляет линейную диаграмму.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
fetchData () { //We are bound to new-expense on Pusher and it listens for changes to the dataset so it can automatically rebuild the Line Chart in realtime. channel.bind('new-expense', data => { let _results = data.newExpense.data let dateresult = _results.map(a => a.date); let expenseresult = _results.map(a => a.expense); let incomeresult = _results.map(a => a.income); //The instance data are updated here with the latest data gotten from Pusher this.expense = expenseresult this.income = incomeresult this.date = dateresult //The Chart's dataset is updated with the latest data gotten from Pusher this.datacollection = { labels: this.date, datasets: [ { label: 'Expense Charts', backgroundColor: '#f87979', data: this.expense }, { label: 'Income Charts', backgroundColor: '#5bf8bf', data: this.income } ] } }); } |
Ваш последний файл Home.vue должен выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
<template> <div class="hello"> <div class="container"> <div class="row"> <h2 class="title">Realtime Chart with Vue and Pusher</h2> <h3 class="subtitle">Expense and Income Tracker</h3> <line-chart :chart-data="datacollection"></line-chart> </div> </div> <div class="container"> <div class="row"> <form class="form" @submit.prevent="addExpenses"> <h4>Add New Entry</h4> <div class="form-group"> <label>Expenses</label> <input class="form-control" placeholder="How much did you spend today?" type="number" v-model="expenseamount" required> </div> <div class="form-group"> <label>Income</label> <input class="form-control" placeholder="How much did you earn today?" type="number" v-model="incomeamount" required> </div> <div class="form-group"> <button class="btn btn-primary">Add New Entry</button> </div> </form> </div> </div> </div> </template> <script> import axios from 'axios' import moment from 'moment' import Pusher from 'pusher-js' import LineChart from '@/components/LineChart' const socket = new Pusher('3e6b0e8f2442b34330b7', { cluster: 'eu', encrypted: true }) const channel = socket.subscribe('finance') export default { name: 'home', components: {LineChart}, data () { return { expense: null, income: null, date: null, expenseamount: null, incomeamount: null, datacollection: null } }, created () { this.fetchData() this.fillData() }, mounted () { this.fillData() }, methods: { fillData () { axios.get('/finances') .then(response => { let results = response.data.data let dateresult = results.map(a => a.date) let expenseresult = results.map(a => a.expense) let incomeresult = results.map(a => a.income) this.expense = expenseresult this.income = incomeresult this.date = dateresult this.datacollection = { labels: this.date, datasets: [ { label: 'Expense', backgroundColor: '#f87979', data: this.expense }, { label: 'Income', backgroundColor: '#5bf8bf', data: this.income } ] } }) .catch(error => { console.log(error) }) }, addExpenses () { let expense = this.expenseamount let income = this.incomeamount let today = moment().format('MMMM Do YYYY') axios.post('/expense/add', { expense: expense, income: income, date: today }) .then(response => { this.expenseamount = '' this.incomeamount = '' channel.bind('new-expense', function (data) { let results = data.newExpense.data let dateresult = results.map(a => a.date) let expenseresult = results.map(a => a.expense) let incomeresult = results.map(a => a.income) this.expense = expenseresult this.income = incomeresult this.date = dateresult this.datacollection = { labels: this.date, datasets: [ { label: 'Expense', backgroundColor: 'transparent', pointBorderColor: '#f87979', data: this.expense }, { label: 'Income', backgroundColor: 'transparent', pointBorderColor: '#5bf8bf', data: this.income } ] } }) }) .catch(error => { console.log(error) }) }, fetchData () { channel.bind('new-expense', data => { let results = data.newExpense.data let dateresult = results.map(a => a.date) let expenseresult = results.map(a => a.expense) let incomeresult = results.map(a => a.income) this.expense = expenseresult this.income = incomeresult this.date = dateresult this.datacollection = { labels: this.date, datasets: [ { label: 'Expense Charts', backgroundColor: '#f87979', data: this.expense }, { label: 'Income Charts', backgroundColor: '#5bf8bf', data: this.income } ] } }) } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .title { text-align: center; margin-top: 40px; } .subtitle { text-align: center; } .form { max-width: 600px; width: 100%; margin: 20px auto 0 auto; } .form h4 { text-align: center; margin-bottom: 30px; } h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style> |
Еще кое-что!
Прежде чем мы сможем запустить наше приложение, нам нужно сделать что-то, называемое API-прокси. API-прокси позволяет нам интегрировать наше приложение vue-cli с backend сервером (в нашем случае — Node-сервер). Это означает, что мы можем запускать сервер dev и бэкэнд API бок о бок и позволить серверу-разработчику проксировать все запросы API на фактический сервер.
Мы можем включить API-прокси, отредактировав параметр dev.proxyTable в config/index.js. Вы можете редактировать с помощью кода ниже.
1 2 3 4 5 6 7 8 9 10 |
proxyTable: { '/expense/add': { target: '//localhost:5000', changeOrigin: true }, '/finances': { target: '//localhost:5000', changeOrigin: true }, } |
После этого мы, наконец, готовы увидеть наше приложение, и вы можете запустить npm run dev, чтобы запустить приложение.
Это оно! На этом этапе вы должны иметь диаграмму приборной панели реального времени, которая обновляется в реальном времени.
Вы можете проверить демо-версию здесь или перейти к коду для всего приложения, которое размещено на Github для вашего прочтения.
Заключение
Мы видели, как создать базовую линейную диаграмму с ChartJS в Vue с помощью vue-chartjs, а также добавить функции в реальном времени через Pusher.
Затем мы увидели, как использовать реактивные приложения, чтобы сделать ChartJS обновлением своего набора данных, если произошел сбой в наборе данных. Мы также видели, как использовать Pusher для запуска событий на сервере и прослушивания их на стороне клиента с помощью JS.
Вы недавно создали что-нибудь интересное с Pusher, может быть, диаграмма? Дайте знать в ответах ниже.
Автор: Yomi
Источник: //medium.com/
Редакция: Команда webformyself.