模块化历程

模块化

模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能。

基于 Node.js 的服务端 CommonJS 模块化,到前端基于浏览器的 AMDCMD 模块化,再到 ECMAScript 6 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。

模块化历程

  • CommonJS
  • AMD
  • UMD
  • ESM

无论是那种模块化规范,都着重于保证模块独立性 的同时又能很好的 与其它模块进行交互

  • 如何定义一个模块与模块内部私有作用域
  • 通过何种方式导出模块内部数据
  • 通过何种方式导入其它外部模块数据

基于服务端、桌面端的模块化

CommonJS

在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS 规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。

因为 NodeJS 就是 CommonJS 的实现,所以使用 node 就行,不用引入其他包

  • 独立模块作用域

一个文件就是模块,拥有独立的作用域

  • 导出模块内部数据

通过 module.exportsexports 对象导出模块内部数据

// ./a.js
let a = 1;
let b = 2;

module.exports = {
    x: a,
    y: b
}
// or
exports.x = a;
exports.y = b;
复制代码

exportsmodule.export 的区别

exports:对于本身来讲是一个变量(对象),它不是 module 的引用,它是 {} 的引用,它指向module.exports{} 模块。 只能使用 . 语法 向外暴露变量。

module.exportsmodule 是一个变量,指向一块内存。exportsmodule 中的一个属性,存储在内存中,exports 属性指向 {} 模块既可以使用 . 语法,也可以使用 = 直接赋值。

require 引用模块后,返回给调用者的是 module.exports 而不是 exports

  • 导入外部模块数据 require

通过 require 函数导入外部模块数据,必须加 ./ 路径,不加的话会去 node_modules 文件找

导入自定义的模块时,参数包含路径,可省略后缀

// ./b.js
let a = require('./a'); // 导入自定义模块,可省略后缀
const Koa = require('koa'); // 不加 `./` ,导入 node_modules 内的模块
const fs = require('fs'); // 引用核心模块,不需要带路径

a.x;
a.y;
复制代码

基于浏览器的模块化

AMD

CommonJS同步的方式加载模块(基于文件系统)。在服务端,模块文件都存放在本地磁盘,读取非常快,所以这样做不会有问题。

但是在浏览器端,限于网络原因,更合理的方案是使用异步加载,所以另外定义了适用于浏览器端的规范:AMD(Asynchronous Module Definition)github

AMDrequireJS 在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS 是对这个概念的实现。

requireJS

requireJS依赖前置、异步定义

  • 引用 requireJS
<!-- ./index.html -->

<!-- cdn -->
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js" data-main="./js/a.js"></script>

<!-- or -->

<!-- 本地引入 -->
<script src="./libs/require.min.js" data-main="./js/a.js"></script>
复制代码
  • 独立模块作用域 define

通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域

// ./js/b.js
define(function() {
    // 模块内部代码
})
复制代码
  • 导出模块内部数据 return

通过 return 导出模块内部数据

// ./js/b.js
define(function() {
    // 模块内部代码
    let a = 1;
    let b = 2;
    
    // 通过 return 导出
    return {
        x: a,
        y: b
    }
})
复制代码
  • 导入外部模块数据

如果我们定义的模块本身依赖其他模块,那就需要将它们放在 [] 中作为 define() 的第一参数。通过前置依赖列表导入外部模块数据。

// ./js/a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(b) {
    console.log('define', b);
})

// 引用 b 模块
require(['./b'], function(b) {
    console.log('require', b);
});
复制代码
requireJS 的 config 配置

首先我们需要引入 require.js 文件和一个入口文件 main.js。main.js 中配置 require.config() 并规定项目中用到的基础模块。

/** 网页中引入 requireJS 及 main.js **/
<script src="./libs/require.min.js" data-main="./js/main.js"></script>

/** main.js 入口文件/主模块 **/
// 首先用 config() 指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  // 实际路径为 js/lib/jquery.min.js
  }
});
复制代码

引用模块的时候,我们将模块名放在 [] 中作为 reqiure() 的第一参数

// ./js/math.js

// 定义 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);
});
复制代码
requireJSCommonJS 风格

require.js 也支持 CommonJS 风格的语法:

  • 导出模块内部数据
// ./js/b.js
define(function(require, exports, module) {
    let a = 1;
    let b = 2;
    
    // 通过 module.exports 对象导出
    // exports 与 module.exports 是一样的
    module.exports = {
        x: a,
        y: b
    }
})
复制代码
  • 导入外部模块数据
// ./js/a.js
define(function(require, exports, module) {
    // require 与 前置列表 相似
    let b = require('./b')
    console.log(b);
})
复制代码

CMD

AMD的实现者require.js在申明依赖的模块时,会在第一时间加载并执行模块内的代码:

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等于在最前面声明并初始化了要用到的所有模块
    if (false) {
      // 即便没用到某个模块 b,但 b 还是提前执行了。**这就CMD要优化的地方**
      b.foo()
    } 
});
复制代码

CMD 是另一种 js 模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行CMD 推崇依赖就近、延迟执行

通过 define() 定义没有依赖前置,通过 require 导入

define(function(require, exports, module) {
    var a = require('./a'); // 在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});
复制代码

CMDSeaJS 在推广过程中对模块定义的规范化产出,是一个同步模块定义。SeaJSCMD 概念的一个实现, SeaJS 是淘宝团队提供的一个模块开发的 js 框架。

SeaJS

SeaJS

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

// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});
复制代码

AMDCMD 的优缺点

AMDCMD 的优缺点,一个的优点就是另一个的缺点。

  • AMD

    • 优点:加载快速,尤其遇到多个大文件,因为并行解析,所以同一时间可以解析多个文件
    • 缺点:并行加载,异步处理,加载顺序不一定,可能会造成一些困扰,甚至为程序埋下大坑。
  • CMD

    • 优点:只有在使用的时候才会解析执行js文件执行顺序在代码中是有体现的,是可控的。
    • 缺点:使用时执行,没法利用空闲时间,执行等待时间会叠加。

UMD

严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,使模块代码能在前面不同的模块环境下都能正常运行。

随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式。

// root 解决不同环境下全局变量不同的问题,如浏览器是 window,node.js 是 global
(function (root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
        // AMD 模块环境下
        define(factory);
    } else {
        // 不使用任何模块系统,直接挂载到全局
        root.kkb = factory();
    }
}(this, function () {
    let a = 1;
    let b = 2;

    // 模块导出数据
    return {
        x: a,
        y: b
    }
}));
复制代码

JavaScript 原生模块化

ESM

ECMAScript 6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一。

  • 独立模块作用域

一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict'

script 标签需要声明 type="module"

<script type="module"></script>
复制代码
  • 导出模块内部数据

使用 export 语句导出模块内部数据。

export 可以导出多个export default 只能导出一个

export 导出的,导入时命名要保持一致export default 导出的,导入时命名可以自定义

// 1. 直接导出
export let name1 = …, name2 = …, …, nameN;
export function FunctionName(){...}
export class ClassName {...}

// 2. 导出列表
export { name1, name2, …, nameN };

// 3. 重命名导出
// 关键字 "as" 重命名。as 前是原名,as 后是新名。注意不是解构赋值哦。
export { variable1 as name1, variable2 as name2, …, variableN as nameN };

// 4. 默认导出,只能导出一个
export default expression;
export default function () { … }
export default function name1() { … }

// 5. 模块重定向导出 ???
export * from …; // 通配符 "*" 导出
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
复制代码
  • 导入外部模块数据

    • 静态导入 import

    使用 import 语句导入模块,这种方式称为:静态导入

    静态导入方式不支持延迟加载,import 必须在模块的最开始

    // 直接导入,里面的内容直接用
    import "module-name";
    
    // export default 导出的,导入时命名可以自定义
    import defaultExport from "module-name";
    
    // 通配符 "*" 导入
    import * from "module-name";
    import * as name from "module-name";
    
    // export 导出的,导入时命名要保持一致
    import { export1 } from "module-name";
    import { export1 as alias } from "module-name";
    import { export1 , export2 } from "module-name";
    import { export1 , export2 as alias2 , [...] } from "module-name";
    
    // 前面是默认导出的,后面是其余导出的
    import defaultExport, { export1 , export2 } from "module-name";
    import defaultExport, * as name from "module-name";
    复制代码

    函数内的静态导入,import 也要在函数内部最开始的位置

    document.onclick = function () {
    
        // import 必须放置在当前模块的最开始加载
        import m from './m.js'
        console.log(m);
    
    }
    复制代码
    • 动态导入/按需导入 import()

    此外,还有一个类似函数的动态 import(),它不需要依赖 type="module"script 标签

    关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise

    let res = import("./m.js");
    console.log(res);
    
    import('./m.js').then(m => {
        console.log(m)
    });
    
    // 也支持 await
    let m = await import('./m.js');
    复制代码

    通过 import() 方法导入返回的数据会被包装在一个对象中,即使是 default 也是如此。

导出导入对应使用

/* 1. 直接导入 */
// ./a.js 导出
let a = 10;

// ./index.html 导入
import './a.js';
console.log("a"); // 10

/* 2. 导出列表 */
// ./a.js 导出
export let a = 10;
export let b = 20;

// index.html 导入
// export 导出的,命名要保持一致,可以重命名。
import {a as c, b} from './a.js';
console.log(b); // 20
console.log(c); // 10

/* 3. 默认导出 */
// ./a.js 导出
// export 导出:导出多个
export let a = 10;
export let b = 20;
// export default :导出一个  
let obj = {
    fn(){
        console.log("fn");
    }
}
export default obj;
// 等同于
// export {obj as default};

// ./index.html 导入
// export default 导出的,命名可以自定义
import myfn, {a, b} from './a.js';
myfn.fn(); // 'fn'

/* 4. 通配符 "*" 导入 */
// ./a.js 导出
export let obj = {
  x: 1
}

// ./index.html 导入
import * as m1 from './a.js'
console.log(m1.obj.x) // 1
复制代码

ESMCommonJS 的差异

  • CommonJS 输出的是一个值的拷贝ESM 输出的是值的引用

    • CommonJS 输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

    • ESM 的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ESM动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

  • CommonJS运行时加载,ES6 是编译时输出接口

    • 运行时加载: CommonJS 就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。

    • 编译时加载: ESM 不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。模块内部引用的变化,会反应在外部。

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

首先看个 CommonJS 输出拷贝的例子:

/* CommonJS */
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
module.exports = { a, b };

// main.js
let {a, b} = require('./a');
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 1
    console.log(b);  // { num: 1 }
}, 500);
复制代码

所谓输出拷贝,如果了解过 NodeJS 或者 webpack 对 CommonJS 的实现(不了解可以看这篇文章),就会知道:exports 对象是模块内外的唯一关联, CommonJS 输出的内容,就是 exports 对象的属性,模块运行结束,属性就确定了

再看 ESM 输出的例子:

/* ESM */
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
export { a, b };

// main.js
import {a, b} from './a';
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 2
    console.log(b);  // { num: 2 }
}, 500);
复制代码

ESM 模块内部引用的变化,会反应在外部

总结

  • AMD / CMD / CommonJs 是 js 模块化开发的规范,对应的实现是 require.js / sea.js / Node.js

  • UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,使模块代码能在前面不同的模块环境下都能正常运行。

  • CommonJs 主要针对服务端,AMD / CMD / ESM 主要针对浏览器端。

  • AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。

  • CommonJsESM 区别:CommonJs 模块输出的是一个值的拷贝,ESM 输出的是值的引用。

针对服务器端和针对浏览器端有什么区别?

服务器端一般采用 同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行。
浏览器端要保证效率,需要采用 异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享