JavaScript modules 模块(一)发展史

前言

使用JavaScript模块很久了,也知道js模块化的标准CommonJS AMD CMD及现在的ES Module写法,一直理不清他们之前的关系及差异。抽时间学习研究记录下。

发展历史

在JavaScript发展的初期,其实JS做一个脚本语言只用于辅助实现页面特效及交互。所有JS的脚本都执行在一个环境变量中(即window全局对象)且脚本的执行顺序是从上到下运行的。但是随着程序的复杂化,程序的全局变量越来越多。造成变量的混乱及相互污染,越来越难以维护等问题。

随着JavaScript和Node.js发展,JavaScript才慢慢有了模块化的方案。不同的方案也在不同端有自己的发展。

在讲解模块话的发展之前,需要说一下JavaScript中一个特别有用的函数表达式IIFE(立即调用函数表达式)。引用MDN上的解释:

IFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数

(function () {
  statements
})();

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

看到这个结构是不是让你想起了什么,没错jQuery,jQuery的标准写法代码如下:

(function($) {    
   // other code here    
 $(document).ready(function() {   
    // other code here    
  });    
})(jQuery);

正是因为IFFE函数的独立的词法作用域这种特性,在JavaScript没有统一模块化之前,相当长的一段时间在浏览器端支持模块化都是使用此类技术,在现在对于使用Babelwebpackvite之类的工具让浏览器端执行CommonJS代码也是以此为基础来实现。

浏览器端

  • AMD (Asynchronous Module Definition) – RequireJS
  • CMD (Common Module Definition)- SeaJS
  • UMD (Universal Module Definition)
  • ES Modules

Node.js

模块化的介绍就按时间线来说吧,在程序发展的复杂程序上,后端往往大于前端。最初的成熟模块化方案也是出现于后端:CommonJS

CommonJS

这个标准最早是由Mozilla工程师Kevin Dangoor于2009年1月发起的,其目标是为JavaScript网页浏览器之外建立模块约定。在CommonJS的规范中,每个js文件都有一个独立的上下文(module context,在Node.js环境下由Node.js提供),文件中所有变量(包括类和函数)都是创建在该对象的下面的私有属性。 要想暴露这些属性需要使用关键字 exports 即通过module.exports.x = xxx,进行导出。

let face = '?';
module.exports = {
  feeling: '?',
  face: face
}
const util = require('./util');

console.log(util) // { feeling: '?', face: '?' }
console.log(util.face) // ?
console.log(util.feeling)// ?

以上就是一个最简单的CommonJS例子,其中的module和require都是由Node.js本身提供。更详细的介绍可以查看这里。但是需要注意的一点就是:CommonJS最早是为服务端设计,且本身依赖的加载是同步的。如果到浏览器的场景下使用,一个模块的实现可能依赖于N个模块。也就是说浏览器瞬时可以会加载N+1模块文件,等待加载完成再来解析页面。对于浏览器来说需要一个异步加载的模块化规范,这就是 AMD

AMD

AMD即Asynchronous Module Definition,“异步模块化定义”。看下基本的定义:

define(id?, dependencies?, factory);

说明:

  • id 为该模块的标识字段
  • dependencies 一般为数组,表示该模块的依赖模块
  • factory 是一个函数,为对应依赖模块加载后返回的对象

对于AMD这个模块化标准,代表性框架就是:RequireJS。下以Require依赖的代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AMD-demo</title>
    <script
      defer
      async="true"
      data-main="js/main"
      src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"
    ></script>
  </head>
  <body></body>
</html>
requirejs(['math'], function(math) {
    console.log(math.add(1, 2))
})

define('math', function() {
    var add = function(a, b) {
        return a + b;

    }

    return {
        add: add
    }
});

最终运行的结果为:

且网络对应的加载文件顺序如下:

Dom结构

总结:从上面的例子可以有如下结果,AMD模块在加载时使用的是异步加载方式,每个模块会通过appendChild将模块,插入到DOM结构中,且在对应的脚本加载完成之后,才会执行对应逻辑代码。define在整个AMD模块规则中,既是引用模块的一种方式,也是定义模块的关键字。 支持异步加载是AMD与CommonJS最不同的地方。

CMD

说完了AMD,就不得不说下玉伯的CMD和SeaJS。初衷是让 CommonJS Modules/1.1 模块也能便捷快速迁移运行在浏览器端(NodeJS 的生态繁荣),因此更贴近 CommonJS 规范。 上一个简单的模块代码

define(function(require, exports) {
    exports.add = function(a, b) {
        return a + b;

    }

});

是不是和AMD的写法都傻傻分不清楚了,都是如下的语法:

define(id?, deps?, factory)

其中的 iddeps是可以省略的,由工具自动生成。需要注意的是:iddeps并不是CMD的规范。是而属于 Modules/Transport 规范(也可以参考知乎文章)。从代码中我们可以看到CMD提倡使用exports或者module.exports进行导出对象(当然也可以使用return)。更多对于CMD的说明可以参考这里

CMD与AMD的区别:

  1. AMD 是提前执行, CMD 是延迟执行(as lazy as possible)( 可以从network面板查看文件加载顺序)
  2. CMD 提倡 依赖就近, AMD提倡依赖前置
    • AMD
requirejs(['math', 'increment'], function(math) {
    			console.log(math.add(1, 2))
})

    • CMD
define(function(require) {
    // 先require就会先加载该文件
    var add = require('./add').add;
    console.log(`2 + 3 = `, add(2,3))
    
    var inc = require('./increment').increment;
    console.log(`5 - 3 = `, inc(5,3))
});

  1. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹

更多可参考:github.com/seajs/seajs…

UMD

UMD(Universal Module Definition)即“通用模块话定义”,该规范的出现,主要为了让一个模块的代码能兼容AMD(CMD)和CommonJS这两种规范。上代码

(function(root, factory){

     if(typeof module === 'object' && module.exports) { // CommonJS
        console.log('执行了CommonJS规范')
        module.exports = factory()
    } 
    // 先判断AMD规范
    // @see https://github.com/favrio/notes/issues/4
    else if(typeof define === 'function' && define.amd) {
        console.log('执行了AMD规范')
        define(factory)
    } else if (typeof define === "function" && define.cmd) {
        // @see https://github.com/Ziphwy/Blog/issues/8
        console.log('执行了CMD规范')
        define(function(require, exports, module) {
            module.exports = factory();
        });
    } else { // 如果都不是以上模块环境,就封装成 IIFE 模块
        console.log('window Global')
        root.returnExports = factory() 
    }
}(this, function() {
    function Time() {
        this._date = new Date();
    }
    return {
        Time: Time,
        name: 'umd-depended'
    }
}))
(function(root, factory){

    typeof define === 'function' && (define.amd) ? define(['umdModuleDepended'], factory) :
    typeof define === 'function' && (define.cmd) ? define(function(require, exports){
        exports = factory(require('./umdModuleDepended'))
    }):
    typeof module === 'object' && module.exports ? module.exports = factory(require('./umdModuleDepended')) : root.returnExports = factory(root.returnExports)
}(this, function(umdModule) {
    console.log(this)
    console.log('我调用了依赖模块', umdModule)
    return {
        name: 'umd-module'
    }
}))

从代码可以看出,是通过判断不同的执行环境,进而执行不同规范的代码。最后默认为浏览器环境。从而达到写一次,上面所有规范全包。缺点就是代码看起来有点臃肿。

ESM(ES Module)

百家绽放的模块化规范,终于在2015年的版本 ECMAScript 2015(ES6)进行了统一。官方发布自己的模块化规范。

在ESM标准里使用import引入其它模块的内容,使用export来导出模块

export function add (a, b) {
    return a + b;
}

export function increment (a, b) {
    return a - b;
}

import { add } from "./add.js";
import { increment } from "./increment.js";



console.log('3 + 2 = ', add(3,2))
console.log('3 - 2 = ', increment(3, 2))
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ESM-demo</title>
    <!-- 浏览器里加载模块必须使用 type="module" -->
    <script type="module" src="js/index.js"></script>
  </head>

区别

最后总结下CommonJS和ESM之间的区别:

  1. ESM的导出是值引用导出,如果通过方法修改了导出的值,其它地方再次导入会使用新值,而CommonJS是值导出(即复制一份)
// add.js
export let addInit = 1;



export function computeInit() {
    addInit++
}



// index.js
console.log('index.js addStr = ', addInit) // index.js addStr =  1
computeInit()
console.log('index.js addStr = ', addInit) // index.js addStr =  2
// utils.js
let face = '?';



function changeFace () {
  face = '?';
}

module.exports = {
    face: face,
    changeFace: changeFace
}

// index.js
const util = require('./util');
console.log(util.face) // ?

util.changeFace()
console.log(util.face) // ?
  1. CommonJS是运行时加载,ESM是编译时加载
  2. CommonJS的require是同步加载,而ESM的import时异步加载,有一个独立的模块解析阶段

参考

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

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY4ft46m' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片