Blog-Archiv

Montag, 8. Januar 2018

ES6 Export Import Ways

This Blog is about the (too) many possibilities that ES6 gives us for exporting and importing fields, functions and classes.

I just read some articles about how we should export and import, and now I'm puzzled and confused. The technical backgrounds about why some export/import pattern is good or bad stay in the dark. Or what would you suppose to be behind a sentence like "You lose some ES6 module benefits such as tree-shaking and faster access to imports" - ? All I can give by now is a quick overview.

General

Both ES6 exports and imports are possible just on ground-level, they can not be inside some function or code-block.

Imports moreover are "hoisted", that means they get resolved before the source-code is executed where they occur.

Export

Exports are some kind of access modifiers. They state which field, function or class are publicly available for importers.

Named Inline Exports

Simply put the export keyword before the field, function or class to make it public.

./modules/io/output.js
/* publics */
export const ERROR = 0
export const SUCCESS = 1

export function display(text, code) {
    message(text, code === ERROR ? "error" : "success")
}

/* privates */
function message(text, elementId) {
    const outputElement = document.getElementById(elementId)
    outputElement.innerHTML += text+"<br>"
}
./main.js
import { display, SUCCESS, ERROR } from "./modules/io/output.js"

display("Hello Success", SUCCESS)
display("Goodbye ERROR", ERROR)

Advantages:

  • You clearly state which parts of the module are are public
  • Short, simple, concise, the exported names are not duplicated in a separate export clause

Disadvantages:

  • On the import side you need { curly braces }, even when importing just one expression

Separate Export Clause

The export keyword is used as the opener for a list of names that should be exported.

./modules/io/output.js
/* publics */
const ERROR = 0
const SUCCESS = 1

function display(text, code) {
    message(text, code === ERROR ? "error" : "success")
}

export { ERROR, SUCCESS, display }

/* privates */
function message(text, elementId) {
    const outputElement = document.getElementById(elementId)
    outputElement.innerHTML += text+"<br>"
}

The according import is the same as with named inline exports.

Disadvantages:

  • Duplication of possibly lots of names. This seems to be a compatibility-pattern for the now obsolete revealing module pattern.

Export Renaming

./modules/io/info.js
function displayInfo(text) {
    alert(text)
}

export { displayInfo as info }
./main.js
import { info } from "./modules/io/info.js"

info("Hello Info")

Disadvantages:

  • Can create confusion, and is not needed at all. Renaming on the import side is what we will need (in case two imported functions have the same name)

One Default Export per Module

You add default after the export keyword. You can combine the default-export with any number of named inline exports.

./modules/io/info.js
export default function displayInfo(text) {
    const outputElement = document.getElementById("info")
    outputElement.innerHTML += text+"<br>"
}
./main.js
import displayInfo from "./modules/io/info.js"

displayInfo("Hello Info")

Advantages:

  • The import doesn't need the enclosing { curly braces }

Disadvantages:

  • You can have just one default-export per module.
Export Rules of Thumb:
  • Big modules: no default export, just named inline exports.
    Although we should avoid big modules, it's inevitable sometimes, e.g. you want to make a constant for each keyboard key.

  • Small modules: make it even smaller until one default export is sufficient.

Import

Other than export, which plays the role of an access modifier, import is a dependency statement. The ES6 interpreter searches for imports before executing the source code in a module it loads. When it can't resolve all dependencies, or one has a syntax-error, it doesn't execute the loading module either.

ES6 seems to give three options for resolving imported files:

  1. path relative to the location of the importer, always starting with "./", that's what I used in all examples of this article
  2. absolute path (I wouldn't use that unless you want to bind your library-structure to the file-system of your computer)
  3. module names, has to be configured, not specified how, maybe the best way but not yet ready

Named Import

Here I refer to the exports of output.js module presented at the beginning of this article.

./main.js
import { display, SUCCESS, ERROR } from "./modules/io/output.js"

display("Hello Success", SUCCESS)
display("Goodbye ERROR", ERROR)

Advantages:

  • You clearly state what you depend on, not more or less, it is a precise dependency statement, and it is easy to understand

Disadvantages:

  • The import needs the enclosing { curly braces }

Wildcard Import onto Module-Object

The star (asterisk, *) is called "wildcard". It denotes all exports.

./main.js
import * as output from "./modules/io/output.js"

output.display("Hello Success!", output.SUCCESS)

This import-statement creates a new object output that contains all exports of the imported module. You can use them by dereferencing the module object, e.g. output.SUCCESS.

The "as modulename" clause is required! You could say modulename is a "namespace".

Advantages:

  • No duplication of names in the import statement
  • Easy to find out from which module some name comes from, because it is always module.name
  • Ambiguities that must be fixed via "as xxx" (aliasing) will not occur

Disadvantages:

  • You possibly import things that are not needed, your dependency statement is not precise enough and may handycap refactoring
  • Necessity to always use "modulename." as prefix for imported things

Import Renaming

To resolve ambiguity when importing the same name from different modules, import provides renaming via the "as" keyword.

./main.js
import { display as displayOnConsole } from "./modules/io/console.js"
import { display as displayOnPage } from "./modules/io/page.js"

displayOnConsole("Hello Console!")
displayOnPage("Hello Page!")

You wouldn't need that when importing by wildcard, then each of the functions would be bound to the imported module-object.

Advantages:

  • This is needed to fix ambiguities of named imports

Disadvantages:

  • Naming magic, possibly confusion about what the real name of the function is
Import Rules of Thumb:
  • Try to be as explicit as possible.
    Avoid wildcard imports unless it is needed, like e.g. for importing constants for all keyboard keys.

  • Avoid renaming functions and classes whenever possible, especially avoid abbreviations.
    You just create confusing source code.

Resume

This article is not complete. For example I did not discuss anonymous default exports, or re-exporting, because I consider them to be dispensable features.

The question rises whether it makes sense to provide so many different export/import ways in a programming language that claims to become a standard, running in millions of web-browsers. Standards live from being understood by a maximum number of people, thus they need to be unambiguous and simple, a base for communication.

Nevertheless this is a big step forward. Finally JavaScript got dependency management, built into browsers!
Please refer to the many web-pages about ES6 modules for deeper information about how to export and import.




Keine Kommentare: