# ES Module

es module的功能主要由两个命令完成, 一个是export, 一个是import, export用于规定模块对外的接口, import用于输入其他模块的功能。

# 1、export

其实主要就是要知道各种export

export输出变量,可以输出三种变量声明中的任何一种

export var value1 = 'hello1';
export let value2 = 'hello2';
export const value3 = 'hello3';

还有一种统一输出的办法,和上面的效果是等价的。 推荐使用下面的办法, 这样子可以清楚的知道自己输出了哪些变量。

var value1 = 'hello1';
let value2 = 'hello2';
const value3 = 'hello3';
export { value1, value2, value3 };	

也可以输出函数和类等

export function log(...args) {
  console.log.apply(null, args);
}

export class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  print() {
    console.log(this.name, this.age);
  }
}

使用as关键字给输出的变量重命名

const name = 'hello, world';
export { name as username };

下面的几种写法都是错误的,因为export命令规定的是对外的接口, 必须与模块内部的变量建立一对一的关系。

// 错误
export 1;

// 错误
var m = 1;
export m;

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

// export.js
export var foo = 'foo1';
setTimeout(() => (foo = 'foo2'), 1000);

// import.js
import { foo } from './1.js';
console.log(foo); // foo1
setTimeout(() => {
  console.log(foo); // foo2
}, 2000);

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

# 2、import

# 2.1 import基础知识

通过import加载外部的模块。导入的模块名称必须和导出的接口对应。

import { log, Person } from 'xx.js'

同样可以使用as关键字给重命名输入的变量

import { foo as func } from 'xx.js';

WARNING

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

但是如果导入的是一个对象, 那么改写他的属性是被允许的

import { obj } from './1.js';
obj.x = 'x';

上面代码中,obj的属性可以成功改写, 同时也会影响其他导入的模块,非常不建议这种操作。

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。下面的代码不会报错

console.log(obj);
obj.x = 'x';
import { obj } from './1.js';

import语句会执行加载的模块, 因此可以下面这么写

import 'lodash'

上面的代码仅仅执行 lodash模块, 但是不输入任何的值。

# 2.2 模块的整体加载

可以使用*加载所有的输出的输出到一个对象上面。 不需要使用大括号

import * as module1 from './1.js';
console.log(module1.obj);

# 3、export default 命令

export default用于输出一个默认接口。输入的时候也可以自己定义变量名称。一个模块只能输出一个默认接口。

// default.js
export default function() {
  console.log('log');
}

// import.js
import log from './default.js';
log();

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

// export.js
function log() {
  console.log('log');
}
export { log as default, foo };
// 等同于
// export default log

// import.js
import {default as log} from "export.js"
// 等同于
// import log from "export.js"

所以我们可以认为export default 只是输出一个叫做default的变量, 所以下面这种写法是会报错的。

export default var a = 1;

同时导入默认接口和其他接口

import defaultInterface, { otherInterface } from "export.js"

# 4、export 和 import 的复合写法

先导入一个模块,再导出, 两个语句就可以写在一起

// log.js
export function log(...args) {
  console.log.apply(null, args);
}

// export.js
export { log } from './log.js';
// 等同于
// import { log } from './log.js';
// export { log }

// import.js
import { log } from "export.js"

但需要注意的是,写成一行以后,log实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用log

模块的接口改名和整体输出,也可以采用这种写法。

// 接口改名
export { foo as myFoo } from 'my_module';

// 整体输出--使用的时候和从my_module导入的用法一致
export * from 'my_module';

# 5、import()

import()函数主要用于动态加载模块, 返回一个Promise。

import('./1.js').then(module => {
  //   console.log(module);
  console.log(module.x);
  module.foo();
  console.log(module.default); // 获取默认的导出接口
});

主要作用有几个

  1. 按需加载
  2. 条件加载
  3. 动态的模块路径

# 6、import.meta

开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。import 命令的元属性import.meta,返回当前模块的元信息。

1、(1)import.meta.url

// 0.js
const url = import.meta.url
console.log(url); // http://127.0.0.1:8080/0.js

2、(2)import.meta.scriptElement

import.meta.scriptElement是浏览器特有的元属性,返回加载模块的那个<script>元素,相当于document.currentScript属性。

这个我目前没有在我的Chrome看到, 不过一般也不用到

# 7、Module的加载实现

# 7.1 浏览器加载

主要是script标签需要使用type="module"属性。

<!-- index.html -->
<script type="module" src="./index.js"></script>
// index.js
import('./1.js').then(module => {
  //   console.log(module);
  console.log(module.x);
  module.foo();
  console.log(module.default);
});

# 7.2 Node.js的模块加载办法

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。

它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module

一旦设置了以后,该项目的 JS 脚本,就被解释成 ES6 模块。

如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

# 7.3 package.json的main字段

package.json文件有两个字段可以指定模块的入口文件:mainexports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。

// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。

然后,import命令就可以加载这个模块。

// ./my-app.mjs

import { something } from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js

上面代码中,运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找es-module-package模块,然后根据该模块package.jsonmain字段去执行入口文件。

这时,如果用 CommonJS 模块的require()命令去加载es-module-package模块会报错,因为 CommonJS 模块不能处理export命令。

# 7.4 package.json的exports字段

exports字段的优先级高于main字段。它有多种用法。

  1. 子目录别名

    package.json文件的exports字段可以指定脚本或子目录的别名。

    // ./node_modules/es-module-package/package.json
    {
      "exports": {
        "./submodule": "./src/submodule.js"
      }
    }
    

    上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。

    import submodule from 'es-module-package/submodule';
    // 加载 ./node_modules/es-module-package/src/submodule.js
    
  2. main 的别名

    exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。

    {
      "exports": {
        ".": "./main.js"
      }
    }
    
    // 等同于
    {
      "exports": "./main.js"
    }
    

ES Module (opens new window)模块加载实现 (opens new window)

上次更新: 1/22/2025, 9:39:13 AM