Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Real-Time Vue: Never Say "Try Refreshing?" Again

Avatar for Ari Clark Ari Clark
October 18, 2019

Real-Time Vue: Never Say "Try Refreshing?" Again

Tired of hitting refresh to make sure you’re seeing most up-to-date state? Stagnant data layer got you down? I’ll show you how to build real-time user interfaces with Vue and event-driven architecture methods like web sockets.

Avatar for Ari Clark

Ari Clark

October 18, 2019
Tweet

Other Decks in Programming

Transcript

  1. #ConnectTech @GloomyLumi What are you talking about? ➡ What are

    real time technologies? ➡ Why would you use real time? ➡ How do you integrate real time with Vue to provide an enhanced user experience?
  2. #ConnectTech @GloomyLumi What is ‘real time’? A real time web

    application receives updates without requesting an update or refreshing the page. This is accomplished through the use of a real time technology, which sends data over an open connection to the page.
  3. #ConnectTech @GloomyLumi What problems does real time solve? Provides value

    increase in applications in which the value of information is correlated to the expedience of its delivery.
  4. ‣ Chat apps ‣ Stock tickers ‣ IoT device monitoring

    ‣ Twitter dashboard ‣ Flight status info ‣ Massively Multiplayer Online (MMO) games #ConnectTech @GloomyLumi
  5. #ConnectTech @GloomyLumi SSE ➡ One-way communication (server to client) ➡

    HTTP transport ➡ Supports multiplexing via use of event types ➡ Text based messages only ➡ Messages must be in a specific format
  6. SSE - Message Format #ConnectTech @GloomyLumi data: <event data -

    plain text, JSON, ... - required> id: <messageId - optional>\n event: <eventType - optional>\n \n \n
  7. #ConnectTech @GloomyLumi const simpleEventSource = new EventSource('/api/sse') // define event

    listeners / lifecycle callbacks simpleEventSource.onopen = function (evt) { console.log('simpleEventSource connection opened') } simpleEventSource.onmessage = function (evt) { doSomeStuff(evt.data) } simpleEventSource.onclose = function (evt) { console.log('simpleEventSource closed'); Server Sent Events EventSource interface
  8. #ConnectTech @GloomyLumi const simpleEventSource = new EventSource('/api/sse') // define event

    listeners / lifecycle callbacks simpleEventSource.onopen = function (evt) { console.log('simpleEventSource connection opened') } simpleEventSource.onmessage = function (evt) { doSomeStuff(evt.data) } simpleEventSource.onclose = function (evt) { console.log('simpleEventSource closed'); Server Sent Events EventSource interface
  9. #ConnectTech @GloomyLumi const simpleEventSource = new EventSource('/api/sse') // define event

    listeners / lifecycle callbacks simpleEventSource.onopen = function (evt) { console.log('simpleEventSource connection opened') } simpleEventSource.onmessage = function (evt) { doSomeStuff(evt.data) } simpleEventSource.onclose = function (evt) { console.log('simpleEventSource closed'); Server Sent Events EventSource interface
  10. #ConnectTech @GloomyLumi const simpleEventSource = new EventSource('/api/sse') // define event

    listeners / lifecycle callbacks simpleEventSource.onopen = function (evt) { console.log('simpleEventSource connection opened') } simpleEventSource.onmessage = function (evt) { doSomeStuff(evt.data) } simpleEventSource.onclose = function (evt) { console.log('simpleEventSource closed'); Server Sent Events EventSource interface
  11. #ConnectTech @GloomyLumi Web Sockets ➡ Bi-directional communication ➡ UTF-8 or

    binary data ➡ TCP transport ➡ No headers means low resource consumption ➡ Limited features ➡ No auto-reconnect ➡ No multiplexing support
  12. #ConnectTech @GloomyLumi const vanillaWebSocket = new WebSocket('wss://www.someurl.meh') // define event

    listeners / lifecycle callbacks vanillaWebSocket.onopen = function (evt) { console.log('vanillaWebSocket connection opened') } vanillaWebSocket.onmessage = function (evt) { doSomeStuff(evt.data) } vanillaWebSocket.onclose = function (evt) { console.log('vanillaWebSocket closed'); } Web Sockets
  13. #ConnectTech @GloomyLumi const vanillaWebSocket = new WebSocket('wss://www.someurl.meh') // define event

    listeners / lifecycle callbacks vanillaWebSocket.onopen = function (evt) { console.log('vanillaWebSocket connection opened') } vanillaWebSocket.onmessage = function (evt) { doSomeStuff(evt.data) } vanillaWebSocket.onclose = function (evt) { console.log('vanillaWebSocket closed'); } Web Sockets
  14. #ConnectTech @GloomyLumi const vanillaWebSocket = new WebSocket('wss://www.someurl.meh') // define event

    listeners / lifecycle callbacks vanillaWebSocket.onopen = function (evt) { console.log('vanillaWebSocket connection opened') } vanillaWebSocket.onmessage = function (evt) { doSomeStuff(evt.data) } vanillaWebSocket.onclose = function (evt) { console.log('vanillaWebSocket closed'); } Web Sockets
  15. #ConnectTech @GloomyLumi const vanillaWebSocket = new WebSocket('wss://www.someurl.meh') // define event

    listeners / lifecycle callbacks vanillaWebSocket.onopen = function (evt) { console.log('vanillaWebSocket connection opened') } vanillaWebSocket.onmessage = function (evt) { doSomeStuff(evt.data) } vanillaWebSocket.onclose = function (evt) { console.log('vanillaWebSocket closed'); } Web Sockets
  16. #ConnectTech @GloomyLumi Choosing an abstraction ➡ Libraries compatible with your

    backend ➡ Number of data structures that will be pushed to the front end ➡ Modularity: should the client be able to pick and choose what information it cares about
  17. #ConnectTech @GloomyLumi Socket.io ➡ Node.js server is only official implementation,

    limited third- party implementations available ➡ Full featured ➡ Supports multiplexing ➡ API is documented… not necessarily well ➡ Uses listener mental model, rather than channel subs
  18. #ConnectTech @GloomyLumi SockJS ➡ Server implementations available for wide variety

    of backends ➡ WebSocket emulator with support other transports/protocols ➡ Fallbacks that support back to IE6 (wow.) ➡ Requires additional library for multiplexing ➡ API isn’t fully documented, full feature set unclear ➡ Does not appear to support auto-reconnect
  19. #ConnectTech @GloomyLumi STOMP ➡ Server implementations available for wide variety

    of backends ➡ Supports multiplexing via subscriptions ➡ Auto-reconnect supported ➡ Uses native Web Sockets by default ➡ Can fallback to using SockJS to support older browsers
  20. Vuex: initial state and mutations #ConnectTech @GloomyLumi export const state

    = { devicesList: [], } export const mutations = { SET_DEVICES_LIST(state, data) { state.devicesList = data }, }
  21. Vuex: actions #ConnectTech @GloomyLumi export const actions = { setDevicesList({commit},

    data) { commit('SET_DEVICES_LIST', data) }, getDevicesList({dispatch, commit}) { return axios.get('/device_list') .then(res =>" { dispatch('setDevicesList', res.data) return res }) .catch(err =>" { dispatch('logError', err, { root: true }) return err }) } }
  22. Vuex Getters: Method-style Access #ConnectTech @GloomyLumi export const getters =

    { getMachineDevices: (state) =>" (machineId) =>" { return state.devicesList.filter(device =>" { return device.machineId ===$$ machineId }) } }
  23. // in a component... computed: { ...mapGetters('devicesList', ['getMachineDevices']), machineDevices: function

    () { return this.getMachineDevices(this.machineId) } } Vuex Getters: Method-style Access #ConnectTech @GloomyLumi
  24. Vuex Getters: Who needs GraphQL? #ConnectTech @GloomyLumi export const getters

    = { coallatedData: (state) =>" { return complexCoallationFunctionYouWrote( state.parentDataArray, state.childDataArray, state.grandChildDataArray, ) } } Reusable Computed Properties
  25. Integrating with Vue #ConnectTech @GloomyLumi Option 1: In a component

    data() { return { socket: function() { return {} }, } }, computed: { ...mapGetters('baseApiUrl', { url: 'webSocketUrl', }), }, mounted: function() { this.socket = new Socket(this.url) // set up other options like reconnectDelay etc this.socket.on('eventOne', this.handleEventOne(data)) this.socket.on('eventTwo', this.handleEventTwo(data)) }, beforeDestroy: function() { this.socket.close() }
  26. Integrating with Vue #ConnectTech @GloomyLumi Option 1: In a component

    data() { return { socket: function() { return {} }, } }, computed: { ...mapGetters('baseApiUrl', { url: 'webSocketUrl', }), }, mounted: function() { this.socket = new Socket(this.url) // set up other options like reconnectDelay etc this.socket.on('eventOne', this.handleEventOne(data)) this.socket.on('eventTwo', this.handleEventTwo(data)) }, beforeDestroy: function() { this.socket.close() }
  27. #ConnectTech @GloomyLumi Component Approach ➡ Exposes component lifecycle hooks ➡

    Useful when a source is only relevant to a small part of your app ➡ Familiar environment
  28. Integrating with Vue #ConnectTech @GloomyLumi Option 2: Vuex Plugin export

    default function createWebSocketPlugin(socket) { return (store) =>" { socket.onmessage = (data) =>" { store.commit('receiveData', data) } store.subscribe((mutation) =>" { if (mutation.type ===$$ 'UPDATE_DATA') { socket.emit('update', mutation.payload) } }) } }
  29. Integrating with Vue #ConnectTech @GloomyLumi Option 2: Vuex Plugin export

    default function createWebSocketPlugin(socket) { return (store) =>" { socket.onmessage = (data) =>" { store.commit('receiveData', data) } store.subscribe((mutation) =>" { if (mutation.type ===$$ 'UPDATE_DATA') { socket.emit('update', mutation.payload) } }) } }
  30. Vuex Plugin Approach #ConnectTech @GloomyLumi ➡ Can react to any

    number of arbitrary events by keying off mutations or actions ➡ Manage subscriptions and connections based on arbitrary events ➡ Logic is centralized
  31. v-bind directive #ConnectTech @GloomyLumi <template> <div> <DevicePool v-bind:device-list="groupDevices" /> <DevicePool

    :device-list="machineDevices" /> </div> </template> ➡ Bind reactive properties to HTML elements or components ➡ Dynamic HTML attributes ➡ Pass data (props) to child components
  32. transition-group #ConnectTech @GloomyLumi <transition-group name="slideAdd" tag="ul"> <Device v-for="device in deviceList"

    :device=“device" :key=“device.id” ></Device> </transition-group> // ... .slideAdd-enter, .slideAdd-leave-to { opacity: 0; transform: translateX(50%); } .slideAdd-leave-active { position: absolute; }
  33. transition-group #ConnectTech @GloomyLumi <transition-group name="slideAdd" tag="ul"> <Device v-for="device in deviceList"

    :device=“device" :key=“device.id” ></Device> </transition-group> // ... .slideAdd-enter, .slideAdd-leave-to { opacity: 0; transform: translateX(50%); } .slideAdd-leave-active { position: absolute; }
  34. #ConnectTech @GloomyLumi Opacity: 0 Opacity: 0 Opacity: 1 Opacity: 1

    v-enter v-enter-to v-enter-active v-leave v-leave-to v-leave-active Enter Leave Shamelessly copied from the official Vue.js docs’ guide
  35. Dynamic class binding #ConnectTech @GloomyLumi <div :class="[ $style.powerButtonWrapper, powerState ===$$

    'on' ? $style.on : $style.off, {[$style.pulsePowerYellow]: powerStateChanging}, ]" @click="clickPowerButton" > <!-- power button markup or something -->" </div>
  36. Dynamic class binding #ConnectTech @GloomyLumi <div :class="[ powerState ===$$ 'on'

    ? $style.on : $style.off, {[$style.pulsePowerYellow]: powerStateChanging}, ]" @click="clickPowerButton" > <!-- power button markup or something -->" </div>
  37. Dynamic class binding #ConnectTech @GloomyLumi <div :class="[ powerState ===$$ 'on'

    ? $style.on : $style.off, {[$style.pulsePowerYellow]: powerStateChanging}, ]" @click="clickPowerButton" > <!-- power button markup or something -->" </div>
  38. #ConnectTech @GloomyLumi Optimistic UI ➡ Assume successful request and immediately

    reflect changes in the UI ➡ If call fails, just revert changes
  39. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const mutations =

    { REMOVE_ITEM_AT_INDEX(state, index) { state.data.itemList.splice(index, 1) }, ADD_ITEM_AT_INDEX(state, { index, item }) { state.data.itemList.splice(index, 0, item) }, } Preserve order of items in lists when adding and removing
  40. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, }
  41. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, } create references to item and item index
  42. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, } Delete the item from state
  43. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, } Send API request
  44. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, } Request failed =(
  45. Optimistic Updates in Vuex #ConnectTech @GloomyLumi export const actions =

    { deleteItem({ dispatch, commit, state, getters }, itemId) { const deletedItem = getters.getItemById(itemId) const itemIndex = state.data.itemList.findIndex((item) =>" { return item.id ===$$ itemId }) commit('REMOVE_ITEM_AT_INDEX', itemIndex) return axios.delete(`/item/${itemId}`) .catch((err) =>" { commit('ADD_ITEM_AT_INDEX', { item: deletedItem, index: itemIndex, }) dispatch('logError', err, { root: true }) return err }) }, } Put the item back and pretend it never happened…
  46. Keepin’ it real (time) #ConnectTech @GloomyLumi ➡ Toast notifications ➡

    Long(ish) transitions ➡ Loading/“thinking” spinners ➡ managing loading states with vue-wait Confirm the user action and pending completion with…
  47. #ConnectTech @GloomyLumi The End Ari Clark Senior UI/UX Engineer @

    Liqid ‘Views On Vue’ Podcast Panelist twitter: @GloomyLumi