JavaScript模块化

300 阅读10分钟

什么是模块化

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化的好处

  • 解决命名冲突
  • 依赖管理
  • 提高可读性
  • 提高复用性
  • 提高可维护性

早期的模块

函数

将复杂功能封装到一个个函数中,一个函数就相当于一个模块,模块的调用即函数的调用。

function fn1() {
    // do something
}
function fn2() {
    // do something
}

优点:有模块的私有属性(函数内的变量,外部无法改变) 缺点:污染全局变量,依赖关系不能很容易的看出来

对象(又称namespace)

用对象的方式组织模块,一个对象就是一个命名空间。

var myModule = {
    name: 'myModule',
    sayName: function() {
       console.log(this.name)
    },
    fn2: function() {}
}

优点: 降低了对全局变量的污染,但是还不能彻底避免,myModule这个变量 还是会污染全局变量

缺点: 会暴露所有模块成员,模块内的数据可以被意外修改

myModule.sayName() // myModule
myModule.name = '篡改的名字'
myModule.sayName() // 篡改的名字

IIFE(立即调用函数)

Immediately-Invoked Function Expression

利用闭包封装了模块的私有属性

var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      // ...
    };
    var m2 = function(){
        //...
    };
    return {
        m1 : m1,
        m2 : m2
    };
})();

优点: 模块有私有属性 缺点: 模块依赖问题,还是会污染全局变量。

IIFE优化

模块应该具有独立性,模块内部最好不与程序的其他部分直接交互,如果需要,则显示注入。

var module1 = (function(window, $){
  var name 'myModule';
  function sayName () {
    console.log(this.name)
  }
  function fn2 () {
    $('#app').css('color', 'red')
  }
  return {
    sayName,
    fn2
  }
})(window, jquery)  // 一眼就能看出模块内部需要使用全局变量,和jquery对象

优点:私有变量,模块依赖清晰

CommonJS

CommonJS是Node使用的一种模块规范

概述

CommonJS 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。这一点和浏览器不同,浏览器都是在全局作用域中。

对外接口的导出

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var name: 'myModule'; // 定义在模块作用域中,而不是全局
function sayName () {
   console.log(name)
}
module.exports.sayName = sayName; // 使用module.exports对外暴露接口

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

上面的例子可写成下面这样

var name: 'myModule'; // 定义在模块作用域中,而不是全局
function sayName () {
   console.log(name)
}
exports.sayName = sayName;

导入模块

使用require命令,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

var myModule = require('./myModule.js');

myModule.sayName() // 'myModule'

同步性

CommonJS规范是为了服务于node的,在服务器端,文件读取通常都是在本地完成,速度快,所以采用同步方式,也就是说,只有加载并执行完require的模块,才能执行后面的操作,但是在浏览器中,模块的请求通常都是通过网络,速度慢,同步模式不适合。

模块缓存

CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象,以后再加载该模块,就直接从缓存取出该模块的module.exports属性

{
  id: '...', // 模块名
  exports: { ... }, // 模块输出的各个接口
  loaded: true, // 模块的脚本是否执行完毕
  ...
}

动态加载(又称运行时加载)

CommonJS规范是动态加载,比如可以使用条件导出,缺点是无法进行静态分析

let moduleName  = useA ? 'moduleA' : 'moduleB';

let module = require(moduleName) // require导入的模块是由上面的语句的执行结果决定的

总结

  • CommonJS规范是同步的(浏览器端不适用)。
  • 使用module.exports导出
  • 使用require导入
  • 模块缓存:只执行一次,缓存结果。
  • 动态加载

AMD规范和RequireJS

CommonJS规范是同步加载,不适合用在浏览器,所以针对浏览器端,就诞生了AMD规范

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

require.js是一个遵循了AMD规范的库,下面主要针对require.js进行介绍,除了require.js也有其他的库实现了AMD,如curl.js

模块定义:define

模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

第一个参数,是一个数组,指明该模块的依赖性。 define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应

这个函数必须返回一个对象,即模块的导出部分,供其他模块使用。

 // 定一个了一个模块,且该模块依赖了另外一个模块otherLib
 define(['otherLib'], function(myLib){
    function foo(){
        otherLib.doSomething();
    }
    return {
        foo : foo
    };
});

模块导入:require

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数

require(['math'], function (math) {
    math.add(2, 3);
});

配置

require.js有一些配置项

  • baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。
  • paths:paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。
  • shim:有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。
require.config({
    //baseUrl: '/', // 指定本地模块位置的基准目录
    paths: {  // paths参数指定各个模块的位置。
        jquery: [
            '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
            'lib/jquery'
        ]
    },
    shim: { // 
        "backbone": {
            deps: [ "underscore" ],
            exports: "Backbone"
        },
        "underscore": {
            exports: "_"
        }
    }
});

插件

require.js还提供一系列插件,实现一些特定的功能

如 text和image插件,允许require.js加载文本和图片文件。

define(['text!review.txt','image!cat.jpg'], function(review,cat){
    console.log(review);
    document.body.appendChild(cat);
});

CMD规范和sea.js

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

ES6 Module

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

export和import

ES6 模块使用export定义导出接口,使用import导入其他模块

// math.js
let addNum = function (a, b) {
    return a + b;
};
export { addNum };

// index.js
import { addNum } from './math';

addNuma(1, 2)  // 3
}

使用export default默认导出

// math.js
let addNum = function (a, b) {
   return a + b;
};

let math = {
   addNum
}
export default math;

// index.js
import math from './math';

math.addNuma(1, 2) // 3
}

静态加载(又称编译时加载)

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高

动态更新

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500 毫秒之后变成baz。

这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新

动态加载:import()

动态加载解决方案,import()类似于CommonJs中的require,不用点在于require是同步的,而import()是异步的。

import()返回一个promise对象

ES6模块与CommonJS模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是动态加载,ES6模块是编译时输出接口(使用import()方法也可以动态加载)。
  • ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

在浏览器中使用ES6模块

使用type="module"标记的script,支持浏览器会将其当做ES6模块解析,不支持的浏览器会忽略。

<script type="module" src="module.mjs"></script>

<script nomodule src="fallback.js"></script>

nomodule属性提供了一种兼容方案,支持ES6模块的浏览器会将其忽略,不支持的浏览器将会执行。

type="module"的scripe标签,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了script标签的defer属性

如果网页有多个type="module"的标签,它们会按照在页面出现的顺序依次执行。

总结

  • ES6 模块设计为前后端都可使用的方案,
  • ES6 模块支持性差,node原生不支持,浏览器端支持也一般。
  • import导入
  • export导出
  • import命令为静态导入
  • import()动态导入,返回promise
  • 多次导入,只执行一次
  • 动态更新

webpack模块

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)

什么是 webpack 模块

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件(<img src=...>)中的图片链接(image url)
  • webpack 通过 loader 可以支持各种语言和预处理器编写模块

即基于CommonJS/AMD/ES6 module规范的js文件,以及其他配置了loader的文件(如图片,css),都是webpack的模块。

使用webpack可以让你使用各种模块规范编写基于浏览器端的代码,webpack会帮你编译成浏览器可以识别和执行的代码。

参考