От автора: в последнее время данные стали очень важной частью нашей жизни, и понимание того, что данные одинаково важны. Нет смысла иметь данные, если вы не можете отслеживать или анализировать их, особенно если эти данные имеют какое-либо отношение к финансам.
Вот почему мы будем строить график отслеживания расходов и доходов, используя функции реального времени с помощью Pusher. На нашей интерактивной информационной панели будет отображаться линейная диаграмма, отображающая ваши доходы и расходы на каждый день. Вы сможете добавить новые расходы и доход и посмотреть обновление диаграммы для Vue js в режиме реального времени.
На диаграмме приборной панели будет работать Node.js + Express в качестве внутреннего сервера и Vue + vue-chartjs для интерфейса, загруженного vue-cli.
Создание приложения с помощью vue-cli
vue-cli — это простой CLI для создания проектов Vue.js. Мы установим vue-cli, а затем используем его для загрузки приложения с помощью шаблона webpack со следующими командами:
npm install -g vue-cli vue init webpack-simple realtime-chart-pusher
Совет. шаблон webpack-simple — это простая настройка webpack + vue-loader для быстрого прототипирования.
Настройка сервера Node.js
Следующее, что нужно сделать, это настроить сервер, который поможет нам общаться с Pusher. Я собираюсь предположить, что и Node и npm установлены в вашей системе. Затем мы установим зависимости, которые мы будем использовать для сервера Node.
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.
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 следующим образом.
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 с данными, содержащими расходы и доходы за определенные дни.
app.get('/finances', (req,res) => { res.send(expensesList); });
Этот маршрут просто отправляет объект costList как JSON. Мы используем этот маршрут для получения данных и отображения на интерфейсе.
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 должен выглядеть следующим образом:
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 и замените следующим кодом:
<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.
npm install axios vue-chartjs pusher-js moment
Как только это будет сделано, мы импортируем axios и зарегистрируем его глобально в нашем приложении. Мы можем это сделать, открыв файл main.js в папке src.
// 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 и отредактируйте с помощью приведенного ниже кода.
<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 и измените его следующим образом:
<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.
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
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 и автоматически обновляет линейную диаграмму.
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 должен выглядеть следующим образом:
<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. Вы можете редактировать с помощью кода ниже.
proxyTable: { '/expense/add': { target: 'http://localhost:5000', changeOrigin: true }, '/finances': { target: 'http://localhost:5000', changeOrigin: true }, }
После этого мы, наконец, готовы увидеть наше приложение, и вы можете запустить npm run dev, чтобы запустить приложение.
Это оно! На этом этапе вы должны иметь диаграмму приборной панели реального времени, которая обновляется в реальном времени.
Вы можете проверить демо-версию здесь или перейти к коду для всего приложения, которое размещено на Github для вашего прочтения.
Заключение
Мы видели, как создать базовую линейную диаграмму с ChartJS в Vue с помощью vue-chartjs, а также добавить функции в реальном времени через Pusher.
Затем мы увидели, как использовать реактивные приложения, чтобы сделать ChartJS обновлением своего набора данных, если произошел сбой в наборе данных. Мы также видели, как использовать Pusher для запуска событий на сервере и прослушивания их на стороне клиента с помощью JS.
Вы недавно создали что-нибудь интересное с Pusher, может быть, диаграмма? Дайте знать в ответах ниже.
Автор: Yomi
Источник: https://medium.com/
Редакция: Команда webformyself.