ES6 modules, Node.js and the Michael Jackson Solution

JavaScript does not have a standard way to import or export functionality from one file to another. Fortunately, it has the global variable property. Such as:

` < script SRC = "https://code.jquery.com/jquery-1.12.0.min.js" > ` < / script > < script > / / ` $` variable available here < / script >Copy the code

This way is very imperfect, as it may lead to some problems

  • If other libraries you use use the same variable, your code may conflict. This is why many libraries have a noConflict() method.
  • You can’t do circular references. If module A depends on module B, in what order should we place them<script>Label?
  • Even if there is no circular reference in the code, you place it<script>The order of tags is also very important and this writing style can be particularly difficult to maintain in the future

CommonJs rescue

As Node.js and other server-side JavaScript solutions began to emerge, they agreed on a solution to this problem. They created a specification called CommonJS. For import and export issues, the specification defines a require() function that is injected at Runtime, as well as an exports variable to export functionality.

Note: CommonJs is not the only specification. In addition, there is a specification called UMD that can be used not only on the front end but also on the back end

There was an explosion of front-end tools, some of which were built for single-page applications (SPAs). With the increasing amount of front-end code and the tendency to share front-end and back-end code, the lack of modular management of front-end code becomes more and more obvious, especially on the browser side. Tools like Browserify and WebPack followed. They use CJS specification, cleverly to make up for the above defects.

This approach is a complete hack. Because browsers don’t implement require() or exports, all those tools do is package code together in some way. To learn more, go to how JavaScript Bundlers Work

How does the ES6 module work, and why hasn’t Node.js implemented it yet

With the development of JavaScript, this problem has finally been solved in ES6. This is where the ES module comes in, which is syntactic similar to CJS.

Now let’s compare these approaches. Let’s see how we can introduce something in different systems.

const { helloWorld } = require('./b.js') // CommonJS
import { helloWorld } from './b.js' // ES modules
Copy the code

You can export functions as follows:

// CommonJS
exports.helloWorld = () => {
  console.log('hello world')
}
Copy the code
// ES modules
export function helloWorld () {
  console.log('hello world')
}
Copy the code

It’s similar, isn’t it?

Node already implements 99% of ECMAScript 2015 (ES6) features, but modules support isn’t expected until late 2017 and will need to be turned on manually. Why did it take node.js so long to support ES6 modules when they are so similar to CJS?

The devil is in the details. The syntax of the two systems is very similar, but the semantics are completely different. Special attention to detail is required to achieve 100% specification compatibility.

Even when Node.js does not support ES modules, some browsers have implemented ES module support. For example, you can test it in Safari 10.1. Let’s look at some examples. Through these examples we will see why semantics are important. First, create the following three files.

// index.html
``<script type="module" src="./a.js">``</script>
Copy the code
// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
Copy the code
// b.js
console.log('executing b.js')
export function helloWorld () {
  console.log('hello world')
}
Copy the code

When the file executes, we will see the following result in the browser console:

executing b.js
executing a.js
hello world
Copy the code

However, in Node.js the same code is executed using CJS syntax:

// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
Copy the code
// b.js
console.log('executing b.js')
export function helloWorld () {
  console.log('hello world')
}
Copy the code

The console shows this:

executing a.js
executing b.js
hello world
Copy the code

So… The same code executes in a different order! This is because the ES module parses the code first (it doesn’t execute it directly), the Runtime finds the imports and loads them, and finally executes the code. This approach is called asynchronous loading.

Node.js, on the other hand, only loads the required dependencies when the code is executed. The two implementations are different. While in some cases there is no difference, in others the performance is completely different.

Node.js and Web browsers need to load code in the first way. But how do they decide which system to use? The browser knows because you can specify the type attribute on the

`<script type="module" src="./a.js">`</script>
Copy the code

However, how do you know node.js? There has been a lot of discussion and advice on this (first check the syntax and then decide whether to treat it as a module? Define it directly in package.json? …). . Finally, the Solution was: Michael Jackson Solution. Basically, if you want a file to be loaded as an ES6 module, use a different file extension:.mjs instead of.js

This extension (.mjs) is why this solution is called Michael Jackson Solutionde.

At first, this approach seemed like a bad decision, but now I think it’s a great solution. Because it’s so simple and other tools (Text Editor,IDE, Preprocessor) can easily tell if a file needs to be treated as an ES6 module. At the same time, in terms of loading the project, this method increases the cost of the least.

If you want to learn more about how well ES6 modules are implemented in Node.js, you can read this Update

Just a hint about Babel

Bable implements the ES6 module, but it doesn’t exactly implement all the specifications. Be careful if you are using Babel to escape a native ES6 module, this may have some side effects.

Why is the ES6 module good and how to achieve the best of both worlds

The ES6 module has the following two main advantages:

  • They are cross-platform and work in both browsers and Node.js.

  • Import and export are static methods, and only with this implementation can we see how dependent loading works. Because the Runtime loads the file first, parses it and then we need to load the dependencies before we execute them, which we can only do by implementing them as static methods. It means you can’t use itimport 'engine-' + browserVersionThis grammar. There is a benefit to this approach: the tool can statically analyze the code, figure out what part of the code is actually being used and then load that part of the code on demand (tree shake it). This is especially useful when using third-party libraries: you can’t use all the methods they provide, so you can remove a lot of code that isn’t executed.

But does that mean there is no way to introduce a feature asynchronously? For me, this approach is very useful. Many times I do something like this:

const provider = process.env.EMAIL_PROVIDER
const emailClient = require(`./email-providers/${provider}`)
Copy the code

This way, I can get different implementations of the same interface with configuration changes without having to load code for all implementations.

So what happens if you use the ES6 module? Don’t worry, there is a stage-3 proposal (meaning it is likely to be approved soon) that adds an import() function. This method takes a path and then exports the functionality as a promise.

So with the ES6 module and import(), we’ll get the best of both worlds

The ES6 module is great, but it can take a while to get used to. Hopefully this article will help you prepare!

  • JavaScript
  • ES6
  • Nodejs