├── .gitignore ├── LICENSE ├── README.md ├── client.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Justin MacArthur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-remote 2 | 3 | A websocket based remote event system for [Vue.js](http://vuejs.org). 4 | 5 | Works with `Vue 2.0`, untested with `Vue 1.0`. 6 | 7 | ## Installation 8 | 9 | ##### 1.) Install package via NPM 10 | ``` 11 | npm install vue-remote 12 | ``` 13 | 14 | 15 | ##### 2.) Activate plugin within project. 16 | ``` 17 | import Vue from 'vue'; 18 | import VueRemote from 'vue-remote'; 19 | 20 | Vue.use( 21 | VueRemote, 22 | { 23 | secure: false, 24 | host: "localhost", 25 | port: 8080, 26 | identifier: 'identifier' 27 | } 28 | ) 29 | ``` 30 | 31 | or 32 | 33 | ``` 34 | window.Vue = require('vue'); 35 | const VueRemote = require('vue-remote'); 36 | 37 | Vue.use( 38 | VueRemote, 39 | { 40 | secure: false, 41 | host: "localhost", 42 | port: 8080 43 | } 44 | ) 45 | ``` 46 | 47 | ## Usage 48 | 49 | #### The `Vue.Remote` global object. 50 | This plugin provides a direct interface into the websocket client using `attach`, `detach`, and `emit` functions. 51 | 52 | #### The `$remote` prototype object. 53 | This plugin provides a `vm.$remote` object with functionally based on Vue's event system with `$on`, `$once`, `$off`, and `$emit`. 54 | 55 | --- 56 | 57 | A Note on `$emit` and `emit` functions, unlike the normal event system these calls send the information through the client and the server triggers the callback. 58 | 59 | --- 60 | 61 | #### The `remote` component object 62 | This plugin provides a mixin object `remote` which simplifies the attachment of server events. 63 | 64 | #### Firing an event 65 | There are 2 methods that fire events. They're functionally identical and only differ in name. 66 | ``` 67 | ... 68 | this.$remote.$emit('trigger', ...Arguments) 69 | 70 | Vue.Remote.emit('trigger', ...Arguments) 71 | ... 72 | ``` 73 | 74 | The structure used by `vue-remote` is as follows 75 | ``` 76 | // One Argument 77 | // Vue.Remote.emit('trigger', 5) 78 | { 79 | "identifier": 'trigger', 80 | "arguments": 5 81 | } 82 | 83 | // One Argument which is an object 84 | // Vue.Remote.emit('trigger', { 'data': 5, 'id': 10 }) 85 | { 86 | "identifier": 'trigger', 87 | "data": 5, 88 | "id": 10 89 | } 90 | 91 | // Two or more Arguments 92 | // Vue.Remote.emit('trigger', 5, 10, 15) 93 | { 94 | "identifier": 'trigger', 95 | "arguments": [ 96 | 5, 97 | 10, 98 | 15 99 | ] 100 | } 101 | 102 | ``` 103 | 104 | #### Listening for an event 105 | There are 3 methods that register event listeners. They're functionally identical and only differ in name. 106 | ``` 107 | ... 108 | this.$remote.$on('trigger', function(data) {}) 109 | 110 | Vue.Remote.attach('trigger', function(data) {}, thisArg) 111 | 112 | new Vue({ 113 | remote: { 114 | trigger: function(data) { 115 | ... 116 | } 117 | } 118 | }) 119 | ... 120 | ``` 121 | 122 | The structure expected by the client is as follows. 123 | ``` 124 | { 125 | "identifier": , 126 | "data": 127 | } 128 | 129 | // example 130 | { 131 | "identifier": 'trigger', 132 | "data": { 133 | "id": 10, 134 | "foo": "bar" 135 | } 136 | } 137 | ``` 138 | 139 | ## server 140 | vue-remote comes with a basic websocket server script based on the [Websocket](https://www.npmjs.com/package/websocket) node package. 141 | 142 | #### Usage 143 | ``` 144 | // testServer.js 145 | 146 | const Server = require('vue-remote/server'); 147 | 148 | function messageHandler(message) { 149 | // message = { 150 | // identifier: "trigger", 151 | // arguments: [...] 152 | // } 153 | 154 | ... Handle Message object 155 | 156 | return { 157 | identifier: "trigger", 158 | data: "Handled Message" 159 | }; 160 | } 161 | 162 | let socketServer = Server( messageHandler, [options]); 163 | ``` 164 | 165 | use `node testServer.js` to run the server. 166 | 167 | ## Example 168 | 169 | [Vue-remote-chat](https://github.com/MacArthurJustin/vue-remote-chat) is a quick chat example of the system in action. 170 | 171 | ## License 172 | 173 | [MIT](http://opensource.org/licenses/MIT) 174 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Installs the Vue-Remote Client 3 | * Vue Plugin to utilize the Authority server model 4 | * 5 | * @module vue-remote/client 6 | * @author Justin MacArthur 7 | * @version 1.0.4 8 | * 9 | * @param {Object} Vue 10 | * @param {Object} [options] 11 | */ 12 | module.exports.install = function(Vue, options) { 13 | Vue.Remote = (function(options) { 14 | let Client = null, 15 | Handlers = Object.create(null), 16 | socketPump = [], 17 | pumpInterval = null 18 | 19 | options = options || {} 20 | options.secure = options.secure || false 21 | options.host = options.host || "localhost" 22 | options.port = options.port || 8080 23 | options.identifier = options.identifier || 'identifier' 24 | options.endpoint = options.endpoint || '' 25 | options.camelCase = options.camelCase || true 26 | 27 | /** 28 | * Connect to Websocket Server 29 | */ 30 | function connect() { 31 | Client = new WebSocket(`${(options.secure ? 'wss://' : 'ws://')}${options.host}${options.port ? ':' + options.port : ''}/${options.endpoint}`, options.protocol) 32 | 33 | Client.onopen = openHandler 34 | Client.onerror = errorHandler 35 | Client.onmessage = messageHandler 36 | Client.onclose = closeHandler 37 | } 38 | 39 | /** 40 | * Handle Server Connection Event 41 | * 42 | * @param {Event} open 43 | */ 44 | function openHandler(open) { 45 | console.log("Connected to Web Server") 46 | console.log(open) 47 | 48 | if (options.openHandler) options.openHandler(open) 49 | } 50 | 51 | /** 52 | * Handle Server Errors 53 | * 54 | * @param {Event} error 55 | */ 56 | function errorHandler(error) { 57 | console.log("Error occured") 58 | console.log(error) 59 | 60 | if (options.errorHandler) options.errorHandler(error) 61 | } 62 | 63 | /** 64 | * Handle Messages Returned from the Server 65 | * 66 | * @param {MessageEvent} message 67 | * @returns 68 | */ 69 | function messageHandler(message) { 70 | let Json = JSON.parse(message.data), 71 | identifier = options.camelCase ? Json[options.identifier].replace( 72 | /-([A-Za-z0-9])/gi, 73 | (s, group1) => group1.toUpperCase() 74 | ) : Json[options.identifier], 75 | Events = Handlers[identifier] 76 | 77 | if (Events) { 78 | Events.forEach( 79 | (Event) => { 80 | //Event.callback.apply(Event.thisArg, [Json.data]) 81 | //Adapt to all respone format 82 | Event.callback.apply(Event.thisArg, [Json]) 83 | } 84 | ) 85 | } 86 | } 87 | 88 | /** 89 | * {EventListener} For When the Websocket Client Closes the Connection 90 | * 91 | * @param {CloseEvent} close 92 | */ 93 | function closeHandler(close) { 94 | if (options.closeHandler) options.closeHandler(close) 95 | 96 | if (pumpInterval) { 97 | window.clearInterval(pumpInterval) 98 | pumpInterval = null 99 | } 100 | 101 | Client = null 102 | } 103 | 104 | /** 105 | * Attaches Handlers to the Event Pump System 106 | * 107 | * @param {Boolean} server True/False whether the Server should process the trigger 108 | * @param {String} identifier Unique Name of the trigger 109 | * @param {Function} callback Function to be called when the trigger is tripped 110 | * @param {Object} [thisArg] Arguement to be passed to the handler as `this` 111 | */ 112 | function attachHandler(identifier, callback, thisArg) { 113 | identifier = options.camelCase ? identifier.replace( 114 | /-([A-Za-z0-9])/gi, 115 | (s, group1) => group1.toUpperCase() 116 | ) : identifier 117 | 118 | !(Handlers[identifier] || (Handlers[identifier] = [])).push({ 119 | callback: callback, 120 | thisArg: thisArg 121 | }) 122 | } 123 | 124 | /** 125 | * Detaches Handlers from the Event Pump System 126 | * 127 | * @param {String} identifier Unique Name of the trigger 128 | * @param {Function} callback Function to be called when the trigger is tripped 129 | */ 130 | function detachHandler(identifier, callback) { 131 | if(arguments.length === 0) { 132 | Handlers = Object.create(null) 133 | return 134 | } 135 | 136 | let Handler = Handlers[identifier] 137 | if(!Handler) return 138 | 139 | if(arguments.length === 1) { 140 | Handlers[identifier] = null 141 | return 142 | } 143 | 144 | for(let index = Handler.length - 1; index >= 0 ; index--) { 145 | if(Handler[index].callback === callback || Handler[index].callback.fn === callback) { 146 | Handler.splice(index, 1) 147 | } 148 | } 149 | } 150 | 151 | 152 | /** 153 | * Handles Event Triggers 154 | * 155 | * @param {String} identifier 156 | * @returns 157 | */ 158 | function emitHandler(identifier) { 159 | let args = arguments[1] || [] 160 | if(arguments.length > 2) { 161 | args = arguments.length > 1 ? [].slice.apply(arguments, [1]) : [] 162 | } 163 | 164 | if(typeof args === "object") { 165 | args.identifier = identifier 166 | 167 | socketPump.push(JSON.stringify(args)) 168 | return 169 | } 170 | 171 | socketPump.push( 172 | JSON.stringify({ 173 | 'identifier': identifier, 174 | 'arguments': args 175 | }) 176 | ) 177 | } 178 | 179 | /** 180 | * Sends Messages to the Websocket Server every 250 ms 181 | * 182 | * @returns 183 | */ 184 | function pumpHandler() { 185 | if (socketPump.length === 0) return 186 | if (!Client) connect() 187 | 188 | if (Client.readyState === WebSocket.OPEN) { 189 | socketPump.forEach( 190 | (item) => Client.send(item) 191 | ) 192 | 193 | socketPump.length = 0 194 | } 195 | } 196 | 197 | if (!pumpInterval) window.setInterval(pumpHandler, 250) 198 | 199 | return { 200 | connect: connect, 201 | disconnect: () => { 202 | if (Client) { 203 | Client.close() 204 | Client = null 205 | } 206 | }, 207 | attach: attachHandler, 208 | detach: detachHandler, 209 | emit: emitHandler 210 | } 211 | })(options) 212 | 213 | Vue.mixin({ 214 | created: function() { 215 | if (this.$options.remote) 216 | { 217 | let Handlers = this.$options.remote 218 | for (let name in Handlers) { 219 | if (Handlers.hasOwnProperty(name) && typeof Handlers[name] === "function") { 220 | Vue.Remote.attach(name, Handlers[name], this) 221 | } 222 | } 223 | } 224 | }, 225 | beforeDestroy: function() { 226 | if (this.$options.remote) 227 | { 228 | let Handlers = this.$options.remote 229 | for (let name in Handlers) { 230 | if (Handlers.hasOwnProperty(name) && typeof Handlers[name] === "function") { 231 | Vue.Remote.detach(name, Handlers[name]) 232 | } 233 | } 234 | } 235 | } 236 | }); 237 | 238 | Vue.prototype.$remote = { 239 | $on: function(identifier, callback) { 240 | Vue.Remote.attach(identifier, callback, this) 241 | return this 242 | }, 243 | $once: function(identifier, callback) { 244 | const thisArg = this 245 | function once() { 246 | Vue.remote.detach(identifier, callback) 247 | callback.apply(thisArg, arguments) 248 | } 249 | 250 | once.fn = callback 251 | 252 | Vue.Remote.attach(identifier, once, thisArg) 253 | return thisArg 254 | }, 255 | $off: function(identifier, callback) { 256 | Vue.Remote.detach(identifier, callback, this) 257 | return this 258 | }, 259 | $emit: Vue.Remote.emit 260 | }; 261 | }; 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-remote", 3 | "version": "1.0.8", 4 | "description": "A websocket based remote event system for Vue.js 2.0", 5 | "dependencies": { 6 | "websocket": "^1.0.24" 7 | }, 8 | "devDependencies": { 9 | "vue": "^2.0.0" 10 | }, 11 | "main": "client.js", 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "start": "node server.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/MacArthurJustin/vue-remote.git" 19 | }, 20 | "keywords": [ 21 | "event", 22 | "vuejs", 23 | "websocket", 24 | "socket" 25 | ], 26 | "author": "Justin MacArthur ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/MacArthurJustin/vue-remote/issues" 30 | }, 31 | "homepage": "https://github.com/MacArthurJustin/vue-remote#readme" 32 | } 33 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | /** 5 | * Vue-Remote Server 6 | * Handles the creation and management of a basic Websocket Server for SPA Communications 7 | * 8 | * @module vue-remote/server 9 | * @author Justin MacArthur 10 | * @version 1.0.4 11 | */ 12 | 13 | const http = require('http'); 14 | const WebSocket = require('websocket'); 15 | 16 | /** 17 | * Default System Handler that Echos Input 18 | * 19 | * @param {connection} connection 20 | * @returns {Function} Message Handling Function for the Server 21 | */ 22 | function defaultHandler(message) { 23 | console.log(message); 24 | return { 25 | identifier: message.identifier, 26 | data: "Handled Message" 27 | }; 28 | } 29 | 30 | /** 31 | * 32 | * 33 | * @param {Function} messageHandler 34 | * @param {Object} [options] 35 | */ 36 | module.exports = function(options) { 37 | options = options || Object.create(null); 38 | 39 | /** 40 | * Default HTTP Server Handler 41 | * Returns 404 to any nonWebserver requests 42 | * 43 | * @param {request} request 44 | * @param {response} response 45 | */ 46 | let server = http.createServer( 47 | function(request, response) { 48 | response.writeHead(404); 49 | response.end(); 50 | } 51 | ); 52 | 53 | server.listen( 54 | options.port || 8080, 55 | function() { 56 | console.log((new Date()) + ' Server is listening on port ' + (options.port || 8080)); 57 | } 58 | ); 59 | 60 | let wsServer = new WebSocket.server({ 61 | httpServer: server, 62 | autoAcceptConnections: false 63 | }); 64 | 65 | /** 66 | * Basic CORS test 67 | * 68 | * @param {String} origin 69 | * @returns 70 | */ 71 | function originIsAllowed(origin) { 72 | return typeof options.originIsAllowed === "function" ? options.originIsAllowed(origin) : true; 73 | } 74 | 75 | wsServer.on( 76 | 'request', 77 | function(request) { 78 | if (!originIsAllowed(request.origin)) { 79 | request.reject(); 80 | console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.'); 81 | return; 82 | } 83 | 84 | console.log((new Date()) + ' Connection accepted.'); 85 | 86 | let connection = request.accept(undefined, request.origin), 87 | handler = options.messageHandler || defaultHandler; 88 | 89 | if(options.connectHandler) options.connectHandler(connection) 90 | 91 | connection.on( 92 | 'message', 93 | function(message) { 94 | console.log(message); 95 | let Json = JSON.parse(message.utf8Data), 96 | value = handler(Json, connection); 97 | 98 | connection.send(JSON.stringify(value)); 99 | } 100 | ); 101 | 102 | connection.on( 103 | 'close', 104 | function(reasonCode, description) { 105 | if(options.disconnectHandler) options.disconnectHandler(connection) 106 | console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); 107 | } 108 | ); 109 | } 110 | ); 111 | }; --------------------------------------------------------------------------------