1
\$\begingroup\$

My project uses a Promise polyfill and the fetch polyfill, that I am only including if the browser does not yet support it

// Browser doesn't have Promise
if (!window.Promise) {
  require(['es6-promise'], function(promise) {
    promise.polyfill();
    // Browser doesn't have Promise or fetch
    if (!window.fetch) {
      require(['fetch'], function(fetch) {
        run();
      });

    // Browser doesn't have Promise but does have fetch
    } else {
      run();
    }
  });
// Browser has Promise but doesn't have fetch
} else if (!window.fetch) {
  require(['fetch'], function(fetch) {
    run();
  });
// Browser has Promise and fetch
} else {
  run();
}

This accounts for every permutation of browser support for Promise and fetch, but it seems like a verbose way of doing things. Is there a better to dynamically load these polyfills?

\$\endgroup\$
1
\$\begingroup\$

Something tells me that requirejs probably has some features to help with this, but I'm not finding them at the moment. Your situation is complicated by the fact that you have to run custom code after es6-promise is loaded before fetch is loaded and by the fact that a usual tool for these kinds of problems is promises, but you can't use them here because they may not yet be loaded.

Here's a solution that is not less overall code, but is more DRY and is more general purpose and could be reused in other projects:

The code to do the custom loading would look like this:

var loaders = [];
if (!window.Promise) {
    loaders.push({modules: 'es6-promise', fn: function(promise) {
        promise.polyfill();
    }});
}
if (!window.fetch) {
    loaders.push({modules: 'fetch'});
}
loadSequential(loaders, run);

And, the implementation of the reusable loadSequential() would look like this:

function loadSequential(list, done) {
    var i = 0;

    // load the next item in the list
    function next() {
        var item;
        if (i < list.length) {
            item = list[i++];
            require(item.modules, function() {
                if (item.fn) {
                    item.fn.apply(this, arguments);
                }
                next();
            });
        } else {
            done();
        }
    }
    // start first iteration
    next();
}

If you can allow fetch to load potentially before promises are available, then you could use the fact that fetch presumably can't exist without promises in a native browser environment (because fetch returns a promise) and could use this shortcut:

if (!window.Promise) {
    // if there's no promise, there can't be a fetch either
    require(['es6-promise', 'fetch'], function(promise) {
        promise.polyfill();
        run();
    });
} else if (!window.fetch) {
    require('fetch', run);
} else {
    run();
}
\$\endgroup\$
0
\$\begingroup\$

TL;DR: Keep it simple.

Unless you have backing evidence (from a profiler, or network inspector) that loading these polyfills are a problem, I suggest you don't really worry about it. I'm not so worried about size, and that's because you can always take advantage of minification, gzipping, browser caching. If you worry about cache invalidation, it can always be solved with cache busting.

The promise library is just ~30KB unminified. The fetch polyfill is just ~8KB unminified. Minified, they can even be smaller. If you are able to pre-process your HTML, you can use usemin. Usemin lets you write scripts and styles in special comment blocks. Running the task during build, it will process the HTML, compressing those blocks into one file.

<head>
  ...
  <!-- build:js dist/js/polyfills.js -->
  <script src="path/to/promise.js"></script>
  <script src="path/to/fetch.js"></script>
  <!-- other polyfills -->
  <!-- endbuild -->
  ...
</head>

<!-- becomes -->
<head>
  <!-- contains a concatenated, minified bundle of the files -->
  <script src="dist/js/polyfills.js"></script>
</head>

If you rather take the RequireJS optimizer route, the optimizer doesn't handle dynamic dependencies. Additionally, since either libs look like they aren't AMD modules, use RequireJS's shim config. Then let the parts of your code that need fetch or promises require them.

// Config
require.config({
  shim:{
    promise: {
      path: 'path/to/promise.js',
      exports: 'Promise',
    },
    fetch: {
      path: 'path/to/fetch.js',
      exports: 'fetch',
      deps: ['promise'],
    }
  }
});

// Usage

define(['fetch'], function(fetch){
  fetch(...).then(...);
});

Now I never did say it wasn't possible with RequireJS to dynamically load dependencies, or at least in the browser-run version (I usually call it "runtime" version). Since the config is executed on the browser during runtime as opposed to written statically for the optimizer, you can dynamically add configs or modules.

var myShims = {
  // your other shims, if any.
};

// Have a utility function that either exposes the native API as a module
// or shims the library as a module, pointing to the polyfill.
function polyfillOrNative(globalName, path, deps){
  if(window[globalName]){

    // Note that this is a named module. So you have to require it by name.  
    // For the sake of brevity, I just used the global name as module name.
    // Otherwise, names are usually small letters, and separated by dashes.
    define(globalName, deps, function(){
      return window[globalName];
    });  
  } else {
    myShims[globalName] = {
      path: path,
      exports: globalName,
      deps: deps,
    }
  }
}

polyfillOrNative('Promise', 'path/to/fallback/promise.js', []);
polyfillOrNative('fetch', 'path/to/fallback/fetch.js', ['Promise']);

require.config({
  shim: myShims,
});

// You can now depend on fetch and/or promise as modules.
define(['fetch'], function(fetch){
  fetch(...).then(...);
});

But then, what I wrote here introduced unnecessary logic into your codebase. It's much easier to just add your polyfills up in the head or just hard-written dependencies than having this convoluted logic in your code. I'd say weigh your options, because in the long run, JS is really hard to maintain without discipline.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.