前端模块化的前世今生

Robin · 2019-10-18 · 次阅读


一.背景

  近年来,web应用变得庞大且复杂,web前端的技术应用范围也越来越广泛。从复杂庞大的管理后台到性能苛刻的移动网页,再到类似ionic的跨平台移动应用框架,web工程师面临着巨大的机遇,也面临着前所未有的挑战。当web应用程序变得越来越复杂,系统架构设计显得尤为重要。由此,模块化思维应运而生,它能简化代码的层次结构,让浏览器更容易“理解”我们的代码。

二.什么是模块化

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

三.发展历程

1.IIFE

  IIFE(Immediately-Invoked Function Expression),全称为立即执行函数表达式普通的函数声明和函数调用:

1
2
3
4
5
function bar() {
var a = 10;
console.log(a);
}
bar(); // 函数调用

  IIFE函数声明和调用:

1
2
3
4
(function foo(){
var a = 10;
console.log(a);
})();

  IIFE函数是由一对()将函数声明包裹起来的表达式,使得JS编译器不再认为这是一个函数声明,而是一个IIFE,会立刻执行函该数。

  模块化的一大作用就是用来隔离作用域,避免变量冲突。而Javascript没有语言层面的命名空间概念,只能将代码暴露到全局作用域下。作为脚本语言的Javascript为了避免全局变量污染,只能使用闭包来实现模块化。好在我们可以利用自执行函数(IIFE)来执行代码,从而避免变量名泄漏到全局作用域中。

  虽然IIFE可以有效解决命名冲突的问题,但是对于依赖管理,还是束手无策。由于浏览器是从上至下执行脚本,因此为了维持脚本间的依赖关系,就必须手动维护好script标签的相对顺序。

2.AMD

(1)定义:AMD(Asynchronous Module Definition)全称是异步模块定义。它是一中js模块化的规范,主要用于浏览器,由于该规范不是原生js支持的,使用它进行开发的时候需要引入第三方的库函数RequireJS。

(2)RequireJs解决的问题:

  • 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  • js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长fs

(3)用法

①定义模块:RequireJS定义了一个define函数,用来定义模块,语法如下:

1
define([id], [dependencies], factory)

参数意义:

  • id:可选,字符串类型,定义模块标识,如果没有提供参数,默认为文件名;
  • dependencies:可选,字符串数组,AMD推崇依赖前置,即当前模块依赖的其他模块,模块依赖必须在真正执行具体的factory方法前解决;
  • factory:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// mod1.js
define('mod1',[],function(){
// ...
return {
// ...
}
})

// mod2.js
define('mod2', ['mod1'], function (mod1) {
// ...
return {
// ...
}
})

②模块的加载:使用require函数

1
require([dependencies], function(){})

参数

  • dependencies:字符串数组,该模块的依赖;
  • function:Function类型,所依赖的模块都加载成功之后,回调,依赖的模块会以参数的形式传入该函数,从而在回调函数内部就可以使用这些模块。
1
2
3
require(['mod1','mod2'],function(mod1, mod2){
// TODO
});

注意:require()函数加载模块是异步加载,这样浏览器就不会失去响应。

3.CMD

(1)定义:CMD(Common Module Definition),即通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。
(2)使用:在 CMD 规范中,一个模块就是一个文件。代码的书写格式:define(factory);

  define是一个全局函数,用来定义一个模块。接下来会有对参数factory的详细介绍,另外,与define相关的一个属性cmd。define.cmd:一个空对象,可用来判定当前页面是否有 CMD 模块加载器。

1
2
3
if (typeof define === "function" && define.cmd) {
// 有 Sea.js 等 CMD 模块加载器存在时
}

(3)factory函数

  factory:可以是一个函数,也可以是一个对象或字符串。

①factory为对象、字符串时:表示模块的接口就是该对象、字符串,比如可以如下定义一个JSON数据模块:

1
2
define({"uName":"xdl"});//factory是对象
define('Hello world,my name is {{name}}.'); //factory是字符串

②factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供 的接口。方法在执行时,默认会传入三个参数:require、exports、module:

1
2
3
define(function(require, exports, module) {
// 模块代码
});
  • require:它是一个方法(函数)。他接收 模块标识 作为唯一参数,用来接受其他模块的接口;
  • exports:它是一个对象。用来向外提供模块接口;
  • module:是一个对象,上面存储了与当前模块相关联的一些属性和方法;

注:CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。

4.UMD

  UMD(Universal Module Definition),即通用模块定义规范。它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身,我们看看它的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
((root, factory) => {
if (typeof define === 'function' && define.amd) {
//AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
//CommonJS
var $ = requie('jquery');
module.exports = factory($);
} else {
root.testModule = factory(root.jQuery);
}
})(this, ($) => {
//todo
});

5.CommonJs

(1)CommonJs是什么

  common.js是服务器端模块的规范,由nodejs推广使用。该规范的核心思想是:允许模块通过require方法来同步加载所要依赖的其他模块,然后通过exports或module.exports来导出需要暴露的接口。

(2)使用

  采用CommonJS 规范导入导出:

1
2
3
4
// 导出
module.exports = moduleA.someFunc;
// 导入
const moduleA = require('./moduleA');

实例:

1
2
3
4
5
6
7
8
9
10
//导出math.js
var num = 0;
function add(a, b) {
return a + b;
}
module.exports = {
//需要向外暴露的变量、函数
num: num,
add: add
}
1
2
3
4
//引入自定义的模块时,参数包含路径,可省略.js
//引入核心模块时,不需要带路径,如var http = require("http");
var math = require('./math');
math.add(1, 2)//3

缺点:

  • 不能并行加载模块,会阻塞浏览器加载;
  • 代码无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5;

6.ES6 Modules

  在ES6中,从语法层面就提供了模块化的功能。然而受限于浏览器的实现程度,如果想要在浏览器中运行,还是需要通过Babel等转译工具进行编译。

(1)ES6提供了import和export命令,分别对应模块的导入和导出功能。具体实例如下:

1
2
3
4
5
6
7
8
9
10
// demo-export.js 模块定义
let name = "scq000"
let sayHello = (name) => {
console.log("Hi," + name);
}
export {name, sayHello};

// demo-import.js 使用模块
import {sayHello} from "./demo-export";
sayHello("scq000");

(2)ES6还提供了export default,注意到(1)中使用export导出,使用import导入使用{},而使用export default导出,import导入时,不用{},而改用引号(单或双):

1
2
3
4
5
//------ MyClass.js ------
class MyClass{}
export default MyClass;
//------ Main.js ------
import MyClass from 'MyClass';

关于export/import,以下几点需要说明:

  • ES6使用的是基于文件的模块。所以必须一个文件一个模块,不能将多个模块合并到单个文件中去;
  • ES6模块API是静态的,一旦导入模块后,无法再在程序运行过程中增添方法;
  • ES6模块采用引用绑定(可以理解为指针)。这点和CommonJS中的值绑定不同,如果你的模块在运行过程中修改了导出的变量值,就会反映到使用模块的代码中去;
  • ES6模块采用的是单例模式,每次对同一个模块的导入其实都指向同一个实例;export/import是ES6的语法,浏览器不能直接解析,需要借用babel工具;

四.总结

  模块化方案解决了代码之间错综复杂的依赖关系,不仅降低了开发难度同时也让开发者将精力更多地集中在业务开发中。随着ES6标准的推出,模块化直接成为了Javascript语言规范中的一部分。这也意味着CommonJS, AMD, CMD等模块化方案终将退出历史的舞台。

模块化方案 加载 同步/异步 浏览器 服务端 模块定义 模块引入
IFEE 取决于代码 取决于代码 支持 支持 IFEE 命名空间
AMD 提前预加载 异步 支持 构建工具r.js define require
CMD 按需加载 延迟执行 支持 构建工具spm define define
Common 值拷贝,运行时加载 同步 原生不支持,需要使用browserify提前打包编译 原生支持 module.exports require
UMD 取决于代码 取决于代码 支持 支持 IFEE 命名空间
ES Modules(ES6) 实时绑定,动态绑定,编译时输出 同步 需用babel转译 需用babel转译 export import

前端开发攻城狮