详解ESM/Typescript中import和export的使用

tc39/proposal-export-ns-from: Proposal to add export * as ns from "mod"; to ECMAScript. (github.com)

export

在创建JavaScript模块时,export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。

被导出的绑定值依然可以在本地进行修改。

在使用import进行导入时,这些绑定值只能被导入模块所读取,但在export导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。

:zap: 存在两种 exports 导出方式:

  1. 命名导出(每个模块包含任意数量)
  2. 默认导出(每个模块包含一个)

:cat: 主要语法:

// 导出单个特性
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}

// 导出列表
export { name1, name2, …, nameN };

// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };

// 解构导出并重命名
export const { name1, name2: bar } = o;

// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };

// 导出模块合集
export * from …; // does not set the default export
export * as name1 from …; // Draft ECMAScript® 2O21
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;

:100: 在每一个模块中定义多个命名导出,但是只允许有一个默认导出。

:tiger: :100: export聚合实例

举个例子,假如我们有如下层次结构:

  • childModule1.js: 导出 myFunctionmyVariable
  • childModule2.js: 导出 myClass
  • parentModule.js: 作为聚合器(不做其他事情)
  • 顶层模块:调用 parentModule.js 的导出项

你的代码看起来应该像这样:

// childModule1.js 中
let myFunction = ...; // assign something useful to myFunction
let myVariable = ...; // assign something useful to myVariable
export {myFunction, myVariable};

// childModule2.js 中
let myClass = ...; // assign something useful to myClass
export myClass;

// parentModule.js 中
// 仅仅聚合 childModule1 和 childModule2 中的导出
// 以重新导出他们
export { myFunction, myVariable } from 'childModule1.js';
export { myClass } from 'childModule2.js';

// 顶层模块中
// 我们可以从单个模块调用所有导出,因为 parentModule 事先
// 已经将他们“收集”/“打包”到一起
import { myFunction, myVariable, myClass } from 'parentModule.js'

:zipper_mouth_face: 真实代码:

// a.ts 中
let myFunction = function(){
    console.log("myFunction----------")
}
let myVariable = "myVariable";
export { myFunction, myVariable };


// b.ts 中
export let myClass =  class {
 constructor(){
     console.log("------myClass constructor----------")
 }
}
// export { myClass }

//ab.ts
export { myFunction, myVariable } from './a';
export { myClass } from './b';
// myFunction, myVariable,myClass 在ab.ts 此文件中都不可用

// 测试 index.ts
import { myClass,myFunction, myVariable} from './ab'

console.log(myVariable)
myFunction()
new myClass()

// 输入如下:
// myVariable
// myFunction----------
// ------myClass constructor----------

ab.ts 属于聚合文件,负责导入后立即导出,绑定的值在当前文件无法使用

import

静态的import 语句用于导入由另一个模块导出的绑定。

无论是否声明了 strict mode ,导入的模块都运行在严格模式下。

在浏览器中,import 语句只能在声明了 type="module"script 的标签中使用。

此外,还有一个类似函数的动态 import(),它不需要依赖 type="module" 的script标签。

而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和 tree shaking 中受益。

:zap: import 语法

import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");//这是一个处于第三阶段的提案。

import * as name语法导入所有导出接口,即导入模块整体。

导入整个模块的内容

//a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}

export const fn2 = ()=>{
    console.log("-----fn2-")
}

//index.ts
import * as All from './a'

console.log(All)

All.fn1()
All.fn2()

// 输入结果如下:
{ fn1: [Function: fn1], fn2: [Function: fn2] }
-----fn1-
-----fn2-

导出单个和多个接口

//导入单个接口,将myExport插入当前作用域。
import {myExport} from '/modules/my-module.js';

//导入多个接口,将foo和bar插入当前作用域。
import {foo, bar} from '/modules/my-module.js';

// ------------------分割线------------------------------------------------------------
//导入带有别名的接口,在导入时重命名接口。例如,将shortName插入当前作用域 ,原有的名称不可用
import {reallyReallyLongModuleExportName as shortName} from '/modules/my-module.js';


//例如
// a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}
// index.ts
import { fn1 as func1} from './a'

// fn1()  // fn1 不在当前作用域
func1()   // 输出: -----fn1-


//-----------------------仅为副作用而导入一个模块------------
//整个模块仅为副作用(中性词,无贬义含义)而导入,而不导入模块中的任何内容(接口)。 
//这将运行模块中的全局代码, 但实际上不导入任何值。
// a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}

console.log("-------doSomething-------------")

//index.ts
import './a'   //输出: -------doSomething-------------

导入默认值

//直接导入默认值
import myDefault from '/modules/my-module.js';

//也可以同时将default语法与上述用法(命名空间导入或命名导入)一起使用。在这种情况下,default导入必须首先声明。
import myDefault, * as myModule from '/modules/my-module.js';
// myModule used as a namespace

// 或者
import myDefault, {foo, bar} from '/modules/my-module.js';

//---测试代码-------------
//a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}

export const fn2 = ()=>{
    console.log("-----fn2-")
}

export default function(){
    console.log("--export default----")
}

//index.ts
import myDefault, * as myModule from './a';
myDefault()
myModule.default()
myModule.fn1()
myModule.fn2()

//输出结果如下:
--export default----
--export default----
-----fn1-
-----fn2-

动态 import()

标准用法的import导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。

有些场景中,你可能希望**根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。**下面的是你可能会需要动态导入的场景:

  • 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
  • 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
  • 当被导入的模块,在加载时并不存在,需要异步获取
  • 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
  • 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)

动态导入语法

//返回一个 promise
import('/modules/my-module.js')
  .then((module) => {
    // Do something with the module.
});

//也支持 await 关键字。
let module = await import('/modules/my-module.js');

当用动态导入的方式导入默认导出时,其工作方式有所不同。你需要从返回的对象中解构并重命名 "default" 键。

(async () => {
  if (somethingIsTrue) {
    const { default: myDefault, foo, bar } = await import('/modules/my-module.js');
  }
})();

import()类似于Node的require方法,区别主要是前者是异步加载,后者是同步加载

动态导入使用场景

//按需加载
button.addEventListener('click', event => {
    import('./dialogBox.js')
    .then(dialogBox => {
        dialogBox.open();
    })
    .catch(error => {
        /* Error handling */
    })
});

// 根据条件加载
if (isLegacyPlatform()) {
    import(···)
    .then(···);
}

// 动态计算导入
import(`messages_${getLocale()}.js`)
.then(···);

使用示例

参考: ES2020: import() – dynamically importing ES modules (2ality.com)

// 1. 解构export
// a1.ts
export function v1() {
   console.log("V1-------")
}
export function v2() {
   console.log("V2-------")
}
// import.ts
import('./a1')
.then(({v1,v2 }) => {
   v1()  //  V1-------
   v2()  //  V2-------
});


// 2.访问 default exports 
// a1.ts
export default function d(){
    console.log("default---------")
}

//import.ts
import('./a1')
.then(({ default }) => {
    // 由于default是关键字,因此不能直接解构出来
   //console.log(default) 
});
// 正确的做法是:
import('./a1')
.then(({default: myDefault}) => {
   myDefault()  // default---------
});


// 3. 动态导入多个模块
Promise.all([
    import('./module1.js'),
    import('./module2.js'),
    import('./module3.js'),
])
.then(([module1, module2, module3]) => {
    
});

// 4.  await import。 因为import()返回promise,因此可以使用await,但是不必在Async中
async function main() {
    const myModule = await import('./myModule.js');

    const {export1, export2} = await import('./myModule.js');

    const [module1, module2, module3] =
        await Promise.all([
            import('./module1.js'),
            import('./module2.js'),
            import('./module3.js'),
        ]);
}

export与import重难点举例

e.g. 1

export as关键字重命名导出后,只能import重命名后的模块名

//a.ts
function v1() {
    console.log("V1-------")
}
export {
    v1 as streamV1,
};
// index.ts
import { streamV1 } from "./a";
//import { v1 } from "./a";  // v1 不可用
streamV1() // ok

e.g. 2

import as 重命名导入后,只能使用重命名后的模块

//a.ts
function v1() {
    console.log("V1-------")
}
export {
    v1 as streamV1,
};
//index.ts
import { streamV1 as v1} from './a' 
// streamV1()   // 不可用
v1()  //ok

e.g. 3

export和import的复合写法

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

:zap: :rose: 虽说以上写法等同,实际上用法还是有区别的。

//a1.ts
export function v1() {
    console.log("V1-------")
}

export function v2() {
    console.log("V2-------")
}

//b1.ts
import { v1, v2 } from './a1';
export { v1, v2 };
v1()  //ok
v2()  //ok
// 因为有import 语句,所以v1,v2 可用

// 如果b1.ts 如下写, 只是起了一个中介的作用,即导入后立即导出,供其他模块使用,在当前模块不可用
export { v1,v2 } from './a1'
//v1()  //Error 
//v2()  //Error


// 但是上面两种写法,其他模块都是可以使用的
//index.ts
import { v1,v2 } from "./b1";
v1()  //OK
v2()  //OK

e.g. 4

import * as myModule 导入整个模块的内容, 在当前作用域插入 myModule 变量, 包含文件中全部导出的绑定

//a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}

export const fn2 = ()=>{
    console.log("-----fn2-")
}

//index.ts
import * as All from './a'

console.log(All)

All.fn1()
All.fn2()

// 输入结果如下:
{ fn1: [Function: fn1], fn2: [Function: fn2] }
-----fn1-
-----fn2-

e.g. 5

import 仅为副作用而导入一个模块

//-----------------------仅为副作用而导入一个模块------------
//整个模块仅为副作用(中性词,无贬义含义)而导入,而不导入模块中的任何内容(接口)。 
//这将运行模块中的全局代码, 但实际上不导入任何值。
// a.ts
export const fn1 = ()=>{
    console.log("-----fn1-")
}

console.log("-------doSomething-------------")

//index.ts
import './a'   //输出: -------doSomething-------------

e.g.6

具名接口改为默认接口的写法

export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

//默认接口也可以改名为具名接口
export { default as es6 } from './someModule';

e.g.7

export * as ns ,与 import * as 整体导入对于,ES2020 新增了整体导出

export * as ns from "mod";

// 等同于
import * as ns from "mod";
export { ns };

e.g.8

:100: 整体导出export * from, 注意和 export * as 的比较

export * from 'my_module';
// 即将my_module 文件中所有的 export 全部导出,一般用作中转文件或index文件中。

:zap: 整体导出,不会导出 export default

具体看实例:

//a1.ts
export function v1() {
    console.log("V1-------")
}

export function v2() {
    console.log("V2-------")
}


export default function d(){
    console.log("default---------")
}

//b1.ts
export * from './a1'
// 即等价于
export {v1,v2} from './a1'

//index.ts
import { v1,v2 } from './index1'
v1()  // V1-------
v2()  // V2-------

:rose: :cat: 整体导出后,还可以再次整体导出:

//a1.ts
export type $FetchInput = RequestInfo
export function createFetch (){
  
}

//b1.ts
export class FetchError extends Error {
  name: 'FetchError' = 'FetchError'
}

//base.ts
export * from './fetch'
export * from './error'

//index.ts
//即将a1.ts 以及b1.ts两个文件中的export 全部导出
export * from './base'