Skip to content
On this page

模块化方案

立即执行函数

早期常用的手段。解决命名冲突污染全局作用域的问题

js
let a = {};
(function(globalVariable){
	globalVariable.test = 1
	// 声明各种变量、函数都不会污染全局作用域
})(a)

console.log(a.test) // 1

CommonJS

  1. commonjs是Node中的模块规范,通过require以及export进行导入和导出。 module.export属于commonjs2
js
// math.js
module.exports = {
  add: (x,y) => {
    return x + y;
  }
}

// main.js
const math = require('./CommonJS/math')
console.log(math.add(1, 1))

  1. cjs是动态加载,可以直接require一个变量,如:
javascript
require(`./${a}`);
  1. require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象,下次require时,直接在缓存中取。

id是模块名,exports是该模块导出的接口,loaded表示模块是否加载完毕。

text
{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

commonjs默认只能在node环境下使用,不能在浏览器中直接使用。

根本原因是缺少moduleexportsrequireglobal四个模块。只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。

另外,由于CommonJS同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。

但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。所以在浏览器端,不适合于CommonJS规范

于是,在浏览器端又出现了一个规范—AMD

AMD

AMD是Asynchronous Module Definition的缩写,采用异步方式加载模块,模块的加载不影响它后面语句的运行。

RequireJS是一个非常小巧的JavaScript模块载入框架,是AMD规范最好的实现者之一。

RequireJS内部,会使用head.appendChild()将每一个模块依赖加载为一个script标签RequireJS等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。

模块功能主要的几个命令:definerequirereturndefine.amd

define是全局函数,用来定义模块,define(id?, dependencies?, factory)

require命令用于输入其他模块提供的功能

return命令用于规范模块的对外接口

define.amd属性是一个对象,此属性的存在来表明函数遵循AMD规范。

我们借助require.js来使用AMD规范:

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>Document</title>
	</head>
	<body>
	  <span>body</span>
	  <!-- AMD -->
	  <!-- require.js在加载的时候会检查data-main属性,当requireJS自身加载执行后,就会再次异步加载data-main属性指向的main.js。 -->
	  <script src="./AMD/require.js" data-main="main.js"></script>
	</body>
</html>

js
// main.js 
// AMD
require(['./AMD/math'], function (math) {
  console.log(math.zero)
  alert(math.add(1, 1));
})

// math.js
// 定义math.js模块,依赖zero模块
define(['./zero'], function (zero) {
  var add = function (x, y) {
    return x + y;
  };
  return {
    add: add,
    zero: zero
  };
});

// zero.js
define(function () {
  return 0;
})

CMD

CMD(Common Module Definition - 通用模块定义)规范主要是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。

AMD类似,不同点在于:

  • AMD 推崇依赖前置、提前执行
  • CMD推崇依赖就近、延迟执行
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>Document</title>
</head>
<body>
  <span>body</span>
  
  <!-- CMD -->
  <script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
  <script>
    seajs.use('./main.js')
  </script>
	
</body>
</html>
js
// main.js
define(function (require, exports, module) {
  var a = require('./CMD/a'); //在需要时申明
  console.log(a.getHello());
  var b = require('./CMD/b'); //在需要时申明
  console.log(b.getHello());
});

// a.js
define(function (require, exports, module) {
  console.log('a.js');
  exports.getHello = function () {
    return 'a';
  }
});

// b.js
define(function (require, exports, module) {
  console.log('b.js');
  exports.getHello = function () {
    return 'b';
  }
});

UMD

一种兼容cjsamd的模块,既可以在 node 环境中被 require 引用,也可以在浏览器中直接用 CDN 被 script.src 引入。

  1. 判断define是否函数,并且是否存在define.amd,来判断是否为AMD规范
  2. 判断module是否为一个对象,并且是否存在module.exports来判断是否为CommonJS规范
  3. 如果以上两种都没有,设定为原始的代码规范。
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>Document</title>
</head>
<body>
  <!-- UMD -->
  <script src="./UMD/math.js"></script>
  <script>
    console.log(math.add(1,1));
  </script>
</body>
</html>
js
(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD
    define(["xxx"], factory);
  } else if (typeof exports === "object" && typeof module !== 'undefined') {
    // CommonJS
    module.exports = factory(require("xxx"));
  } else {
    // 全局变量
    global.math = factory(global["xxx"]);
  }
})(this, function () {
  return {
    add: (x, y) => {
      return x + y;
    }
  }
});

esm(ES Module)

  1. esmtc39 对于 ESMAScript 的模块化规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持

tc39是一个维护和发展 JavaScript的组织。 ESMAScriptJavaScript 的标准,JavaScript是基于ESMAScript的语言。

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>Document</title>
</head>
<body>
  <span>body</span>
  <!-- ES Module -->
  <script src="./main.js" type="module"></script>
</body>
</html>
js
// main.js
import { add } from './ESModule/math.js'
console.log(add(1, 1));

// math.js
export const add = (x, y) => x + y;
  1. 模块的导入导出,通过importexport来确定。 可以和Commonjs模块混合使用。

  2. esm是静态导入,所以在编译期间进行Tree Shaking,减少js体积。

  3. 同时,tc39也为动态加载模块定义了API:import(module),该方法返回的是一个 Promise 对象,可以支持按需加载,大大提高了模块引用的灵活性。

  4. import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。export 命令会有变量声明提前的效果。

import 优先执行:

js
// a.js
console.log('a.js')
import { age } from './b.js';

// b.js
export let age = 1;
console.log('b.js 先执行');

// 执行结果:
// b.js 先执行
// a.js

export 变量声明提升:

js
// a.js
import { foo } from './b.js';
console.log('a.js,b的foo:',foo);
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3() {
  console.log('bar3');
}

// b.js
export let foo = 1;
import * as a from './a.js';
console.log(a);

// node环境下输出结果
[Module: null prototype] {
  bar: <uninitialized>,
  bar2: <uninitialized>,
  bar3: [Function: bar3]
}
a.js,b的foo: 1

esm是未来的趋势。目前一些CDN厂商、前端构建工具都有对cjs模块向esm模块转化的趋势,如skypackvite等。

其他

ESModule和CommonJS的对比

  1. ES modules 输出的是值的引用,而 CommonJS 输出的是值的拷贝
  2. ES modules 模块编译时加载,而 CommonJS 模块总是在运行时加载

TreeShaking

  1. 没有使用额外的模块系统,直接定位import来替换export的模块
  2. 去掉了未被使用的代码

ES Modules 之所以能 Tree-shaking 主要为以下四个原因(摘自尤雨溪在知乎的回答):

  1. import 只能作为模块顶层的语句出现,不能出现在 function 里面或是 if 里面。
  2. import 的模块名只能是字符串常量。
  3. 不管 import 的语句出现的位置在哪里,在模块初始化的时候所有的 import 都必须已经导入完成。
  4. import bindingimmutable 的,类似 const。比如说你不能 import { a } from ‘./a’ 然后给 a 赋值。

副作用

js
// effect.js
console.log(unused());
export function unused() {
    console.log(1);
}

js
// index.js
import {unused} from './effect';
console.log(42);

此例子中 console.log(unused()); 就是副作用。在 index.js 中并不需要这一句 console.log。而 rollup 并不知道这个全局的函数去除是否安全。因此在打包地时候你可以显示地指定treeshake.moduleSideEffects 为 false,可以显示地告诉 rollup 外部依赖项没有其他副作用。

不指定的情况下的打包输出。 npx rollup index.js --file bundle.js

js
console.log(unused());

function unused() {
    console.log(1);
}

console.log(42);

指定没有副作用下的打包输出。npx rollup index.js --file bundle-no-effect.js --no-treeshake.moduleSideEffects

js
console.log(42);

当然以上只是副作用的一种,详情其他几种看查看 treeshake.moduleSideEffects

参考链接

《模块化系列》彻底理清 AMD,CommonJS,CMD,UMD,ES6

MIT Licensed | Copyright © 2021 - 2022