几种常用的模块化规范总结

988 阅读6分钟

0030.jpg

一、CommonJS规范

在CommonJS目录下创建三个文件:

  • CommonJS/moduleB.js
module.exports = new Date().getTime();
  • CommonJS/moduleA.js
const timestamp = require('./moduleB');
console.log('moduleA:::', timestamp);
  • CommonJS/index.js
require('./moduleA');
const timestamp = require('./moduleB');
console.log('index:::', timestamp);

其中,CommonJS/index.js引用了moduleA,而moduleA中引用了moduleB,另外,CommonJS/index.js还直接引用了moduleB,可见moduleB是被引用了两次。在这样的ModuleB被两次依赖的情况下,ModuleB是否会被执行两次呢?

通过执行node CommonJS/index.js我们来看看输出结果:

moduleA::: 1617595616025
index::: 1617595616025

可以看出,虽然ModuleB被依赖了两次,但是输出的时间戳的值是一样的,可见,ModuleB并没有被执行两次。

总结一下,关于CommonJS规范:

1、JavaScript原生并不支持该规范,在没有经过打包工具打包的情况下,它是只可以在Node.js环境中运作的(在浏览器环境下不支持该规范,会报错);

2、CommonJS模块被多次引用时,会保持模块是一个单例,而不会被重复执行多次;

3、用module.exports导出模块,用require引入模块。

二、AMD规范

受到CommonJS模块化规范的启发,浏览器端逐步发展起来了AMD规范。AMD全称为Asynchronous module definition,意为异步的模块定义。正如其名,所有模块默认都是异步加载的,这也是当时为了满足Web开发的需要,因为如果在Web端也像CommonJS规范那样同步加载的话,那么页面在解析脚本文件的过程中可能由于耗时过长,而使得页面暂停响应。

在AMD文件夹下新建4个文件:

  • AMD/index.html
<!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>test AMD</title>
</head>
<body>
  <h2>test AMD</h2>
  <script src="https://requirejs.org/docs/release/2.1.16/minified/require.js"></script>
  <script src="./index.js"></script>
</body>
</html>
  • AMD/index.js
require([
  './moduleA.js',
  './moduleB.js'
], function(moduleA, moduleB) {
  console.log('index:::', moduleB);
});
  • AMD/moduleA.js
define(function (require) {
  const timestamp = require('./moduleB.js');
  console.log('moduleA:::', timestamp);
});
  • AMD/moduleB.js
define(function (require) {
  return new Date().getTime();
});

然后执行npm install -g anywhere安装静态服务启动工具anywhere,并执行

cd AMD
anywhere -p 80

在80端口上启动静态资源服务器。

然后通过 http://localhost 访问该服务,在Chrome Devtools的console面板上可看到如下输出:

moduleA::: 1617597603392
index::: 1617597603392

这表明同CommonJS规范一样,AMD规范中,模块被多次引用时,也是单例的,不会被执行多次。

接下来,我们把AMD/index.js文件的内容改成如下,即去除AMD/index.js对moduleB的依赖:

require([
  './moduleA.js',
], function(moduleA) {
});

然后,我们打开Chrome Devtools的Network面板,把网络模式Slow 3G(如下图)。

image.png

然后刷新一下页面,查看到各JS文件的加载顺序如下图:

image.png

可见,ModuleA和ModuleB这两个AMD模块确实是异步加载的,且因为ModuleA所依赖的ModuleB,也是在ModuleA加载完之后才去异步地加载ModueB的。

AMD异步加载的核心原理是使用了JSONP来加载模块。

总结一下:

1、AMD规范是运行在浏览器端的,不能运行在Node.js环境中。

2、AMD默认是支持模块异步加载的。因为如果在Web端也像CommonJS规范那样同步加载的话,那么页面在解析脚本文件的过程中可能由于耗时过长,而使得页面暂停响应。

3、AMD模块被多次引用时,会保持模块是一个单例,而不会被重复执行多次;

4、用define(function (require) {})(匿名模块,以文件名为模块名) 或者 define('模块id', ['依赖数组'], function([依赖数组]){});(具名模块)定义模块,用require(['./xxx.js'], function(xxx) {})引入模块,其中第二个参数是回调。

三、UMD规范

UMD全称是Universal Module Definition(通用模块规范),它是由社区想出来的一种整合了CommonJS和AMD两个模块定义规范的方法。所以,严格意义上说,它并不是一个独立的模块化规范。其基本原理是用一个工厂函数来统一不同的模块定义规范。

下面基于UMD规范写一个简单的例子(模块内容只是纯演示,该内容不是一个值得封装成模块的东西):

  • UMD/index.html
<!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>test 111</title>
</head>
<body>
  <button id="btn">按钮</button>
  <div id="container"></div>
  <script src="./jquery.js"></script>
  <script src="./moduleA.js"></script>
  <script src="./index.js"></script>
</body>
</html>
  • UMD/index.js
window.moduleA.init();
  • UMD/jquery.js

略去,从网上下载一个jQuery.js文件放在这里即可。

  • UMD/moduleA.js
((global, factory) => {
  //如果 当前的上下文有define函数,并且AMD  说明处于AMD 环境下
  if (typeof define === 'function' && define.amd) {
    // 检查AMD是否可用
    define(['./jquery'], factory);
  } else if (typeof exports === 'object') {
    // 检查CommonJS是否可用
    modules.exports = factory(require('./jquery'));
  } else {
    // 两种都不能用,把模块添加到JavaScript的全局命名空间中
    global.moduleA = factory(global.jQuery);
  }
})(this, ($) => {
  //本模块的定义
  function init() {
    var $container = $('#container');
    var count = 0;
    var text = ['关', '开'];
    document.getElementById('btn').addEventListener('click', function() {
      count += 1;
      $container.html(text[count % 2]);
    });
  }

  return {
    init: init
  }
})

四、ESModule

不管是CommonJS还是AMD,都是由语言上层的运行环境中实现的模块化规范,模块化规范由环境自己定义。但是ESModule是在语言层面实现的。

image.png

先创建如下5个文件:

  • ESModule/index.html
<!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>test ESModule</title>
</head>
<body>
  <h2>test ESModule</h2>
  <script src="./dist/index.bundle.js"></script>
</body>
</html>
  • ESModule/index.js
import './moduleA.js';
import timestamp from './moduleB.js';

console.log('index:::', timestamp);
  • ESModule/moduleA.js
import timestamp from './moduleB.js';

console.log('moduleA:::', timestamp);
  • ESModule/moduleB.js
export default new Date().getTime();

然后安装依赖:

yarn add webpack webpack-cli @babel/core babel-loader @babel/preset-env

并修改package.json中的scripts为:

"scripts": {
    "build": "webpack"
},

执行npm run build进行打包。然后执行anywhere -p 80启动静态服务器。

然后通过 http://localhost 访问该服务,在Chrome Devtools的console面板上可看到如下输出:

moduleA::: 1617601623808
index::: 1617601623808

不过,由于兼容性的问题,ESModule目前在浏览器环境下还不能直接使用,需要进行打包编译。

Babel会将ES6中的importexport编译成CommonJS规范的requireexports。如下图所致,左边是编译前的代码,右边是编译后的代码(可以到www.babeljs.cn/中体验测试):

image.png

但是我们知道,CommonJS是无法运行在浏览器端的,怎么让它能够变成能运行在浏览器端的代码呢?这就是打包要做的事情了。

那么Node.js中是如何做到能运行CommonJS模块的呢?

例如,对于这样一段CommonJS代码:

require('./moduleA');
const timestamp = require('./moduleB');
console.log('index:::', timestamp);

它是这样处理的,通过fs模块读到模块的代码字符串,然后在其头部拼接上function(require, module, exports) {,在其尾部拼接上},最后将拼接得到的字符串放入vm.runInNewContextAPI中执行:

const str = 'require('./moduleA');const timestamp = require('./moduleB');console.log('index:::', timestamp);'

const functionWrapper = [
    'function(require, module, exports) {',
    '}'
];

const result = functionWrapper[0] + str + functionWrapper[1];
const vm = require('vm');

vm.runInNewContext(result);

vm是Node.js中的一个内置模块,其runInNewContext方法的作用相当于new Function(codeStr)()参见这里)或者 eval(codeStr)参见这里),关于runInNewContext方法的用法可参见Node.js的官方文档

此外,还有人可能用过CMD规范,但因为它毕竟当前应用已经不那么多了,这里就不介绍了,感兴趣的可以自行了解下。