error handling in knockout spa using amd and requirejs - javascript

So I have a fairly large Single-Page-Application based on Steve Sandersons SPA template. custom bindingHandlers, validation etc all working fine.
We require the use of the XMLHttpRequests, Session and Local storage as well as some other libraries that don't have universal support (jquery 2.0 etc). I have a core module which handles identity and exposes a few services and both of these are reliant on these features. This is loaded when the user first visits the page.
I have an issue with older browsers where either a) they don't support some of the modules being loaded so they throw an error before hitting the onload function, or b) it hits the onload function but they don't support the browser features so I need to raise an exception myself and handle it in some manner.
Example: IE8 throws an 'Object doesn't support property or method 'addEventListener'' (jquery error) and IE9 Doesn't support everything I need so I throw a custom error.
define('core', ['jquery', 'browser'], function($, browser) {
if(!browser.hasFullSupport) {
throw new Error('Update your browser');
}
// aload of gubbins that requires sessionStorage etc
return {
identity: identity,
serviceA: serviceA,
serviceB: serviceB
}
}, function(err) {
// doesn't catch the ie8 error here
});
I've tried setting a window.onerror but it works differently across browser versions and in some I can't capture the error message properly, I just get a 'Script Error'.
I know I can add an error event handler for the module definitions but it doesn't capture the errors i'm experiencing.
I've seen people use
requirejs.onError = function(err) {
// something here
}
but i'm using the requirejs gulp bundler and I can't see where this object would exposed (if it is at all)
var require = {
baseUrl: ".",
paths: {
"modernizr": "bower_modules/modernizr/modernizr",
"browser": "app/browser-detect",
"crossroads": "bower_modules/crossroads/dist/crossroads.min",
"hasher": "bower_modules/hasher/dist/js/hasher.min",
"jquery": "bower_modules/jquery/dist/jquery.min",
....
// gulpfile
var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require.config.js') + '; require;');
requireJsOptimizerConfig = merge(requireJsRuntimeConfig, {
out: 'scripts.js',
baseUrl: './src',
name: 'app/startup',
paths: {
requireLib: 'bower_modules/requirejs/require'
},
include: [
'requireLib',
'components/nav-bar/nav-bar',
.....
gulp.task('js', function () {
return rjs(requireJsOptimizerConfig)
.pipe(uglify())
.pipe(gulp.dest('./dist/'));
});
There's loads of components/pages and each could throw an error, but it's this initial core component loading that i'm keen to sort out. I'd like to distinguish between errors (that may or may not be caused by the browser being too old) and between me detecting that they're too old before hand, is there an elegant way to achieve this using this template model?
I'd rather not deviate too far from the template as i'm not too familiar with requirejs / gulp etc but i'm open to suggestions.

You contradict yourself:
We require the use of the XMLHttpRequests, Session and Local storage as well as some other libraries that don't have universal support (jquery 2.0 etc)
But:
IE8 throws an 'Object doesn't support property or method 'addEventListener'' (jquery error) and IE9 Doesn't support everything I need so I throw a custom error.
You can't use libraries and HTML 5 features for newer browsers and expect older browsers to still work. Libraries that do not support legacy browsers like IE 8 do not fail gracefully so you cannot handle the failure very well. And even if you found a workaround to somehow circumvent the errors, you would still need to test them all in old browsers periodically which beats the intent to support only newer browsers.
So I recommend you two approaches:
Either decide not to support olders browsers at all and test the app only in newest modern browsers. You could use modern versions of libraries that dropped support of old browsers and not worry about old browsers at all. It depends on the state of your user base. Or:
Downgrade libraries to versions that support your minimal required browsers and continue developing the app with them. Test the app and fix all errors that occur in old browsers. Use polyfills that enable new HTML 5 features in old browsers via emulation.

Related

Is there a way to silence syntax errors in IE11 in a modern JS code?

We're developing a modern JS library which uses the ES6 syntax heavily and doesn't support IE11.
However we have a small number of users who want to use our library on IE11-compatible sites and we don't want to break their sites on IE11.
Question: is there some way to prevent our library from "exploding" on IE11? (All of the library functions can do nothing and return undefined if IE11 is detected)
For example, we were trying the following approach based on browser detection:
function libFunction() {
if(isIe11()) {
return;
}
// otherwise do some real stuff with ES6-heavy code
}
However the approach above doesn't work because IE11 throws syntax error even in the code that never gets executed, se we end up with errors like:
SCRIPT1002: Syntax error
File: main.db33ab01aedf59e2f70a.hot-update.js, Line: 47, Column: 1
Other approaches that we consider:
make our server return a fake implementation of our library if IE11 User-Agent is detected in the request headers. This will partially solve our problems, but won't help the users, who integrate our library into their bundle via NPM/webpack instead of getting it from our servers at runtime.
transiple our ES6 code to IE11-friendly code and polyfill all the APIs - we don't do that because we don't want the modern browser users to pay the price of the bloated ES5 code and polyfills.
override global error handler to silence the errors - this won't work, because the errors we get are syntax error, that are not handled by the error handler.
Is there any other possible solution?

Webpack output two builds, one for ES6, one for IE11

I have a webpack configuration of ES5/ES6 modules that works fairly well. Until recently it also ran through TargetsPlugin to generate a IE11-compatible build.
I say "until recently" because while experimenting, I noticed that dropping the babel transpiling to older language specs, we instantly shed 300KB from our bundles. If we turned this on, 90-point-something percent of our users would get a leaner, faster, better experience.
But I'm contractually obliged to support IE11. Can I do that separately?
What I want is a ES6-ish build (what we have) and a companion IE11 build. It's not difficult to target these browsers programmatically when it comes to switching out what version they see... But how do I get webpack to do it?
If it makes any difference, I don't need the IE11 version to have any fancy features. It can be one 1MB amorphous blob, or fully chunked out. However it comes, as long as it works.
Okay, turns out webpack can handle multiple configs at the same time. Where you would normally do module.exports = { ... config ... }, you can actually return an array of config objects.
So I went for something like this:
var realConfig = {
...
}
var ie11 = _.cloneDeep(real)
ie11.output.filename = 'js/ie11.[name].js'
ie11.plugins.push(
new TargetsPlugin({ browsers: ["IE >= 11"] })
)
module.exports = [ realConfig, ie11 ]
And that gives me an IE-compatible version of everything. In my templates I just detect "MSIE" in the user agent and feed that out. Probably not the most reliable but build should also work on modern browsers so I'm not concerned.

What happens if polyfill script fails to download?

I have a SPA (built on webpack, with babel, etc) that includes a polyfill in the index.html:
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise,Array.prototype.includes,Element.prototype.remove"></script>
One use-case for the polyfill is in order to use the Promise API on IE 10-11.
My error monitoring reporting an error on an IE 11 client of the following:
ReferenceError: 'Promise' is undefined
So I assume that particular session failed to download the polyfill for some reason.
My question is: How should I deal with this case? Is it a scenario I should expect to happen sometimes? Is the user expected to noticed the application not working properly, and reload the page?
There is an error event you can attach to allow for more control if you are really worried. You don't usually need to handle this explicitly though.
In this particular case you could migrate towards using babel to build a bundle with polyfills included in your scripts. This adds an additional build step to your process though.
Since you mentioned you're using webpack, it would just be best to include the necessary polyfills directly in the project via an import statement (something like core.js) rather than using a cdn - polyfill.io.
However, you could alternatively add an ID to the script element and listen to the onload and onerror events to determine whether the script (un)successfully loaded like so:
<script id="polyfillScript" src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise,Array.prototype.includes,Element.prototype.remove"></script>
In your project index.js:
document.getElementById('polyfillScript').addEventListener('error', () => {
alert('Failed to load polyfill!');
});

Use Promises in Firefox OS

I want to use the great Promise interface described on MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with my Firefox OS app. But as of Firefox 1.3 (simulator) it doesn't seem to be supported ("Promise is undefined").
Is there a working polyfill whose syntax agrees with the specification on MDN? I don't want to rewrite everything once the real Promises interface is supported by a future version.
This one doesn't work in Firefox OS with Web Workers: https://github.com/jakearchibald/es6-promise The reason: It makes use of the "window" global variable.
This one slightly differes from the MDN specification: https://github.com/slightlyoff/Promises - Namely: While the MDN specification says
new Promise(function (resolve, reject) { /*...*/ resolve(); /*...*/ });
... this polyfill wants to use this syntax (it uses a so called "resolver"):
new Promise(function (r) { /*...*/ r.resolve(); /*...*/ });
I've just tried the simulator and can just get the promises working on a certified apps. Actually if you get the simulator, and debug any app that comes with the simulator (dialer, camera ...), you'll have a console and there you can inspect the available resources, you'll see how Promises is available.
Also tried on a phone, running gecko master, and it's available at least for privilege apps.
Just an idea that came to my mind, if you go to Tools > Add-ons you can see the simulator as an extension, in preferences it has an option to select your binary, perhaps you could download a latest build from here:
http://ftp.mozilla.org/pub/mozilla.org/b2g/nightly/latest-mozilla-central/
and give it a try.
Just putting a simple
var window = self;
in my Web Worker will make the polyfill from https://github.com/jakearchibald/es6-promise work.
It is an answer, and it at least works around the problem. Still, I'm not happy with it. It seems to me like defining a global window variable in the worker scope will have fatal consequences sooner or later - e.g. when some included JavaScript library tries to check whether it's running inside of a Web Worker etc.
However a fix is in the making: https://github.com/jakearchibald/es6-promise/pull/6

Conditionally require code in commonJS AMD module

I'm trying to write a cross-browser extension for Firefox and Chrome. Firefox uses the commonJS specification and Chrome just lumps everything into the global namespace like a webpage.
In order to be able to write reusable code, I'm trying to use requireJS to lood code in the Chrome extension, that way I can write commonJS modules and have them work in both environments.
I'm running into a problem when I need to conditionally require modules. For example, Firefox provides access to a simple-storage module which you should use to access the local storage. In chrome, I need to use the localStorage API that they provide. So, I've been trying to do this:
// storage.js
define(function(require, exports, module){
var store;
try {
// This module will only be available in the FF extension.
store = require('simple-storage').storage
} catch(error) {
// If it's not available, we must be in Chrome and we
// should use the localStorage object.
store = localStorage
}
// Use the store object down here.
});
However this doesn't seem to work. When I try to load the Chrome extension I get the following error:
Is there a better way to require modules with a fallback?
There are two caveats here:
1) Detect if chrome is running
// detects webKit (chrome, safari, etc..)
var isChrome = 'webKitTransform' in document.documentElement.style
2) Requirejs will parse the define() function and search for require('module') calls. To prevent the error on chrome you have write the require in some way that when requirejs parses the function body it does not recognize the call as a module dependency:
if (isChrome)
// use localStorage
else {
// set the module name in a var does the trick,
// so requirejs will not try to load this module on chrome.
var ffStorageModule = 'simple-storage';
return require(ffStorageModule);
}

Categories