目录

一、为什么需要模块化?

1.原始写法

2.添加命名空间

3.立即执行函数

二、CommonJS、AMD和CMD规范

1.CommonJS规范

2.AMD规范与RequireJS

3.CMD规范与Sea.js

三、ECMAScript6标准的模块支持

推荐阅读


一、为什么需要模块化?

JavaScript发展初期,代码简单地堆积在一起,只要能顺利地从上往下一次执行即可。但随着网站越来越复杂,实现网站功能的JavaScript代码也越来越庞大,网页越来越像桌面程序,很多问题开始暴露出来,比如全局变量冲突、函数命名冲突、依赖关系处理等。

1.原始写法

既然模块是要实现某个功能,那么可以把实现功能的一组函数放在同一文件中,像下面这样

function a1() {
  // ...
}
function b2() {
  // ...
}

函数a1和b2组成一个模块,其他文件先加载该模块,再对函数进行调用。

缺点:容易发生变量命名冲突,“污染”全局变量,模块成员之间没有太多必然的联系。

2.添加命名空间

使用命名空间来管理模块,即使用单全局变量的模式。

var module_special = {
  _index: 0,
  a1: function () {
    // ......
  },
  b2: function () {
    // ......
  }
}

// 调用
module_special.a1()
module_special.b2()

通常在属性名前加下划线表示该属性为私有属性,不过这只是一种开发规范上的约定,这里实际上该属性仍然向外暴露。那么怎样让私有属性不被暴露呢?那就需要下面的模块化方式。

3.立即执行函数

立即执行函数简称“IIFE”(Immediately-Invoked Function Expression),也称为匿名函数。

匿名函数能够形成一个独立的作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以匿名函数内部定义的变量不会与外部的变量发生冲突。

var module_special = (function () {
  var _index = 0
  var a1 = function () {
    // ......
  }
  var b2 = function () {
    // ......
  }
  return {
    a1: a1,
    b2: b2
  }
})()

// 调用
module_special.a1()
module_special.b2()

这种方式既笔名了命名冲突,又使得私有变量_index不能被外部访问和修改。jQuery源码大量采用了这种方式。

二、CommonJS、AMD和CMD规范

时下流行的模块化规范主要有CommonJS、AMD和CMD规范

CommonJS规范的实现代表是Node.js,AMD规范的实现代表是RequireJS,CMD规范的实现代表是Sea.js

1.CommonJS规范

Node.js应用由模块组成,采用CommonJS规范,通过全局方法require来加载模块

var http = require('http')                            // 引入http模块
var server = http.createServer(function (req, res) {  // 用http模块提供的方法创建一个服务
  res.statusCode = 200                                // 返回状态码为200
  res.setHeader('Content-Type', 'text/plain')         // 指定请求和响应的HTTP内容类型
  res.end('Hello World\n')                            // 返回的数据
})
server.listen(3000, '127.0.0.1', function () {        // 监听的端口和主机名
  console.log('Server running at http://127.0.0.1:3000') // 服务启动成功后控制台打印信息
})

如何编写一个CommonJS规范的模块?这就需要Module对象。

Node.js内部提供一个Module构建函数,所有模块都是Module的实例。每个模块内部,都有一个Module对象,代表当前模块,包含如下属性:

  • id:模块的识别符,通常是带有绝对路径的模块文件名
  • filename:模块的文件名,带有绝对路径
  • loaded:返回一个布尔值,表示模块是否已经完成加载
  • parent:返回一个对象,表示调用该模块
  • children:返回一个数组,表示该模块要用到的其他模块
  • exports:表示模块对外输出的值

其中exports是编写模块的关键,其表示当前模块对外输出的接口。其他文件加载该模块,实际读取的是module.exports。

// moduleA.js
module.exports = function (params) {
  console.log(params)
}

// 假设两个文件在同一目录下
var moduleA = require('./moduleA')
moduleA()

// 为了方便,Node.js为每个模块提供一个exports变量指向module.exports
// 那么moduleA也可以这样编写
exports.moduleA = function (params) {
  console.log(params)
}

注意:不能把值直接赋给exports,因为这样等于切断了exports与module.exports的联系

总结CommonJS模块的特点如下:

  • 所有模块都有单独作用域,不会污染全局作用域
  • 重复加载模块只会加载一次,后面都从缓存读取
  • 模块加载的顺序按照代码中出现的顺序
  • 模块加载是同步的

2.AMD规范与RequireJS

CommonJS模块采用同步加载,适合服务端却不适合浏览器。AMD规范支持异步加载模块,规范中定义了一个全局变量define函数,描述如下:

define(id?, dependencies?, factory)

第一个参数id,为字符串类型,表示模块标识,为可选参数。若不存在则模块标识默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识。

第二个参数dependencies,定义当前所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。

第三个参数factory,为模块初始化时要执行的函数或对象。如果为函数,只被执行一次。如果是对象,此对象应该为模块的输出值。如果工厂方法返回一个值(对象、函数或任意强制类型转换为true的值),应该设置为该模块的输出值。

创建一个标准AMD模块

define('alpha', ['require', 'exports', 'beta'], function (require, exports, beta) {
  exports.berb = function () {
    return beta.verb()
    // 或者 return require('beta').verb()
  }
})

创建模块标识为“alpha”的模块,依赖于内置的“require”和“exports”模块和外部标识为“beta”的模块。require函数取得模块的引用,从而即使模块没有作为参数定义,也能够被使用。exports是定义的alpha模块的实体,在其上定义的任何属性和方法也就是alpha模块的属性和方法。

RequireJS库能够把AMD规范应用到实际浏览器Web端的开发中,其主要解决了两个问题:实现JavaScript文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。

// AMD Wrapper
define(
  ['types/Employee'],     // 依赖
  function(Employee) {    // 这个回调会在所有依赖都被加载后才执行
    function Programmer() {
      // do something
    }
    Programmer.prototype = new Employee()
    return Programmer // return Constructor
  }
)

3.CMD规范与Sea.js

CMD规范全称为Common Module Definition

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();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

 

三、ECMAScript6标准的模块支持

ECMAScript5及之前的版本不支持原生模块化,需要引入AMD规范的RequireJS或者AMD规范的Seajs等第三方库来实现。直到ECMAScript6才支持原生模块化,其不但具有CommonJS规范和AMD规范的优点,而且实现得更加友好,语法较之CommonJS更简洁、支持编译时加载(静态加载),循环依赖处理得更好。

ES6模块功能主要由两个命令构成:export和import,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

1.export

在ES6中,一个模块也是一个独立的文件,具有独立的作用域,通过export命令输出内部变量

// car.js代码
let name = 'bus'
let color = 'green'
let weight = '20吨吨吨'
export {name, color, weight}

// export命令除了输出变量,还可以输出函数或类
export function run() {
  console.log('Bus is running')
}
// 可以使用as关键字对输出的变量、函数、类重命名
let name = 'bus'
let color = 'green'
let weight = '20吨吨吨'
function run() { console.log('Bus is running') }
export {
  name as busName,
  color as busColor,
  weight as busWeight,
  run as busRun
}

2.import

import命令用于导入模块

import { name, color, weight, run } from './car'

// 导入一个模块的时候也可以用as关键字对模块进行重命名
import {name as busName } from './car'

// 通过星号'*'整体加载某个文件
import * as car from './car'
console.log(car.name)   // bus
console.log(car.color)  // green

3.export default命令

从前面的例子可以看出,使用import命令加载模块时需要知道变量名或者函数名,或者整个文件,否则无法加载。为了方便,可以使用export default命令为模块指定默认输出,加载该模块时,可以使用import命令为其指定任意名字。

// 定义模块math.js
let basicNum = 0
let add = function(a, b) {
  return a+b
}
export default { basicNum, add }

// 引入
import math from './math'
function test() {
  console.log(math.add(99 + math.basicNum))
}

附:阮一峰《ES6标准入门》

import命令是静态加载而不是动态加载的,如果import命令要取代require方法,就要能实现动态加载。

有一个提案:建议引入import()函数,完成动态加载,import命令能够接收什么参数,import()函数命令就能接受什么参数。

两者的主要区别是,后者为动态加载。

Vue的路由懒加载是使用了这个标准吗?

推荐阅读

https://juejin.im/post/5aaa37c8f265da23945f365c?utm_medium=fe&utm_source=weixinqun

http://huangxuan.me/js-module-7day/#/6