├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── example ├── config.js ├── counting.js └── main.js ├── gruntfile.js ├── index.html ├── package.json └── src ├── worker-fake.js └── worker.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.html text diff=html 5 | *.css text 6 | *.less text 7 | *.js text 8 | *.md text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Chad Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using Web Workers with RequireJS 2 | 3 | This is a simple plugin that allows you to declare a dependency on a web worker script using requireJS with a simple syntax. See here for [primer on Web Workers](https://developer.mozilla.org/en-US/docs/DOM/Using_web_workers). 4 | 5 | ```javascript 6 | define(["worker!my-web-worker"], function(worker) { 7 | worker.onmessage = function (event) { 8 | alert("I got me a message!"); 9 | }; 10 | }); 11 | ``` 12 | 13 | The plugin will return an initialized `Worker` object which will resolve the given module ID with the currently configured requireJS configuration (no need to hardcode script paths just for web workers). 14 | 15 | If Worker is not defined (IE < 10), the plugin will load a [fake Worker implementation](http://code.google.com/p/fakeworker-js/) so that your scripts can utilize the same Worker API whether the browser supports it or not. 16 | 17 | ## Install with [Bower](http://bower.io/) 18 | 19 | ``` 20 | bower install requirejs-web-workers 21 | ``` 22 | 23 | Then add `src/worker.js` and `src/worker-fake.js` to your project. 24 | 25 | ## How to Run Example Page 26 | 27 | The example page just loops and counts to a very large number on a background thread. In order to run the example, you will need to run index.html from a server (e.g. `localhost`) rather than the `file://` protocol for web workers to work. 28 | 29 | To run the example with a lightweight node server, first install dependencies: 30 | 31 | ``` 32 | npm install 33 | ``` 34 | 35 | Make sure you have grunt-cli installed if you don't already have it: 36 | 37 | ``` 38 | npm install grunt-cli -g 39 | ``` 40 | 41 | Then, run the server and load the example page: 42 | 43 | ``` 44 | npm start 45 | ``` 46 | 47 | This will spin up a server at `localhost:1337` and open your web browser. 48 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs-web-workers", 3 | "version": "1.0.1", 4 | "main": "src/worker.js", 5 | "ignore": [ 6 | "node_modules", 7 | "bower_components" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /example/config.js: -------------------------------------------------------------------------------- 1 | var require = (function (window) { 2 | return { 3 | //by default load modules from src 4 | baseUrl: "/src", 5 | paths: { 6 | //except if the module ID starts with "app" 7 | app: "../example", 8 | 9 | //CDN 10 | "jquery": "//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min" 11 | } 12 | }; 13 | })(this); -------------------------------------------------------------------------------- /example/counting.js: -------------------------------------------------------------------------------- 1 | //note: can also use requireJS in this file - see here for more info: http://requirejs.org/docs/api.html#webworker 2 | 3 | onmessage = function (event) { 4 | if (event.data == "start") { 5 | count(); 6 | } 7 | } 8 | 9 | function count() { 10 | var end = 1e8, tmp = 1; 11 | postMessage("hello there"); 12 | while (end) { 13 | end -= 1; 14 | tmp += end; 15 | if (end === 5e7) { // 5e7 is the half of 1e8 16 | postMessage("halfway there, tmp is now " + tmp); 17 | } 18 | } 19 | postMessage("all done"); 20 | } -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "worker!app/counting"], function ($, counter) { 2 | counter.onmessage = function (event) { 3 | $("#results").append("
  • message from the background thread: " + event.data + "
  • "); 4 | }; 5 | 6 | $("#doIt").click(function () { 7 | counter.postMessage("start"); 8 | }); 9 | }); -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | connect: { 4 | server: { 5 | options: { 6 | port: 1337, 7 | keepalive: true, 8 | open: "http://localhost:1337/" 9 | } 10 | } 11 | } 12 | }); 13 | 14 | grunt.loadNpmTasks("grunt-contrib-connect"); 15 | grunt.registerTask("default", ["connect"]); 16 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Web Workers with RequireJS Example 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 |

    18 | 19 |

    20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requirejs-web-workers", 3 | "version": "1.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "grunt" 7 | }, 8 | "devDependencies": { 9 | "grunt": "~0.4.2", 10 | "grunt-contrib-connect": "~0.6.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/worker-fake.js: -------------------------------------------------------------------------------- 1 | ; 2 | var fakeworker = (function(global){ 3 | function extend(dest, src){ 4 | for (var i in src) { 5 | dest[i] = src[i]; 6 | } 7 | } 8 | // >>>>>>>>>> this part is copied and modified from jQuery 1.2.6 (Copyright 9 | // (c) 2008 John Resig (jquery.com)) 10 | var userAgent = navigator.userAgent.toLowerCase(); 11 | // Figure out what browser is being used 12 | var browser = { 13 | version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1], 14 | safari: /webkit/.test(userAgent), 15 | opera: /opera/.test(userAgent), 16 | msie: /msie/.test(userAgent) && !/opera/.test(userAgent), 17 | mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent) 18 | }; 19 | // Determines if an XMLHttpRequest was successful or not 20 | function httpSuccess(xhr){ 21 | try { 22 | // IE error sometimes returns 1223 when it should be 204 so treat it 23 | // as success, see #1450 24 | return !xhr.status && location.protocol == "file:" || 25 | (xhr.status >= 200 && xhr.status < 300) || 26 | xhr.status == 304 || 27 | xhr.status == 1223 || 28 | browser.safari && xhr.status == undefined; 29 | } 30 | catch (e) { 31 | } 32 | return false; 33 | }; 34 | // <<<<<<<<<<<<<<<<<<<< 35 | 36 | function __syncXhrGet(url, fn){ 37 | var xhr = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest(); 38 | // sync request 39 | xhr.open("GET", url, false); 40 | /* 41 | xhr.onreadystatechange = function(){ 42 | if (xhr.readyState == 4) { 43 | if (httpSuccess(xhr)) { 44 | try { 45 | fn(xhr); 46 | } 47 | catch (e) { 48 | throw e; 49 | } 50 | } 51 | else { 52 | throw new Error("Could not load resource(" + url + ") result=" + xhr.status + ":" + xhr.statusText); 53 | } 54 | } 55 | }; 56 | */ 57 | xhr.send(""); 58 | if (httpSuccess(xhr)) { 59 | fn(xhr); 60 | } 61 | else { 62 | throw new Error("Could not load resource(" + url + ") result=" + xhr.status + ":" + xhr.statusText); 63 | } 64 | } 65 | 66 | // >>>>>>>>>> this part is copied from parseUri 1.2.2 67 | // (c) Steven Levithan 68 | // MIT License 69 | 70 | function parseUri(str){ 71 | var o = parseUri.options, m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), uri = {}, i = 14; 72 | 73 | while (i--) 74 | uri[o.key[i]] = m[i] || ""; 75 | 76 | uri[o.q.name] = {}; 77 | uri[o.key[12]].replace(o.q.parser, function($0, $1, $2){ 78 | if ($1) 79 | uri[o.q.name][$1] = $2; 80 | }); 81 | 82 | return uri; 83 | }; 84 | 85 | parseUri.options = { 86 | strictMode: false, 87 | key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"], 88 | q: { 89 | name: "queryKey", 90 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 91 | }, 92 | parser: { 93 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 94 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 95 | } 96 | }; 97 | // <<<<<<<<<<<<<<<<<<<< 98 | 99 | // >>>>>>>>>> this part is copied from http://d.hatena.ne.jp/brazil/20070103/1167788352 100 | function absolutePath(path){ 101 | var e = document.createElement('span'); 102 | e.innerHTML = ''; 103 | return e.firstChild.href; 104 | } 105 | // <<<<<<<<<<<<<<<<<<<< 106 | 107 | function FakeMessageEvent(worker){ 108 | extend(this, Event); 109 | Event.constructor.call(this); 110 | this.currentTarget = worker; 111 | this.srcElement = worker; 112 | this.target = worker; 113 | this.timestamp = new Date().getTime(); 114 | } 115 | FakeMessageEvent.prototype = { 116 | initMessageEvent: function(type, canBubble, cancelable, data, origin, lastEventId, source, ports){ 117 | this.initMessageEventNS("", type, canBubble, cancelable, data, origin, lastEventId, source, ports); 118 | }, 119 | initMessageEventNS: function(namespaceURI, type, canBubble, cancelable, data, origin, lastEventId, source, ports){ 120 | this.namespaceURI = namespaceURI; 121 | this.type = type; 122 | this.canBubble = canBubble; 123 | this.cancelable = cancelable; 124 | this.data = data; 125 | this.origin = origin; 126 | this.lastEventId = lastEventId; 127 | this.source = source; 128 | this.ports = ports; 129 | } 130 | }; 131 | function FakeErrorEvent(worker){ 132 | extend(this, Event); 133 | Event.constructor.call(this); 134 | this.currentTarget = worker; 135 | this.srcElement = worker; 136 | this.target = worker; 137 | this.timestamp = new Date().getTime(); 138 | } 139 | FakeErrorEvent.prototype = { 140 | initErrorEvent: function(type, canBubble, cancelable, message, filename, lineno){ 141 | this.initErrorEventNS("", type, canBubble, cancelable, message, filename, lineno); 142 | }, 143 | initErrorEventNS: function(namespaceURI, type, canBubble, cancelable, message, filename, lineno){ 144 | this.namespaceURI = namespaceURI; 145 | this.type = type; 146 | this.canBubble = canBubble; 147 | this.cancelable = cancelable; 148 | this.message = message; 149 | this.filename = filename; 150 | this.lineno = lineno; 151 | } 152 | }; 153 | 154 | var nativeWorker = global["Worker"]; 155 | var FakeWorker = function(url){ 156 | var self = this; 157 | this._listenerNamespaces = {}; // event listeners 158 | this._eventQueues = {}; 159 | 160 | __syncXhrGet(url, function(xhr){ 161 | try { 162 | self._workerContext = new FakeWorkerContext(url, xhr.responseText, self); 163 | } 164 | catch (e) { 165 | throw e; 166 | } 167 | }); 168 | }; 169 | FakeWorker.prototype = { 170 | isFake: true, 171 | addEventListener: function(type, listener, useCapture){ 172 | this.addEventListenerNS("", type, listener, useCapture); 173 | }, 174 | addEventListenerNS: function(namespaceURI, type, listener, useCapture){ 175 | var namespace = this._listenerNamespaces[namespaceURI]; 176 | if (!namespace) { 177 | this._listenerNamespaces[namespaceURI] = namespace = {}; 178 | } 179 | var listeners = namespace[type]; 180 | if (!listeners) { 181 | namespace[type] = listeners = []; 182 | } 183 | listeners.push(listener); 184 | }, 185 | removeEventListener: function(type, listener, useCapture){ 186 | this.removeEventListener("", type, listener, useCapture); 187 | }, 188 | removeEventListenerNS: function(namespaceURI, eventName, fn, useCapture){ 189 | var namespace = this._listenerNamespaces[namespaceURI]; 190 | if (namespace) { 191 | var listeners = namespace[type]; 192 | if (listeners) { 193 | for (var i = 0; i < listeners.length; i++) { 194 | if (listeners[i] === listener) { 195 | delete listeners[i]; 196 | } 197 | } 198 | } 199 | } 200 | }, 201 | dispatchEvent: function(event){ 202 | if (typeof this["on" + event.type] == "function") { 203 | this["on" + event.type].call(this, event); 204 | } 205 | var namespace = this._listenerNamespaces[event.namespaceURI]; 206 | if (namespace) { 207 | var listeners = namespace[event.type]; 208 | if (listeners) { 209 | for (var i = 0; i < listeners.length; i++) { 210 | listeners[i].call(this, event); 211 | } 212 | } 213 | } 214 | return true; 215 | }, 216 | postMessage: function(msg){ 217 | var self = this; 218 | var workerContext = this._workerContext; 219 | if (typeof workerContext.onmessage == "function") { 220 | // for testability, we don't do the "structual clone". 221 | var event = new FakeMessageEvent(self); 222 | event.initMessageEvent("message", false, false, msg, "", "", null, null); 223 | setTimeout(function(){ 224 | try { 225 | workerContext.onmessage.call(workerContext, event); 226 | } 227 | catch (e) { 228 | var errorEvent = new FakeErrorEvent(self); 229 | var lineno = e.line || e.lineNumber; 230 | errorEvent.initErrorEvent("error", false, false, e.message, workerContext.location.filename, lineno); 231 | self.dispatchEvent(errorEvent); 232 | throw e; 233 | } 234 | }, 0); 235 | } 236 | }, 237 | terminate: function(){ 238 | this._workerContext.close(); 239 | } 240 | }; 241 | 242 | function FakeWorkerLocation(url){ 243 | var absolute = absolutePath(url); 244 | var parsed = parseUri(absolute); 245 | this.href = absolute; 246 | this.protocol = parsed.protocol + ":"; 247 | this.host = parsed.port ? parsed.host + ":" + parsed.port : parsed.host; 248 | this.hostname = parsed.host; 249 | this.port = parsed.port; 250 | this.pathname = parsed.path; 251 | this.search = parsed.query ? "?" + parsed.query : ""; 252 | this.hash = parsed.anchor ? "#" + parsed.anchor : ""; 253 | this.filename = parsed.file; 254 | } 255 | FakeWorkerLocation.prototype = { 256 | toString: function(){ 257 | return this.href; 258 | } 259 | }; 260 | 261 | function FakeWorkerContext(url, source, worker){ 262 | var postMessage = this.postMessage = function(msg){ 263 | var event = new FakeMessageEvent(worker); 264 | event.initMessageEvent("message", false, false, msg, "", "", null, null); 265 | setTimeout(function(){ 266 | worker.dispatchEvent(event); 267 | }, 0); 268 | }; 269 | var setTimeout = this.setTimeout = global.setTimeout; 270 | var clearTimeout = this.clearTimeout = global.clearTimeout; 271 | var setInterval = this.setInterval = global.setInterval; 272 | var clearInterval = this.clearInterval = global.clearInterval; 273 | var XMLHttpRequest = this.XMLHttpRequest = global.XMLHttpRequest; 274 | var openDatabase = this.openDatabase = global.openDatabase; 275 | var openDatabaseSync = this.openDatabaseSync = global.openDatabaseSync; 276 | var WebSocket = this.WebSocket = global.WebSocket; 277 | var EventSource = this.EventSource = global.EventSource; 278 | var MessageChannel = this.MessageChannel = global.MessageChannel; 279 | var Worker = this.Worker = FakeWorker; 280 | //var SharedWorker = this.SharedWorker = SharedWorker; 281 | 282 | var location = this.location = new FakeWorkerLocation(url); 283 | var close = this.close = function(){ 284 | this.closing = true 285 | // not yet implemented 286 | }; 287 | var navigator = this.navigator = global.navigator; 288 | var self = this.self = this; 289 | var importScripts = this.importScripts = function(){ 290 | /* 291 | for (var i = 0; i < arguments.length; i++) { 292 | __syncXhrGet(arguments[i], function(xhr){ 293 | with (global) eval(xhr.responseText); 294 | }); 295 | } 296 | */ 297 | throw new Error("importScripts is not supported."); 298 | } 299 | //var __importScriptsSource = "(function(__global){" + __syncXhrGet.toString() + ";importScripts=" + __importScripts.toString() + "})(this);"; 300 | //eval(__importScriptsSource + source); 301 | // execute worker 302 | eval(source); 303 | 304 | // pick up the onmessage global handler in eval context to this context 305 | try { 306 | if (typeof onmessage == "function") { 307 | this.onmessage = onmessage; 308 | } 309 | } 310 | catch (e) { 311 | } 312 | } 313 | var ret = { 314 | nativeWorker: nativeWorker, 315 | install: function(){ 316 | global["Worker"] = FakeWorker; 317 | }, 318 | uninstall: function(){ 319 | global["Worker"] = nativeWorker; 320 | } 321 | }; 322 | // auto install 323 | ret.install(); 324 | return ret; 325 | })(this); 326 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | return { 3 | version: "1.0.1", 4 | load: function (name, req, onLoad, config) { 5 | if (config.isBuild) { 6 | //don't do anything if this is a build, can't inline a web worker 7 | onLoad(); 8 | return; 9 | } 10 | 11 | var url = req.toUrl(name); 12 | 13 | if (window.Worker) { 14 | onLoad(new Worker(url)); 15 | } else { 16 | req(["worker-fake"], function () { 17 | onLoad(new Worker(url)); 18 | }); 19 | } 20 | } 21 | }; 22 | }); --------------------------------------------------------------------------------