前言
在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> 加载的脚本一样按顺序执行。解析到 <scripttype="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 ...}
主要语句形式:
// 从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