# 模块化

模块化解决了命名冲突问题,可以提高代码的复用率,提高代码的可维护性。

  • 模块化好处;
    • 避免命名冲突
    • 更好的分离,按需加载
    • 更高复用性
    • 高可维护性

# 模块化方式

  1. 最初实现模块化方式使用函数进行封装,将不同功能的代码实现封装到不同的函数中,通常就是一个文件为一个模块,有自己的作用域,只向外暴露特定的变量和函数。这种方式容易发生命名冲突和数据不安全
  2. 采取立即执行函数:立即执行函数中的匿名函数中有独立的词法作用域,避免了外界访问此作用域的变量。通过函数作用域解决了命名冲突,污染全局作用域的问题,不过不能直接访问到内部的变量,是这种方式的一个弊端
// module.js
(function(window) {
    let name = 'linjiaheng'
    //暴露接口来访问数据
    function a() {
        console.log(`name:${name}`)
    }
    // 暴露接口
    window.myModule = { a }
})(window)

<script src="module.js"></script>
<script>
myModule.name = 'xixi' //无法访问
myModule.foo() // linjiaheng
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. CommonJS规范
  2. AMD和CMD
  3. ES6

TIP

  • 模块化发展历程
    • 早期假模块化时代

    早期借助函数作用域来模拟模块化,称其为函数模式。这样存在命名冲突的风险。这没有从根本上解决模块的问题,只是将代码分成了更小的函数单元而已。所以有了第二种模式:利用对象,实现命名空间的概念。这样会导致数据不安全,可以被开发者修改。通过立即执行函数构造一个私有作用域,再通过闭包将需要对外暴露的数据和接口输出。

    • 规范标准时代:CommonJS/AMD/CMD/UMD
    • ES6原生时代
      1. 无法使用代码分片(code splitting)和删除死代码(tree shaking)Webpack的两个特别重要的特性)。
      2. 大多数 npm 模块还是 CommonJS 的形式,而浏览器并不支持其语法,因此这些包没有办法直接拿来用。
      3. 仍然需要考虑个别浏览器及平台的兼容性问题。

随着技术的发展,JavaScript 已经不仅仅用来实现简单的表单提交等功能,引入多个 script 文件到页面中逐渐成为一种常态,但我们发现这种做法有很多缺点。

  • 需要手动维护 JavaScript 的加载顺序。页面的多个 script 之间通常会有依赖关系,但由于这种依赖关系是隐式的,除了添加注释以外很难清晰地指明谁依赖了谁,所以当页面中加载的文件过多时很容易出现问题。
  • 每一个 script 标签都意味着需要向服务器请求一次静态资源,在 HTTP 2 还没有出现的时期,建立连接的成本是很高的,过多的请求会严重拖慢网页的渲染速度。
  • 在每个 script 标签中,顶层作用域即全局作用域,没有任何处理而直接在代码中进行变量或函数声明会污染全局作用域。

模块化则解决了上述所有问题。

  • 通过导入和导出语句我们可以清晰地看到模块间的依赖关系,这点在后面会做详细的介绍。
  • 模块可以借助工具来进行打包,所以在页面中只需要加载合并后的资源文件,减少了网络开销。
  • 多个模块之间的作用域是隔离的,彼此不会有命名冲突。

# 浏览器模块加载实现

  1. 传统方法 浏览器通过script标签加载脚本。这种方式会造成浏览器堵塞,所以浏览器允许脚本异步加载:
<script src="" defer></script>

<script src="" async></script>
1
2
3

渲染引擎遇到这命令会开始下载外部脚本,但不会等他下载和执行,而是直接执行后面的命令。

TIP

defer等到整个页面正常渲染结束才会执行,渲染完再执行。多个defer脚本,会按照在页面出现的顺序加载 async一旦下载完成,渲染引擎就会中断渲染,执行这个脚本之后再继续渲染。下载完就执行。不能保证加载顺序

  1. 浏览器加载ES6模块时,要在script标签中加入type='module'。浏览器都是异步加载了,不会堵塞浏览器。等到整个页面渲染完再执行模块脚本,等同于添加了defer属性。async属性也可以在ES6中添加使用。

# ES6和CommonJS模块的差异

  • CommonJS 模块导出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(module.exports)该对象只有在脚本运行结束时才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

第一个差异:值的复制,也就是说一旦输出一个值,模块内部的变化就不会影响到这个值,CommonJS 会产生缓存。ES6 的运行机制却不同,表现在遇到模块加载命令import就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值。ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块,由于ES6的输入的模块变量只是一个符号连接,所以这个变量为只读,对它重新赋值就会报错,类似定义了一个const变量。

CommonJSES6 Module 是目前使用较为广泛的模块标准。它们的主要区别在于前者是在运行时建立模块依赖关系,后者是在编译时建立;在模块导入方面,CommonJS 导入的是值副本,ES6 Module 导入的是只读的变量映射;ES6Module 通过其静态特性可以进行编译过程中的优化,并且具备处理循环依赖的能力。