With Vuex I think it would make sense to encapsulate socket.js
and the channel based code in an event API module. That module would then export some functions for emitting outgoing events. For initialization a function would start the process of listening for events but would also receive a set of functions used to forward any incoming event to the store.
The Vue components render entirely based on the state contained in the store. The following page demonstrates this in principle:
The eventApi
module simulates
- An incoming
('increment', quantity)
event. When the event arrives a callback (_eventCb_
) configured during initialization is used to forward the quantity to the store.
- An outgoing
('changeIncrement', delta)
event to adjust the “increment quantity”. The module exposes the changeIncrement
function to “emit” this event.
- An outgoing
('changeInterval', delta)
event to adjust the event interval. The module exposes the changeInterval
function to “emit” this event.
The mutations
module contains the store’s mutation functions (and their names).
The actions
module contains the store’s action functions (and their names). Both the changeIncrement
and changeInterval
actions first invoke the equivalent functions of the eventApi
module before committing the change to the store.
The store
module (src/store/index.js
) is the Vuex store. The mutations and actions from the modules of the same name are simply “spliced” into the store.
The App
component rendering only depends on the store.
The ButtonIncrement
component is used by App
.
The app
module creates the store and the Vue instance. The root instance also starts up the event listening process in the mounted
life cycle event where it creates the function callback that eventApi
uses to update the store (the listening process is stopped in beforeDestroy
).
<!DOCTYPE html>
<html>
<!-- index.html -->
<head>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
</head>
<body>
<div id="app"></div>
<script>
// -------------------
// src/api/eventApi.js
const eventApiImport = (() => {
function start (increment, interval, eventCb) {
_increment = increment;
_interval = interval;
_eventCb = eventCb
scheduleEvent();
}
function stop () {
if (_timeoutId) {
window.clearTimeout(_timeoutId);
_timeoutId = null;
}
}
// process "outgoing" events
function changeIncrement(n) {
_increment += n;
}
function changeInterval(n) {
_interval += n;
}
// simulate "incoming" events
function scheduleEvent () {
_timeoutId = window.setTimeout(emit, _interval);
}
function emit () {
_timeoutId = null;
// fake incoming event
_eventCb(_increment);
scheduleEvent();
}
var _eventCb; // callback for "increment" event
// the rest is faking server state
var _timeoutId;
var _increment;
var _interval;
return {
exportFunctionStart: start,
exportFunctionStop: stop,
exportFunctionChangeIncrement: changeIncrement,
exportFunctionChangeInterval: changeInterval
};
})();
// ----------------------
// src/store/mutations.js
const mutationsImport = (() => {
const types = {
CHANGE_INCREMENT: 'CHANGE_INCREMENT',
CHANGE_INTERVAL: 'CHANGE_INTERVAL',
INCREMENT: 'INCREMENT'
};
return {
exportTypes: types,
exportDefault: {
[types.CHANGE_INCREMENT] (state, n) {
state.incrementValue += n;
},
[types.CHANGE_INTERVAL] (state, n ) {
state.intervalValue += n;
},
[types.INCREMENT] (state, n ) {
state.counter += n;
}
}
};
})();
// --------------------
// src/store/actions.js
const actionsImport = (() => {
// simulate
// import {
// changeIncrement as changeIncrementEvent,
// changeInterval as changeIntervalEvent
// } from '../api/eventApi';
const changeIncrementEvent =
eventApiImport.exportFunctionChangeIncrement;
const changeIntervalEvent =
eventApiImport.exportFunctionChangeInterval;
// simulate
// import { types as mutation } from './mutations';
const mutation = mutationsImport.exportTypes;
const types = {
CHANGE_INCREMENT: 'changeIncrement',
CHANGE_INTERVAL: 'changeInterval',
INCREMENT: 'increment'
};
return {
exportTypes: types,
exportDefault: {
[types.CHANGE_INCREMENT] ({ commit }, n) {
changeIncrementEvent(n); // emit outgoing event
commit(mutation.CHANGE_INCREMENT, n);
},
[types.CHANGE_INTERVAL] ({ commit }, n) {
changeIntervalEvent(n); // emit outgoing event
commit(mutation.CHANGE_INTERVAL, n);
},
[types.INCREMENT] ({ commit }, n) {
commit(mutation.INCREMENT, n); // incoming event action
}
}
};
})();
// ------------------
// src/store/index.js
const storeImport = (() => {
// simulate
// import actions from './actions';
const actions = actionsImport.exportDefault;
// simulate
// import mutations from './mutations';
const mutations = mutationsImport.exportDefault;
Vue.use(Vuex);
const incrementDelta = 1;
const intervalDelta = 500;
const createStore = () => {
return new Vuex.Store({
state: {
counter: 0,
intervalDelta,
incrementDelta,
intervalValue: intervalDelta,
incrementValue: incrementDelta
},
mutations,
actions
});
};
return {
exportFunctionCreateStore: createStore
};
})();
// -----------
// src/App.vue
// simulate
// import { types as action } from './store/actions';
const action = actionsImport.exportTypes;
Vue.component('app', {
template: `<div id="app">
<div>
<p>Counter: {{counter}}</p>
<p>Increment: {{incrementValue}}</p>
<p>Interval: {{intervalValue}}</p>
</div>
<div>
Change Increment:
<button-increment
:quantity="incrementDelta"
:handler="changeIncrement" />
<button-increment
:quantity="-(incrementDelta)"
:handler="changeIncrement" />
</div>
<div>
Change Interval:
<button-increment
:quantity="intervalDelta"
:handler="changeInterval" />
<button-increment
:quantity="-(intervalDelta)"
:handler="changeInterval"
:disabled="isMinInterval"/>
ms
</div>
</div>`,
methods: {
...Vuex.mapActions([
action.CHANGE_INCREMENT,
action.CHANGE_INTERVAL
])
},
computed: {
isMinInterval () {
return (this.intervalDelta >= this.intervalValue);
},
...Vuex.mapState([
'counter',
'incrementValue',
'intervalValue',
'incrementDelta',
'intervalDelta'
])
}
});
// ----------------------------------
// src/components/ButtonIncrement.vue
Vue.component('button-increment', {
template:
`<button @click="increment" :disabled="disabled">{{ label }}</button>`,
props: {
'quantity': {
type: Number,
default: 1
},
'disabled': {
type: Boolean,
default: false
},
'handler': {
type: Function,
required: true
}
},
methods: {
increment () {
this.handler(this.quantity);
}
},
computed: {
label () {
const text = this.quantity.toString();
return (this.quantity > 0 ? '+' + text : text);
}
}
});
// ----------
// src/app.js
// simulate
// import {
// start as startEvents,
// stop as stopEvents
// } from './api/eventApi';" simulation
const startEvents = eventApiImport.exportFunctionStart;
const stopEvents = eventApiImport.exportFunctionStop;
// simulate
// import { createStore } from './store';
const createStore = storeImport.exportFunctionCreateStore;
new Vue({
el: app,
store: createStore(),
render: h => h('app'),
mounted () {
const vm = this;
startEvents(
vm.$store.state.incrementDelta,
vm.$store.state.intervalDelta,
(amount) => {
vm.$store.dispatch(action.INCREMENT, amount);
}
);
},
beforeDestroy () {
stopEvents();
}
});
</script>
</body>
</html>