Free email newsletter: “ES.next News

2017-01-09

ES proposal: import() – dynamically importing ES modules

The ECMAScript proposal “import()” by Domenic Denicola is currently at stage 3. It enables dynamic loading of ECMAScript modules and is explained in this blog post.

ECMAScript modules are static

ECMAScript modules are completely static: you must specify what you import and export at compile time and can’t react to changes at runtime. That has several advantages, especially w.r.t. tooling, which are explained in “Exploring ES6”.

The static structure of imports is enforced syntactically in two ways. Consider the following example:

    import * as someModule from './dir/someModule.js';

First, this import declaration can only appear at the top level of a module. That prevents you from importing modules inside an if statement or inside an event handler.

Second, the module specifier './dir/someModule.js' is fixed; you can’t compute it at runtime (via a function call etc.).

The proposal enables dynamic module imports

The proposed operator for loading modules dynamically works as follows:

    const moduleSpecifier = './dir/someModule.js';
    import(moduleSpecifier)
    .then(someModule => someModule.foo());

The operator is used like a function:

  • The parameter is a string with a module specifier that has the same format as the module specifiers used for import declarations. In contrast to the latter, the parameter can be any expression whose result can be coerced to a string.

  • The result of the “function call” is a Promise. Once the module is completely loaded, the Promise is fulfilled with it.

Even though it works much like a function, import() is an operator: In order to resolve module specifiers relatively to the current module, it needs to know from which module it is invoked. Normal functions have no straightforward way of finding that out.

Use cases

Loading code on demand

Some functionality of web apps doesn’t have to be present when they start, it can be loaded on demand. Then import() helps, because you can put such functionality into modules. For example:

    button.addEventListener('click', event => {
        import('./dialogBox.js')
        .then(dialogBox => {
            dialogBox.open();
        })
        .catch(error => {
            /* Error handling */
        })
    });

Conditional loading of modules

Sometimes you may want to load a module depending on whether a condition is true. For example, to load a polyfill on legacy platforms. That looks as follows.

    if (isLegacyPlatform()) {
        import(···)
        .then(···);
    }

Computed module specifiers

For applications such as internationalization, it helps if you can dynamically compute module specifiers:

    import(`messages_${getLocale()}.js`)
    .then(···);

Tips

Accessing exports via destructuring

Destructuring helps with accessing a module’s exports:

    import('./myModule.js')
    .then(({export1, export2}) => {
        ···
    });

Accessing default exports

For default exports, you need to know that default is a keyword. Using it as a property name via the dot notation is OK:

    import('./myModule.js')
    .then(myModule => {
        console.log(myModule.default);
    });

However, you can’t use it as a variable name:

    import('./myModule.js')
    .then(({default: theDefault}) => {
        console.log(theDefault);
    });

Dynamically loading multiple modules

You can dynamically load multiple modules at the same time via Promise.all():

    Promise.all([
        import('./module1.js'),
        import('./module2.js'),
        import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
        ···
    });

Async functions and import()

import() returns Promises, which means that you can use it via async functions (which are part of ECMAScript 2017) and get nicer syntax:

    async function main() {
        const myModule = await import('./myModule.js');
    
        const {export1, export2} = await import('./myModule.js');
    
        const [module1, module2, module3] =
            await Promise.all([
                import('./module1.js'),
                import('./module2.js'),
                import('./module3.js'),
            ]);
    }
    main();

At the top-level of a module or script, you may find Immediately Invoked Async Arrow Functions useful:

    (async () => {
        const myModule = await import('./myModule.js');
    })();

Support for import()

Further reading

No comments: