javascript模块化方案

144 阅读8分钟

javascript模块化

模块方案

模块方案
├─tree.txt
├─浏览器端
|  ├─CMD
|  |  ├─SeaJS
|  ├─AMD
|  |  ├─RequireJS
├─服务器端
|  ├─CommonJS
|  |    ├─Node
├─UMD

AMD与CMD的主要区别:

  • 对于依赖的模块,AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行了(写法不同)
  • CMD推崇依赖就近,AMD推崇依赖前置
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    var b = require('./b')
    b.doSomething()
})
// AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好  
    a.doSomething()
    b.doSomething()
})

AMD

AMD是RequreJS在推广过程中对模块定义的规范化产出

define() Function 定义模块

define(id?, dependencies?, factory);

id

第一个参数 id 是字符串文字。 它指定正在定义的模块的 id。 这个参数是可选的,如果它不存在,模块 id 应该默认为加载器为给定响应脚本请求的模块的 id。 当存在时,模块 id 必须是“顶级”或绝对 id(不允许使用相对 id)。

module id format
  • 模块标识符是由正斜杠分隔的“术语”字符串。
  • 术语必须是驼峰式标识符、“.”或“..”。
  • 模块标识符可能没有像“.js”这样的文件扩展名。
  • 模块标识符可以是“相对的”或“顶级的”。 如果第一项是“.”,则模块标识符是“相对的”。 或者 ”..”。
  • 顶级标识符从概念模块名称空间根目录中解析出来。
  • 相对标识符相对于写入和调用“require”的模块的标识符进行解析。
dependencies

第二个参数依赖项是模块 ID 的数组文字,这些模块 ID 是正在定义的模块所需的依赖项。 这个参数是可选的,依赖项必须在模块工厂函数执行之前解析,解析的值应该作为参数传递给工厂函数,参数位置对应于依赖项数组中的索引。

依赖项 id 可能是相对 id,并且应该相对于正在定义的模块进行解析。 换句话说,相对 id 是相对于模块的 id 解析的,而不是用于查找模块 id 的路径。

factory

factory:工厂方法,返回定义模块的输出值

总结一段话:声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行,依赖前置)

Example

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
   exports.verb = function() {
       return beta.verb();
       //Or:
       return require("beta").verb();
   }
});

define(["alpha"], function (alpha) {
   return {
     verb: function(){
       return alpha.verb() + 2;
     }
   };
});

define({
   add: function(x, y){
     return x + y;
   }
});


define(function (require, exports, module) {
    var a = require('a'),
     b = require('b');
    
    exports.action = function () {};
});

require() Function 加载模块

require([dependencies],function(){});
  • 第一个参数是一个数组,表示所依赖的模块
  • 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用.加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题

Example

require(['alpha'],function(alpha){
    alpha.verb ();
})

CMD

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出
在 CMD 规范中,一个模块就是一个文件

define() Function定义模块

define(factory)

factory可以是一个函数,也可以是一个对象或字符串。
factory为对象、字符串时,表示模块的接口就是该对象、字符串。

define({'foo': 'bar'})
define('I am a template. My name is {{name}.}')

factory为函数时,表示模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory方法在执行时,默认会传入三个参数: require、exports和module:

define(function(require, exports, module) {
  // 模块代码
})

define(id?, deps?, factory)

字符串id表示模块标识,数组deps是模块依赖。

define('hello', ['jquery'], function(require, exports, module) {
  // 模块代码
})

define.cmd Object

一个空对象,可以用来判定当前页面是否有CMD模块加载器

if (typeof define === 'function' && define.cmd) {
  // 有Sea.js等CMD模块加载器的存在
}

require() Function

require(id)

接受模块标识作为唯一参数,用来获取其他模块提供的接口。

define(function(require, exports, module) {
  // 获取模块 a 的接口
  var a = require('./a')
  // 调用模块 a 的方法
  a.doSomething();
})

require.async(id, callback?)

require.async方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback参数可选。

define(function(require, exports, module) {
  // 异步加载一个模块,在加载完成时,执行回调
  require.async('./b', function(b)  {
    b.doSomething()    
  })  
  // 异步加载多个模块,在加载完成时,执行回调
  require.async(['./c', './d'], function(c, d) {
    c.doSomething()
    d.doSomething()
  })
  
})

exports Object

exports是一个对象,用来向外提供接口。

define(function(require, exports, module) {
  // 对外提供foo属性
  exports.foo = 'bar'
  // 对外提供doSomething方法
  exports.doSomething = function () {}
})

除了给exports对象添加成员,还可以使用return直接向外提供接口

define(function(require, exports, module) {
  return {
    foo: 'bar',
    doSomething: function() {}
  }
})

module Object

module是一个对象,上面存储了与当前模块相关联的一些属性和方法。

module.id string

模块的唯一标识

module.exports Object

当前模块对外提供的接口。
传给factory函数的exports参数是module.exports对象的一个引用。只通过exports参数来提供接口 ,有时无法满足开发者的所有需求。比如模块的接口是某个类的实例时,需要通过module.exports来实现。

define(function(require, exports, module) {
  // exports是module.exports的一个引用
  console.log(module.exports === exports) // true
  // 重新给module.exports赋值
  module.exports = new SomeClass()
  // exports不再等于module.exports
  console.log(module.exports === exports) // false
})

CommonJS

CommonJs是服务器端模块的规范。
CommonJs定义的模块分为:模块引用(require)/模块定义(exports)/模块标识(module)

所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行过一次,然后运行结果就被缓存了,以后再加载, 就直接读取缓存结果。想要让模块再次运行,必须清除缓存。
模块的加载顺序,按照其在代码中出现的顺序。

// common.js
module.exports = function(a, b) {
  return a - b
}
let minus = require('./common.js')
console.log(minus(5, 4))

ESM

导出

分命名导出和默认导出

命名导出

// 写法1
export const name = 'test'
export const add = function (a, b) {
  return a + b
}
// 写法2
const name = 'test'
const add = function(a, b) {
  return a + b
}
export {name, add}

在使用命名导出时,可以通过as关键字对变量重命名。如:

const name = 'test'
const add = function(a, b) {
  return a + b
}
export {name, add as getSum}

默认导出

// calc.js
export default {
  name: 'test',
  add: function(a, b) {
    return a + b
  }
}

导入

针对命名导出模块的导入

// calc.js
const name = 'test'
const add = function(a, b) {
  return a + b  
}
export {name, add}
// 一般导入方式
import {name, add} from './calc.js'
add(2, 3)

// 通过as关键字对导入的变量重命名
import {name, add as calcSum} from './calc.js'
calcSum(2, 3)

// 使用 import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中,从而减少了对当前作用域的影响
import  * as calcObj from './calc.js'
calcObj.add(2, 3)

针对默认导出模块的导入

import后面直接跟变量名,并且这个名字可以自由指定,它指代了calc.js中的默认导出值。

import calc from './calc.js'

命名导出模块和默认导出模块混合导入

import React, {Component} from 'react'

注意:默认导出模块的导入必须在命名导出模块的导入之后

复合写法

在工程中,有时候需要把一个模块导入后立即导出,比如专门用来集合所有页面或组件的入口文件。采用复合写法:

export {name, add} from './calc.js'

复合写法只支持通过命名导出方式暴露出来的变量,默认导出则没有复合写法,只能将导入和导出拆开。

import calc from './calc.js'
export default calc

UMD

UMD并不能说是一种模块标准,不如说它是一组模块形式的集合更准确。UMD全称Universal Module Difinition(通用模块标准)。 也是随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。 未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了, 它的目标是使一个模块能运行在各种环境下。UMD是AMD和CommonJS的糅合。

它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身,我们看看它的具体实现:

(function(root, factory) {
   if (typeof define === 'function' && define.amd) {
       // AMD
       define(['jquery'], factory);
   } else if (typeof exports === 'object') {
       // Node, CommonJS之类的
       module.exports = factory(require('jquery'));
   } else {
       // 浏览器全局变量(root 即 window)
       root.returnExports = factory(root.jQuery);
   }
}(this, function($) {
   // 方法
   function myFunc() {};

   // 暴露公共方法
   return myFunc;

}));

不难发现,它在定义模块的时候回检测当前使用环境和模块的定义方式, 将各种模块化定义方式转化为同样一种写法。它的出现也是前端技术发展的产物, 前端在实现跨平台的道路上不断的前进,UMD 规范将浏览器端、服务器端甚至是 APP 端都大统一了, 当然它或许不是未来最好的模块化方式,未来在 ES6+、TypeScript、Dart 这些拥有高级语法的语言回代替这些方案。

参考《阮一峰:CommonJS规范》 

参考JS之AMD、CMD、CommonJS、ES6、UMD的使用笔记

参考什么是 CommonJs 、UMD、CMD、AMD