likes
comments
collection

前端基础知识-模块

作者站长头像
站长
· 阅读数 16

前言

本系列记录和整理前端的基础知识,希望通过系统性地学习来巩固自己对前端的理解。以《高级程序设计》第 4 版(红宝书),作为学习主体,结合自己工作中遇到的实际案例来了解、理解文中的基础知识点。接下来就一起来看看吧!

预置知识

什么是模块

把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。

模块标识符

模块标识符是所有模块系统通用的概念。模块系统本质上是键/值实体,其中每个模块都有个可用与引用它的标识符。

通常模块标识符就是模块文件的实际路径。

模块加载

在浏览器中,加载模块涉及几个步骤。

  1. 加载模块涉及执行其中的代码,但必须是在所有依赖都加载并执行之后。
  2. 如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。
  3. 收到模块代码之后,浏览器必须确定刚收到的模块是否也有依赖。然后递归地评估并加载所有依赖,直到所有依赖模块都加载完成。

模块加载器

简单介绍下在 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 脚本