模块化历程

前言

在李录的《文明、现代化、价值投资与中国》里,他把人类的文明史分为三大跃升阶段:即 1.0 狩猎采集文明、2.0 农业文明和 3.0 科技文明。针对 JavaScript 的模块化,笔者认为模块化历程也可类比为这样的历程。在前端页面不复杂的情况下,我们只需引入需要的库、js 文件或模块,这就像“狩猎采集文明”时,饿了去打猎、摘果子一样,及时反馈,速度 NO1;后来前端页面复杂起来,各路前辈将优秀的模块思维带入前端,有了各种模块化方案,这就好比农业文明时代,你需要按照一种规范或说标准来写,它有约束性,但能极高的解决 1.0 时代的缺点——依赖顺序。有代表性的库如 CommonJS,RequireJS,SeaJS。再后来就是目前的 3.0 科技文明阶段,前端需要各种打包器来进行打包,它不再是简简单单的脚本文件,写几个文件就引用那么简单了,在这个阶段最出名是的 webpack,也有 gulp,rollup。当然现在还有 vite 和 snowpack 等。接下来让我们揭开模块化的历程,从历史演变来看清模块化的本质

狩猎采集阶段——IIFE 主导全球化

模块化解决的问题:

  • 命名冲突

  • 文件依赖

CommonJS

  1. 模块可以 i 多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
  2. 模块记载会阻塞接下来代码的执行,需要等到模块加载完成才能继续执行——同步加载

适用场景:服务器环境。nodejs 的模块规范是参考 commonJS 实现的

用法:

1.导入:require("路径")

2.导出:module.exports 和 exports

// a.js
export.a = "hello world";

// b.js
var moduleA = require('./a.js')
console.log(moduleA.a);	// hello world

module.exports 和 exports 的区别是 exports 是 module.exports 的一个引用,详单与 Node 为每个模块提供一个 exports 变量,指向 module.exports。这相当于在模块顶部,有一行 var exports = module.exports 这样的命令

以上例子可以理解为:

// 默认执行 var exports = module.exports
exports.a = 'hello world'; // 相当于:module.exports.a = 'hello world'
...

AMD

1.异步加载

2.管理模块之间的依赖性,便于代码的编写和维护

适用场景:浏览器环境,requireJS 是参考 AMD 规范实现的

用法:

1.导入:require(['模块名称'], function())

因为 Node ,有了 CommonJS,所有大家有了想在浏览器端实现模块化的想法,这个时候分为三个分支,amd,cmd 以及 es6,amd 的代表库是 requirejs,cmd 的代表是玉伯的 seajs,后来 es6 才是集大成者。而玉伯的 seajs 是从 requirejs 中分离

无论是 cmd 还是 amd 都是浏览器上的规范,是为了想在浏览器端实现模块化而做的

为什么会有模块化

  • 网站正在变成 Web Apps
  • 随着站点的扩大,代码的复杂性也在增加
  • 需要高度解耦的 JS 文件/模块
  • 部署需要几个 HTTP 调用中的优化代码

最开始的模块

从设计模块说起

function foo() {}

function bar() {}

全局被污染,很容易命名冲突

简单封装:命名空间模式

var MYAPP = {
  foo: function () {},
  bar: function () {},
};

MYAPP.foo();

减少全局上的变量数

本质是对象,一点都不安全

匿名闭包:IIFE 模块

var Module = (function () {
  var _private = 'safe now';
  var foo = function () {
    console.log(_private);
  };
  return {
    foo: foo,
  };
})();

Module.foo(); // 私有变量
Module._private; // undefined

函数是 JavaScript 唯一的 Local Scope(作用域)

再增强一点:引入依赖

var Module = (function ($) {
  var _$body = $('body'); // we can use jQuery now!
  var foo = function () {
    console.log(_$body); // 特权方法
  };
  return {
    foo: foo,
  };
})(jQuery);

Module.foo();

这就是模块模式,也是现代模块实现的基石

石器时代——脚本加载器

只有封装性可不够,我们还需要加载

让我们回到脚本标签

body script(src="jquery.js") script(src="app.js") // do some $ thins...

顺序是根本

并行加载

DOM 顺序即执行顺序

但现实是这样的...

body script(src="zepto.js") script(src="jhash.js") script(src="fastClick.js")
script(src="iScroll.js") script(src="underscore.js") script(src="handlebar.js")
script(src="datacenter.js") script(src="deferred.js")
script(src="util/wxbridge.js") script(src="util/login.js")
script(src="util/date.js") script(src="app.js") ...

难以维护

依赖模糊 不清楚的依赖关系

请求过多的 HTTP 调用

LABjs-脚本加载器

时间:2009 年

作用:加载和阻止 JavaScript

效果:使用 LABjs 将取代所有难看的“脚本标签”

它是怎么工作的
script(src="LAB.js" async)
$LAB.script("framework.js").wait()
	.script("plugin.framework.js")
	.script("myplugin.framework.js").wait()
	.script("init.js")

先到先得(执行顺序不重要)

另外
$LAB .script(["script1.js", "script2.js", "script3.js"]) .wait(function() { //
wait for all scripts to execute first script1Func(); script2Func();
script3Func(); })

它基于文件的依赖管理

蒸汽朋克——模块加载器

YUI3 Loader - Module Loader

2009

YUI 的轻量级内核和模块化体系结构使其具有可扩展性,快速性和鲁棒性

写法

// YUI - 编写模块
YUI.add("dom", function(Y) {
    Y.DOM = { ... }
})

// YUI - 使用模块
YUI().use('dom', function(Y) {
    Y.DOM.doSomeThing()
     // use some methods DOM attach to Y
})

创建自定义模块

// hello.js
YUI.add(
  'hello',
  function (Y) {
    Y.sayHello = function (msg) {
      Y.DOM.set(el, 'innerHTML', 'Hello!');
    };
  },
  '3.0.0',
  {
    requires: ['dom'],
  },
);
// main.js
YUI().use('hello', function (Y) {
  Y.sayHello('hey yui loader');
});

基于模块的依赖管理

让我们再深入一下

function Sandbox() {
  for (i = 0; i < modulex.length; i += 1) {
    Sandbox.modulex[modules[i]](this);
  }
}

YUI 其实是一个强沙箱

所有依赖模块通过 attach 的方式被注入沙盒

但还是”脚本标签“

script(src="/path/to/yui-min.js") script(src="/path/to/my/module1.js")
script(src="/path/to/my/module2.js") script(src="/path/to/my/module3.js")
YUI().use('module1', 'module2', 'module3', function (Y) {
  // you can use all this module now
});

你不必按照固定顺序写脚本写标签

加载与执行分离

漏了一个问题

HTTP 调用过多

YUI 组合的工作原理

script(src="http://www.baidu.com/build/yui/yui-min.js")
script(src="http://www.baidu.com/build/dom/dom-min.js")

改造成

script(src="http://www.baidu.com/combo?
       build/yui/yui-min.js&
       build/dom/dom-min.js")

在单个请求中处理多个文件

GET 请求,需要服务器支持

以及

合并 concat

压缩 Minify

混淆 丑化

CommonJS

CommonJS

2009.08

不仅适用于浏览器

用法:
// math.js
exports.add = function (a, b) {
  return a + b;
};
// main.js
var math = require('./math.js');
console.log(math.add(1, 2)); // 3

AMD / CMD——浏览器环境预设方案

AMD(Async Module Definition)

RequireJS 对模块定义的规范化产出

CMD(Common Module Definition)

SeaJS 对模块定义的规范化产出

browserify/gulp/webpack

CommonJs in Browser

npm install -g browserify

只需要写 CommonJs 的代码,用 browserify 帮你编译就好

browserify main.js -o bundle.js

Browserify 分析 AST 的 require() 调用,以遍历项目的整个依赖关系图

Webpack-集大成者

几乎对任何资源或资产进行转换,捆绑或打包

Rollup

ES6 模块

用 ES6 的模块来写,然后用 babel 转移成低版本的 JavaScript 语法

Vite

我想说的就是模块化的发展啊

介绍模块化发展历程

IIFE(声明即执行的函数表达式),特点:在一个单独的函数作用于中执行代码,避免变量冲突

(function () {
  return {
    data: [],
  };
})();

AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好

define('./index.js', function (code) {
  // code 就是index.Js 返回的内容
});

CMD: 使用 seaJS 来编写模块化,特点: 支持动态引入依赖

define(function (require, exports, moduke) {
  var indexCode = require('./index.js');
});

CommonJS: nodejs 中自带的模块化

var fs = require('fs');

exports 指向 module.exports,即 exports = model.exports

UMD: 是 AMD 和 CommonJS 的糅合,跨平台的解决方案

ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js

ES6 模块与 CommonJS 模块的差异

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

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

AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同

AMD:依赖前置,提前执行

CMD:依赖就近,延迟执行

ESM(ES6 module)

export 只支持对象形式导出,不支持值的导出,export default 命令用于指定模块的默认输出,只支持值导出,但是只能指定一个,本质上它就是输出一个叫做 default 的变量或方法

CommonJs 因为是同步执行,所以如果在浏览器上使用 CommonJS,会引起浏览器的假死(卡住)

AMD 规范是异步加载模块,允许指定回调函数,代表函数库:require.js

require.js 主要解决两个问题:

  • 异步加载模块
  • 模块之间依赖模糊

CMD 是阿里的玉伯提出的,js 的函数为 sea.js 。它与 require.js 最主要的区别是实现了按需加载,推崇依赖就近原则,模块延迟执行

UMD 是 AMD 和 CommonJS 的综合产物

ifelse 的写法

ES6 的模块化,可以使用 import 关键字引入模块, 通过export 关键字导出模块

与 CommonJS 的差异

  • CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

IIFE

<script>
  var module1 = (function () {
    var x = 1;
    return { a: x };
  })();
</script>
<script>
  var module2 = (function () {
    var a = module1.a;
    return { b: a };
  })();
</script>

在 ES6 的模块化还没出来之前,一些库或者模块是通过 IIFE(立即执行函数)来实现的。关于 IIFE,我们之前在 JavaScript 篇的 立即执行函数(IIFE) 中提到过

但用这种方式的缺点:扩展的模块无法共享原有模块的内部属性,还有模块本身常常依赖其他模块,模块模式无法实现这些依赖关系

缺点:

  • 随着项目扩展,html 文件中会包含大量的 script 标签
  • script 的依赖关系难以用 script 标签的先后顺序组织

为了解决这个问题,出现了两个互相竞争的标准,即 Asynchronous Module Definition(AMD)和 CommonJS

AMD 和 CommonJS 是两个相互竞争的标准,均可以定义 JavaScript 模块。除了语法和原理的区别之外,主要的区别是 AMD 的设计理念是明确基于浏览器,而 CommonJS 的设计是面向通用 JavaScript 环境(如 Node.js 服务端),而不局限于浏览器。

AMD 异步

CommonJS 同步

AMD

AMD 可以很容易指定模块及依赖关系。同时,它支持浏览器。目前,AMD 最流行的实现是 RequireJS(http://requirejs.org/)。open in new window

使用姿势

define(['jquery', 'common', 'errorMsg'], function($, common, errorMsg) {
    var Home = function() {
        ...
    };
    return Home;
})

RequireJS 声明一个模块是,必须指定所有的依赖项,这些依赖项会被当做形参传到 factory 中,对于依赖的模块会提前执行(在 RequireJS 2.0 也可以选择延迟执行),这被称为:依赖前置

但是这会带来说明问题呢?

加大了开发过程中的难度,无论是阅读之前的代码还是编写新的内容,会出现这样的情况:引入的另一个模块中的内容是条件性执行的

CMD(Common Module Definition) & SeaJS

CMD 是除 AMD 以外的另外一种模块组织规范。CMD 即 Common Module Definition,意为“通用模块定义”。

针对 AMD 规范中可以优化的部分,CMD 规范open in new window 出现了,而 SeaJSopen in new window 则作为它的具体实现之一,与 AMD 十分相似:

// AMD 的一个例子,当然这是一种极端的情况
define(['header', 'main', 'footer'], function (header, main, footer) {
  if (xxx) {
    header.setHeader('new-title');
  }
  if (xxx) {
    main.setMain('new-content');
  }
  if (xxx) {
    footer.setFooter('new-footer');
  }
});

// 与之对应的 CMD 的写法
define(function (require, exports, module) {
  if (xxx) {
    var header = require('./header');
    header.setHeader('new-title');
  }
  if (xxx) {
    var main = require('./main');
    main.setMain('new-content');
  }
  if (xxx) {
    var footer = require('./footer');
    footer.setFooter('new-footer');
  }
});

AMD 与 CMD 最大的区别:

一方面,在依赖的处理上

  • AMD 推崇依赖前置,即通过依赖数组的方式提前声明当前模块的依赖
  • CMD 推崇依赖就近,在编译需要用到的时候通过调用 require 方法动态引入

另一方面,在本模块的对外输出上

  • AMD 推崇通过返回值的方式对外输出
  • CMD 推崇通过给 module.exports 赋值的方式对外输出

AMD && CMD 背后的实现原理

一种解决方 案是采用 UMD(Universal Module Definition, https://github.com/umdjs/umd),这种模式的语法有点复杂,它同时支持open in new window AMD 和 CommonJS。

ES6 模块

Babel 作为 ES6 官方指定的编译器

CommonJS 与 Sea.js

Sea.js 的初衷是为了让 ComoonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上,无法达到,也没必要达到

image-20210316161915559

参考资料

Last Updated:
Contributors: johan