模块
什么是模块?
export 关键字表示在当前模块之外可以访问的变量和功能
import 关键字允许从其他模块中导入一些诸如函数之类的功能等等
ex.导出一个函数
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
在其他文件里导入并使用:
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
在浏览器环境中使用模块;由于模块使用了特殊的关键词和功能,所有我们必须通过使用属性<script type="module">
来告诉浏览器群,脚本应该被当作模块
来看待
<!doctype html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
ex.使用export default 直接导出函数
// sayHi.js
export default function sayHi (user) { // ...}
// main.js
import sayHi from './say.js'
sayHi()
核心模块功能
始终使用"use strict"
例如,对一个未声明的变量赋值将会抛出错误。
模块级作用域(Module-level scope)
模块有自己的顶级作用域(top-level scope)
另外,在浏览器中,每个 <script type="module">
也存在独立的顶级范围的作用域。
如果我们真的需要创建一个窗口级别(window-level)的全局变量,我们可以显式地将它分配给 window
并以 window.user
来访问它。但是这样做需要你有足够充分的理由,否则就不要这样。
模块代码仅在第一次导入时解析
如果将一个模块导入到多个其他位置,则仅在第一次导入时解析其代码,然后将导出提供给所有导入的位置。(第一次是解析创建,之后的导入都只是引用第一次创建的变量和功能)
// 📁 alert.js
alert("Module is evaluated!");
// 从不同的文件导入相同模块
// 📁 1.js
import `./alert.js`; // Module is evaluated!
// 📁 2.js
import `./alert.js`; // (nothing)
另外
<!--执行两次-->
<script src="alert.js"></script>
<script src="alert.js"></script>
<!--执行一次-->
<!--模块引入多次, 也只会加载执行一次-->
<!--原因是模块加载完后, 会将模块存在内存里, 然后将其暴露的变量和功能提供给所有导入的位置-->
<script src="alert.js" type="module"></script>
<script src="alert.js" type="module"></script>
<!--执行一次-->
<script src="a.js" type="module"></script>
<script src="b.js" type="module"></script>
在开发中,顶级模块主要用于初始化。我们创建数据结构,预填充它们,如果我们想要可重用某些东西,只要导出即可
// 📁 admin.js
export let admin = {
name: "John"
};
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// 1.js 和 2.js 导入相同的对象
// 1.js 中对对象的修改,在 2.js 中是可访问的
显示Pete而非模块中初始定义的John!
原因是:如果一个模块(admin)被导入到多个文件中,模块仅仅在第一次导入的时候解析创建对象( admin对象, 1.js中)。然后将其传入所有导入的位置(简而言之,模块仅在第一次导入时会解析,之后其他位置都只是引用第一次解析的结果: 变量和功能)
虽然admin模块中name初始值是John, 但由于所有导入位置得到的都是模块第一次导入时解析创建的同一个admin对象, 因此1.js中的修改在这里(2.js)中是可访问的
所以,让我们重申一下:模块只执行一次。生成导出,然后在导入的位置共享同一个导出,当在某个位置修改了 admin
对象,在其他模块中是可以看到修改的。
这种行为对于需要配置的模块来说是非常棒的。我们可以在第一次导入时设置所需要的属性,然后在后面的导入中就可以直接使用了。
import.meta
import.meta
对象包含当前模块的一些信息。
顶级"this"是未定义(undefined)的
在一个模块中,顶级 this
是未定义的,而不是像非模块脚本中的全局变量。
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
特定于浏览器的功能
与常规脚本相比,拥有 type="module"
标识的脚本有几个特定于浏览器的差异。
模块脚本是延迟解析的
对于外部和内联模块脚本来说,它 总是 延迟解析的,就和 defer
属性一样。
也就是说:
1.外部模块脚本 <script type="module" src="...">
不会阻塞 HTML 的解析,它们与其他资源并行加载。
2.直到 HTML 文档完全解析渲染后(即使模块脚本体积很小,且比 HTML 先加载完成),模块脚本才会开始运行。
3.执行脚本的相对顺序:在前面的先执行。(脚本为了保持依赖关系,避免竞争状态需要保持按顺序执行;但可以不一定按顺序下载)
<script type="module">
alert(typeof button); // object: 脚本可以‘看见’下面的 button
// 当脚本模块延迟时,脚本在整个页面加载完成之后才执行
</script>
<!--相较于普通脚本:-->
<script>
alert(typeof button); // Error: button is undefined,脚本不能“看到”下面的元素
// 普通脚本在剩余页面加载完成前就执行了
</script>
<button id="button">Button</button>
上面的第二个脚本要先于前一个脚本执行,所以我们先会看到 undefined
,然后才是 object
。
这是因为模块脚本被延迟执行了,所以要等到页面加载结束才执行。而普通脚本就没有这个限制了,它会马上执行,所以我们先看到它的输出。
当使用模块脚本的时候,我们应该知道当 HTML 页面加载完毕的时候会显示出来,然后 JavaScript 在其后开始执行,所以用户会在使用了js脚本的组件功能生效前(JavaScript 脚本解析执行完成前)看到页面内容。某些依赖于 JavaScript 的功能可能还不能正常工作。我们应该使用透明层或者 “加载指示”,或者其他方法以确保用户不会感到莫名其妙。(待脚本解析完毕后,隐藏"加载指示")
defer
:
这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded
事件前执行。如果缺少 src
属性(即内嵌脚本),该属性不应被使用,因为这种情况下它不起作用。对动态嵌入的脚本使用 async=false
来达到类似的效果。
浏览器会异步下载该文件并且不会影响到后续DOM
的渲染;(比起普通script外链的好处就是不阻塞后续html渲染)
如果有多个设置了defer
的script
外链存在,则会按照顺序执行所有的script(因此defer是需要等待所有的脚本加载完之后才能开始按照顺序执行)
defer
脚本会在文档渲染完毕后,DOMContentLoaded
事件调用前执行
async
:
该布尔属性指示浏览器是否在允许的情况下异步执行该脚本。该属性对于内联脚本无作用 (即没有src属性的脚本)。
async
使得script
外链脚本异步加载并在允许的情况下执行
async
的执行,并不会按着script
在页面中的顺序来执行,而是谁先加载完谁执行。
DOMContent
事件的触发并不受async
脚本加载的影响。(可能在脚本加载完之前,就已经触发DOMContentLoaded
;如果async
脚本加载足够快,或者html
文档有成千上万个节点需要渲染,那么async
脚本也有可能在DOMContentLoaded
事件之前就执行)
这也说明了async
的执行是加载完之后就立即执行,而不像defer
那样需要等待所有的脚本加载完后按照顺序执行
推荐的应用场景
defer
:如果你的脚本代码依赖于页面中的DOM
元素(文档是否解析完毕),或者被其他脚本文件依赖。(defer
与普通脚本一样,保证了执行的顺序,因此后一个defer
脚本可以依赖前一个defer
脚本)
- 代码语法高亮(需要html都解析完毕才能高亮)
- polyfill.js
async
: 如果你的脚本并不关心页面中的DOM元素(文档是否解析完毕), 并且也不会产生其他脚本需要的数据(async
脚本是加载完立即执行,因此并不能保证执行的顺序,一些async
脚本中哪个先加载完哪个就先执行)
- 百度统计
模块脚本使内联脚本也具备异步功能
常规非模块脚本, async
异步属性只在外链脚本中有效。异步脚本加载完后立刻执行,而不需要等待其他脚本或者HTML的加载。
有了模块脚本,即使是内联脚本也可以使用async
异步属性了
比如,下面的内联脚本使用了async
,因此它不需要等待其他任何加载就可以立刻执行
它执行导入脚本(fetches ./analytics.js
),导入完成就开始运行,即使 HTML 文档还未解析完毕或者其他脚本仍在待处理的状态。
这对于不依赖任何其他东西的功能来说是非常棒的,比如计数器,广告和文档级的事件监听器。
<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
外部脚本
- 相同
src
的外链只允许一次
<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>
<!--普通, 执行两次-->
<script src="alert.js"></script>
<script src="alert.js"></script>
<!--模块,执行一次-->
<!--模块引入多次, 也只会加载执行一次-->
<!--原因是模块加载完后, 会将模块存在内存里-->
<!--<script src="alert.js" type="module"></script>-->
<!--<script src="alert.js" type="module"></script>-->
<!--从不同文件中导入相同模块-->
<!--如果将一个模块导入到多个其他位置,则仅在第一次导入时解析其代码,然后将导出提供给所有导入的位置。-->
<!--<script src="a.js" type="module"></script>-->
<!--<script src="b.js" type="module"></script>-->
- 从另一个域获取的外链脚本需要
CORS
头。换言之,如果一个模块外链脚本是请求自另一个源,么它所在的远端服务器必须提供Access-Control-Allow-Origin: `(可能使用加载的域名代替 `
)响应头以指明当前请求是被允许的。
<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
总结
模块的核心概念
-
模块就是文件。浏览器需要使用
<script type="module">
属性以使import/export
可用,这里有几点差别:默认是延迟解析的;类似于
defer
;使得行内脚本的defer
也生效使得行内脚本的
async
异步功能也生效;加载外部不同源(domain、protocol、port)脚本时,必须提供CORS响应头
重复的外部脚本只会加载执行一次(重复的外部脚本会被忽略)
-
模块有自己的本地顶级作用域,可以通过
import/export
交换功能 -
模块始终使用
use strict
-
模块代码只执行一次。导出的代码创建一次然后会在各导入之间共享