模块化编程思想学习

382 阅读6分钟

前端开发需遵循模块化编程。

模块化开发,带来的好处有:公用性和复用性好、能提高开发效率、方便代码管理、利于团队协作。

模块化编程进化历史

单例模式 -> AMD设计模式 -> CommonJS规范 -> CMD(sea.js) -> ES6Module规范 ->

单例模式

示例: 假如需要存储小明和小花两个人的信息

// 小明
let name = '小明';
let age = 24;
let friendName = '小芳'// 小花
let name = '小花';
let age = 22;
let friendName = '敏敏'

同一作用域下,我们如此使用变量名,必将引起变量冲突。

那么如何来解决这个变量冲突的问题呢?

有两种解决办法:

  1. 使用对象来进行分组管理,来避免变量污染。

    每一个对象都是一个单独的堆内存。用堆内存单独空间的概念,把描述同一个事物的属性和方法放在同一个堆内存下。这样两个堆内存之间是不冲突的。

    这种方案其实就是“单例设计模式”:每一个对象都是Object的单独实例,基于每一个实例对象来管理自己的属性和方法,实现分组的效果。

  2. 还可以使用闭包的思想,利用单独的私有的执行上下文,来进行分组管理,以避免变量污染。

示例:单例设计模式

  • 使用对象(实例)来存储小明和小花两个人的信息
  • 示例中的 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 怎么用呢?

requirejs - 官网

文件结构

|- index.html
|- [js](文件夹)
    |- require.min.js
    |- main.js
    |- [lib](文件夹)
        |- moduleA.js
        |- moduleB.js

使用描述:

  1. 我们需要在 index.html 文件中, 先导入 require.min.js , 再导入 main.js

  2. main.js 做全局配置、导入其他模块

  3. 调用 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 文件

  1. 在页面用引入 main.js 文件
  2. 设置 type="module" 让浏览器支持ES6Module规范
  3. 页面需要基于标准的HTTP/HTTPS协议预览,不能是file协议
  4. 在 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();