前端开发需遵循模块化编程。
模块化开发,带来的好处有:公用性和复用性好、能提高开发效率、方便代码管理、利于团队协作。
模块化编程进化历史
单例模式 -> AMD设计模式 -> CommonJS规范 -> CMD(sea.js) -> ES6Module规范 ->
单例模式
示例: 假如需要存储小明和小花两个人的信息
// 小明
let name = '小明';
let age = 24;
let friendName = '小芳';
// 小花
let name = '小花';
let age = 22;
let friendName = '敏敏';
同一作用域下,我们如此使用变量名,必将引起变量冲突。
那么如何来解决这个变量冲突的问题呢?
有两种解决办法:
-
使用对象来进行分组管理,来避免变量污染。
每一个对象都是一个单独的堆内存。用堆内存单独空间的概念,把描述同一个事物的属性和方法放在同一个堆内存下。这样两个堆内存之间是不冲突的。
这种方案其实就是“单例设计模式”:每一个对象都是Object的单独实例,基于每一个实例对象来管理自己的属性和方法,实现分组的效果。
-
还可以使用闭包的思想,利用单独的私有的执行上下文,来进行分组管理,以避免变量污染。
示例:单例设计模式
- 使用对象(实例)来存储小明和小花两个人的信息
- 示例中的 person1 和 person2 我们又称之为命名空间 (namespace)
- 把描述同一个事物的属性和方法放在相同的命名空间中,以此来避免全局变量污染
let person1 = {
name: '小明',
age: 24,
friendName: '小芳'
};
let person2 = {
name: '小花',
age: 22,
friendName: '敏敏'
};
示例:使用闭包思想
使用了自执行函数,创建出单独的私有的执行上下文,来分别保存小明和小花两个人的信息
// 小明
(function () {
let name = '小明';
let age = 24;
let friendName = '小芳';
const query = () => {
console.log('query');
};
window.query = query;
})();
// 小花
(function () {
let name = '小花';
let age = 22;
let friendName = '敏敏';
})();
query(); // => 'query'
上面代码中,我们给小明所属的私有上下文中,添加一个 query 方法,并这个方法挂载到全局对象 window 下。
这样我们就可以在全局上下文中访问到小明所属的私有上下文中的 query 方法了。
我们将这种做法叫做“暴露API,挂载方法到 GO 中",不过不宜过多以这种方式挂载方法,因为挂载过多也会引起冲突。
高级单例模式
实际开发中,我们时常把单例模式和闭包思想结合起来使用,就形成了“高级单例模式”。
这种模式,既保证模块之间的某些方法可以相互调用,也保证模块之间的独立性。
// 模块 AModule
// AModule 指向 return 出来的那个对象
let AModule = (function () {
let n = 10;
const query = () => {};
const sum = () => {};
// 暴露API
return {
query
};
})();
// 模块 BModule 中
let BModule = (function () {
let n = 20;
const sum = () => {};
// 使用 AModule 暴露出来的方法
AModule.query();
return {
};
})();
单例模式的局限性
单例设计模式需要自己写代码来管理;
并且如果每一个模块是一个单独的JS,最后导入JS的时候,我们需要非常认真的去管理一下先后导入的顺序,需按照模块之间的依赖关系依次导入。
在文件非常多的情况下,这种依赖顺序处理起来会非常的不方便,所以就产生了AMD设计模式。
AMD设计模式
AMD设计模式的核心思想就是:按需导入,有效管理模块之间的依赖,。
经典代表是:require.js
AMD设计模式特点:
- 依赖第三方插件,比如:require.js
- 按需导入,能有效管理模块之间的依赖。
- 但是需要将依赖前置导入。
尽管AMD设计模式目前有些过时,项目中已经很少使用了。但是其对模块化开发的思想还是值得深入学习的。
require.js 怎么用呢?
文件结构
|- index.html
|- [js](文件夹)
|- require.min.js
|- main.js
|- [lib](文件夹)
|- moduleA.js
|- moduleB.js
使用描述:
-
我们需要在 index.html 文件中, 先导入 require.min.js , 再导入 main.js
-
main.js 做全局配置、导入其他模块
-
调用 define() 方法来定义模块
define() 方法接收一个匿名作为实参,这个匿名函数返回一个对象,对象里面会包含若干方法。
define() 方法也可以设置依赖模块。把第一个参数设为数组,按依赖顺序写模块名字。
/** main.js **/
// 全局配置
require.config({
baseUrl: 'js/lib',
});
// 导入其他模块
// require() 方法
// 第一个参数,是个数组,
// 把需要依赖的模块按依赖顺序放入数组
// 我们把这种依赖设置叫做“前置依赖”
// 第二个参数,是个回调函数,
// 被依赖的模块导入成功后,会触发回到函数执行
require(['moduleB', 'moduleA'], function (moduleB, moduleA) {
console.log(moduleB.average(10, 20, 30, 40, 50));
});
/** moduleA.js **/
define(function () {
return {
// 任意数求和
sum(...args) {
let len = args.length,
firstItem = args[0];
if (len === 0) return 0;
if (len === 1) return firstItem;
return args.reduce((total, item) => {
return total + item;
});
}
};
});
/** moduleB.js **/
// 依赖 moduleA
define(['moduleA'], function (moudleA) {
return {
// 求平均数(去掉最大最小值)
average(...args) {
let len = args.length,
firstItem = args[0];
if (len === 0) return 0;
if (len === 1) return firstItem;
args.sort((a, b) => a - b);
args.pop();
args.shift();
// 使用 moduleA 中的 sum 方法
return (moudleA.sum(...args) / args.length).toFixed(2);
}
};
});
自己如何实现一个AMD设计模式?
let factories = {};
function define(moduleName, factory) {
factories[moduleName] = factory;
function require(modules, callback) {
modules = modules.map(function (item) {
let factory = factories[item];
return factory();
});
callback(...modules);
}
/* 使用AMD */
define('moduleA', function () {
return {
fn() {
console.log('moduleA');
}
};
});
define('moduleB', function () {
return {
fn() {
console.log('moduleB');
}
};
});
require(['moduleA', 'moduleB'], function (moduleA, moduleB) {
moduleB.fn();
moduleA.fn();
});
CommonJS规范
- 导入用:require
- 导出(暴露API):module.exports
- 特点:只能在Node环境下运行;随用随导入,无需依赖前置。
文件结构
|- index.html
|- [js](文件夹)
|- main.js
|- A.js
|- B.js
A.js文件 代码如下
const fn = function fn() {
console.log('AModule FN');
};
const sum = function sum() {
console.log('AModule SUM');
};
// 使用 module.exports 暴露API
// 暴露了一个对象,里面有多个方法
module.exports = {
fn,
sum
};
B.js文件 代码如下
let n = 10;
// 导入模块require「放在任何的位置」
let AModule = require('./A.js');
const query = function query() {
AModule.fn();
console.log('BModule QUERY');
};
// 暴露API
module.exports = query;
main.js文件 代码如下
let AModule = require('./A');
AModule.sum();
let query = require('./B');
query();
seajs
淘宝玉伯研发的一个插件,旨在把CommonJS规范搬到浏览器端运行,起了个规范名字“CMD”
只流行了一段时间,目前只做了解
ES6Module
- ECMA官方出来的模块规范
- 导出:export & export default
- 导入:import
- 特点:依赖前置;浏览器可以直接支持;NodeJS环境是不支持的;
使用示例:文件结构
|- [ES6Module](文件夹)
|- index.html
|- main.js
|- A.js
|- B.js
使用示例:index.html 文件
- 在页面用引入 main.js 文件
- 设置 type="module" 让浏览器支持ES6Module规范
- 页面需要基于标准的HTTP/HTTPS协议预览,不能是file协议
- 在 vscode 中可使用「 Live Server」插件预览,有热更新功能。
<script type="module" src="main.js"></script>
使用示例:A.js 文件
const sum = function sum() {
console.log('A SUM');
};
const fn = function fn() {
console.log('A FN');
};
/*
// 一个个的导出,并且导出多个
// 导入的时候 import * as TYPE from './A.js';
// TYPE.n / TYPE.m
export const n = 10;
export const m = 20;
*/
// 一次导出多个方法
export default {
sum,
fn
};
使用示例:B.js 文件
const query = function query() {
console.log('B QUERY');
};
export default query;
main.js 文件
// 导入必须放在最开始
import {
fn,
sum
} from './A.js';
import query from './B.js';
// A.fn();
// A.sum();
query();