模块化发展史

462 阅读7分钟

模块化不是一个陌生的词,听说过很多次,也了解了很多次,但是一直没有深入了解,这篇笔记用来将模块化知识串联起来,希望能有更深的理解。

主要有以下内容:

  • 模块化之前:IIFE介绍
  • 模块化发展史
  • 模块化使用方法和特点(CommonJS、AMD、CMD、UMD、ESM)
  • webpack和rollup处理模块化

没有模块化的时候(IIFE)

在模块出现之前,人们在写js的时候遇到了问题:全局污染(变量冲突)

// 全局污染
<html>
    <script>
        var name = "fanfan"
        console.log(name) // "fanfan"
    </script>
    <script>
        function name() {}
    </script>
    <script>
        console.log(name) // ƒ name() {}
    </script>
</html>

所以人们采用各种方法达成隔离变量的功能,比如:通过不同对象隔离和立即执行函数(IIFE)隔离,下面我们通过在同一脚本中和不同脚本中的两种使用方法来介绍立即执行函数

同一脚本中IIFE的使用方法

<html>
    <script>
        var name = (function() {
            var name = "fanfan"
            // 通过return实现导出
            return name
        }());
        // 注意使用';'隔开,不然下面的部分会被当成函数传参,会报错
        (function() {
            function name() {}
        }());

        (function(arg) {
            console.log(arg) // "fanfan"
        }(name));
        // 通过参数方式实现导入功能
    </script>
</html>

不同脚本中IIFE的使用方法

// 立即执行函数
<html>
    <script>
        (function() {
            var name = "fanfan"
            // 通过挂载到window上实现导出功能
            window.name = name
            console.log(name)
        }())
    </script>
    <script>
        (function() {
            function name() {}
        }())
    </script>
    <script>
        (function(arg) {
            console.log(arg)
        }(name))
        // 通过参数方式实现导入功能
        // 这里的name === window.name, 就是上面script导出的
    </script>
</html>

注意: 独立性是模块的重要特点,模块内部最好不要直接使用其他模块的变量。可以通过参数方式导入,这样除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

如果在不同脚本中使用立即执行函数, 将有以下缺点:

  1. 输出的变量可能影响全局变量(如上面window.name = name);引入依赖包时依赖全局变量。
  2. 需要使用者自行维护 script 标签的加载顺序,否则将导致代码运行顺序出错。

模块化的发展

我觉得虽然使用方法非常重要,但是了解下模块化发展史可以帮助我们理解模块化以及更好的记住他们。

模块化一开始出现在服务端,为什么服务端需要模块化呢?因为服务端需要与操作系统和其他应用模块互动,而模块化的作用就表现出来了,既能很好的划分作用也能让不同作用的模块相互配合和使用。

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。node.js的模块系统是参照CommonJS规范实现的。

CommonJS适用于服务端编程但是却不适用于浏览器端编程,主要原因是CommonJS采用同步方式加载模块,在服务端中模块存储在本地电脑磁盘上(加载模块时间取决于读取磁盘速度),所以使用同步方式加载模块不会使用过多时间。而在浏览器端,一些模块存储在服务器中,需要通过网络获取(加载模块时间取决于网络速度),如果采用同步方式加载模块,那么过长的加载时间就会影响后面代码的运行(浏览器假死情况)。所以服务端可以使用同步加载模块,但是浏览器端需要使用异步加载模块。

这时产生了AMD(Asynchronous Module Definition)规范,而require.js就是这个规范的js实现库(还有curl.js)。还有一种CMD(Common Module Definition)规范,此规范其实是在sea.js推广过程中产生的。它与AMD很类似,不同点在于:AMD推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行

现在我们有了服务端模块化规范和浏览器端模块化规范,然后人们就想要一种通用的既可以运行在服务端又可以运行在浏览器端的模块化规范,就是UMD(Universal Module Definition),主要就是通过判断不同模块化规范进行不同操作。

不管CommonJS还是AMD还是CMD都是运行时加载,ESM(ES Module)提出了完全不一样的加载方式:编译时加载,ESM是javascript提出的规范,可以说是最强的也是我们现在用的最多的模块化规范。它主要有以下特性:

  • 写法简单(只需要import和export)
  • 异步加载
  • nodejs和浏览器端通用于一体
  • 编译时加载,使得后续tree-shaking成为可能
  • 也可通过import()实现运行时加载(懒加载)

以上就是js模块化的发展史,注意有了模块化概念之后,我们一般认为一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数

不同模块的使用方法和特性

以下内容是从不同文章复制过来的,会贴上不同文章的链接

前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS

Javascript 中的 CJS, AMD, UMD 和 ESM是什么?

CommonJS

使用方法

// importing 
const doSomething = require('./doSomething.js'); 

// exporting
module.exports = function doSomething(n) {
	// do something
}

特性

  • node.js使用的模块规范,在浏览器中无法使用,如需使用需要进行处理(转换和打包)
  • 同步导入模块
  • 导入CommonJS时,导入对象的副本

AMD

使用方法

// 定义math.js模块
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});

/ 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

特性

  • AMD 是异步(asynchronously)导入模块的,
  • 一开始被提议的时候,AMD 是为前端而做的(而 CJS 是后端)
  • AMD推崇依赖前置、提前执行

CMD

使用方法

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});

特性

  • 其他特性与AMD一致
  • CMD推崇依赖就近、延迟执行

UMD

使用方法

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["jquery", "underscore"], factory);
    } else if (typeof exports === "object") {
        module.exports = factory(require("jquery"), require("underscore"));
    } else {
        root.Requester = factory(root.$, root._);
    }
}(this, function ($, _) {
    // this is where I defined my module implementation
    var Requester = { // ... };
    return Requester;
}));

特性

  • 在前端和后端都适用(“通用”因此得名)
  • 与 CJS 或 AMD 不同,UMD 更像是一种配置多个模块系统的模式。这里可以找到更多的模式
  • 当使用 Rollup/Webpack 之类的打包器时,UMD 通常用作备用模块

ESM

使用方法

import {foo, bar} from './myLib';

...

export default function() {
	// your Function
};
export const function1() {...};
export const function2() {...};

html中调用

<html>
    <script type="module">
      import {func1} from 'my-lib';
      func1();
    </script>
</html>

特性

  • 写法简单(只需要import和export)
  • 异步加载
  • nodejs和浏览器端通用于一体
  • 编译时加载,使得后续tree-shaking成为可能
  • 也可通过import()实现运行时加载(懒加载)

面试常问CommonJS和ESM区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

参考:ES6 模块与 CommonJS 模块的差异

webpack、rollup中的模块化

现在我们写代码主要使用的就是ESM模块,但是由于一些浏览器对ESM不支持,所以在实际项目中我们还需要使用一些打包工具帮我们进行模块的转换(值得一提的是vite在开发模式就是利用浏览器原生支持ESM达到快速的项目启动速度和热更新)。我们主要讲webpack和rollup对于模块化的处理。

webpack支持不同模块化文件的转换,最终产物是一个IIFE函数。rollup也支持不同模块化文件的转换,最终产物可通过format字段指定(可指定6种格式)。

参考

因为本文主要是想要将模块化的知识点串接起来,所以有些部分可能写的不是特别详细,下面列出是一些关于模块化的好文章,也是这篇文章参考的文章: