通俗易懂讲解前端模块化

137 阅读6分钟

模块化

什么是模块:

模块化思想:隔离不同的js文件,仅暴露当前模块所需要的其他模块

将复杂的文件编成一个个独立的模块,有利于复用和维护。但会产生模块之间相互依赖的问题,可通过js打包工具webpack解决

模块化的进化过程

  • 全局function模式 : 将不同的功能封装成不同的全局函数

    • 编码: 将不同的功能封装成不同的全局函数
    • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
  • namespace模式 : 简单对象封装

    • 作用: 减少了全局变量,解决命名冲突
    • 问题: 数据不安全(外部可以直接修改模块内部的数据)
  • IIFE模式:匿名函数自调用(闭包)

    • 作用: 数据是私有的, 外部只能通过暴露的方法操作
    • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
    • 问题: 如果当前这个模块依赖另一个模块怎么办?
  • IIFE模式增强 : 引入依赖

    • 引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

引入多个<script>后出现出现问题

  • 请求过多
  • 依赖模糊
  • 难以维护

而这些问题可以通过模块化规范来解决,下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。

模块规范化

在服务器端,模块的加载是运行时同步加载的;在浏览器端,异步加载,模块需要提前编译打包处理

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。eS6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西

ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的****引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

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

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // ES6模块与 CommonJS模块 均输出3
incCounter();
console.log(counter); // ES6模块输出4   CommonJS模块输出3

ES6模块化

eS6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西

  1. ES6模块中自动采用严格模式,规定:

    • 变量必须先声明
    • 函数参数不能有同名属性
    • 禁止this指向全局
    • 对只读属性赋值、删除不可删除属性直接报错
    • arguments不可重新赋值,不会自动反应函数参数变化
    • 增加保留字static、interface、producted等
  2. export export语句输出的接口是对应值的引用,也就是一种动态绑定关系,通过该接口可以获取模块内部实时的值;export命令要处于模块顶层

    • 把export直接加到声明前面
    • export {a, b, c}
    • export default默认导出(一个js文件中只能有一个默认导出,但可以导出多个方法)
  3. import import是静态执行,Singleton模式;import命令要处于模块顶层

    • import {XX} from ‘./test.js’
    • import {XX as YY} from ‘./test.js’
    • import * as YY from ‘./test.js’

模块的循环引用

"循环加载"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。通常,"循环加载"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。但是实际上,这是很难避免的

CommonJS模块的循环加载

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。 (如果没有已执行部分的输出可能导致错误)

// a.js
import {bar} from './b.js';
export function foo() {
  bar();  
  console.log('执行完毕');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}

按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。但是es6可以执行上诉代码

ES6模块的循环加载

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。

因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。