前端模块化

模块可以理解为独立且通用的代码单元,是一个实现特定功能的文件。所谓模块化只要就是解决代码分隔、作用域隔离、模块之间的依赖管理等问题。

模块化的优点:

  • 避免变量污染、命名冲突
  • 可维护性;模块是独立的,测试、修改都可独立进行
  • 重用代码;通过模块的导出、引入的方式,避免代码的拷贝

早期模块化方式

  1. 函数封装,缺点:污染全局变量,变量命名冲突,模块成员之间没有关系
  2. 对象封装,缺点:外部可随意修改内部成员变量
  3. 立即执行函数(闭包),缺点:代码量大,闭包有成本,影响性能
  4. script标签顺序引入 js 文件,缺点:污染全局变量,命名冲突,依赖关系不明显,不利于维护

模块化规范

常见的模块化规范有: CommonJSAMD(Asynchronous Module Definition)CMD(Common Module Definition)ES6 Module

CommonJS

  • CommonJS 采用 同步加载模块的策略,只有加载完成后才会之下后续操作。

  • CommonJS 是服务端模块的规范,Node.js 就是采用该规范。

  • 在 CommonJS 的规范中,每一个 js 文件就是一个独立的模块上下文(module context),在该上下文中默认创建的属性都是私有的。即在一个文件定义的变量,是私有的,对其他文件是不可见的。

  • requiremodule关键字,实现了模块导入、对外暴露

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function aModule() {
    let name = "jiang";
    this.foo = function () {
    console.log(">>> hello " + name);
    };
    this.bar = function () {};
    }
    module.exports = aModule; // 对外暴露

    // 引入 模块 aModule
    let a = require("./aModule");
    let aa = new a();
    aa.foo(); // >>> hello jiang
  • require 中文件的查找策略

    文件查找策略

AMD 异步模块定义

  • AMD 优先照顾浏览器的模块加载场景,使用异步加载和回调的方式

  • 使用define方法,来定义模块、引入模块。该方法需要三个参数:模块名称、依赖数组、回调函数

  • RequireJS 遵循 AMD 规范

    1
    2
    3
    4
    5
    6
    7
    8
    define(["module1", "module2"], function (module1, module2) {
    function foo() {
    module1.foo();
    }
    return {
    foo: foo,
    };
    });

ES6 Module

  • 通过 import 引入模块,export 导出模块

  • 每一个模块只加载一次,并缓存

  • 一个模块就是一个单例

  • 模块中的值属于 动态 只读 引用,不允许修该

    1
    2
    3
    4
    5
    6
    7
    8
    export let foo = {
    name: "jiang",
    age: "25",
    }; // 导出

    // 引入模块
    import { foo } from "./foo.js";
    console.log(foo.age);

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,原始类型的值会被缓存,ES6 模块输出的是值的引用,且是动态的只读的引用,不会缓存,对它进行重新赋值会报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // a.js
    let counter = 3;
    function incCounter() {
    counter++;
    }
    module.exports = {
    counter,
    get getCounter() {
    return counter;
    },
    incCounter,
    };

    // a1.js
    let a = require("./a");
    console.log(a); // { counter: 3, getCounter: [Getter], incCounter: [Function:incCounter] }
    console.log(a.counter); // 3
    a.incCounter();
    console.log(a.counter); // 3
    console.log(a.getCounter); // 4

    a.js模块加载后,它的内部变化就影响不到counter了,因为counter是是一个原始类型的值,会被缓存。写成 get 函数,就能得到内部变动后的最新值了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // b.js
    export let counter = 3;
    export function incCounter() {
    counter++;
    }

    // b1.js
    import { counter, incCounter } from "./b";
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4

    ES6 模块,遇到import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块去读值

  • CommonJS 模块在运行时加载,ES6 模块在 编译时就输出接口,在静态解析阶段就会生成

  • 处理循环加载不同,CommonJS 模块,脚本代码在require的时候,就会全部执行,一旦出现某个模块被”循环加载”,就只输出已执行的部分,还未执行的部分不会输出;ES6 模块是动态引用的,在真正取值的时候 需要 开发者自己去保证,能取到值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // c.js
    exports.done = false;
    var d = require("./d");
    console.log("在c.js 中,d.done = %j", d.done);
    exports.done = true;
    console.log("c.j 执行完毕");

    //d.js
    exports.done = false;
    var c = require("./c");
    console.log("在 d.js 中, c.done = %j", c.done);
    exports.done = true;
    console.log("d.js 执行完毕");

    //c_d.js
    var c = require("./c");
    var d = require("./d");
    console.log("在 c_d.js 之中, c.done=%j, d.done=%j", c.done, d.done);
    1
    2
    3
    4
    5
    6
    $ node.exe c_d.js
    在 d.js 中, c.done = false
    d.js 执行完毕
    在c.js 中,d.done = true
    c.j 执行完毕
    在 c_d.js 之中, c.done=true, d.done=true