Best Practices: shimming libraries which use global variables

Suppose I need to use a library which is accessed via a global variable e.g. jQuery, lodash, d3, etc. Let’s assume that the library does not export modules of any flavor.

What is the best way to import such a library?

Option 1: Just use the global

This seems counterproductive – modules are a superior method of sharing code. Nonetheless, it is apparently an option some pursue.

Example usage with lodash, which uses the _ global variable, in an adapter:

// adapters/my-custom-adapter.js
findQuery: function(store, type, query) {
  return this.findAll(store, type).then(function(data) {
    return _.filter(data, _.matches(query))
  });
}
  • Pros
    • No effort, just import the library in your Brocfile
  • Cons
    • Undermines the benefits of modules

Option 2: Create a vendor/shims.js file

Another option is to wrap the global in a module so it can be imported in the standard fashion.

Example usage:

// vendor/shims.js
define('lodash', [], function() {
  'use strict';

  return {default: _};
});

// adapters/my-custom-adapter.js
import _ from 'lodash';

findQuery: function(store, type, query) {
  return this.findAll(store, type).then(function(data) {
    return _.filter(data, _.matches(query))
  });
}
  • Pros
  • Cons
    • You must use AMD module syntax (i.e. the transpiler target module syntax). Using AMD seems bad because then the transpiler abstraction leaks. Shouldn’t we always use ES6 module declarations?

Sidebar Anyone know why the transpiler doesn’t support the module declaration (which I think is part of the ES6 spec)?

module 'lodash' {
  export default: _;
}

So I guess option 2b is modify the transpiler to support module.

Option 3: Create a vendor/lodash.js, use ES6 modules

Similar to option 2, but each vendored lib gets its own file.

Example usage:

// vendor/lodash.js
export default _;

// adapters/my-custom-adapter.js
import _ from 'vendor/lodash';

findQuery: function(store, type, query) {
  return this.findAll(store, type).then(function(data) {
    return _.filter(data, _.matches(query))
  });
}
  • Pros
    • Uses ES6 modules
  • Cons
    • Clunkier import path and more ceremony for adding new libs

Option 4: Modify the loader to use globals

It looks like there is some precedent in ES6 polyfills for loading globals when all else fails. That is, when a module has not been registered, there is a final check to see if the required module name is defined globally. I’m curious if this is considered a bad idea.

Example usage:


// adapters/my-custom-adapter.js
import _ from '_';

findQuery: function(store, type, query) {
  return this.findAll(store, type).then(function(data) {
    return _.filter(data, _.matches(query))
  });
}

Am I missing potential solutions? Which solution is preferable?

Thanks!

7 Likes

Have a look at Injecting a global variable into your Ember classes

1 Like

Thanks for the reply!

Using Ember’s DI facilities is certainly one solution, but I’m not sure it’s always the right solution. From my perspective, modules, DI and Ember DI are different creatures befitting different problems.

  • Modules - lightweight code organization
  • DI - a way to abstractly depend on an interface
  • Ember DI (i.e. register and inject) - only necessary because the framework is responsible for instantiating framework objects (e.g. components and controllers), so we don’t get an opportunity to perform non-fancy DI.

When DI is appropriate and when it is not

A logger is a good fit for DI. We want to instantiate and share a single logger instance at runtime. We want the ability to swap out logger implementations.

Conversely, lodash is a bunch of utility functions, and is not a good fit for DI. It will only ever have a single implementation. We don’t want the ability (or onus) to inject lodash. We want to depend on it concretely.

When Ember DI is appropriate and when it is not

Ember DI has the following mechanics. In an initializer, I can do something like:

var logger = new Logger();
application.register('logger:main', logger);
application.inject('controller', 'logger', 'logger:main'); 

This says that my instance of logger is registered under the name “logger:main”. Then, I can inject logger into all controllers (as specified by “controller”). It will appear on a given controller as the property logger i.e. can be accessed via this.logger.

This is pretty neat, but also pretty limiting (has anyone ever been happy with a DI framework?).

  • Limitation 1. The inject target, 'controller' has to be a valid “factory name”. That is, it must be a “major framework class”.

  • Limitation 2. The inject target, must be instantiated by the framework

  • Limitation 3. The dependency is injected as an attribute of the object.

Let’s return to lodash, the pile of utility functions. If I define a vanilla JS class (POJO), which in turn depends on lodash, limitations 1 and 2 are killers. I instantiate POJOs myself, and they’re not major framework classes, so I cannot take advantage of Ember DI.

But even if I’m working on a major framework class, do I really want lodash’s _ to become a property of the object? Not really. this._ seems weird, and I don’t want to bind this everywhere just to access _.

tl;dr

The availability of a Ember DI does not obviate the need for modules because 1. Ember DI is limited, and 2. DI is not always the right pattern.

I think it is important to create module shims for globals before we even get into register/inject territory; a sacred shim barrier over which no code may cross unless it is wrapped by a module. In other words, I would modify the blog post example to:

import io from 'socket-io';

// stuff

app.register('io:main', io, {instantiate: false});
2 Likes

THIS IS AN EXCELLENT QUESTION (bump) :sunglasses:

1 Like