In 2017, major browsers started natively supporting ES2015 modules, which means it’s time to relearn script tags. And I promise that this is not just another “menstrual” article about ES Module grammar without practice.

Remember when you started front-end development and wrote Hello World? We started by creating an HTML file and putting the web content in the tag. Later, when it is necessary to learn the page interaction logic, a

With the development of modular JavaScript in the front-end community, we are now used to breaking up JS code modules and packaging them into a bundle.js file using Webpack. Then use the

Until browser native support for the ES Module standard changed that. Most browsers already support loading standard ES modules with

Revision: Can’t you tell defer and Async clearly?

Please listen to:

Q: There are two script elements, one loading Lodash from the CDN and the other loading script.js from the local. Assuming that local scripts are always faster to download, what do the following plain.html, async.html and defer.html output?

// script.js
try {
    console.log(_.VERSION);
} catch (error) {
    console.log('Lodash Not Available');
}
console.log(document.body ? 'YES' : 'NO');
Copy the code
// A. plain.html
<head>
	<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
    <script src="script.js"></script>
</head>

// B. async.html
<head>
	<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js" async></script>
    <script src="script.js" async></script>
</head>

// C. defer.html
<head>
	<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js" defer></script>
    <script src="script.js" defer></script>
</head>
Copy the code

If you know the answer, congratulations, you can skip this section, otherwise it’s time to review.

First, the output of A. plain.html is:

4.17.10
NO
Copy the code

That is, when script.js is executed, Lodash has been downloaded and executed, but Document.body has not been loaded.

Before the defer and Async properties were born, the original browser-loading script was based on the synchronous model. The browser parser parses HTML tags from top to bottom, and when it encounters a Script tag, it pauses parsing the rest of the document and reads the script tag. At this time:

  • If the script tag has no SRC attribute and is an inline script, the parser will directly read the tag’s textContent, and the JS interpreter will execute the JS code
  • If the script has a SRC attribute, a network request is made from the URI specified by SRC to download the script, which is then executed by the JS interpreter

In either case, it blocks the browser’s parser. The browser parses the HTML Markup from top to bottom, so this blocking property determines that the DOM element above the script tag is available when the script is executed. The DOM element below it is not available.

Such blocking makes sense if the execution of our script requires manipulation of previous DOM elements, and the loading and rendering of subsequent DOM elements depend on the execution of the script. If it is the other way around, the execution of the script will only slow down the rendering of the page.

For this reason, there is a famous rule in Yahoo Web Optimization Recommendations from 2006:

Place the script at the bottom of the body

But modern browsers have long supported adding defer or async attributes to

When the document has only one Script tag, defer is not significantly different from Async. But when there are multiple script tags, they behave differently:

  • Each async script is executed immediately after downloading, regardless of the order in which the script tags appear
  • The defer script executes in order of the script tags

Therefore, in the above problems, the last two cases are output respectively:

// B. async.html
Lodash Not Available
YES

// C. defer.html
4.17.10
YES
Copy the code

Because script.js in async.html is smaller and faster to download, it is executed earlier than lodash loaded from CDN, so _.VERSION is Not Available and lodash is Not Available; Script.js in defer.html is not executed immediately after it has been downloaded, but after Lodash has been downloaded and executed.

In the following picture, you can intuitively see the differences in loading modes of Default, defer and Async scripts. The light blue is the script download stage, and the yellow is the script execution stage.

One more thing…

I’ve only looked at the script tag with the SRC attribute, which requires a network request to load the script from outside, but what happens when the inline

The answer is simply no, writing async and defer properties to the script tag as follows has no effect, which means that the inline JS script must block execution synchronously.

// defer attribute is useless
<script defer>
    console.log(_.VERSION)
</script>

// async attribute is useless
<script async>
    console.log(_.VERSION)
</script>
Copy the code

This point is worth mentioning separately because we’ll see later that browsers handle ES Modules asynchronously and not blocking by default, as opposed to regular script.

Game-changing<script type=module>

TLDR;

  • Adding the type=module attribute to the script tag causes the browser to load the script as ES Module
  • The type=module tag supports both inline and loaded scripts
  • By default, the ES script is deferred, inline or out
  • Specifies the script tag explicitlyasyncProperty to override the default defer behavior
  • The same module is executed only once
  • Remote scripts use the URL as a Key to determine uniqueness
  • The security policy is stricter, and the loading of non-domain scripts is restricted by the CORS policy
  • When the server provides ES Module resources, it must return a valid Content-Type header of JavaScript Type

#1 ES Module 101

Import and export

ES standard modules use import and export to import and export modules.

Export Can export any available JavaScript identifiers (idendifier). Explicit export methods include declaration statements and export {idendifier as name}.

// lib/math.js
export function sum(x, y) {
    return x + y;
}
export let pi = 3.141593;
export const epsilon = Number.EPSILON;
export { pi as PI };
Copy the code

In another file, use import… from … You can import the identifier of other module export. The following commonly used ways are as follows:

  • import * as math from ...Import the entire module and invoke it through the Math namespace
  • import { pi, epsilon } from ...Partial import, can directly call PI, epsilon and other variables
// app.js
import * as math from './lib/math.js';
import { pi, PI, epsilon } from './lib/math.js';
console.log(` PI = 2${math.sum(math.pi, math.pi)}`);
console.log(`epsilon = ${epsilon}`);
console.log(`PI = ${PI}`);
Copy the code

default

The ES module supports the default keyword to implement unnamed imports. The magic point is that it can be imported at the same time with other variables of explicit export.

// lib/math.js
export function sum(x, y) {
    return x + y;
}
export default 123;
Copy the code

There are two ways to import a module. The first way is to import the default value.

import oneTwoThree from './lib/math.js';
// oneTwoThree is 123
Copy the code

The second way is import * to import default and other variables.

import * as allDeps from './lib/math.js'
// allDeps is an object containing sum and default. AllDeps. Default is 123
// { sum: ... , default: 123}
Copy the code

Grammar limited

The ES module specification requires that import and export be written at the top of the script file because, unlike module.exports in CommonJS, Export and import are not traditional JavaScript statements.

  • Export code cannot be written in conditional code blocks like CommonJS

    // ./lib/logger.js
    
    / / right
    const isError = true;
    let logFunc;
    if (isError) {
        logFunc = (message) = > console.log(`%c${message}`.'color: red');
    } else {
        logFunc = (message) = > console.log(`%c${message}`.'color: green');
    }
    export { logFunc as log };
    
    const isError = true;
    const greenLog = (message) = > console.log(`%c${message}`.'color: green');
    const redLog = (message) = > console.log(`%c${message}`.'color: red');
    / / error!
    if (isError) {
        export const log = redLog;
    } else {
        export const log = greenLog;
    }
    Copy the code
  • Import and export cannot be placed in try catch statements

    / / error!
    try {
        import * as logger from './lib/logger.js';
    } catch (e) {
        console.log(e);
    }
    Copy the code

In addition, the ES module specification must import a valid relative path, or absolute path (URI), and does not support the use of expressions as URI paths.

// Error: Module name import of class NPM not supported
import * from 'lodash'

// Error: must be a pure string representation. Dynamic import in the form of expressions is not supported
import * from './lib/' + vendor + '.js'
Copy the code

#2 Meet metype=module

These are the basics of the ES standard module, which is standards-first, implementation lags, and browser support doesn’t catch up right away. As mentioned at the beginning of this article, the good news is that the latest major browsers, Chrome, Firefox, Safari, and Microsoft Edge, now support the

By adding the type=module attribute to the normal

A simple Hello World would look like this:

<! -- type-module.html -->
<html>
    <head>
        <script type=module src="./app.js"></script>
    </head>
    <body>
    </body>
</html>
Copy the code
// ./lib/math.js
const PI = 3.14159;
export { PI as PI };

// app.js
function sum (a, b) {
    return a + b;
}
import * as math from './lib/math.js';
document.body.innerHTML = `PI = ${math.PI}`;
Copy the code

If you open index.html, the page will look like this:

The resource request process can be seen in the Network panel. The browser loads app.js from script.src, and it can be seen in the Initiator that app.js:1 initiates the math.js request. The dependency module math.js is loaded when the import statement on the first line of app.js is executed.

The execution of JavaScript statements in module scripts is the same as that loaded by regular scripts. DOM API, BOM API and other interfaces can be used. However, it is worth noting that scripts loaded as modules do not pollute the global scope like ordinary script scripts.

For example, app.js defines the function sum, math.js defines the constant PI, and if you open Console and type PI or sum, ReferenceError will be generated.

(Finally)…

#3 Type =module The module supports inline

In our example code above, if the app.js code referenced in type-module.html were changed to inline JavaScript, the effect would be the same.

<! -- type-module.html -->
<html>
    <head>
        <script type=module>
            import * as math from './lib/math.js';
	        document.body.innerHTML = `PI = ${math.PI}`;
        </script>
    </head>
    <body>
    </body>
</html>
Copy the code

Of course, the inline module script only makes sense when loaded as an “entry” script, which saves an HTTP request to download app.js, and the math.js path referenced by the import statement needs to be changed to a path relative to type-module.html.

#4 By default defer supports async

You may have noticed that in our Hello World example, the script tag is written inside the head tag, and we use the Document.body. innerHTML API to manipulate the body, but whether we load the script from the outside, The script tag is still inlined, and the browser executes without error.

This is because

The reason I say something like defer rather than confirm is because I tried to check the defer attribute of the default script element in the browser Console (executing script.defer) and got false instead of true.

This means that if there are multiple

In addition, like traditional Script tags, we can write async properties on the

#5 Execute the same module once

The ES module is only executed once when referenced multiple times, and we get the same content when we execute multiple import statements. The same is true for

For example, the following script reads the count value and adds one:

// app.js
const el = document.getElementById('count');
const count = parseInt(el.innerHTML.trim(), 10);
el.innerHTML = count + 1;
Copy the code

If the

<! -- type-module.html -->
<html>
    <head>
        <script type=module src="app.js"></script>
        <script type=module src="app.js"></script>
    </head>
    <body>
        count: <span id="count">0</span>
    </body>
</html>
Copy the code

Question? How do you define “same module”, the answer is the same URL, including not only pathname but also? So if we add different parameters to the same script, the browser will think it’s two different modules and will execute it twice.

If we add the url argument to the second app.js in the HTML code above:

<script type=module src="app.js"></script>
<script type=module src="app.js? foo=bar"></script>
Copy the code

The browser will execute the app.js script twice and the page will display count: 2:

#6 CORS cross-domain restrictions

We know that one important feature of regular script tags is that they are CORS free. Script.src can be any script resource that is not in the same domain. As a result, we “invented” JSONP solutions to “cross domains” earlier this year using this feature.

However, the script tag of type=module reinforces this security policy. If the server does not return a valid Allow-Origin CORS header when the browser loads a script resource from a different domain, the browser will prohibit the loading of the modified script.

To load the app.js script for port 8082, use the following HTML to serve port 5501:

<! -- http://localhost:5501/type-module.html -->
<html>
    <head>
        <script type=module src="http://localhost:8082/app.js"></script>
    </head>
    <body>
        count: <span id="count">0</span>
    </body>
</html>
Copy the code

The browser will stop loading the app.js script.

# 7 the MIME type

When a browser requests a remote resource, it can determine the MIME Type (script, HTML, image format, and so on) of the loaded resource based on the Content-Type in the HTTP return header.

Because of the permissive nature of browsers, browsers will parse and execute regular script tags as JavaScript by default, even if the server does not return a Content-Type header specifying that the script Type is JavaScript.

But browsers do not tolerate script tags of type=module. If the server-side MIME type of the remote script is not a valid JavaScript type, the browser disables execution of the script.

Let’s face it: if we rename app.js to app.xyz, we’ll see that the page prevents the script from executing. Because you can see in the Network panel that the browser returns a Content-Type header with chemical/ X-XYZ instead of a valid JavaScript Type such as text/ JavaScript.

<html>
<head>
    <script type="module" src="app.xyz"></script>
</head>
<body>
    count: <span id="count">0</span>
</body>
</html>
Copy the code

The page content is still count: 0, the value is not modified, you can see the relevant information on the console and Network:

ES Module practice in the real world

Backward compatibility scheme

OK now let’s talk about reality – older browsers have compatibility issues, and browsers have a very clever compatibility scheme for dealing with ES modules.

First, in the old version of the browser, the

So for older browsers, we still need to add a traditional

Second, this second

To solve this problem, the script tag adds a nomodule attribute. Browsers that support type=module should ignore the script tag with the nomodule attribute. Older browsers do not recognize the attribute, so it is meaningless and does not interfere with normal logic to load scripts from the script tag.

<script type="module" src="app.js"></script>
<script nomodule src="fallback.js"></script>
Copy the code

As shown in the code above, the new browser loads the first script tag and ignores the second; Older browsers that do not support type=module ignore the first and load the second.

Pretty elegant, isn’t it? Do not need their own handwriting feature detection JS code, directly use script properties can be.

For this reason, on further reflection, we can boldly conclude that:

Without feature checking, we can immediately use

The benefits

At this point, it’s time to consider the practical benefits of native browser support for the ES module.

#1 Simplify the development workflow

Nowadays, front-end engineering is very popular, and front-end modular development has become a standard workflow. But browsers don’t support type=module when loading ES templates, we still need a Webpack-centric packaging tool that bundles native modular code and loads it.

But thanks to the natural support for

  1. Use the entry. Js file directly<script>The label reference
  2. From entry. Js to all dependent Module code, all using ES Module scheme

Of course, it is theoretical because the first point is easy to do, and the second point requires all of our dependency code to use ES modular solution. In the current front-end engineering ecosystem, our dependency management is using NPM, and most of the NPM packages are using CommonJS standard but not compatible with ES standard.

But there’s no doubt that as long as the above two points are met, native development can easily achieve true modularity, which is a considerable improvement in our debugging experience. To hell with Webpack –watch, Source Map, etc.

Now you can open the Source panel in DevTools and tap your friend directly! Just the debug it!

#2 as a check for new feature supportThe water

The ES module can act as a natural and very reliable browser version checker, and thus serve as a milestone in checking support for many other new features.

~> caniuse typemodule
JavaScript modules via script tag ✔ 70.94% ◒ 0.99% [WHATWG Living Standard]
  Loading JavaScript module scripts using `<script type="module">` Includes support for the `nomodule` attribute. #JS- Edge - 12+ - 15+¹ stocking 16+ Firefox - 2+ - 54+² stocking 60+ Chrome - 4+ - 60+¹ stocking 61+ Safari - 3.1+ ◒ 10.1+ congestion 11+ Opera - 9+ - 47+¹ stocking 48+ ¹Support can be enabled via 'about:flags' ²Support can be enabled via' about:config '⁴Does not Support the `nomodule` attributeCopy the code

PS: recommend a NPM tool: caniuse-cmd, call NPM I -g caniuse-cmd can use the command line to quickly query caniuse, support fuzzy search oh

This means that if a browser supports loading ES modules, the version number must be greater than those specified in the table above.

In Chrome’s case, further thinking, this means that we can leave Polyfill in our ES template code and use all of Chrome 61’s supported features. This list contains quite a wealth of new features, many of which we wouldn’t dare use directly in production, but with the guarantee of

Here is a slides screenshot from Google engineer Sam Thorogood sharing ES6 Modules in the Real World at Polymer Summit 2017. The table describes the comparison between type=module and other common new features supported by several major browsers at that time, which can help us to get a general idea.

Challenge — Rethink the front end build

OK, now it’s time to think about the not-so-fun part. There are no silver bullets in software development, and today’s ES template is no exception. Let’s take a look at some of the new problems and challenges that come with introducing ES templates natively in the browser.

#1 The number of requests increases

For browsers that already support ES templates, we naturally face this problem if we introduce ES Modules starting with script tags. Assuming we have a dependency chain like this, that means the browser loads six modules in succession:

├─ > Constants. Js ├─ > pro.js -> constantsCopy the code

For a traditional HTTP site, this would mean sending six separate HTTP requests, contrary to our usual performance optimization practices.

So the contradiction here is actually between reducing the number of HTTP requests and improving module reuse:

  • In modular development, there are more and more modules as the code grows naturally
  • The more modules there are, the more requests the browser has to make

In the face of this contradiction, we need to consider the optimization strategy based on business characteristics, and make compromise decisions or compromises.

One direction worth considering is to optimize module loading with the help of HTTP 2 technology.

With Server Push technology, you can select the common modules that are reused the most in your application and Push them to the browser as early as possible. For example, when requesting HTML, the server uses the same connection to push the util.js, Lodash.js, and constants.js modules from the above example to the browser, along with the HTML document, so that the browser does not have to initiate another request when it needs to load these modules. Direct execution.

PS: Highly recommended reading Jake Archibald: HTTP/2 Push is Demands than I thought

HTTP/2’s merge request and header compression capabilities can also help alleviate the problem of slow loading caused by increased requests.

Of course using HTTP/2 presents challenges for our back-end HTTP service providers, and of course it can be an opportunity to learn and apply the HTTP/2 protocol.

PS: Other articles have also discussed the optimization of resource loading using prefetch caching mechanism, which can be further explored as a direction

#2 Beware dependency hell – version and cache management

There’s a famous joke in software engineering:

There are only two hard things in Computer Science: cache invalidation and naming things.

— Phil Karlton

The management of the visible cache is something that should not be trifled with.

How do we traditionally deploy JS scripts for version control? In conjunction with the HTTP caching mechanism, the general best practices are as follows:

  • File name plus version number
  • Example Set max-age to the long cache
  • When there is a version update, modify the version number part of the file name and modify the script.src path

If we only introduced one or two stable *.js library scripts at a time, and then introduced the business script bundle.xxx.js, this practice would not be a problem.

But imagine now that we’re going straight to the new ship ES browser module. As the business grows, we’ll be managing dozens or even dozens of dependency modules. It’s not surprising for a large site to have hundreds of modules on dozens of pages.

Is it easy to manage the cache and update JS files when the dependency graph is so complex and there are so many modules?

For example, we have dependency graphs like this:

. / page - one/entry. Js ├ ─ ─ > logger. Js - > util. Js - > lodash. Js ├ ─ ─ > the js ├ ─ ─ > router. Js - > util. Js └ ─ ─ > event. Js - > ├─ > Logg.js -> util.js -> lodad.js -> constants.jsCopy the code

In the production environment, the browser side has a long cache of the old uti-1.0.0. js component, but since logger, Router, and Event components all depend on util, This means that we need to modify import statements in other components as well as

// router-2.0.0.js -> router-2.1.0.js
import * as util from '. / util - 1.1.0. Js'

// page-one/entry-3.1.2.js -> page-one/entry-3.2.0.js
import * as util from '. / util - 1.1.0. Js'

// page-one.html
<script type="module" src=". / page - one/entry - 3.2.0. Js. ""> / /... The page-two script should also be modifiedCopy the code

The version numbers of these dependent components trace up the dependency graph, and we need to modify and refactor them. This process can of course be implemented in conjunction with our build tool without manual modification, requiring us to develop build tool plug-ins or use NPM scripts.

#3 Must be backward compatible

As mentioned above, it is important to remember that when deploying to production, you still need to package a bundle. Js file that is available in older browsers. This step is an existing workflow, just add a nomodule attribute to the script tag.

The problem is that sometimes, in order to minimize the number of page requests, we will inline key JS scripts directly into HTML markup, rather than

If we use a

script tag, it will be ignored by newer browsers. Therefore, for newer browsers, the nomodule script content should not be inline, otherwise it will increase the file size and will not execute this part of the script.

So it’s still up to the developer to decide whether the

#4 Upgrade CommonJS module to ES standard module

If we introduce ES standard modules in production using script tags, then we must refactor all the code as dependency modules and dependency libraries into ES modules, and the current state of the front-end ecosystem is as follows:

  • Most of the dependency library modules are CommonJS compliant, with a few being ES compliant.
  • Dependency packages are deployed on NPM and installed in the node_modules directory.
  • Existing business code adoptedRequire (${NPM module name})Node_modules.

The challenges for us are:

  • A large number of CommonJS modules need to be reconstructed as ES standard modules, and the workload is large.
  • You need to reconstruct the reference mode of the node_modules package to use a relative path.

Don’t forget to compress the ES module files

Another important optimization practice for deploying traditional JS static resources in a production environment is minify processing code to reduce file size, as smaller files undeniably transfer faster.

If we want to ship native ES modules to new browsers, we can’t ignore the compression of ES module files.

OK, so uglify is the popular uglify for ES5 code. Unfortunately uglify doesn’t support Minify for ES6 code very well. A common uglify scenario is that we use Babel to escape ES6 code to get ES5 code, and then use Uglify to minify ES5 code.

A better choice for compressing ES6 code is Babel-Minify (formerly Babili) from the Babel team.

Conclusion # 6?

God said to write the article to have a conclusion, chat up to now, we were surprised to find that the problem than the benefits of the space is much more (I have what way, I also very helpless ah).

So my attitude to browsers loading ES modules is this:

  • Development phase, as long as browser support, though use aggressively! Just do it!
  • Don’t lose sight of webPack’s native build bundle; native builds remain and will remain the core of front-end engineering for the long term
  • Even if the production environment serves native modules directly, the build process is still required
  • Don’t blindly use it in a production environment, first design a good dependency management and cache update solution, and deploy back-end HTTP/2 support

The future of ES modules?

As a matter of fact, we still face many challenges to embrace ES Module in the production environment. In order to make the native ES Module play its maximum role, we still need to optimize many details and step on the pit to settle the best practices. Again, there are no silver bullets.

But in the area of front-end modularity, the ES module is definitely the future.

The EcmaScript Standards Committee TC39 has also been promoting the update of the module standard. Students who are interested in the development of the standard can further explore it. Some points worth noting include:

  • Tc39 /proposal-dynamic-import dynamic import feature is supported and has entered Stage 3
  • Tc39 /proposal-import-meta Specifies import.meta is a programmatic way to obtain modular meta-information in code
  • Tc39 / TC39-module-keys is currently in Stage 1 for security enhancement when third-party modules are referenced
  • Tc39 /proposal-modules-pragma is similar to the “user strict” directive to specify strict modes, and uses the “Use Module” directive to specify a regular file to be loaded in module mode, which is currently in Stage 1
  • tc39/proposal-module-getLike Object.defineProperty defines a getter for a property, which is allowedexport get prop() { return ... }This syntax implements dynamic exports

The resources

  • Using JavaScript Modules on the Web Fundamentals tutorial
  • ES6 Modules in the Real World Polymer Summit 2017
  • ES6 modules support lands in browsers: is it time to rethink bundling?
  • Can I use… JavaScript modules via script tag
  • How Well Do You Know the Web? Google I/O ’17
  • Why doesn’t ES Module’s browser support make sense – some contrary voice to help think and discuss

Note: The caption is from Contentful