JavaScirpt 模块化

129 阅读5分钟

写在前面

模块化指的是将特定的一组函数创造自己的作用域,只提供模块中的特定函数变量函数,并且对外只提供一个接口,无法直接访问局部成员。

前端为什么要进行模块化

让代码更加 ”模块化“ 可以在不同的场景中实现代码的可重用性。

  1. 命名冲突
  2. 功能代码的松耦合
  3. 代码扩展性
  4. 代码维护性

image.png

私有命名空间函数

将模块定义在某个函数内部实现。在一个函数中定义的变量和函数都是局部成员,在函数外部是不可见的。可以将这个函数作用域用作模块的私有命名空间(模块函数)

// 全局声明变量collctions,使用一个函数的返回值给它赋值
// 函数结束时紧跟的一对圆括号说明这个函数定义后立即执行
// 它的返回的构造函数赋值给collctions,而并不是这个函数赋值给collctions
// 注意它是一个函数表达式
var collections = (function(){
    function Sets(){
        this.value = {};
        this.nu = 0
    }
    Sets.prototype.contains = function(){
        //省略
    }
    Sets.prototype.v2s = function(a){}
    return Sets;
}());//定义后立即执行

注意:这里使用了立即执行函数,这是JavaScript中是一种惯写法。如果想让代码在一个私有命名空间中运行,只需给这段代码加上(function(){}())

私有命名空间函数(导出)

以上写法是将构造函数形式模块导出,一旦将模块代码封装成一个函数,就需要一些方法导出公用API。以便于在外部调用它们,如果模块API过多,可以这么编写。

var collections;
if(!collctions) collections={};
collctions.Tools = (function namepace(){
    //······这里面省略很多代码······
    function intToByte4 (i) {
        var targets = [];
        targets[0] = (i & 0xFF);
        targets[1] = (i >> 8 & 0xFF);
        targets[2] = (i >> 16 & 0xFF);
        targets[3] = (i >> 24 & 0xFF);
        return targets;
    }
    return {
        //导出的属性名称:局部变量名字
        intToByte4:intToByte4
    }
}())

CommonJS规范

什么是CommonJS

CommonJS是JavaScript服务端的模块化规范,根据Node的出现有了CommonJS。

将一系列的函数封装起来,对外提供一个接口(用就引入)。

使用方式一

//util.js
function foo() {
    return "foo";
}

module.exports = {
    foo
}
//使用
const { foo } = require("./util.js");
//or
const util = require("./util.js");
util.foo();

使用方式二

//api.js
module.export = function(){
    //.....
}
//api.js
const apis = require("./api.js")

总结

CommonJS是同步的,用于js服务器端,它会把加载的内容放到内存当中,CommonJS的模块加载是输出值的拷贝,也就是说当模块导入后的值,如果模块发生了修改,则当前值不会变。

AMD

AMD是浏览器的模块加载器,基于RequireJS的加载机制

AMD的模块写法:

// 模块名 utils
// 依赖 jQuery, underscore
// 模块导出 foo, bar 属性
<script data-main="scripts/main" src="scripts/require.js"></script>

// main.js
require.config({
  baseUrl: "script",
  paths: {
    "jquery": "jquery.min",
    "underscore": "underscore.min",
  }
});

// 定义 utils 模块,使用 jQuery 模块
define("utils", ["jQuery", "underscore"], function($, _) {
    var body = $("body");
    var deepClone = _.deepClone({...});
    return {
        foo: "hello",
        bar: "world"
    }
})
</script>

CMD

CMD是浏览器的模块加载器,基于SeaJS的加载机制

CMD的模块写法:

<script src="scripts/sea.js"></script>
<script>
// seajs 的简单配置
seajs.config({
  base: "./script/",
  alias: {
    "jquery": "script/jquery/3.3.1/jquery.js"
  }
})

// 加载入口模块
seajs.use("./main")
</script>

// 定义模块
// utils.js
define(function(require, exports, module) {
  exports.each = function (arr) {
    // 实现代码 
  };

  exports.log = function (str) {
    // 实现代码
  };
});

// 输出模块
define(function(require, exports, module) {
  var util = require('./util.js');
  
  var a = require('./a'); //在需要时申明,依赖就近
  a.doSomething();
  
  exports.init = function() {
    // 实现代码
    util.log();
  };
});
</script>

AMD和CMD区别

  • AMD推崇依赖前置,CMD推送依赖就近
  • AMD需要提前定义,加载完就执行
  • CMD就近原则,只有用到某个模块才会加载

例子:

// main.js
define(function(require, exports, module) {
  console.log("I'm main");
  var mod1 = require("./mod1");
  mod1.say();
  var mod2 = require("./mod2");
  mod2.say();

  return {
    hello: function() {
      console.log("hello main");
    }
  };
});

// mod1.js
define(function() {
  console.log("I'm mod1");
  return {
    say: function() {
      console.log("say: I'm mod1");
    }
  };
});

// mod2.js
define(function() {
  console.log("I'm mod2");
  return {
    say: function() {
      console.log("say: I'm mod2");
    }
  };
});

以上代码分别用SeaJS和RequireJS打印执行结果

RequireJS

// I'm mod1
// I'm mod2
// I'm main
// say: I'm mod1
// say: I'm mod2

SeaJS

// I'm main
// I'm mod1
// say: I'm mod1
// I'm mod2
// say: I'm mod2

UMD

UMD(Universal Module Definition) 是AMD和CommonJS提出的跨平台的解决方案

本质上就是一个立即执行函数

(function (root, factory) {
    if (typeof exports === 'object') {
        // commonJS
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        // AMD
        define(factory);
    } else {
        // 挂载到全局
        root.eventUtil = factory();
    }
})(this, function () {
    function myFunc(){};

    return {
        foo: myFunc
    };
});

应用 UMD 规范的 JS 文件其实就是一个立即执行函数,通过检验 JS 环境是否支持 CommonJS 或 AMD 再进行模块化定义。

jQuery中的源码中也用到了该方案去解决适配不同平台

image.png

ESModule

CommonJS 和 AMD 规范都只能在运行时确定依赖。而 ES6 在语言层面提出了模块化方案, ES6 module 模块编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块化这种加载称为“编译时加载”或者静态加载。

image.png

// math.js
// 命名导出
export function add(a, b){
    return a + b;
}
export function sub(a, b){
    return a - b;
}
// 命名导入
import { add, sub } from "./math.js";
add(2, 3);
sub(7, 2);

// 默认导出
export default function foo() {
  console.log('foo');
}
// 默认导入
import someModule from "./utils.js";

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

另,在 webpack 对 ES Module 打包, ES Module 会编译成 require/exports 来执行的。

总结

JS 的模块化规范经过了模块模式、CommonJS、AMD/CMD、ES6 的演进,利用现在常用的 gulp、webpack 打包工具,非常方便我们编写模块化代码。掌握这几种模块化规范的区别和联系有助于提高代码的模块化质量,比如,CommonJS 输出的是值拷贝,ES6 Module 在静态代码解析时输出只读接口,AMD 是异步加载,推崇依赖前置,CMD 是依赖就近,延迟执行,在使用到模块时才去加载相应的依赖。