38

If not, for one thing, I would be all on board for writing all of my modules like

import A from './a.js';

var B = function(){
  //use A
};

export default B;

and then using a compiler to build that into some browser or server format.

My one issue with the above however is the explicit specification of ./a.js in the import.

I understand why the spec went this way1, to be in favour of static analysis. But there are two very practical reasons why baking in both a module's filename and its path are trouble.

  1. As already raised here, when recycling modules frequently from project to project, it's very likely you won't be able to maintain a consistent path to that resource in your project tree. Baking an import call like import myModule from './../../vendor/lib/dist/mod.js' into a module's code doesn't exactly feel future-proof to me.
  2. Besides the path itself, specifying the filename also ties you down. Something like this seems innocent enough:

    import $ from 'vendor/jquery.js'

    But what about the day when I want to use Zepto instead of jQuery? I've found abstraction, particularly around vendor libraries, to be immensely useful when dealing with large codebases, opinionated developers, and an ever-changing JavaScript ecosystem. I may want to import React as my component library today, but what about tomorrow? Moreover, what if I'm going to be using the same module on both the client and server, but I need different versions of a dependent library?

I insist on robust (but clear and consistent) abstraction in my teams. Often times, abstraction has taken the form of some kind of namespacing. I fantasize a bit about this:

//BAD: Bakes React into my component modules
import ComponentLib from './React.js';

//GOOD: Leaves me free to use any React-like library
import ComponentLib from 'vendor.lib.component';

Where vendor.lib.component, in a Java-like way, has been registered somewhere previously.

Note that unlike in this question, my aim is not to have dynamic control over my imports. I don't want run-time flexibility, I'd like build-time flexibility. I should be able to sub-out a dependent framework for another one, or for a mock, or for something that will work in a particular environment, without having to worry about what dependencies my modules are calling, or trying to duplicate some crazy directory tree for every build product I'm after.

Similar questions have led to the suggestion of a library that leverages the System specification, like SystemJS. You can then use something like jspm to introduce a module map to get abstraction. But the moment I do that, I'm writing all of my modules differently:

System.import('a', function(A){
  //use 'A'
});

Is that suddenly the future? If so, why don't I just keep using AMD? Why even bother with ES2015 modules and running transpilers if I'm just going to go back to using an asynchronous-looking loader API?

More eye-rolling, I don't see much or any mention of tackling a module loader API standard in the ES2017 spec.

(EDIT: Question revised to meet standards of a non-opinion-based answer)

Given all of the above, I'm asking the community -- how do I write a JavaScript module that (i) abides by the ES2015 standard, (ii) does not reference a dependent module by its filename or path, and (iii) does not rely on extensive intermediate tools/configuration that would make sharing the module with multiple teams prohibitive.

--

Note 1 As @zeroflagL noted in the comments, the spec doesn't explicitly state that a module should be specified as a path, just a string (see ModuleSpecifier - http://www.ecma-international.org/ecma-262/6.0/#table-41). However, there is also a clear instruction to account for circular references, implying some kind of static analysis (http://www.ecma-international.org/ecma-262/6.0/#sec-imports), with file paths seemingly being the reference context of choice to this point. So we can't blame the spec for being rigid here, rather the opposite. The onus may then be on the rest of us to develop more robust implementations of import/ModuleSpecifier that lead to a secondary standard.

Community
  • 1
  • 1
Dan
  • 6,022
  • 3
  • 20
  • 28
  • 3
    That seems like an interesting topic, but not a good fit for Stack Overflow, since there isn't a single correct answer. – Felix Kling May 01 '16 at 01:44
  • You can customize/augment the loader to do anything you want. –  May 01 '16 at 05:56
  • @ Felix there may not be a correct answer, but I think there's a *best* answer. @ torazaburo I know that, but my question isn't about ability, it's about standards. Why would I write modules I want others to use based on a crazy loader implementation only I'm using? – Dan May 01 '16 at 15:02
  • i agree with @FelixKling, if we interpret the rules strictly, but let's bend them so we can continue to have an important discussion on an important topic. This should **absolutely** be addressed in the next spec, or there will be pain – code_monk Dec 10 '16 at 23:16
  • I do not understand your issue with SystemJS. If using a transpiler you do not need to concern yourself with the (after transpile) asynchronous API and you do have your system config where you get to choose your package names freely. `import { A, B } from 'wherever'` looks good to me. I'm not suggesting SystemJS is the way to go. Just trying to understand your objections. – Hampus Dec 23 '16 at 10:25
  • @Hampus I'm speaking in terms of broader module re-usability, modules that will be re-used not only by myself but by other teams and other organizations. Forcing the inclusion of (i) a particular library like SystemJS, and (ii) a transpiler, means a lot of extra configuration work for anyone wanting to compile my module in to their project. Sure it's do-able, and it's what most JS devs end up having to do in these situations, but I wouldn't call that a best solution. What we need is a standard, but the ES2015 module standard seems insufficient for reasons stated above. – Dan Dec 23 '16 at 15:45
  • _"I understand why the spec went this way ... But there are two very practical reasons why baking in both a module's filename and its path are trouble."_ The specification doesn't require you to do that. – a better oliver Jan 03 '17 at 10:32
  • @zeroflagL you're completely correct, I made too big a jump. I've added a note to my question. Thanks for the comment. – Dan Jan 03 '17 at 18:39

3 Answers3

1

To me, this seems to be one of the biggest unaddressed issues of the JS community. There are no best practices around dependency management and dependency injection around (at least to my knowledge).

There is a long discussion on this thread: Do I need dependency injection in NodeJS, or how to deal with ...?, but most of the solutions seem to work for specific cases only, and require changing the way you write modules somehow. And most shocking is that many answers argue that you don't even need DI.

My own solution for the issue is this minimal DI framework, which lets you define modules once, and it will wire them up for you with proper dependencies.

Community
  • 1
  • 1
gafi
  • 12,113
  • 2
  • 30
  • 32
0

I know that you are asking for a solution that you can use specifically in case of a JS compiler, but since you are asking for a best practice I feel that we have to take into consideration the complete playing field in which JavaScript dependency management takes place, including different possible host environments like the browser and Node.js, front-end packaging systems like Webpack, Browserify and jspm, and to some extent neighboring technologies like HTML and HTTP/HTTP2.

History of ES modules

There is a reason that you will not find a loader API in any ECMA specification: dependency resolvement was not finalized when the deadline for the ECMA2015 specification arrived, and it was decided that the ECMA specification would only describe the module syntax, and that the host environment (e.g. browser, node.js, JS compiler/transpiler) would be responsible for resolving modules through their specifiers, through a hook called HostResolveImportedModule, which you will be able to find in the ECMA2015 and ECMA2017 specs.

The specification of the semantics behind the ES2015 module pattern is now in the hands of the WHATWG. Two notable developments have been the fruit of their efforts:

  1. They have determined that modules in HTML may be denoted by <script type="module">.
  2. There is a draft for a loader specification, which will take into account different host environments and front-end packaging systems. This does sound like the specification that will ultimately help answer your question, but unfortunately it is far from finished, and has not been updated for quite some time.

Current implementations

Browser

With the addition of the <script type="module"> tag, everything seems in place for a simplistic implementation of ES6 modules in the browser. The way it works now is without any regard for performance (see this and this comment). Also, it does not support any front-end packaging systems. It is clear that the concept of modules has to be expanded for it to be usable at all in production websites, and therefore browser vendors have not been making any haste in implementing it in their browsers.

Node.js

Node.js is an entirely different story, as it has implemented the CommonJS style modules from the start. Currently, no support for ES2015 modules is present. Two separate suggestions have been made to incorporate ES6 modules in Node.js alongside CommonJS modules. This blog post discusses these suggestions in great detail.

compilers and packaging systems

  • Webpack 2 supports ES2015 modules. Furthermore it can be adjusted to use custom dependency resolvement.
  • Babel supports ES2015 modules and has the possibility to convert them to AMD, CommonJS, SystemJS, and UMD modules. It seems that package managers other than Webpack support ES2015 modules through Babel.

Conclusion

So when you are asking for a best way to write modules, you can see that this is very difficult. Coming up with a solution that is comopatible with all possible host environments is preferrable, because:

  • You might want to share your code between different environments.
  • There is a risk that some class of developers (e.g. front-end developers) would embrace ES2015 modules, whereas others (e.g. Node.js developers) would stick to another solution (e.g. CommonJS modules). This would further depreciate the possibility of cross-environment code.

But you will see from the above that there different environments have different demands. In this sense the best answer to your question might be: there is currently no best way of writing modules that covers your abstraction concerns.

Solution

However, if I would have the task right now of writing ES2015 modules that compile to JS, I would stay away from relative paths and always use absolute paths from the project root as an identifier for the modules, which I don't see as problematic. Java actually mirrors namespaces and its directory structure in much the same way. I would use Webpack or Babel to compile my source to code that is runnable in today's JS environments.

As for your other problem, if I would want to be able to substitute vendor libraries, I would probably create one module that aliases the vendor libraries to names that I will use internally. Somewhat like:

//  /lib/libs.js
import jQuery from 'vendor/jquery.js'
export const $ = jQuery;

All other modules would then import $ from lib/libs.js and you would be able to switch vendor libraries by changing the reference in one place.

Borre Mosch
  • 4,404
  • 2
  • 19
  • 28
-1

If you want to follow best practises then follow AirBnb JavaScript styleguide. In my opinion the best and most complete JavaScript styleguide out there

https://github.com/airbnb/javascript#classes--constructors

Import

This looks bad for reusing modules: import myModule from './../../vendor/lib/dist/mod.js'

Publish your module on NPM(which can also be private or self-hosted NPM) and import like that import myModule from 'my-module';

Eventually set your NODE_PATH as a root folder and refer to modules relatively from the root.

In package.json

'start': 'NODE_PATH=. node index.js'

// in Windows
'start': 'NODE_PATH=. && node index.js'

Now import like that:

import myModule from 'vendor/lib/dist/mod.js'

Variables

var is not part of ES6. Use:

  • constant - when the value of the variable won't change, also objects and imports. Even if object's parameters change it's still a const.

  • let - when the value of the variable changes i.e. for(let = i; i < 100; i++)

  • From my own experience always set const as a default and only change to let if the ESLint complains (btw. use ESLint http://eslint.org/)

Classes

There's now a proper way to define classes in JavaScript

class B {
  constructor() {
  }
  doSomething() {
  }
}

Your example updated:

import A from './a';

Class B {
  constructor() {
  }
  doSomething() {
  }
};

export default B;

If you want to extend A:

import A from './a';

Class B extends A{
  constructor(argumentA, argumentB) {
    super(argumentA, argumentB);
    this.paramA = argumentA;
  }
};

export default B;

Tips

  • Use Webpack with NPM as build tool. Don't use Gulp or Grunt
  • Use Babel to transpile your code(JSX loader may be not enough)
  • Learn to not use jQuery at all, but instead pick the right polyfills and tools for jobs that you need to do from NPM
  • There are tons of well-written boilerplate repos on github so steal from the best. Here are some React ones I'm using.

My answer in essence is:

The pattern/library you asked for is AirBnb JavaScript styleguide and forget about jQuery

Pawel
  • 16,093
  • 5
  • 70
  • 73
  • 2
    "*var is not part of ES6*" - what? – Bergi Aug 19 '16 at 21:38
  • I don't think the OP meant `A` and `B` to be classes, so your examples for that aren't really on topic. – Bergi Aug 19 '16 at 21:39
  • @Bergi https://www.youtube.com/watch?v=vJKDh4UEXhw&feature=youtu.be&t=28m4s – Pawel Aug 19 '16 at 21:44
  • @Bergi it's true that var is still part of the language but now it's meant to be used only in very specific cases. In general it has been replaced by const/let – Pawel Aug 19 '16 at 21:52
  • 2
    You should not take everything that Crockford says literally :-) – Bergi Aug 20 '16 at 09:59
  • @Bergi actually I brought him up as an authority only to support the practice replacing "var" with "const/let". Every ES6 article covers this subject https://strongloop.com/strongblog/es6-variable-declarations/ I just want to stress on putting "const" wherever possible because people who are switching to const/let make a mistake of simply replacing "var" with "let" without understanding that the change is more than that. "const" should go first to allow for more predictable code. – Pawel Aug 20 '16 at 15:23
  • 3
    Meh, Crockford is an opinion not an authority :-) Regardless, I think only the "Import" section of your post answers the OPs question. You should drop the rest. – Bergi Aug 20 '16 at 15:52
  • @Pawel thanks for the thorough answer, you've highlighted a lot of good practices here. Agreed that the Airbnb styleguide is one of the better ones. However it doesn't really answer my question, which is more about referencing modules in an abstract manner. I don't think NPM is a valid recourse, pushing my modules to a centralized repo is a roundabout and restrictive way to make them portable, and I'm still referencing libraries explicitly (e.g. `import $ from 'jquery'`). And since Node/NPM just looks for modules in a specific spot, it's not really solving the path abstraction issue either. – Dan Aug 20 '16 at 19:55
  • @Dan what do you mean that node looks for modules in a specific spot? do you mean node_modules folder? – Pawel Aug 20 '16 at 21:38
  • @Pawel correct. Yes you can change that, but again, if my project has 500 modules I'm not going to check in 500 npm projects, nor am I going to want them all in the same flat folder structure, nor is it going to help me when I want to sub-out one of those libraries for something else depending on the environment I'm building in. – Dan Aug 20 '16 at 21:54
  • @Dan So what about setting node_path to . and then importing all the modules relatively from the root? Then you can have a git repo with shared components which you can clone to every project and import it's from the root i.e. import myModule from 'shared-modules/category/myModule' https://gist.github.com/branneman/8048520 – Pawel Aug 20 '16 at 22:03
  • @Pawel yep I've used that method (actually I think I've read that gist too!). But using npm in this way isn't sustainable unless it's infrequently changing code. Many of my modules would not be shared resources, and even if they are I'd be changing them frequently, which means updating the repo, incrementing the npm version, etc., every time I change code. But again, more importantly, the idea is that I should be able to change what my module reference points to at build time, potentially pointing to a completely different folder. npm is not going to help with that. – Dan Aug 21 '16 at 00:55