preface

In the early days of Javascript, there was no concept of modularity. It wasn’t until nodeJS came along that module systems were introduced into JS. Nodejs uses the Commonjs specification (require, Module. exports). The standard Module specification of JS language is ESM (Ecmascript Module), which is the import and export syntax that we use extensively in front-end engineering. Nodejs has been gradually supporting ESM, and many major browsers now support ESM natively.

Is the project using ESM or CJS?

Node.js 8.5.0 adds experimental support for ESM, using the — experimental-Modules flag and a file name ending with.mjs allows NodeJS to execute modules imported and exported from the ESM specification. Such as:

node --experimental-modules index.mjs
Copy the code

Node.js 12.17.0, remove –experimental-modules flag. Although the ESM is still experimental, it is relatively stable.

In later versions, NodeJS determines whether a module system is using ESM or CJS:Those who do not meet the above criteria will use CJS. If your project follows the CJS specification, no special filename suffixes or Settings are requiredpackage.json typeFields and other additional processing.

You can also explicitly tell Nodejs to use CJS in the same way:

  • File to.cjsFor the suffix
  • package.jsonDefined in the"type": "commonjs"
  • --evalorSTDINPipe mode to execute nodeJS, belt--input-type=commonjslogo

In fact, it’s rare to see a project using file suffix-like.mjs or.cjs to distinguish a module system, usually using the type field in package.json.

Module entry

We know that there are many third-party libraries that support both NodeJS and browser environments. These libraries typically package both CJS and ESM artifacts, with CJS artifacts for NodeJS and ESM artifacts for Bundler like WebPack. So, when we import the module moduleA using require and import, the entry file path is often different. So how do you get NodeJS or Bundler to find the corresponding entry file?

Json’s main field defines the entry file for CJS, and the Module field defines the entry file for ESM.

{
	"name": "moduleA"."main": "./dist/cjs/index.js"."module": "./dist/esm/index.js"
}
Copy the code

Thus, Nodejs and Bundler know to import modules from./dist/ CJS /index.js and. Dist /esm/index.js, respectively.

Node.js v12.16.0 adds an exports field to package.json that allows us to match different paths under different conditions. Exports have many uses, including distinguishing between NodeJS and Browser environments, distinguishing between development and production environments, restricting access to private paths, etc. The focus here is on its impact on CJS and ESM module imports.

We can define it this way:

{
	"name": "moduleA"."main": "./dist/cjs/index.js"."module": "./dist/esm/index.js"."exports": {
		"import": "./dist/esm/index.js"."require": "./dist/cjs/index.js"}}Copy the code

When you use the require (‘ moduleA ‘), the actual import is node_modules/moduleA/dist/CJS/index. The js, and using the import moduleA from ‘moduleA’, Import is node_modules/moduleA/dist/esm/index, js.

Exports take precedence over main and Module, which means that a path that matches exports will not use main and Module.

At first glance it doesn’t look like Exports brings much new to CJS and ESM. It is true that the main and Module fields suffice for common scenarios, but exports are obviously more flexible when introducing different CJS or ESM modules for different paths or environments. Also, exports is a new specification and it is necessary for us to know and even try to use it in engineering.

Of course, it is recommended to keep the main and Module fields for nodeJS versions or Bundler that do not support the Exports field.

interoperability

Nodejs14 and later ESM modules can import CJS modules through default import, name Import, and Namespace Import, but the CJS module can import ESM modules only through dynamic import(import()).

// default_add.mjs
export default function add(a, b) {
  return a + b;
}

// name_add.mjs
export function add(a, b) {
  return a + b;
}

// index.cjs
import('./default_add.mjs').then(
  ({ default: add }) = > {
    console.log('default import: ', add(1.2)); // default import: 3});import('./name_add.mjs').then(
  ({ add }) = > {
    console.log('name import: ', add(1.2)); // name import: 3});Copy the code

The difference between

Feature removed

If you want to write NodeJS using ESM, you need to be careful here.

The ESM module does not have __dirname and __filename variables, but dirname and filename can be resolved using import.meta.url and the NODEjs URL module (firedirName also works).

// dir-path/index.mjs
import filedirname from 'filedirname';

const [filename, dirname] = filedirname(import.meta.url);
console.log('dirname: ', dirname); // dirname: dir-path
console.log('filename: ', filename); // filename: dir-path/index.mjs
Copy the code

The introduction of JSON modules by ESM is currently only possible with the experimental identifier, experimental-Json-modules

// index.mjs
import { readFile } from 'fs/promises';
const json = JSON.parse(
  await readFile(new URL('./package.json'.import.meta.url))
);
console.log(json);
Copy the code
node index.mjs --experimental-json-modules
Copy the code

Remove require.resolve, but both can be done via module.createrequire ().

In addition, ESM removes NODE_PATH, resolve.extensions, and resolve.cache (ESM has its own caching mechanism).

CreateRequire () can be used in ESM to remove many of the capabilities mentioned above, and can be saved by using require in ESM (normally, ESM modules require an error).

// util.cjs
exports.add = function add(a, b) {
  return a + b;
};

// index.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { add } = require('./util.cjs');

console.log(add(1.2)); / / 3
Copy the code

Strict vs. non-strict

CJS defaults to non-strict mode, while ESM defaults to strict mode.

Reference vs copy

The CJS module requires imports copies of values, whereas ESM imports references to values.

// a.cjs
let age = 18;

exports.setAge = function setAge(val) {
  age = val;
};
exports.age = age;

// index.cjs
const { age, setAge } = require('./a.cjs');

console.log(age); / / 18
setAge(19);
console.log(age); / / 18

// a.mjs
export let age = 18;
export function setAge(val) {
  age = val;
}

// index.mjs
import { age, setAge } from './a.mjs';

console.log(age); / / 18
setAge(19);
console.log(age); / / 19
Copy the code

As you can see, index. CJS imports age from A.cjs and changes the age in A.cjs with setAge, but the printed age remains the same, while ESM does the opposite.

Dynamic vs Static

We all know that javascript is a JIT language, and v8 takes js code and executes it as it is compiled. At compile time v8 creates static references to imported modules that cannot be changed at run time. So import is placed at the beginning of the file, not in conditional statements.

The require import module copies values at run time, so require’s path can use variables, and require can be placed anywhere in the code.

Because of this difference, ESM does tree-shaking better than CJS.

Asynchronous vs Synchronous

ESM is the top-level await design, whereas require is loaded synchronously, so require cannot import ESM modules, but can import them via import().

ESM processing in Web projects

When we use React and VUE to develop services, we follow the ESM specification. However, the ESM code is not delivered to the browser for execution, because the browser needs to be compatible with older versions. The processing process is roughly as follows:

  1. ESM specification writing code, usingimport,export;
  2. Compilers such as Babel convert ESM code into CJS code;
  3. But browsers don’t support the CJS specification, so WebPack implements something similar to the CJS specificationrequireandmodule.exportsModule loading mechanism.

By the way, here is a hot topic recently: ESBuild 0.14.4 introduces breaking change in the conversion between CJS and ESM, which sets off a heated discussion in the community. Esbuild also recorded the reason in detail in Changelog. Export default 0 = module.exports.default = 0 Foo from ‘bar’ and const are then guaranteed by whether __esModule is true when importing foo from ‘bar’, whether foo is module.exports.default or module.exports Foo = require(‘bar’) equivalent. But the NodeJS ESM implementation pairs export default with module.exports. This inconsistency causes esBuild to incorrectly handle tripartite libraries used in nodeJS and Browser environments.

The last

This article combines the hot topics to talk about some ESM and CJS knowledge points, talk more miscellaneous, but it is also a personal summary, I hope to be useful to you.

The resources

  1. Github.com/nodejs/node…
  2. Nodejs.medium.com/announcing-…
  3. Nodejs.org/api/package…
  4. Nodejs.org/api/esm.htm…
  5. Nodejs.org/api/modules…
  6. zhuanlan.zhihu.com/p/113009496
  7. Github.com/evanw/esbui…
  8. Redfin. Engineering/node – the module…