Dynamic import() of native JS modules

Time:2022-6-22

In the previous article “native ECMAScript modules: new features and differences of webpack modules”, we understood the differences between ES modules and their applications in packaging / compilation (such as webpack / Babel). So far, we have learned a lot and know how to use import / export declarations and the possible problems when we use them in JS.
However, JavaScript was asynchronous many years ago. In current network practice, using non blocking promise based syntax is a good choice. By default, ECMAScript modules are static through default: you must use them at the top level of the moduleimport / exports。 This is helpful for optimizing the JS engine, but it limits the best way for developers to implement asynchronous module loading.
Some missing functions can be added based on promise API to realize dynamicimport()Operation of.
Some developmentsimport()Can add missing functions. The best implementation is based on promise API.

Purpose and principle

Every progress begins with a small idea.Domenic DenicolaAnd module loading communitiesIntroduction and promotionDynamically import ideas.

Now, we have aTC39Phase IIIDraft specificationAn initial model of.

This means that before the fourth phase, there are still several implementation schemes to be completed, and additional feedback from users and the schemes themselves needs to be collected and processed.

You can also be one of them, dynamicimport()Already inSafari Technology Previewupperdeployrealization. You can download, start using and test (here is a simpledemonstration)。

Your feedback is very important to us. You canquestionnaire investigationOr commentsWhatwg proposalCome and contact us.

grammar

The grammar is simple:

import("./specifier.js"); // returns a Promise

This is an example of static to dynamic import transformation (you can trydemo):

// STATIC
import './a.js';

import b from './b.js';
b();

import {c} from './c.js';
c();

// DYNAMIC
import('./a.js').then(()=>{
  console.log('a.js is loaded dynamically');
});

import('./b.js').then((module)=>{
  const b = module.default;
  b('isDynamic');
});

import('./c.js').then(({c})=>{
  c('isDynamic');
});

isDynamicThe passing of makes function calls different in modules. The following is a screenshot of the console:

[image upload failed… (image-7d1416-1629977329148)]

Screenshot March 1, 2017 10.06.40 PM png

Let’s analyze: the first unusual thing is that we introduced A.js twice, but only got feedback once. As you may remember, this is one of the ES modulescharacteristic, when they are singletons, they are called only once.
Second, dynamic import is performed before static import. This is because I introduced the traditional script to call dynamicimport()(you can also use dynamic import in traditional scripts, not just in modules!):

<script type="module" src="static.js"></script>
<script src="dynamic.js"></script>

We knowtype="module"Script will default delay, which will not be introduced in sequence until the DOM is parsed. That’s whydynamicThe script is executed first. Using import() skillfully will let you find a key to open all native es modules. You can load and use them anytime, anywhere.
The third difference is that static import ensures that your scripts are executed in order, but dynamically imported scripts are not executed in the order they appear in the code. You must know that each dynamic import exists independently. They are not related to each other and will not wait for other implementations to complete.

Let’s summarize:

  • dynamicimport()Provide promise based API
  • import()Follow es module rules: singleton, matcher, CORS, etc
  • import()It can be used in traditional scripts or modules
  • Used in codeimport()The order of is not related to the order in which they are resolved

Script validation and environment

As mentioned above, you can call from traditional or module scriptsimport()。 But how does it execute as a module or in a global environment?
You might think that dynamic import is implemented as a module, which provides a completely different environment from the global environment.
We can do a test:

// imported.js
console.log(`imported.js "this" reference is: ${this}`);

If the script is executed in a global context, the “this” reference points to a global object. So let’s start with a <a href=“ https://plnkr.co/edit/pHoD7S9kXicUvvpsLoEz?p=preview “> traditional script </a> and a <a href=” ” https://imgs.developpaper.com/imgs/2022-05-04-20-49-51-t03ock32egq.png

This means that,import()Executing a script as a module is actually the same asthen()In the function, we can use the same syntax of module export (such as module.default).

Additional functions

This additional feature allows us to use more than just the dynamic import operator at the top. For example:

function loadUserPage(){
    import('user-page.js').then(doStuff);
}

loadUserPage();

This allows you to use deferred loading and import to implement new features on requirements (e.g. about user actions):

// load a script and use it on user actions
FBshareBtn.on('click', ()=>{
    import('/fb-sharing').then((FBshare)=>{
        FBshare.do();
    });
});

We already knowimport()The script is loaded only once, which is just one of the advantages.

More importantly, the non static nature of dynamic import allows you to cross module constraints and build code according to your own needs, such as(demo):

const locale = 'en';
import(`./utils_${locale}.js`).then(
  (utils)=>{
    console.log('utils', utils);
    utils.default();
  }
);

As you have noticed, the default import ismodule.defaultAvailable under properties.
Of course, you can also load according to conditions:

if(user.loggedIn){
    import('user-widget.js');
}

Summary:

  • You can use dynamic import in scenarios of delayed loading, conditional loading, and user action
  • dynamicimport()Can be used anywhere in the script
  • import()Can pass strings, you can construct matching characters according to your needs

debugging

About debugging – the most outstanding advantage, you can access the ES module in the browser devtools console, because import() can be used anywhere.
You can easily load, test, or debug modules. Let’s do a simple example to load an official ECMAScript version of lodash(lodash-es)And check its version and some other features:

import("https://cdn.rawgit.com/lodash/lodash/4.17.4-es/lodash.default.js")
.then(({default:_})=>{// load and use lodash 
 console.log(`lodash version ${_.VERSION} is loaded`)
 console.log('_.uniq([2, 1, 2]) :', _.uniq([2, 1, 2]));
});

This is the console output:

[uploading pictures outside the station… (image-436cd1-1629977329148)]

Screenshot March 2, 2017 4.56.29 PM png

Summary:

  • Using dynamic import in the devtools console (helps with development and debugging)

Promise API benefits

Dynamic import uses the JS promise API. So what advantages does it bring us?

First, we can load multiple dynamic scripts in parallel. Let’s redo our originalExampleTo trigger and capture the loading of multiple scripts:

Promise.all([
        import('./a.js'),
        import('./b.js'),
        import('./c.js'),
    ])
    .then(([a, {default: b}, {c}]) => {
        console.log('a.js is loaded dynamically');
        
        b('isDynamic');
        
        c('isDynamic');
    });

I use in scriptsJavaScript deconstructionTo avoid const_ b = b.default。 alsoPromise.raceMethod, which checks which promise is processed earlier or faster.
stayimport()In this case, we can use it to checkWhich CDN works faster

const CDNs = [
  {
    name: 'jQuery.com',
    url: 'https://code.jquery.com/jquery-3.1.1.min.js'
  },
  {
    name: 'googleapis.com',
    url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'
  }
];

console.log(`------`);
console.log(`jQuery is: ${window.jQuery}`);

Promise.race([
  import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')),
  import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded'))
]).then(()=> {
  console.log(`jQuery version: ${window.jQuery.fn.jquery}`);
});

This is the console output after several reloads, showing which CDN loads the file faster (notify in this caseimport(), load and execute these two files and register jQuery)

[uploading pictures outside the station… (image-a97d9-1629977329148)]

Screenshot March 2, 2017 6.06.10 p.m png

Of course, this may seem strange, but it just shows you that you can use all the functions of the promises based API.

Finally, let’s look at some grammar sugar.ECMAScript async/ await functionality is also promise based, which means you can easily refactor with dynamic import.
So let’s try to use a similar static import but with dynamicimport()demonstration)Syntax for all functions of:

// utils_en.js
const test = (isDynamic) => {
  let prefix;
  if (isDynamic) {
    prefix = 'Static import';
  } else {
    prefix = 'Dynamic import()';
  }
  
  const phrase = `${prefix}: ECMAScript dynamic module loader
                    "import()" works in this browser`;
  console.log(phrase);
  alert(phrase);
};

export {test};
// STATIC
import {test} from './utils_en.js'; // no dynamic locale
test();

// DYNAMIC
(async () => {
  const locale = 'en';
  
  const {test} = await import(`./utils_${locale}.js`);
  test('isDynamic');
})();

Summary:

  • usePromise.allParallel load module
  • All promise API functions can be used forimport()Usage of matcher
  • You can use async/await to dynamically import

Promise API considerations

There is an additional warning. In terms of promises, we must not forget error handling. If there is any error in using a static import with a match or in the spelling of the module during execution, an error is automatically thrown. When using promises, you shouldthen()Method, orcatch()Structure, or your program will never run through.

Here is how to import a script that does not existdemo:

 import (`./non-existing.js`)
    .then(console.log)
   .catch((err) => {
     console.log(err.message); // "Importing a module script failed."
     // apply some logic, e.g. show a feedback for the user
   });

Recently, if you haven’t handled the error of promise, the browser /node JS will not give you any feedback. So the community recommended that no error be reported on the console or on the node The function of handling errors globally when the JS application is terminated abnormally.

The following is how to add a listener to handle promises globally:

window.addEventListener("unhandledrejection", (event)=> {
  console.warn(`WARNING: Unhandled promise rejection. Reason: ${event.reason}`);
  console.warn(event);
});
// process.on('unhandledRejection'... in case of Node.js

Other precautions

Let’s discussimport()The relative path of the match. As you would expect, it is relative to the path of the called file. When you import a module from a different folder and execute this method in a third-party module location (such as utils folder or similar folder), an error may be reported.
Let’s think about the following folder structure and code:

[uploading pictures outside the station… (image-bbcac3-1629977329148)]

Screenshot March 2, 2017 8.12.31 PM png

// utils.js - is used to load a dependency
export const loadDependency = (src) => {
    return import(src)
        .then((module) => {
            console.log('dependency is loaded');
            return module;
        })
};

// inner.js - the main file we will use to test the passed import() path
import {loadDependency} from '../utils.js';

loadDependency('../dependency.js');
// Failed to load resource, as import() is called in ../dependency.js

loadDependency('./dependency.js');// Successfully loaded

Demo

As shown in the demo,import()The matcher is always relative to the file that invokes it, so keep this fact in mind to avoid unexpected errors.

Summary:

  • import()The match is always relative to the file being called

Support and polyfills

So far, there is little browser supportimport()。 Node. JS is considering adding this function, which may be more likerequire.import()
To check that it supports a browser or node JS, please run the following code or try thisdemo

let dynamicImportSupported = false;
try{
 Function('import("")');
 dynamicImportSupported = true;
}catch(err){};

console.log(dynamicImportSupported);

For polyfills, the module loading community has prepared aimportModuleFunction solution, which providesimport()Functions of:

function importModule(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" +
        Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}

However, this solution has many vulnerabilities, which are only for reference.
Babel providesdynamic-import-webpackPlug in, you can install it and use it to parseimport()Match.
Webpack 2 supports the use of dynamicimport()Code splitting, using require Ensure.

Importscripts (URLs) replace

In the worker / serviceworker script,importScripts(urls)Interface is used to synchronously import one or more scripts into the scope of a worker program. Its syntax is simple:

importScripts('foo.js', 'bar.js' /*, ...*/);

You canimport()DeemedimportScripts()Advanced, asynchronous and non blocking versions of.
When the worker type is “module”, trying to use importscripts will throw a typeerror exception, which should be noted.

With dynamic import everywhere, when it supports all browsers, it is a good choice to refactor the importscripts () usage with dynamic import (). When executing the module, you should also carefully check the scope to avoid errors.

last

dynamicimport()Provides us with additional functionality to use the ES module asynchronously. Loading them dynamically or conditionally according to our needs enables us to create more excellent applications faster and better.
Webpack2 uses this API. Currently, it runs in the browser on stage 3, which means that this specification will become a standard in the near future.

Here are some additional resources: