From 1723eee29bc3179c0a6dd1a7efb386c1a4e96dfa Mon Sep 17 00:00:00 2001 From: ZiuChen <457353192@qq.com> Date: Sat, 11 Feb 2023 01:22:58 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=89=8D=E7=AB=AF=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E5=8C=96=20ESModule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/note/Front-end Engineering.md | 423 +++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) diff --git a/docs/note/Front-end Engineering.md b/docs/note/Front-end Engineering.md index 09f0adff..1c16c16c 100644 --- a/docs/note/Front-end Engineering.md +++ b/docs/note/Front-end Engineering.md @@ -257,6 +257,10 @@ console.log(name, age) // Ziu 18 - 当通过`require`导入时:`const env = require('env.js')` - `env`这个变量等于`env.js`中的`exports`对象 - 本质上是`env`是`exports`对象的引用赋值 + - `{ id: '...', exports: { ... }, loaded: true, ... }` +- 后续即使再次执行`require`导入模块,模块中的代码也不会重新执行(`module.loaded`属性) + - 当从模块中取值时,会从已经加载的`exports`对象缓存上取值 + ::: code-group @@ -402,3 +406,422 @@ console.log(utils.age) // undefined - CommonJS会被WebPack解析 - 将CommonJS代码转化为bundle 浏览器可以直接运行 +### ESModule + +- ES6 模块采用**编译时加载**,使得**编译时就能确定模块的依赖关系**,有助于**静态优化** +- CommonJS模块在运行时加载,且必须借助对象加载模块内容 + +#### `export`和`import`用法概览 + +ESModule借助`export`和`import`导入导出内容,需要注意的是导入导出的并不是对象 + +`export`定义的是当前模块导出的**接口**,`import`可以导入来自其他不同模块的**接口** + +- `export default`可以设置默认导出对象 +- `export { ... }`可以统一导出多个内容 +- `export`和`import`都可以使用`as`关键字重命名导出/导入的接口 +- `import * from 'xxx'` `export * from 'xxx'`批量导入/导出 + +::: code-group + +```js [utils.js] +// utils.js +export function sum(a, b) { + return a + b +} +export function sub(a, b) { + return a - b +} +export default function log(...args) { + console.log(...args) +} +export { + name: 'Ziu', + age: 18 +} +export const ENV_VARIABLE = 'Hello, World!' +``` + +```js [index.js] +// index.js +import { sum, sub, name, age, ENV_VARIABLE } from './utils' +import log from './utils.js' + +sum(1, 2) // 3 +sub(2, 3) // -1 +log(name, age, ENV_VARIABLE) // 'Ziu' 18 'Hello, World!' +``` + +::: + +需要注意的是,在浏览器中要使用ESModule,需要为`` + +- 当浏览器解析到`type="module"`的JS代码后,会**分析模块中导入的ESModule模块** +- 每导入一个ESModule模块,**浏览器都会发起一个HTTP请求去加载它** +- 在本地运行时加载不同协议头的文件会遇到跨域问题,需要开启本地Web服务器 + +另外,**`export`与`import`必须位于模块的顶层**,如果位于作用域内会报错,因为这就**无法对代码进行静态分析优化了** + +#### `export`详解 + +`export`有两种导出方式: + +- 命名导出 `export const name = 'Ziu'` `export { v1, v2 } export * from 'xxx'` + - 导出时需要指定名字 + - 导入时也需要知道对应的名字 +- 默认导出 `export default AGE = 18` + - 在从其他位置导入时需要为此默认导出指定新的名字 + - 给用户方便:不必阅读文档就可以加载模块 + +#### 值的动态绑定 + +- ESModule模块通过`export`语句输出的接口,与其对应的值是**动态绑定关系**,即通**过该接口,可以取到模块内部实时的值** +- CommonJS模块输出的是值的缓存,不存在动态更新 + +我们援引之前介绍CJS时的案例,**将后缀名改为`mjs`即可在Node中运行ESModule模块代码** + +初始获得的`a`值为0,经过1s后,在`utils.mjs`中修改了a的值,这时导入`utils.mjs`模块的其他模块可以获取到`a`最新的值 + +::: code-group + +```js [utils.mjs] +// utils.mjs +export let a = 0 + +// 1s后修改a值 +setTimeout(() => { + a = 1 +}, 1000) +``` + +```js [index.mjs] +// index.mjs +import { a } from './utils.mjs' + +console.log(a) // 0 + +setTimeout(() => { + console.log(a) // 1 +}, 1500) +``` + +::: + +- 需要注意的是,导入的其他模块的变量是不允许被修改的,因为`index.mjs`导入的本质是一个接口 +- 如果从其他模块导入的是一个对象,也不推荐修改导入内容的任何值,最好将其当做完全只读 + +拓展阅读:CommonJS与ESModule加载模块的异同 + +#### `import`详解 + +检查下述代码: + +```js +foo() + +import { foo } from 'foo' +``` + +- `import`命令具有提升效果,会提升到整个模块的顶部 +- `import`的执行早于函数的调用,`import`命令是在编译阶段执行的,在代码运行之前 +- 由于`import`是静态执行,所以不能使用表达式和变量(只有运行时才有值) + +```js +import 'lodash' +import 'lodash' +``` + +- 如果仅仅导入了一个模块,那么该模块的代码会被执行,但是没有任何变量被导入 +- 如果同一模块被导入多次,那么导入操作只会被执行一次 + +```js +import * from 'utils' +add(1, 2) + +export * from 'utils' +``` + +- 可以通过`*`一次性导入模块中所有导出的变量、函数、类 +- 也可以实现二者的复合操作:导入全部模块的同时导出全部模块 + +#### `import()`函数 + +通过`import`命令导入的模块是静态的,会被提升到模块顶部,并不支持条件导入 + +ES2020引入了`import()`函数,可以通过`import()`函数实现条件导入,动态加载ESModule模块 + +```js +const main = document.querySelector('main'); + +import(`./section-modules/${someVariable}.js`) + .then(module => { + module.loadPageInto(main); + }) + .catch(err => { + main.textContent = err.message; + }) +``` + +- 返回值是一个Promise对象,可以通过`await`同步地操作它 +- `import()`函数可以在模块外的JS脚本中使用,用于**在运行时加载外部模块**,类似于`require()` +- 区别于`require()`,`import()`是异步加载模块 + +通过`.then`函数处理导入的模块时,行为和`import`是相同的: + +- 如果有默认导出对象,则`.then`入参为默认导出对象 +- 可以通过解构直接取到模块中导出的变量或函数:`.then(({ add, sub }) => { ... })` + +**应用场景** + +按需加载:按钮点击后才加载相关的JS文件 + +```js +btn.addEventListener('click', () => { + import('./dialogBox.js') + .then(dialogBox => { + dialogBox.open() + }) + .catch(err => console.log(err)) +}) +``` + +条件加载:根据主题色加载不同JS文件 + +```js +if(darkMode) { + import('dark.js').then(() => ...) +} else { + import('light.js').then(() => ...) +} +``` + +传入动态值 + +```js +let moduleName = () => ['Home', 'History', 'User'][0] +import(`./${moduleName()}.js`) +``` + +#### `import.meta` + +ES2020引入了`import.meta`,它仅能在模块内部使用,包含一些模块自身的信息,即模块元信息 + +- `import.meta.url` 返回当前模块的URL路径 + - 浏览器加载ESModule都是通过HTTP发起请求 + - 例如当前模块为`fetchData.js`,要在模块内引入一个名为`data.json`的数据: + - `import( new URL('data.json', import.meta.url) )` + - Node.js环境下,该值都是`file://`协议的链接 +- `import.meta.scriptElement` + - 浏览器特有的属性 + - 返回加载模块的`