ES6的模块化

前言

在ES6以前,javascript一直没有官方的模块化,所以有人就提出了多种模块化方案,例如:CommonJS,AMD,CMD,UMD等。

从ES6开始,javascript具有了自己的模块化系统。

浏览器兼容性

参考:<script> - HTML(超文本标记语言) | MDN

可以看到主流浏览器基本都已经实现了ES6的模块化加载

script加载与html解析的关系

1、传统script标签与html解析

如图所示,传统脚本的下载会中断html的解析。

如图所示,传统脚本下载完成之后,立刻执行也中断了html的解析。

总结:传统脚本的下载之后会立刻执行,其下载和执行都会中断html的解析。

2、模块script标签与html解析

可以看到,模块的脚本的下载并不会中断html的解析。

模块代码下载完成之后并不会立即执行。 

 

如上面两张图所示,moduleA.js命中断点,但是body中的内容都显示出来了,说明模块代码的执行总在body解析完成之后。

总结:它的行为与defer有点类似,都是异步下载,在body解析完成之后再执行。

与传统脚本不同,所有模块都会像 <script defer> 加载的脚本一样按顺序执行。解析到 <script
type="module"> 标签后会立即下载模块文件,但执行会延迟到文档解析完成。无论对嵌入的模块代码, 还是引入的外部模块文件,都是这样。<script type="module"> 在页面中出现的顺序就是它们执行 的顺序。与<script defer> 一样,修改模块标签的位置,无论是在 <head> 还是在 <body> 中,只会影 响文件什么时候加载,而不会影响模块什么时候加载。
                                              ---------------------------------------《Javascript高级程序设计》

script的模块化脚本的加载执行顺序

1、内嵌模块与外部模块之间的执行顺序

 结果:

 它们之间优先级相同,执行顺序按照html中的顺序从上而下执行。

其它的,内嵌模块之间的执行顺序是从上而下执行的,外部模块之间的执行顺序也是从上而下执行。

2、模块与defer 脚本之间的加载顺序

结果:

 

 可见,defer script与模块脚本的执行顺序也是从上而下执行的。

3、添加defer的模块脚本与正常模块脚本和defer脚本之间的执行顺序

结果:

可见,添加defer的模块脚本并不会改变模块脚本的优先级,与正行的模块脚本优先一致,它们的执行顺序仍然是从上而下的。

4、添加async的模块脚本与正常的模块脚本之间的执行顺序

 结果:

多次刷新结果不一致。

 

 

 

对于添加了async的模块脚本来说,其异步下载,下载完成之后,立刻执行,这就导致了一个结果:其执行的位置完全取决于其下载的快慢,所以多次执行的结果不一致。

4、传统脚本与模块脚本之间的执行顺序

传统脚本总是同步下载,立刻执行,其执行顺序总是在模块脚本之前。

这个就不写例子了,一目了然。

总结:

  • 模块脚本不应该添加async,因为添加async之后,行为不确定。
  • 模块脚本添加defer不改变其行为,也就没有必要添加了。
  • 模块脚本的行为有点类似于defer,异步下载,body解析完之后执行。

 

以上测试环境均为chrome 98版本。

注意:由于内嵌模块没有模块名,所以内嵌模块不能被import。

ES6模块详解

ES6模块的诸多特性:

模块代码只在加载后执行。
模块只能加载一次。
模块是单例。
模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互。
模块可以请求加载其他模块。
支持循环依赖。
ES6 模块系统也增加了一些新行为。
ES6 模块默认在严格模式下执行。
ES6 模块不共享全局命名空间。
模块顶级 this 的值是 undefined (常规脚本中是 window )。
模块中的 var 声明不会添加到 window 对象。
ES6 模块是异步加载和执行的。
                                                        -----------------------《javascript高级程序设计第四版》

1、模块的定义与导出

核心关键字:export

主要语句形式:

export xxxxx:命名行内导出。

export {xxxx,xxxx}:命名子句导出。

export default xxxxx:默认导出。

注意:导出语句必须在模块顶级,不能嵌套在某个块中。

           export不能用在内嵌脚本中。

// 允许
export ...
// 不允许
if (condition) {
     export ...
}

1)、行内导出

// 命名行内导出
// 同一个模块允许多个命名行内导出语句
// 这里有的形式使用了分号,有的没有,这是需要注意的地方
export const baz = 'baz'; 
export const foo = 'foo', bar = 'bar'; 
export function foo() {} 
export function* foo() {} 
export class Foo {}

2)、子句导出

// 命名子句导出
// 同一个模块允许多个命名子句导出
// 这种方式通用得多,唯一需要注意的就是导出符号必须被定义
export { foo }; 
export { foo, bar }; 
export { foo as myFoo, bar };


// 等于export default foo;
export { foo as default };

3)、默认导出

// 默认导出
// 注意:默认导出一个模块只能存在一个,存在多个报错
export default 'foo'; 
export default 123; 
export default /[a-z]*/; 
export default { foo: 'foo' };

export { foo, bar as default }; 
export default foo 
export default function() {} 
export default function foo() {} 
export default function*() {} 
export default class {}

 

// 会导致错误的不同形式:
// 行内默认导出中不能出现变量声明
export default const foo = 'bar'; 
// 只有标识符可以出现在 export 子句中
export { 123 as foo } 
// 别名只能在 export 子句中出现
export const foo = 'foo' as myFoo;

这些导出会导致一些错误,由于需要记住那些会出错,哪些不会出错,其实是一件比较难的事情,所以一般都使用export子句导出,这样标识符的声明和导出都分隔开了,同时导出符号也被集中管理,这样也不容易出错了。

总结:尽量使用命名子句导出,先声明,后导出,这样不容易出错。默认导出只能存在一个。

默认导出的作用:

当模块具备一个默认导出的时候,在导入模块的时候,可以不指明具体的需要导入的符号,

可以随意指明一个符号,然后这个符号会跟默认导出对应起来,使用这个符号,就相当于

使用默认导出符号。

例子:

//moudleB.js
function moduleB(){
    console.log("This is ModuleB!");
}
moduleB();
let helloB = function(){
    console.log("This is helloB!");
}

export {helloB};

let helloB_str="helloB";

export default helloB_str;
import test from "./moduleB.js"

console.log(test);

结果:helloB。

export一个比较有意思的行为,但是应该避免使用:

导出值对模块内部 JavaScript 的执行没有直接影响,因此 export 语句与导出值的相对位置或者 export 关键字在模块中出现的顺序没有限制。 export 语句甚至可以出现在它要导出的值之前:
// 允许,但应该避免
export { foo };
const foo = 'foo';

2、模块的导入

注意:import在浏览器环境中只能用在type=“module”中。

           import语句是静态的,在声明的时候就确定了语义。

           import只能出现模块顶层,不能嵌套在某个块中。

// 允许
import ...
// 不允许
if (condition) {
import ...
}

主要语句形式:

import { xxxx , xxxxxc, xxxxz } from 'path/to/module.js';
import * as xxxx from 'path/to/module.js';
import myDefault from 'path/to/module.js';
上面的语句也组合成一句:
import myDefault , { xxxx , xxxxxc, xxxxz as test } from 'path/to/module.js';
最好的做法是不要组合成一句,分开写表达更清晰。
另外, 模块的导入路径是相对路径,起始路径就是当前路径。
// 从foo.js导入所有符号(不包括默认导出符号)的集合,并把这个集合赋值给别名Foo
// 这种方式用来导入一整个模块的符号,其实挺方便的。
import * as Foo from './foo.js';

// 从foo.js导入foo这个符号

import {foo} from './foo.js';

// 从foo.js导入默认的符号,并命名为myDefault。

import { default as myDefault } from './foo.js';

import myDefault from './foo.js'

import一个有意思的行为,但应该避免:

import 语句被提升到模块顶部。因此,与 export 关键字类似, import 语句与使用导入值的语句
的相对位置并不重要。不过,还是推荐把导入语句放在模块顶部。
// 允许,但应该避免
console.log(foo); // 'foo'
import { foo } from './fooModule.js';

注意:ES6的模块始终处于严格模式下,即使没有添加"use strict",其也是严格模式。

3、ES6的按需加载

前面说到import语句是静态的,且import语句不能嵌套在某个块中,因此,不能使用import来实现按需加载。

ES2020添加了import(),该特性允许用户动态加载模块(异步)。

浏览器兼容性:

除了IE,主流的现代浏览器基本都已经实现了。

ImportCall : import ( AssignmentExpression )
1. Let referencingScriptOrModule be ! GetActiveScriptOrModule ().
2. Let argRef be the result of evaluating AssignmentExpression .
3. Let specififier be ? GetValue ( argRef ).
4. Let promiseCapability be ! NewPromiseCapability ( %Promise% ).
5. Let specififierString be ToString ( specififier ).
6. IfAbruptRejectPromise ( specififierString , promiseCapability ).
7. Perform ! HostImportModuleDynamically ( referencingScriptOrModule , specififierString , promiseCapability ).
8. Return promiseCapability .[[Promise]].

按照标准对import()的定义来说,其最终会返回一个promise对象。

跳转到HostImportModuleDynamically这个抽象操作:

如果成功的话,则会调用resolve更改promise的状态为fullfilled,并且把module的namespace作为resolve的参数。

这个namespace可以认为就是module的所有导出符号集(不包括默认的),类似于import * as myModule from testModule.

如果失败,则会调用reject更改promise的状态为rejected,并且把AbruptCompletion的值作为reject的参数。

一般的浏览器实现会使用throw abruptcompletion,其值一般为TypeError。

一个典型的使用import()的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script type="text/javascript">
    //import成功
    import("./moduleA.js").then((module)=>{
        module.helloA();
    })
    //import失败
    import("./moduleB1.js").then(
        (module)=>{module.helloB();
        },
        (reason)=>{console.log(reason);})
    </script>
</head>
<body>
    使用import()动态加载
</body>
</html>

其中第一个moduleA.js会加载成功,通过module这个参数,可以调用moduleA的导出函数helloA。

第二个moduleB1.js不会加载成功,通过reason这个参数获得了加载不成功的原因,在chrome中为TypeError:Failed to fetch dynamically imported module: http://192.168.11.166/ES6/moduleB1.js。

众多例子:

https://github.com/comefromezero/FrontModule

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值