JavaScript 模块化编程

JavaScript 模块化编程

前言

  • 模块是任何大型应用程序架构中不可缺少的一部分,可以使我们清晰地分离和组织项目中的代码单元
  • 通过移除依赖,松耦合可以使应用程序的可维护性更强
  • 在 JavaScript 中,并没有提供原生的、有组织性的引入模块方式
  • 下面列出目前常见的集中模块化解决方案
    1. 对象字面量表示法
    2. Module 模式
    3. AMD 模块
    4. CommonJs 模块
    5. ES Harmony 模块

对象字面量表示法

  • 对象字面量可以认为是包含一组键值对的对象,每一对键和值由冒号分隔
  • 对象字面量不需要使用 new 运算符进行实例化,在对象的外部也可以给对象添加属性和方法

    var myModule = {
    property: "amanisky",
    
    // 对象字面量可以包含属性和方法
    // 例如,可以声明模块的配置对象
    config: {
      useCaching: true,
      language: "en"
    },
    
    // 基本方法
    method1: function () {
      console.log("method1");
    },
    
    // 根据当前配置输出信息
    method2: function () {
      console.log("Caching is:" + '(this.config.useCaching) ? "enabled" : "disabled"');
    },
    
    // 修改配置
    method3: function (newConfig) {
      if (typeof newConfig === "object") {
        this.config = newConfig;
      }
    }
    }
    
  • 如上所示
    • 使用对象字面量有助于封装和组织代码,然后不同的对象字面量模块再构成复杂的项目

Module 模式

  • Module 模式最初定义在传统的软件工程中,为类提供私有和公有封装的方法
  • 在 JavaScript 中,并不能直接声明类,因此使用闭包来封装私有属性和方法,进而模拟类的概念,从而在 JavaScript 中实现 Module 模式
  • 通过这种方式,就使得一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分,大大降低了变量声明和函数声明之间冲突的可能性

    var myModule = (function () {
    // 私有变量
    var privateVar = 0;
    
    // 私有函数
    var privateFun = function (foo) {
      console.log(foo);
    };
    
    return {
      // 公有变量
      publicVar: "foo",
    
      // 公有函数
      publicFun: function (arg) {
        // 修改私有变量
        privateVar++;
    
        // 调用私有方法
        privateFun(arg);
      }
    };
    })();
    
  • 如上所示
    • 通过使用闭包我们封装了私有变量和方法,只暴露一个接口供其他部分调用
    • 私有变量(privateVar)和私有方法(privateFun)被局限于模块的闭包之中,只能通过公有方法才能访问
    • 该模式返回一个对象,可以为返回的对象添加属性和方法供外部调用者使用
    • 在对象的外部也可以给对象添加属性和方法
  • Module 模式非常简洁,但也有缺点和劣势
    • 当想改变可见性时,需要修改每一个曾经使用该成员的地方,不利于维护和升级,耦合度不理想
    • 在之后新添加的方法里,并不能访问以前声明的私有方法和变量
      • 因为闭包只在创建时完成绑定
    • 无法为私有方法创建自动化单元测试,
    • 修改私有方法及其困难,需要复写所有与私有方法交互的公有方法,bug 修正时工作量会很大
    • 不能轻易的扩展私有方法

脚本加载器

  • 脚本加载器是为了能在应用中使用模块化的 JavaScript 这个目标而服务的
  • 由很多加载器用于 AMD 和 CommonJS 方式中的模块加载,比较出名的有 RequireJScurl.js

AMD 模块

  • AMD 全称是 Asynchronous Module Definition,即异步模块加载机制
  • 它诞生于使用 XHR+eval 的 Dojo 开发经验,其整体目标是提供模块化的 JavaScript 解决方案,避免未来的任何解决方案收到过去解决方案缺点的影响
  • AMD 模块格式本身就是对定义模块的建议,其模块和依赖都可以进行异步加载,具有高度的灵活性,清除了代码和模块之间可能惯有的紧耦合
  • AMD 有两个非常重要的方法
    • define(module-name? array-of-dependencies? module-factory-or-object)
      • 用于模块定义
      • module-name 模块标识字符串,可选参数;如果没有这个属性,则称为匿名模块
      • array-of-dependencies 是一个数组,表示所依赖的模块,可选参数
      • module-factory-or-object 模块的实现或一个 JavaScript 对象
    • require(array-of-dependencies, callback)
      • 用于加载 JavaScript 文件或模块的代码,获取依赖
      • array-of-dependencies 是一个数组,表示所依赖的模块
      • callback 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用;加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
    • 动态加载依赖的示例
    • AMD 模块可以使用插件,当我们加载依赖时,可以加载任意格式的文件
    • 使用 AMD 模块编写模块化的 JavaScript 代码,比现有的全局命名空间和 script 标签解决方案更加简介,没有全局命名空间污染,在需要的时候可以延迟加载脚本
/**
 * 定义 myModule 模块
 * 该模块依赖 foo、bar 
 * foo、bar 作为参数映射到函数上
 */
define("myModule", ["foo", "bar"], function (foo, bar) {
    // 创建模块
    var myModule = {
      myFun: function () {
        console.log("Hello");
      }
    }

    // 返回定义的模块
    return myModule;
  }
);
/**
 * require.js 会先加载 jQuery、underscore,然后再运行回调函数。
 */
require(['jquery', 'underscore'], function ($, _) {
  // some code here
});
define(function (requrie) {
    var isReady = false, foobar;

    requrie(["foo", "bar"], function (foo, bar) {
      isReady = true,
      foobar = foo() + bar();
    });

    // 返回定义的模块
    return {
      isReady: isReady,
      foobar: foobar
    };
  }
);

CommonJS 模块

  • CommonJS 规范建议指定一个简单的 API 来声明在浏览器外部工作的模块
  • 与 AMD 不同,它试图包含更广泛的引人关注的问题,如:IO、文件系统等
  • 从结构来看,CommonJS 模块是 JavaScript 中可以复用的部分,导出特定对象,以便可以用于任何依赖代码
  • CommonJS 有两个非常重要的方法
    • exports
      • exports 包含了一个模块希望其他模块能够使用的对象
    • require
      • require 函数用来导入其他模块的导出,即用来加载其他模块依赖
  • 浏览器端可以使用 CommonJS 组织模块,但不少开发者认为 CommonJS 更适合于服务器端开发,因为 CommonJS API 具有面向服务器的特性,如:io、sysytem 等
  • NodeJS 使用的就是 CommonJS 规范
  • 当一个模块可能用于服务端时,开发人员倾向于选择 CommonJS,其他情况下使用 AMD
// 新定义的模块方法
function log(arg) {
  console.log(arg);
}

// 把方法暴露给其他模块
exports.log = log;
// ./lib是需要的一个依赖
var lib = requrie("./lib");

// 新定义的模块方法
function foo() {
    lib.log("jeri");
}

// 把方法暴露给其他模块
exports.foo = foo;

ES6 Module

  • ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
  • ES6 可以在编译时就完成模块加载(静态加载),效率要比 CommonJS 模块的加载方式高
  • 模块功能主要由两个命令构成:export 和 import
    • export 命令用于规定模块的对外接口;可以使用 as 关键字重命名
    • import 命令用于输入其他模块提供的功能;可以使用 as 关键字重命名
// CommonJS模块
let { stat, exists, readFile } = require('fs');

/**
 * 上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上读取 3 个方法。
 * 这种加载称为"运行时加载";因为只有运行时才能得到这个对象。
 */
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

// ES6模块
import { stat, exists, readFile } from 'fs';

/**
 * 上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。
 * 这种加载称为"编译时加载"或"静态加载",即 ES6 可以在编译时就完成模块加载,效率比 CommonJS 高。
 * 当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
 */