diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..f9ff7be --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,23 @@ +// http://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + }, + // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style + extends: 'standard', + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/README.md b/README.md index 009af80..8d76d8c 100755 --- a/README.md +++ b/README.md @@ -1,36 +1,35 @@ -# Vue-Socket.io +# vue-native-websocket -[![NPM version](https://img.shields.io/npm/v/vue-socket.io.svg)](https://www.npmjs.com/package/vue-socket.io) -![VueJS v2 compatible](https://img.shields.io/badge/Vuejs%202-compatible-green.svg) -Downloads -Dependency Badge -License - -socket.io implementation for Vuejs 2 and Vuex +native websocket implementation for Vuejs 2 and Vuex ## Install ``` bash -npm install vue-socket.io --save +yarn add vue-native-websocket + +# or + +npm install vue-native-websocket --save ``` ## Usage #### Configuration Automatic socket connection from an URL string ``` js -import VueSocketio from 'vue-socket.io'; -Vue.use(VueSocketio, 'http://socketserver.com:1923'); +import VueNativeSock from 'vue-native-websocket' +Vue.use(VueNativeSock, 'ws://localhost:9090') ``` -Bind custom socket.io-client instance +Enable Vuex integration, where `'./store'` is your local apps store: ``` js -Vue.use(VueSocketio, socketio('http://socketserver.com:1923')); +import store from './store' +Vue.use(VueNativeSock, 'ws://localhost:9090', store) ``` -Enable Vuex integration +Optionally enable JSON message passing: ``` js -import store from './yourstore' -Vue.use(VueSocketio, socketio('http://socketserver.com:1923'), store); +import store from './store' +Vue.use(VueNativeSock, 'ws://localhost:9090', store, {format: 'json'}) ``` #### On Vuejs instance usage @@ -67,11 +66,25 @@ delete this.$options.sockets.event_name; #### Vuex Store integration -Socket **mutations** always have `SOCKET_` prefix. +Vuex integration works differently depending on if you've enabled a format -Socket **actions** always have `socket_` prefix and the socket event name is `camelCased` (ex. `SOCKET_USER_MESSAGE` => `socket_userMessage`) +##### Without a format enabled -You can use either one or another or both in your store. Namespaced modules are supported. +Socket events will commit mutations on the root store corresponding to the following events + +`SOCKET_ONOPEN` + +`SOCKET_ONCLOSE` + +`SOCKET_ONERROR` + +`SOCKET_ONMESSAGE` + +Each callback is passed the raw websocket event object + +Update state in the open, close and error callbacks. You can also check the socket state directly with the `this.$socket` object on the main Vue object. + +Handle all the data in the `SOCKET_ONMESSAGE` mutation. ``` js import Vue from 'vue' @@ -80,35 +93,50 @@ import Vuex from 'vuex' Vue.use(Vuex); export default new Vuex.Store({ - state: { - connect: false, - message: null - }, - mutations:{ - SOCKET_CONNECT: (state, status ) => { - state.connect = true; - }, - SOCKET_USER_MESSAGE: (state, message) => { - state.message = message; - } - }, - actions: { - otherAction: (context, type) => { - return true; - }, - socket_userMessage: (context, message) => { - context.dispatch('newMessage', message); - context.commit('NEW_MESSAGE_RECEIVED', message); - if (message.is_important) { - context.dispatch('alertImportantMessage', message); - } - ... - } + state: { + socket: { + isConnected: false, + message: '', } + }, + mutations:{ + SOCKET_ONOPEN (state, event) { + state.socket.isConnected = true + }, + SOCKET_ONCLOSE (state, event) { + state.socket.isConnected = false + }, + SOCKET_ONERROR (state, event) { + console.error(state, event) + }, + // default handler called for all methods + SOCKET_ONMESSAGE (event, message) { + state.message = message + } + } }) ``` -## Example -[Realtime Car Tracker System](http://metinseylan.com/) +##### With `format: 'json'` enabled -[Simple Chat App](http://metinseylan.com/vuesocketio/) +All data passed through the websocket is expected to be JSON. + +Each message is `JSON.parse`d if there is a data (content) response. + +If there is no data, the fallback `SOCKET_ON*` mutation is called with the original event data, as above. + +If there is a `.namespace` on the data, the message is sent to this `namespaced: true` store (be sure to turn this on in the store module). + +If there is a `.mutation` value in the response data, the corresponding mutation is called with the name `SOCKET_[mutation value]` + +If there is an `.action` value in the response data, the corresponding action is called with the name `SOCKET_[action value]` + +Use the `.sendObj({some: data})` method on the `$socket` object to send stringified json messages. + +## Examples + +TODO: post your example here! + +## Credits + +Derived from https://github.com/MetinSeylan/Vue-Socket.io diff --git a/package.json b/package.json index bd16cff..aedf4a2 100755 --- a/package.json +++ b/package.json @@ -1,40 +1,43 @@ { - "name": "vue-socket.io", - "version": "2.1.1-a", - "description": "socket.io implemantation for vuejs and vuex", + "name": "vue-native-websocket", + "version": "1.0.0", + "description": "native websocket implemantation for vuejs and vuex", "main": "dist/build.js", "scripts": { - "build": "webpack --progress --hide-modules" + "build": "eslint --ext .js src && webpack --progress --hide-modules", + "lint": "eslint --ext .js src" }, "repository": { "type": "git", - "url": "git+https://github.com/MetinSeylan/Vue-Socket.io.git" + "url": "git+https://github.com/nathantsoi/vue-native-websocket.git" }, "keywords": [ "vuejs", "socket", "vue", - "socket.io", "websocket", - "socket.io-client", "realtime", "flux", "vuex", "redux" ], - "author": "Metin Seylan", + "author": "Nathan Tsoi", "license": "MIT", "bugs": { - "url": "https://github.com/MetinSeylan/Vue-Socket.io/issues" - }, - "homepage": "https://github.com/MetinSeylan/Vue-Socket.io#readme", - "dependencies": { - "socket.io-client": "^1.4.6" + "url": "https://github.com/nathantsoi/vue-native-websocket/issues" }, + "homepage": "https://github.com/nathantsoi/vue-native-websocket#readme", "devDependencies": { - "babel-cli": "^6.11.4", - "babel-loader": "^6.2.5", - "babel-preset-es2015": "^6.3.13", - "webpack": "^2.2.0-rc.3" + "babel-cli": "^6.24.1", + "babel-eslint": "^7.2.3", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.24.1", + "eslint": "^3.19.0", + "eslint-config-standard": "^10.2.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-node": "^4.2.2", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^3.0.1", + "webpack": "^2.5.1" } } diff --git a/src/Emitter.js b/src/Emitter.js index c98984e..f4dfa1c 100755 --- a/src/Emitter.js +++ b/src/Emitter.js @@ -1,49 +1,49 @@ -export default new class { - constructor() { - this.listeners = new Map(); +class Emitter { + constructor () { + this.listeners = new Map() + } + + addListener (label, callback, vm) { + if (typeof callback === 'function') { + this.listeners.has(label) || this.listeners.set(label, []) + this.listeners.get(label).push({callback: callback, vm: vm}) + return true } + return false + } - addListener(label, callback, vm) { - if(typeof callback == 'function'){ - this.listeners.has(label) || this.listeners.set(label, []); - this.listeners.get(label).push({callback: callback, vm: vm}); + removeListener (label, callback, vm) { + let listeners = this.listeners.get(label) + let index - return true + if (listeners && listeners.length) { + index = listeners.reduce((i, listener, index) => { + if (typeof listener.callback === 'function' && listener.callback === callback && listener.vm === vm) { + i = index } + return i + }, -1) - return false + if (index > -1) { + listeners.splice(index, 1) + this.listeners.set(label, listeners) + return true + } } + return false + } - removeListener(label, callback, vm) { - let listeners = this.listeners.get(label), - index; + emit (label, ...args) { + let listeners = this.listeners.get(label) - if (listeners && listeners.length) { - index = listeners.reduce((i, listener, index) => { - return (typeof listener.callback == 'function' && listener.callback === callback && listener.vm == vm) ? - i = index : - i; - }, -1); - - if (index > -1) { - listeners.splice(index, 1); - this.listeners.set(label, listeners); - return true; - } - } - return false; + if (listeners && listeners.length) { + listeners.forEach((listener) => { + listener.callback.call(listener.vm, ...args) + }) + return true } + return false + } +} - emit(label, ...args) { - let listeners = this.listeners.get(label); - - if (listeners && listeners.length) { - listeners.forEach((listener) => { - listener.callback.call(listener.vm,...args) - }); - return true; - } - return false; - } - -} \ No newline at end of file +export default new Emitter() diff --git a/src/Main.js b/src/Main.js index f19a820..e13fff7 100755 --- a/src/Main.js +++ b/src/Main.js @@ -3,50 +3,45 @@ import Emitter from './Emitter' export default { - install(Vue, connection, store){ + install (Vue, connection, store, opts = {}) { + if (!connection) { throw new Error('[vue-native-socket] cannot locate connection') } - if(!connection) throw new Error("[Vue-Socket.io] cannot locate connection") + let observer = new Observer(connection, store, opts) - let observer = new Observer(connection, store) + Vue.prototype.$socket = observer.WebSocket - Vue.prototype.$socket = observer.Socket; + Vue.mixin({ + created () { + let sockets = this.$options['sockets'] - Vue.mixin({ - created(){ - let sockets = this.$options['sockets'] - - this.$options.sockets = new Proxy({}, { - set: (target, key, value) => { - Emitter.addListener(key, value, this) - target[key] = value - return true; - }, - deleteProperty: (target, key) => { - Emitter.removeListener(key, this.$options.sockets[key], this) - delete target.key; - return true - } - }) - - if(sockets){ - Object.keys(sockets).forEach((key) => { - this.$options.sockets[key] = sockets[key]; - }); - } - }, - beforeDestroy(){ - let sockets = this.$options['sockets'] - - if(sockets){ - Object.keys(sockets).forEach((key) => { - delete this.$options.sockets[key] - }); - } - } + this.$options.sockets = new Proxy({}, { + set (target, key, value) { + Emitter.addListener(key, value, this) + target[key] = value + return true + }, + deleteProperty (target, key) { + Emitter.removeListener(key, this.$options.sockets[key], this) + delete target.key + return true + } }) - } + if (sockets) { + Object.keys(sockets).forEach((key) => { + this.$options.sockets[key] = sockets[key] + }) + } + }, + beforeDestroy () { + let sockets = this.$options['sockets'] + if (sockets) { + Object.keys(sockets).forEach((key) => { + delete this.$options.sockets[key] + }) + } + } + }) + } } - - diff --git a/src/Observer.js b/src/Observer.js index 7983822..3e36f0e 100755 --- a/src/Observer.js +++ b/src/Observer.js @@ -1,59 +1,45 @@ import Emitter from './Emitter' -import Socket from 'socket.io-client' -export default class{ - - constructor(connection, store) { - - if(typeof connection == 'string'){ - this.Socket = Socket(connection); - }else{ - this.Socket = connection - } - - if(store) this.store = store; - - this.onEvent() +export default class { + constructor (connectionUrl, store, opts = {}) { + this.format = opts.format && opts.format.toLowerCase() + this.connect(connectionUrl) + if (store) { this.store = store } + this.onEvent() + } + connect (connectionUrl) { + this.WebSocket = new WebSocket(connectionUrl) + if (this.format === 'json') { + if (!('sendObj' in this.WebSocket)) { + this.WebSocket.sendObj = (obj) => this.WebSocket.send(JSON.stringify(obj)) + } } + } - onEvent(){ - this.Socket.onevent = (packet) => { - Emitter.emit(packet.data[0], packet.data[1]); + onEvent () { + ['onmessage', 'onclose', 'onerror', 'onopen'].forEach((eventType) => { + this.WebSocket[eventType] = (event) => { + Emitter.emit(eventType, event) + if (this.store) { this.passToStore('SOCKET_' + eventType, event) } + } + }) + } - if(this.store) this.passToStore('SOCKET_'+packet.data[0], [ ...packet.data.slice(1)]) - }; - - let _this = this; - - ["connect", "error", "disconnect", "reconnect", "reconnect_attempt", "reconnecting", "reconnect_error", "reconnect_failed", "connect_error", "connect_timeout", "connecting", "ping", "pong"] - .forEach((value) => { - _this.Socket.on(value, (data) => { - Emitter.emit(value, data); - if(_this.store) _this.passToStore('SOCKET_'+value, data) - }) - }) - } - - - passToStore(event, payload){ - if(!event.startsWith('SOCKET_')) return - - for(let namespaced in this.store._mutations) { - let mutation = namespaced.split('/').pop() - if(mutation === event.toUpperCase()) this.store.commit(namespaced, payload) - } - - for(let namespaced in this.store._actions) { - let action = namespaced.split('/').pop() - - if(!action.startsWith('socket_')) continue - - let camelcased = 'socket_'+event - .replace('SOCKET_', '') - .replace(/^([A-Z])|[\W\s_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()) - - if(action === camelcased) this.store.dispatch(namespaced, payload) - } + passToStore (eventName, event) { + if (!eventName.startsWith('SOCKET_')) { return } + if (this.format === 'json' && event.data) { + let msg = JSON.parse(event.data) + let target = msg.namespace || '' + if (msg.mutation) { + this.store.commit([target, msg.mutation].join('/'), msg) + } + if (msg.action) { + this.store.dispatch([target, msg.action].join('/'), msg) + } + } else { + // default mutation + this.store.commit(eventName.toUpperCase(), event) } + } }