模块 (Module
)
概述
- 随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块(
module
) - 一个模块可以包含用于特定目的的类或函数库
- 模块可以相互加载,并可以使用特殊的指令
export
和import
来交换功能,从另一个模块调用一个模块的函数:export
关键字标记了可以从当前模块外部访问的变量和函数import
关键字允许从其他模块导入功能
1 2 3 4 |
// 📁 sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); } |
1 2 3 4 5 |
// 📁 main.js import { sayHi } from './sayHi.js'; alert(sayHi); // function... sayHi('John'); // Hello, John! |
import
指令通过相对于当前文件的路径./sayHi.js
加载模块,并将导入的函数sayHi
分配(assign
)给相应的变量- 由于模块支持特殊的关键字和功能,因此我们必须通过使用
<script type="module">
特性(attribute
)来告诉浏览器,此脚本应该被当作模块(module
)来对待 - 浏览器会自动获取并解析(
evaluate
)导入的模块,然后运行该脚本
- 由于模块支持特殊的关键字和功能,因此我们必须通过使用
1 2 3 4 5 6 |
<!doctype html> <script type="module"> import {sayHi} from './say.js'; document.body.innerHTML = sayHi('John'); </script> |
注意1
- 模块只通过
HTTP(s)
工作,而非本地
始终使用 “use strict
”
- 模块始终在严格模式下运行
模块级作用域
- 每个模块都有自己的顶级作用域(
top-level scope
) - 换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的
1 2 3 |
// user.js let user = "John"; |
1 2 3 |
// hello.js alert(user); // no such variable (each module has independent variables) |
1 2 3 |
<!doctype html> <script type="module" src="user.js"></script> <script type="module" src="hello.js"></script> |
- 在浏览器中,对于
HTML
页面,每个<script type="module">
都存在独立的顶级作用域
1 2 3 4 5 6 7 8 |
<script type="module"> // 变量仅在这个 module script 内可见 let user = "John"; </script> <script type="module"> alert(user); // Error: user is not defined </script> |
注意2
- 在浏览器中,我们可以通过将变量显式地分配给
window
的一个属性,使其成为窗口级别的全局变量 - 例如
window.user = "John"
模块代码仅在第一次导入时被解析
- 如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时
- 然后将其导出(
export
)的内容提供给进一步的导入(importer
)
- 然后将其导出(
- 这里有一条规则:顶层模块代码应该用于初始化,创建模块特定的内部数据结构
- 如果我们需要多次调用某些东西 —— 我们应该将其以函数的形式导出
import.meta
- 包含关于当前模块的信息
在一个模块中,“this
” 是 undefined
1 2 3 4 5 6 7 |
<script> alert(this); // window </script> <script type="module"> alert(this); // undefined </script> |
模块脚本是延迟的
- 模块脚本 总是 被延迟的,与
defer
特性对外部脚本和内联脚本(inline script
)的影响相同 - 也就是说:
- 下载外部模块脚本
<script type="module" src="...">
不会阻塞HTML
的处理,它们会与其他资源并行加载 - 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比
HTML
加载速度更快),然后才会运行 - 保持脚本的相对顺序:在文档中排在前面的脚本先执行
- 下载外部模块脚本
Async
适用于内联脚本(inline script
)
- 对于非模块脚本,
async
特性(attribute)仅适用于外部脚本- 异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档
- 对于模块脚本,它也适用于内联脚本
- 例如,下面的内联脚本具有
async
特性,因此它不会等待任何东西
1 2 3 4 5 6 7 |
<!-- 所有依赖都获取完成(analytics.js)然后脚本开始运行 --> <!-- 不会等待 HTML 文档或者其他 <script> 标签 --> <script async type="module"> import {counter} from './analytics.js'; counter.count(); </script> |
外部脚本
- 具有
type="module"
的外部脚本(external script
)在两个方面有所不同:- 具有相同
src
的外部脚本仅运行一次:
- 具有相同
1 2 3 |
<!-- 脚本 my.js 被加载完成(fetched)并只被运行一次 --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script> |
- 如果一个模块脚本是从另一个源获取的,则远程服务器必须提供表示允许获取的
header
Access-Control-Allow-Origin
1 2 3 |
<!-- another-site.com 必须提供 Access-Control-Allow-Origin --> <!-- 否则,脚本将无法执行 --> <script type="module" src="http://another-site.com/their.js"></script> |
不允许裸模块(“bare
” module
)
- 浏览器中,
import
必须给出相对或绝对的URL
路径 - 没有任何路径的模块被称为“裸(
bare
)”模块。在import
中不允许这种模块
导入导出
在声明前导出
1 2 3 4 5 6 7 8 9 10 11 12 |
// 导出数组 export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 导出 const 声明的变量 export const MODULES_BECAME_STANDARD_YEAR = 2015; // 导出类 export class User { constructor(name) { this.name = name; } } |
导出与声明分开
1 2 3 4 5 6 7 8 9 10 |
// 📁 say.js function sayHi(user) { alert(`Hello, ${user}!`); } function sayBye(user) { alert(`Bye, ${user}!`); } export {sayHi, sayBye}; // 导出变量列表 |
注意
- 导出
class/function
后没有分号
Import *
1 2 3 4 5 |
// 📁 main.js import {sayHi, sayBye} from './say.js'; sayHi('John'); // Hello, John! sayBye('John'); // Bye, John! |
1 2 3 4 5 |
// 📁 main.js import * as say from './say.js'; say.sayHi('John'); say.sayBye('John'); |
Import “as”
1 2 3 4 5 |
// 📁 main.js import {sayHi as hi, sayBye as bye} from './say.js'; hi('John'); // Hello, John! bye('John'); // Bye, John! |
Export “as”
1 2 3 |
// 📁 say.js ... export {sayHi as hi, sayBye as bye}; |
1 2 3 4 5 |
// 📁 main.js import * as say from './say.js'; say.hi('John'); // Hello, John! say.bye('John'); // Bye, John! |
Export default
- 每个文件应该只有一个
export default
1 2 3 4 5 6 |
// 📁 user.js export default class User { // 只需要添加 "default" 即可 constructor(name) { this.name = name; } } |
1 2 3 4 |
// 📁 main.js import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可 new User('John'); |
重新导出
1 2 3 |
export {sayHi} from './say.js'; // 重新导出 sayHi export {default as User} from './user.js'; // 重新导出 default |
1 2 3 4 5 6 7 8 9 10 |
// 📁 auth/index.js // 导入 login/logout 然后立即导出它们 import {login, logout} from './helpers.js'; export {login, logout}; // 将默认导出导入为 User,然后导出它 import User from './user.js'; export {User}; ... |
1 2 3 4 5 6 7 |
// 📁 auth/index.js // 重新导出 login/logout export {login, logout} from './helpers.js'; // 将默认导出重新导出为 User export {default as User} from './user.js'; ... |
动态导入
import()
表达式
import(module)
表达式加载模块并返回一个promise
,该promise resolve
为一个包含其所有导出的模块对象- 或者,如果在异步函数中,我们可以使用
let module = await import(modulePath)
- 或者,如果在异步函数中,我们可以使用
1 2 3 4 5 |
let modulePath = prompt("Which module to load?"); import(modulePath) .then(obj => <module object>) .catch(err => <loading error, e.g. if no such module>) |
1 2 3 4 5 |
let obj = await import('./say.js'); let say = obj.default; // or, in one line: let {default: say} = await import('./say.js'); say(); |
注意
- 动态导入在常规脚本中工作时,它们不需要
script type="module"
- 尽管
import()
看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号- 因此,我们不能将
import
复制到一个变量中,或者对其使用call/apply
。因为它不是一个函数
- 因此,我们不能将
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 【Javascript】原型继承,F.prototype,原生的原型,原型方法,“Very plain”对象04/12
- ♥ 【Javascript】generator,异步迭代,异步generator04/20
- ♥ 【Javascript】Promise错误处理,Promise API,Promisification,微任务,async,await04/14
- ♥ 【Javascript】对象构造05/20
- ♥ 【Javascript】全局对象,函数对象,new_function,setTimeout,setInterval04/06
- ♥ 【Javascript】回调,promise,promise链04/13