无模块化
由于早期前端业务比较简单,JS 承担的业务较少,工程师可能随便几行代码就搞定了,直接写在一个文件里即可,稍微复杂些的会分文件引入,然后手动维护加载顺序。
moduleA.js:
js
function add(a, b) {
return a + b;
}
moduleB.js:
js
function average(a, b) {
return add(a, b) / 2;
}
main.js:
js
var result = average(5, 10);
console.log(result);
然后在 HTML 中的引入如下:
html
<!-- ...一些业务代码 -->
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script src="./main.js"></script>
命名空间
为了解决命名冲突问题及团队成员间方便合作,这个时候提出了命名空间的思路,此时 moduleA.js, moduleB.js 的写法类似这样:
moduleA.js
js
var moduleA = {
add: function (a, b) {
return a + b;
},
foo: function () {},
bar: 1,
};
moduleA.baz = function () {};
js
moduleB.js
var moduleB = {
average: function (a, b) {
return moduleA.add(a, b) / 2;
},
};
引入命名空间后,命名冲突问题得到了一定程度的解决,模块间的依赖也比较容易的看出来,但是 moduleA 和 moduleB 里面的成员变量及方法都暴露了出来,外界可以任意的对其进行更改,无法创建私有变量及私有方法,模块不够安全。
IIFE(自执行函数)
为了创建私有变量或私有方法,引入了自执行函数及闭包的思路,此时写法类似:
js
// moduleA.js
var moduleA = (function () {
var bar = 1; // 私有属性
var moduleA = window.moduleA || {};
// 私有方法
function getBar() {
return bar;
}
moduleA.add = function (a, b) {
return a + b;
};
moduleA.foo = function () {
return getBar();
};
// 或者挂到 window 下: window.moduleA = moduleA
return moduleA;
})();
这样保证了部分变量及方法的安全性,另外还可以通过传参的方式传递模块的引用:
js
// moduleB.js
(function (modA) {
var moduleB = window.moduleB || {};
moduleB.average = function (a, b) {
return modA.add(a, b) / 2;
};
window.moduleB = moduleB;
})(moduleA);
但是,此时问题依然很多:
- 各个模块依然要创建全局变量污染全局
- 编写模块无法保证不影响其它模块
- 模块间的引用及依赖关系还是不够清晰
- 各个 JS 的加载顺序需要手动管理
- 直到 Node.js 的到来、CommonJS 规范的落地,这成为了一切的转折点…
CommonJS
2009 年 1 月,Mozilla 的工程师 Kevin Dangoor 创建了一个项目,当时的名字是 ServerJS。在 2009 年 8 月,这个项目被改名为 CommonJS,以显示其 API 的更广泛实用性,其中,Node.js 采用的就是这个规范。
CommonJS 约定:
- 每个文件就是一个模块, 有自己的作用域
- 每个文件中定义的变量、函数、类都是私有的,对其它文件不可见
- 每个模块内部可以通过 exports 或者 module.exports 对外暴露接口
= 每个模块通过 require 加载另外的模块
CommonJS 代码示例:
js
// moduleA.js
var foo = 1;
function add(a, b) {
return a + b;
}
function foo() {
return foo;
}
module.exports = {
add: add,
foo: foo,
};
// moduleB.js
const moduleA = require('./moduleA');
const result = moduleA.add(5, 10);
console.log(result);
但是,CommonJS 是一套同步的方案,由于 Node.js 主要运行在服务端,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步的加载方式,所以 CommonJS 规范比较适用。但对于浏览器就无法适用了,必须要有异步的加载机制。
AMD
即 Asynchronous Module Definition,几乎与 CommonJS 同一时期,AMD 规范出现了,它采用异步的方式加载 JavaScript 模块,模块的加载并不会影响它后面语句的运行。
AMD 规范规定用 define 定义模块用 require 加载模块,语法如下:
js
// 模块定义
define(id?, dependencies?, factory);
// 模块加载
require([module], callback);
说起 AMD 规范,不得不提其代表产物 RequireJS,它不仅实现了异步加载,还可以按需加载,一时间成为了众多项目的选择,RequireJS + jQuery 几乎成为了前端项目标配。
下面就来看下基于 RequireJS 的代码是什么样子的。
目录结构如下:
.
├── index.html
└── js
├── entry.js
├── handleClick.js
├── lib
│ ├── jquery.js
│ └── require.js
├── main.js
├── moduleA.js
├── moduleB.js
└── utils
└── index.js
然后是在 HTML 文件中引入 require.js 并声明 data-main 属性:
html
<!-- ... -->
<body>
<button id="btn-click">click me</button>
<script data-main="./js/main.js" src="./js/lib/require.js"></script>
</body>
data-main 属性的作用是指定项目的 JS 主模块,这个模块会被第一个加载,并且可以对模块进行一些路径、别名等配置。
js
// main.js
require.config({
baseUrl: 'js/',
paths: {
jquery: './lib/jquery',
utils: './utils/index',
},
});
require(['./entry'], function (entry) {
entry.init();
});
这里对 baseUrl 进行指定,require 模块时就不用每次都写 ./js/ 路径,同理 utils: './utils/index' 对 utils 模块进行了别名声明也能起到同样的效果。
模块定义示例:
js
// moduleA.js
define(['utils', './moduleA', './moduleB'], function (utils, moduleA, moduleB) {
console.log('---- entry.js utils', utils);
console.log('---- entry.js moduleA', moduleA);
console.log('---- entry.js moduleB', moduleB);
return {
init: function () {
moduleB.init();
console.log('entry.js', 'init');
},
};
});
JS 文件的加载顺序会按照模块的依赖声明顺序进行加载,例如上面代码中依赖模块的加载顺序就是 utils/index.js -> moduleA.js -> moduleB.js。当然 RequireJS 会对加载过的模块进行缓存,如果有多次依赖,就只加载一次。
另外,RequireJS 也可以实现模块的懒加载,只需要在需要时再 require 模块即可,代码示例如下:
js
define(['jquery'], function ($) {
return {
init: function () {
var $btn = $('#btn-click');
$btn.click(function () {
// 事件触发时再加载
require(['./handleClick'], function (handleClick) {
handleClick.init($btn);
});
});
},
};
});
上面代码在事件触发时才加载需要的 handleClick.js 文件,这样就实现了 JS 文件的懒加载,不用页面刚进入时就加载过多的 JS 文件。但是代码却不够优雅,这种回调看起来太难受了。
CMD
即 Common Module Definition,以玉伯大大的 Sea.js 为代表,SeaJS 要解决的问题和 RequireJS 一样,都是浏览器端的模块加载方案,只不过在模块定义方式和执行时机上有所不同,AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
下面来看下 Sea.js 的项目代码大概是什么样子的。
目录结构如下:
├── index.html
└── js
├── entry.js
├── handleClick.js
├── lib
│ ├── jquery.js
│ └── sea.js
├── main.js
├── moduleA.js
├── moduleB.js
└── utils
└── index.js
首先在 HTML 文件中引入 sea.js 及 main.js:
html
<!-- ... -->
<body>
<button id="btn-click">click me</button>
<script src="./js/lib/sea.js"></script>
<script src="./js/main.js"></script>
</body>
这里的 main.js 作为页面入口文件,对 SeaJS 进行了配置 baseUrl 配置及别名配置,并且通过 seajs.use() 加载入口 JS 文件:
js
seajs.config({
base: './js/',
alias: {
jquery: 'lib/jquery',
utils: 'utils/index',
},
});
seajs.use(['jquery', 'entry'], function ($, entry) {
entry.init();
});
之后就是模块的定义及引用了,以 moduleA.js 为例:
js
define(function (require, exports, module) {
return {
a: function () {
var utils = require('utils');
utils.formatDate();
console.log('moduleA.js');
},
};
});
这里就近引用 utils 模块,然后代码执行到此位置时 utils 模块相关的代码才会执行。
UMD
即 Universal Module Definition,UMD 是为了兼容浏览器、Node.js 等多种环境所产生的一种模块规范,经典的代码片段如下:
js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory())
: typeof define === 'function' && define.amd
? define(factory)
: (global.mylib = factory());
})(this, function () {
'use strict';
var mylib = {};
mylib.version = '0.0.1';
mylib.say = function (message) {
console.log(message);
};
return mylib;
});
符合 UMD 规范的 JS 模块既可以在 Node.js 中运行,也可以作为 AMD 模块运行,否则就挂载到当前的上下文环境中,如浏览器中的 window。
ES Module
2015 年,ES6 发布,JavaScript 终于在语言标准层面上有了自己的模块系统,ES6 使用 import 引入模块,使用 export 导出模块。
引用模块:
js
import moduleA from './moduleA';
import { foo } from './moduleB';
moduleA();
foo();
document.getElementById('btn-click').onclick = () => {
import('./handleClick').then(({ handleClick }) => {
handleClick();
});
};
导出模块:
js
// moduleA.js 默认导出
export default function () {
console.log('moduleA');
}
// moduleB.js 具名导出
export const NAME = 'moduleB';
export function foo() {
console.log('foo');
}
export function bar() {
console.log('bar');
}
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
另外,CommonJS 模块输出的是值的拷贝,而 ES6 模块输出的是值的引用,并且 CommonJS 模块是运行时加载,而 ES6 模块是编译时输出接口,基于这个特性,ES6 模块就很容做静态分析,比如在 Webpack 打包构建时通过tree shaking 去除无用代码减少代码体积。
Comments | 0条评论