As web applications continue to be developed, our code volume will explode; One Web, one JS becomes a myth. In engineering, it is easy to find a solution: split files, and this js file is the topic of this paper – Module.

introduction

For a long time Javascript didn’t have semantic modules; In those days JS files were small and not very powerful, so there was no need. However, with the development of technology, JS functions appear explosive growth. People have to think about a series of modular solutions in engineering, notably:

  • AMD: a modular solution based on require.js for browser side JS loading
  • CMD: A modular solution that came a little later than AMD. The former advocates preposition dependence, while the latter advocates proximity dependence
  • CommonJS: The modular specification used by NodeJs
  • UMD: A set of rules scheme combining AMD and CommonJS

Of course, these are historical topics, and an old-school interviewer might ask what the difference is, but it’s not possible to build web applications based on the above MD/MD solutions anymore. The main reason is that in 2015, JS introduced the Module mechanism at the language level, which is usually called ES Module. The following paper mainly discusses the modular scheme based on ES Module.

Export & Import

ES Module generally has two keywords export and import.

export

Export is used to expose variables or objects that are currently in a block (function is also an object). The most common way to use export is to place it before declarations of data or objects:

// hello.js
export function hello(name) {
  return `Hello ${name}`;
}

export const MODULES_BECAME_STANDARD_YEAR = 2021;
Copy the code

Sometimes, the above example export method will be messy, some people will prefer centralized processing, the following export method is also legal — incidentally we also use as alias:

// hello.js
export { hello as sayHello, MODULES_BECAME_STANDARD_YEAR };

function hello(name) {
  return `Hello ${name}`;
}

const MODULES_BECAME_STANDARD_YEAR = 2021;
Copy the code

import

Import, as its name implies, is the reverse operation of export; Used to import other module data or objects. In general, we don’t need all the methods for importing dependencies, so the most common is import {… } on demand.

import { hello } from "./hello.js";
console.log(hello("World")); // Hello World
Copy the code

The main benefits of P.S. on-demand import are as followsTree shakingOn.

As mentioned above, ES Module provides an as keyword alias; The above is used in the export stage, and the import stage can also be alias:

import { hello as sayHello } from "./hello.js";
console.log(sayHello("World")); // Hello World
Copy the code

Import * as: import * as: import * as: import * as: import * as: import * as:

import * as hi from "./hello.js";

hi.hello("World");
hi.MODULES_BECAME_STANDARD_YEAR;
Copy the code

The third common form of import, called import “module”, runs the code in “module” directly, but does not import any objects.

// setupTests.js
import "@testing-library/jest-dom";
Copy the code

Export default

The topic of one-time import was mentioned above, but ES Module does not have a simple one-time export method. You may have heard of Export Default, but it can only export one object. For example, when we wrote Java, a file would only have a public class; While javascript does not impose a linguistic limit on the number of classes in a file, as a best practice, it is common to write a lint to ensure that this rule is followed: react, vue, for example, exports only one unique class for a file:

// Welcome.js
export default class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>; }}Copy the code

Note: import * as is not a default object that imports dependent modules; To import the default object, we most commonly use the form import XXX from:

import Welcome from "./Welcome";
Copy the code

The following two forms are also legal, but not as compact:

  • import { xxx as default }

    import { Welcome as default } from "./Welcome";
    Copy the code
  • import *

    import * as Welcome from "./Welcome";
    const welcome = Welcome.default;
    Copy the code

Export and export default are not restricted to be used together in the same module.

// user.js
export default class User {
  constructor(name) {
    this.name = name; }}export function sayHi(user) {
  alert(`Hello, ${user}! `);
}
Copy the code

However, we recommend following these rules:

  • exportTypically used in the Library to package a set of methods;
  • export defaultUsually used to declare a separate entity, such as a class.

Stage summary

To summarize the usage of import and export:

  • Import comes in four forms:

    • Import on demand:import { x [as y], ... } from 'module'
    • Import * as obj from ‘module’
    • Import default export:import x from 'module'import {default as x} from 'module'
    • Run the import module without assigning objects:import 'module'
  • Export also has three forms:

    • Statement export:export class/function/variable
    • Alias export:export {x [as y], ... }
    • Default is derived:export default class/function/variable

Export… export… export… export… The from ‘module’. Import and export can be arranged and combined. For example, if the following two forms are equivalent, you can try some other combinations on your own.

import { hello } from "./hello.js";
export { hello as sayHello };
Copy the code
export { hello as sayHello } from "./hello.js";
Copy the code

Dynamic imports

The import above should accurately be called “static import”; It can only import… From a string:

import.from getModuleName(); // Error! only from 'string' is allowed!
Copy the code

To introduce by conditional judgment or method:

if(...). {import.// Error, not allowed!
}

function load() {
  import. ;// Error, can't pout import in any block!
}
Copy the code

The so-called dynamic import is to overcome these limitations of static imports. Dynamic imports use a function-like operation — import(Module) — that returns a module’s promise to load new resources at run time. Since it “looks like” a function, import(Module) can be used in any scope block, such as:

const {hello} = await import('./hello.js');
console.log(hello('World')); // Hello World

if(...). {const User = await import('./user.js');
  const user = new User();
}
Copy the code

The import(module) function looks like a function and can be called like a function. Import (Module) does not support call/apply, so it is not a Function. Arguments are just array like types, not array. We treat import(module) like super() — a special JS syntax.

The script tag

I mentioned js Modules in “Understanding Script Tags”, and we’ll take a quick look at them here. Please refer to this article for more information.

  • Type = “module” : If you use ES module in the browser, you need to add the “module” keyword to the script tag:

    <script type="module">
      import { hello } from "./hello.js";
      document.body.innerHTML = hello("Onion");
    </script>
    Copy the code
  • The default loading mechanism for the Module is defer, but the import file will be downloaded along the way

  • Nomodule: We usually write a

  • External scripts: that is, the SRC attribute is used to introduce external linked scripts with CORS support

Other Module features

import.meta

The first thing that comes to mind is to view information about the current Module. This information is stored in import.meta. It’s not much, just two things:

  • Import.meta. url: The url path of the current module, for examplehttp://foo.com/bar.js
  • The import. Meta. ScriptElement: equivalentdocument.currentScript, returns the current<script>Attributes of tags
<script type="module">
  alert(import.meta.url); // script url (url of the html page for an inline script)
</script>
Copy the code

use strict

Module.js () {use strict: {}}

<script type="module">
  a = 5; // error
</script>
Copy the code

Top-level scopes are independent of each other

Each module has its own top-level scope; In other words, each module’s top-level variables and functions are not visible to each other and can only be called by import/export.

<script type="module">
  let user = "John"; // The variable is only visible in this module script
</script>
<script type="module">
  alert(user); // Error: user is not defined
</script>
Copy the code

Of course, if you really want to share variables, you can force an assignment to the window, such as window.user = ‘Onion ‘, and raise the variable to global level. But I don’t think we normally do that.

Import shared objects between modules

Personally, I think this is a negative feature. For example, we export an object in admin.js; This object is imported by both 1.js and 2.js. If I change the properties of this object in one place, the other file will also be affected.

// admin.js
export const admin = { name: "Onion" };

// 1.js
import { admin } from "./admin.js";
admin.name = "Garlic";

// 2.js
import { admin } from "./admin.js";
console.log(admin); // {name: 'Garlic'}
Copy the code

The main reason is: Although ES Module can be introduced, it can only be initialized once; Admin = {name: ‘Onion’; }} run only once, all import admin points to the same block of address in the V8 heap, resulting in one change and affected everywhere.

The solution is also simple, write a factory function:

export function adminFactory() {
  return { name: "Onion" };
}
Copy the code

this

This === undefined for the module-level scope; This is only a little knowledge, you can try to run:

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>
Copy the code

summary

This paper introduces the modern JS module management means, very conventional knowledge; Today’s front-end development often uses component tools (WebPack, esbuild, etc.) to help manage such modules, making us sometimes forget the original browser Settings. A few days ago, I talked with my colleagues about JSP => React relocation. Many people were at a loss. I came up with the idea of leaving the legacy code in place. Just add a