ECMAScript6-Module(下)

79 阅读7分钟

7.export与import的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

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

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

// 整体输出
export * from 'my_module';

默认接口的写法如下。

export { default } from 'foo';

具名接口改为默认接口的写法如下。

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

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

同样地,默认接口也可以改为具名接口。

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

ES2020之前,有一种import语句,没有对应的复合写法。

import * as someIndentifier from 'someModule';

ES2020补上这个写法。

export * as ns from 'mod';

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

8.模块的继承

模块之间也可以继承。

假设有一个circleplus模块,继承了circle模块。

// circleplus.js

export * from 'circle';
export var e = 2.314231242342;
export default function(x) {
    return Math.exp(x);
}

上面代码中的export *,表示在输出circle模块的所有属性和方法。注意,export *命令会忽略模块circle模块的default。然后,上面代码又输出了自定义的e变量和默认方法。

这时,也可以将circle的属性或方法,改名后再输出。

// circleplus.js

export { area as circleArea } from 'circle';

上面代码表示,只输出circle模块的area方法,且将其改名为circleArea。 加载上面模块的写法如下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

9.跨模块常量

const声明的常量只在当前代码块有效,如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
cosnole.log(constants.B); // 3

// test2.js 模块
import { A, B } from './constants';
console.log(A); // 1
console.log(B); // 3

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

// constants/db.js
export const db = {
    url:'http:my.couchdbserver.local:5984',
    admin_username: 'admin',
    admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

// constants/index.js
export { db } from './db';
export { users } from './users';

使用的时候,直接加载index.js就可以了。

// script.js
import { db, users } from './constants/index';

10.import()

简介

前面介绍过,import命令会被JavaScript引擎静态分析,先于模块内的其他语句执行(import命令叫做“连接”binding其实更合适)。所以,下面的代码会报错。

// 报错
if(x === 2) {
    import MyModule from './myModule;
}

上面代码中,引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报语法错误,而不是执行时错误。也就是说,importexport命令只能在模块的顶层和底层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。

这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块,在语法上,条件加载就不可能实现。如果import命令要取代Node的require方法,这就形成了一个障碍,因为require是运行时加载模块,import命令无法取代require的动态加载功能。

const path = './' + fileName;
const myModule = require(path);

上面的语句就是动态加载,require到底加载哪一个模块,只有在运行时才知道,import命令做不到这一点。

ES2020提案 引入import()函数,支持动态加载模块。

import(specifier)

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

import()返回一个Promise对象。下面是一个例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
    .then(module => {
        module.loadPageInto(main);
    })
    .catch(err => {
        main.textContent = err.message;
    });

import()函数可以用在任何地方,不仅仅是模块,非模块的脚本可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于Node.js的require()方法,区别主要是前者是异步加载,后者是同步加载。

由于import()返回Promise对象,所以需要使用then()方法指定处理函数,考虑到代码的清晰,更推荐使用await命令。

async function renderWidget() {
    const container = document.getElementById('widget');
    if(container !== null){
        // 等同于
        // import("./widget").then(widget => {
        //   widget.render(container);
        // });
        const widget = await import("./widget.js");
        widget.render(container);
    }
}
renderWidget();

上面实例中,await命令后面就是使用import(),对比then()的写明显更简洁易读。

下面是import()的一些适用场合。

(1)按需加载。 import()可以在需要的时候,再加载某个模块。

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

上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载 import()可以放在if代码块,根据不同的情况,加载不同的模块。

if(condition) {
    import('moduleA').then(...);
} else {
    import('moduleB').then(...);
}

上面代码中,如果满足条件,就加载模块A,否则加载模块B,

(3)动态的模块路径

import()允许模块路径动态生成。

import(f())
.then(...);

上面代码中,根据函数f的返回结果,加载不同的模块。

注意点

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

import('./myModule.js')
.then(({export1, export2}) => {
    // ...'
});

上面代码中,export1export2都是myMoudle.js的输出接口,可以解构获得。

如果模块有default输出接口,可以用参数直接获得。

import('./myModule.js')
.then(myModule => {
    console.log(myModule.default);
});

上面的代码也可以使用具名输入的形式。

import('./myModule.js')
.then(({default: theDefault}) => {
    console.log(theDefault);
});

如果想同时加载多个模块,可以采用下面的写法。

Promise.all([
    import('./module1.js'),
    import('./module2.js'),
    import('./module3.js'),
])
.then(([module1,module2,module3]) => {
    ...
});

import()也可以用在async函数之中。

async function main() {
    const myModule = await import('./myModules.js');
    const {export1, export2} = await import('./myModule.js');
    const [module, module2, module3] = await Promise([
        import('./module1.js'),
        import('./module2.js'),
        import('./module3.js'),
    ]);
}
main();

11.import.meta

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

import.meta只能在模块内部使用,如果在模块外部使用就会报错。

这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定,一般来说,import.meta至少会有下面两个属性。

(1)import.meta.url

import.meta.url返回当前模块的URL路径。举例来说当前模块主文件的路径是https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件data.text,那么就可以用下面的代码,获取这个数据文件的路径。

new URL('data.txt', import.meta.url)

注意,Node.js环境中,import.meta.url返回的总是本地路径,即file:URL协议的字符串,比如file:///home/user/foo.js

(2)import.meta.scriptElement

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

// HTML 代码为
// <script type='module' src="my-module.js" data-foo='abc'></script>

// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// "abc"