前端基础知识-模块
前言
本系列记录和整理前端的基础知识,希望通过系统性地学习来巩固自己对前端的理解。以《高级程序设计》第 4 版(红宝书),作为学习主体,结合自己工作中遇到的实际案例来了解、理解文中的基础知识点。接下来就一起来看看吧!
预置知识
什么是模块
把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。
模块标识符
模块标识符是所有模块系统通用的概念。模块系统本质上是键/值实体,其中每个模块都有个可用与引用它的标识符。
通常模块标识符就是模块文件的实际路径。
模块加载
在浏览器中,加载模块涉及几个步骤。
- 加载模块涉及执行其中的代码,但必须是在所有依赖都加载并执行之后。
- 如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。
- 收到模块代码之后,浏览器必须确定刚收到的模块是否也有依赖。然后递归地评估并加载所有依赖,直到所有依赖模块都加载完成。
模块加载器
简单介绍下在 JavaScript 中未支持模块之前出现了哪些实现模块的规范。后续会介绍 ES6 原生模块才是我们学习的重点。
CommonJS
CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务端
实现模块化代码组织。 CommonJS 模块语法不能在浏览器中直接运行,如果想要在浏览器使用 npm 包(CommonJS 规范),可以通过转换工具实现。
浏览器不兼容 CommonJS 的根本原因,在于缺少四个 Node.js 环境的变量。module、 exports、 require、 global
,可以通过 browserify来实现转化。可以参考 阮一峰浏览器加载 CommonJS 模块的原理与实现
CommonJS 规范特点
- 模块加载是模块系统执行的同步操作。
- 模块永远是单例,无论一个模块在 require 多少次,模块只会被加载一次(加载一次以后就被缓存了)。
const moduleA = require('./moduleA') // 引入
module.exports = 'foo' // 导出单个值
module.exports = { // 导出多个值
a: 'A',
b: 'B'
}
if(condition) {
const moduleA = require('./moduleA') // 由于是同步执行,支持动态依赖
}
AMD (Asynchronous Module Definition) 模块定义
特点
- 以浏览器为目标执行环境,所以是异步模块定义方式。
// ID 为 'moduleA' 的模块定义,moduleA 依赖 moduleB
define('moduleA', ['moduleB'], function(moduleB) {
// 异步加载 moduleB 的回调
return {
stuff: moduleB.doStuff()
}
})
UMD (Universal Module Definition)模块定义
为了统一 CommonJS 和 AMD 生态系统而生,UMD 可以创建两个系统都可以使用的模块代码,本质上, UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置。
ES6 模块
ES6 最大的一个改进就是引入了模块规范。这个规范全方位简化了之前出现的模块加载器。
模块标签及定义
ECMAScript6 模块时作为一整块 JavaScript 代码而存在的。带有 type="module" 属性的 <script> 标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。
<script type="module">
// 模块代码会被执行
</script>
// 外部文件引入
<script type="module" src="path/to/myModule.js"></script>
const foo = 'foo'
export { foo }
示例演示
codepen 示例 通过示例代码可以发现
- 给 script 标签设置 module,在标签内部可以使用 import 引入其他模块暴露的接口。也可以执行我们写的代码
<script type="module">
// 模块代码会被执行
</script>
- 但是我们模块标签作为外部文件引入时,在标签内部的代码是会被忽略的。
那我们可以使用普通的 script 标签引入模块内容吗? 添加一下代码
// 普通标签引入模块文件
<script src="./index.js">
发现当我们在 index.js 使用 export 导出模块时,如果使用普通的 script 标签引入时就会报错。
那么接下来我们来看看如果导入和导出模块。
导出模块
ES6 模块支持两种导出: 命名导出和默认导出。
先来说说比较简单的默认导出
export default 'foo' // 直接导出值
const foo = 'foo'
export default foo // 导出变量
export default {a: 'a'} // 导出对象
export default class {} // 导出类
export default function(){} // 导出函数
再来看看命名导出
const foo = 'foo'
export { // 先定义变量后导出
foo
}
export const koo = 'koo' // 单行导出
export const bar = 'bar' // 导出单个值
export { // 导出多个值
foo,
koo,
bar
}
export {foo as MyFoo}
命名导出与默认导出的区别点
- 命名导出正如其名,导出变量之前需要使用 const、var 先声明该变量。而默认导出可以直接导出值。
- 命名导出可以使用别名修改导出值。默认导出对应的就是 default。
- 命名导出可以使用多次,默认导出一个文件只能有一个。
模块转移导出
export * from './foo.js' // 将 foo.js 文件中的模块批量导出,作为汇总导出文件
export { foo, bar as Mybar } from './foo.js' // 修改 foo.js 中导出的模块名称
这样不会复制导出的值,只是把导出的引用传给了原始模块。
模块导入
import { foo } from './fooModule.js'
import './foo.js' // 不导入具体的模块,可能是想加载模块利用其副作用
import * as Foo from './foo.js' // 批量导入,赋值给 Foo
import { foo, bar, baz as MyBaz, deafult as fooDefault } from './foo.js' // 导入时使用别名
模块行为
- ES6 模块是异步加载和执行的。 打印顺序, 1. sync 2. a: 'a'
// 执行顺序与 <script defer> 一样
<script type="module">
import { a } from './index.js'
console.log('a:', a)
</script>
<script type="module" src="./index.js">
console.log('no log')
</script>
<script> console.log('sync') </script>
- 模块只能加载一次。模块是单例。
- 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互。
- 支持循环依赖。
- ES6 模块默认在严格模式下执行。
参考
《高级程序设计》第 4 版 阮一峰浏览器加载 CommonJS 模块的原理与实现 async 和 defer 脚本
转载自:https://juejin.cn/post/7173301862869762085