模块
模块简介
何为模块
一个模块(module)就是一个文件。一个脚本就是一个模块。
模块之间可以相互加载,使用 export 和 import 来交换功能:
export关键字标记了可以从当前模块外部访问的变量和函数。import关键字允许从其他模块导入功能。
在 A 模块中定义方法,进行导出:
// sayHi.js
export function sayHi(user) {
console.info(`Hello, ${user}!`);
}
在 B 模块导入进行使用:
// main.js
import { sayHi } from './sayHi.js';
console.info(sayHi); // function...
sayHi('John'); // Hello, John!
在浏览器中有专门的关键词 type="module" 来标记此部分属于模块,并根据导入的路径去查找对应的模块。
注意:
模块只能通过
http(s)工作,无法通过本地file路径进行导入。使用
import/export会导入导出失败。
<!doctype html>
<script type="模块">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
模块核心功能
-
始终使用
use strict
所有模块代码自动处于严格模式下。 -
模块之间作用域独立
每个模块内的内容互不可见,除了export的部分。
即使在同一页面内,不同的模块之间同样互不可见。<script type="module"> // 变量仅在这个 module script 内可见 let user = "John"; </script> <script type="module"> console.info(user); // Error: user is not defined </script> -
模块仅第一次被导入时解析
- 同一个模块被导入到多个其他位置,那么它的代码只会执行一次(即在第一次被导入时),然后将其导出(
export)的内容提供给进一步的导入者(importer)。
模块定义规则:
- 顶层模块代码应该用于初始化,创建模块特定的内部数据结构。
- 需要多次调用某些东西 —— 应该将其以函数的形式导出。
- 同一个模块被导入到多个其他位置,那么它的代码只会执行一次(即在第一次被导入时),然后将其导出(
-
import.meta:包含当前模块的信息// main.mjs import { sayHi } from './sayHi.mjs'; // import.meta 对象包含关于当前模块的信息 console.info(import.meta); /* [Object: null prototype] { resolve: [Function: resolve], url: 'file:///d:/DevApps/WorkSpace/Fronted/JS_Module/main.mjs' } */在 Node.js 环境使用模块运行,需要将后缀从
.js改为.mjs。 -
模块中顶层的
this为undefined
浏览器中非模块顶层为window。<script> console.info(this); // window </script> <script type="module"> console.info(this); // undefined </script> -
浏览器特定功能:模块脚本延迟加载
与defer特性对外部脚本和内联脚本(inline script)的影响相同:- 模块脚本不阻塞 HTML 处理,与其他资源并行加载。
- 模块脚本会等到 HTML 文档准备就绪才会运行。
- 模块脚本按序执行。
<script type="module"> // button 对象可见,因为延迟加载 console.info(typeof button); // object </script>相较于下面这个常规脚本:
<script> // 先于上方执行显示,因为不进行延迟加载,此时 button 对象还未定义 console.info(typeof button); // button 为 undefined </script> -
async适用于内联模块脚本
异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档,即使 HTML 还未加载完成,或者其他脚本还在等待处理。- 非模块脚本:
async仅适用于外部脚本。 - 模块脚本:
async也适用于内联脚本。
<!-- 所有依赖都获取完成(analytics.js)然后脚本开始运行 --> <!-- 不会等待 HTML 文档或者其他 <script> 标签 --> <script async type="module"> import {counter} from './analytics.js'; counter.count(); </script> - 非模块脚本:
-
外部脚本
-
相同
src的外部脚本仅运行一次<!-- 脚本 my.js 被加载完成(fetched)并只被运行一次 --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script> -
跨源请求需要 CORS header
远程服务器必须提供表示允许获取的 headerAccess-Control-Allow-Origin。
-
-
禁止裸模块
必须提供具体的路径。浏览器原生环境不支持裸模块。
但打包工具中允许是因为有自己的查找和钩子(hook)方法进行调整。
import {sayHi} from 'sayHi'; // Error,“裸”模块 // 模块必须有一个路径,例如 './sayHi.js' 或者其他任何路径 -
兼容性处理
对于不支持模块的浏览器可以增加提醒:<script type="module"> console.info("Runs in modern browsers"); </script> <!-- 对于不支持的环境,使用 nomodule --> <script nomodule> console.info("Modern browsers know both type=module and nomodule, so skip this") console.info("Old browsers ignore script with unknown type=module, but execute this."); </script>
导入与导出
针对的是静态导入
声明前导出
在声明之前放置 export 来标记任意声明为导出,无论声明的是变量、函数还是类。
导出
class/function后没有分号。
// 导出数组
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;
}
}
声明与导出分离
先完成声明,在结尾处统一导出。
技术上允许在模块顶端进行导出。
function sayHi(user) {
console.info(`Hello, ${user}!`);
}
function sayBye(user) {
console.info(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // 导出变量列表
全部导入 *
通常导入方式:
import {sayHi, sayBye} from './say.js';
sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
允许对导入的内容起一个别名对象:
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
但通常不建议全部导入:
- 不利于打包构建工具的优化:构建工具的优化器会对打包的代码进行检测未使用的函数,并进行删除(称为“摇树”/tree-shaking),加快加载速度。
- 名称冗长:明确列出要导入的内容会使得名称较短:
sayHi()而不是say.sayHi()。 - 可读性差:导入的显式列表可以更好地概述代码结构,易于重构和阅读。
别名导入与导出
别名导入 import "as"
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
别名导出 export "as"
export {sayHi as hi, sayBye as bye};
导入时:
import * as say from './say.js';
say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!
默认导出 export default
适用于声明单个实体的模块,即每个模块只做一件事,避免混乱。
将 export default 放在要导出的实体前:
export default class User { // 只需要添加 "default" 即可
constructor(name) {
this.name = name;
}
}
对于默认导出的内容无需使用花括号 {}:
import User from './user.js';
// 不需要花括号 {User},只需要写成 User 即可
new User('John');
同样支持先定义,之后再默认导出:
function sayHi(user) {
console.info(`Hello, ${user}!`);
}
// 就像我们在函数之前添加了 "export default" 一样
export {sayHi as default};
默认导出允许没有名称:
export default class { // 没有类名
constructor() { ... }
}
export default function(user) { // 没有函数名
console.info(`Hello, ${user}!`);
}
// 导出单个值,而不使用变量
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
默认导出与命名导出对比
| 命名的导出 | 默认的导出 |
|---|---|
export class User {...} | export default class User {...} |
import {User} from ... | import User from ... |
建议使用命名导出,使得更加清晰,以及方便重新导出。
重新导出
允许对一个模块导入的内容进行重新导出。
一般用于大量模块的项目中,提供一个统一的导入路由文件。
重新导出语法
export {sayHi} from './say.js'; // 重新导出 sayHi
export {default as User} from './user.js'; // 重新导出 default
例如,存在大量模块的项目:
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
我们可以提供 index.js 作为统一的入口,用来暴露自定义的包,这样使用者就不应该干涉包的内部结构以及对包内部进行搜索等。
重新导出默认导出时:
-
不能直接使用
// 导出无效 export User from './user.js'; -
export *只导出了命名导出,忽略了默认导出export * from './user.js' -
需要两句才能将命名和默认同时导出
export * from './user.js'; // 重新导出命名的导出 export {default} from './user.js'; // 重新导出默认的导出
动态导入
静态导入非常严格:
- 不能动态生成
import的任何参数。 - 模块路径必须是原始类型字符串,不能是函数调用。
因此无法根据条件进行选择性的
import。
import() 表达式
看似为函数,其实并非函数,无法使用
apply/call,或者将其赋值到变量中。
- 表达式加载模块并返回一个
Promise。 - 该
Promiseresolve 为一个包含其所有导出的模块对象。 - 可以在代码中的任意位置调用这个表达式。
模块代码:
// say.js
export function hi() {
console.info(`Hello`);
}
export function bye() {
console.info(`Bye`);
}
动态导入:
let {hi, bye} = await import('./say.js');
hi();
bye();
默认导出情况:
export default function() {
console.info("Module loaded (export default)!");
}
// 导入默认对象,使用 default
let obj = await import('./say.js');
let say = obj.default;
// or, in one line: let {default: say} = await import('./say.js');
say();
将动态导入封装成函数时,参考 Promise 或者 async/await 即可:
async function load() {
let say = await import('./say.js');
say.hi(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)!
}
动态导入在常规脚本中工作时,它们不需要
script type="module"。