前端模块化

56 阅读8分钟

模块化

一、什么是模块

模块通常指的是一个独立的可重用软件组件,它包含了一组相关的函数、方法、类、变量等,能够实现特定的功能或提供特定的服务。

在编程中,可以将程序的功能划分成不同的模块,每个模块负责不同的任务。JavaScript 的模块化是一种将代码组织到独立的、可重用的、可维护的单元中的方式,使代码更易于管理和复用。模块化可以帮助开发者将代码分解成更小的部分,这样更容易理解和维护,同时也可以防止命名冲突和全局污染。在 JavaScript 中,有多种模块化的标准和实现,如 CommonJS、AMD、ES6 等。其中 ES6 的模块化已经成为 JavaScript 的官方标准,可以直接在浏览器和 Node.js 中使用。

初始阶段,直接导入

文件分离是最基础的模块化

  1. 直接加载,解析到标签立刻 pending,并且下载并执行。
  2. defer 解析到标签的时候会异步下载, 下载完成后开始执行。(在执行到加了 defer 的 script 时会推入下载进程,然后再去执行下一个标签。待到这个 script 的外链下载完毕时,需要看 DOM 是否渲染完毕了,如果渲染完毕了则执行 defer script 的内容,然后触发 DOMContentLoaded 的回调。多个 defer 等待所有 defer 下载完依次执行。)
  3. async 解析到标签的时候会异步下载,下载完成后立刻执行,阻塞继续解析(下载完成后不解析,先执行,执行完成后继续解析),执行完成之后在继续向下执行。(在执行到加了 async 的 script 时会先下载,然后再去执行下一个标签。待到这个 script 的外链下载完毕时,如果 DOM 正在渲染则暂停,执行 async script 的内容。多个 async 先下载完的先执行 async 和 DOMContentLoaded 无任何绑定关系。)

问题导向 => 浏览器渲染原理,同步异步原理,模块化加载解析。

<script src="xxx.js" type="text/javascript"></script>
<script src="xxx.js" type="text/javascript" defer></script>
<script src="xxx.js" type="text/javascript" async></script>

存在问题:变量之间相互污染,不利于大型项目的开发

IIFE(立刻执行函数,作用:作用域隔离)

通过立即执行函数实现作用域把控,如下:

let count = 0;
// 代码块1
const inscease = () => ++count;

// 代码块2
const reset = () => {
  count = 0;
};
inscease();
reset();

利用函数的独立作用域:

(() => {
  let count = 0;
  // ...
})();

通过 定义函数 + 立即执行 得到 独立的空间,初步实现了一个最底层的最简单的模块。

下面将定义一个简单的模块化:

// 外部依赖依赖外部的传参处理
// dependencyModule1, dependencyModule2 代表外部依赖
const module = ((dependencyModule1, dependencyModule2) => {
  let count = 0;

  // dependencyModule1...
  //   代码块...
  // dependencyModule2...
  //   代码块...

  return {
    inscease: () => ++count,
    reset: () => {
      count = 0;
    },
  };
})(dependencyModule1, dependencyModule2);
module.inscease();
module.reset();

// 揭示模式:返回的是能力 => 使用方的传参 + 本身的逻辑实现 + 其他依赖

const module = ((dependencyModule1, dependencyModule2) => {
  let count = 0;
  // dependencyModule1, dependencyModule2 代表外部依赖

  // 代码块1
  const inscease = () => {
    // dependencyModule1...
    // ...
  };

  // 代码块2
  const reset = () => {
    // dependencyModule2...
    // ...
  };

  return {
    inscease,
    reset,
  };
})(dependencyModule1, dependencyModule2);
// 例如:jquery 模块化处理方案
$("xxx").attr("title", "xxx");

面试问题导向:

  • 深入模块化实现
  • 转向框架 jquery, vue, react 模块化组件实现细节,以及框架原理特性。
  • 设计模式 - 注重模块化的设计

CJS(commonjs)

  • CommonJS 是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了体现它的广泛性,修改为 CommonJS,也可简称为 CJS。
  • 所有代码都运行在模块作用域,不会污染全局作用域;
  • 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就会被缓存,以后再加载,将直接读取缓存结果。若想模块再次运行,必须清除缓存;
  • 模块加载顺序,按照其在代码中出现的顺序;

特征:

  1. 通过 modules + export 向外部暴露接口。
  2. 通过 require 方式去调用其他模块
const dependencyModule1 = require('dependencyModule1')
const dependencyModule2 = require('dependencyModule2')


let count = 0;

// 代码块1
const inscease = () => {
  // dependencyModule1...
  // ...
};

// 代码块2
const reset = () => {
  // dependencyModule2...
  // ...
};

// 暴露出去
export.inscease = inscease;
export.reset = reset;
// 或者
module.exports = {
    inscease,
    reset
}

调用方式

const { inscease, reset } = require("dep.js");

inscease();
reset();

优缺点:

  • 优点: CJS 从服务侧的角度解决了依赖全局污染的问题,但是相对于 IIFE 在写法上也实现了在写法上主观感受的模块化。
  • 缺点: 针对服务端 => 存在异步的问题 => AMD模块化

AMD (经典实现框架:require.js)

AMD 是 Asynchronous Module Definition(异步模块定义)的缩写;

  • CommonJS 规范加载模块是同步的,即加载完成,才能执行后面的操作。
  • AMD 规范则是非同步加载模块,允许指定回调函数。
  • 由于 Node.js 主要用于服务器编程,模块文件一已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用服务端变成。但是,如果是浏览器环境,就需要先从服务器将文件下来,之后再加载运行,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。 事实上 AMD 的规范还要早于 CommonJS,但是 CommonJS 目前依然在被使用,而 AMD 使用少了。

特性:支持通过异步加载以及允许定制回调函数

// 通过define定义一个模块,然后通过require加载
define(id, [depends], callback);
require([module], callback);
define("amdModule", [dependencyModule1, dependencyModule2], (
  dependencyModule1,
  dependencyModule2
) => {
  let count = 0;

  // 代码块1
  const inscease = () => {
    // dependencyModule1...
    // ...
  };

  // 代码块2
  const reset = () => {
    // dependencyModule2...
    // ...
  };

  export.inscease = inscease;
  export.reset = reset;

});
// 加载
require(['amdModule'], amdModule => {
    amdModule.inscease()
    amdModule.reset()
})
// 内部加载模块, 异步加载
define(['amdModule'], [], require => {
    const dependencyModule1 = require('dependencyModule1')
    const dependencyModule2 = require('dependencyModule2')

    amdModule.inscease()
    amdModule.reset()
})

面试导向:

  1. 如何区分 CJS 和 AMD 或者是普通 js => - 通过 module 判断是否是普通 js,通过 define 判断是否是 AMD
  2. 手写兼容 CJS 和 AMD 模块
(define(['amdModule'], [], require => {
    const dependencyModule1 = require('dependencyModule1')
    const dependencyModule2 = require('dependencyModule2')

    amdModule.inscease()
    amdModule.reset()
}))(
    // 目标:一次去定位区分CJS和AMD
    // 1. CJS factory
    // 2. module module.exports
    // 3. define
    typeof module === 'Object'
        && module.exports
        && typeof define !== 'function'
            ? // 是CJS
                factory => module.exports = factory(require, exports, module, args...)
            : // 是AMD
                define
)

优点:解决了服务端,客户端异步动态依赖的问题 缺点:会有引入成本,没有考虑按需加载

umd 的实现:

(function (root, factory) {
  if (typeof module === "object" && typeof module.exports === "object") {
    console.log("是commonjs模块规范,nodejs环境");
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    console.log("是AMD模块规范,如require.js");
    define(factory);
  } else if (typeof define === "function" && define.cmd) {
    console.log("是CMD模块规范,如sea.js");
    define(function (require, exports, module) {
      module.exports = factory();
    });
  } else {
    console.log("没有模块环境,直接挂载在全局对象上");
    root.umdModule = factory();
  }
})(this, function () {
  return {
    name: "我是一个umd模块",
  };
});

CMD(阿里:按需加载,代表框架:sea.js)依赖就近原则

CMD 是 Common Module Definition(通用模块定义)的缩写; CMD 规范同样专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。

define('module', (require, exports, modules) => {
    let module1 = require('module1'),
    if (xxx) {
        // 代码块
        return ...
    }

    const dependencyModule1 = require('dependencyModule1')
    // 代码块

})

优点:按需加载,依赖就近 缺点:依赖打包,编译阶段完成,扩大了模块内的体积,内部拆分,空间换性能

ESM

  • 目前可以通过 esmodule 或者 commonjs 实现模块化。两者的区别是 esmodule 需要浏览器支持才能用。webpack 是打包出来变成普通的 js 文件,基本上大多数地方都可以直接用。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
import dependencyModule1 from "dependencyModule1";
import dependencyModule2 from "dependencyModule2";

// 代码块1
const inscease = () => {
  // dependencyModule1...
  // ...
};

// 代码块2
const reset = () => {
  // dependencyModule2...
  // ...
};

export default { inscease, reset };

问题:如何处理异步依赖:

// es 方式
const dependsModule = async () => {
  return await dependsModule.init();
};
// 原生方式
import xxx from "xxx";
// es11,类似代码中的cdn远程加载
import("XXX").then((dependModules) => {
  dependModules.init();
});

ES6 模块与 CommonJS 模块的差异:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

优点:通过一种最统一的形态整合了所有的 js 的模块化

总结:

CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD CMD 解决方案;

AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;

CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行;

UMD 为同时满足 CommonJS, AMD, CMD 标准的实现;

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案;