JS 模块化

Aug 7 · 5 min

介绍:
当今主流的模块化方案主要有五种:

  • IIFE
  • AMD
  • CMD
  • CommonJS
  • ESModule
  • UMD

IIFE 是一个自执行函数,在 JavaScript 早期因为没有模块化的语法,我们常常用自执行函数来模拟模块,IIFE 自带作用域

AMD/RequireJS 规范能使浏览器异步加载模块,原则是依赖前置

CMD/SeaJS 规范和 AMD 规范类似,都用于浏览器环境的模块化加载,原则是就近依赖

CommonJS 规范主要用于服务端编程,由于它的同步加载机制导致的代码阻塞,并不适合浏览器环境,因此就有了 AMDCMD 解决方案

ESModuleES6 提供的的模块化标准,关键字有 importexportexprot defaultasfrom

UMD 全称 Universal Module Definition,是一个兼容了 AMDCMDCommonJSESModule 写法的通用模块

#模块化的优点

  • 可复用性高
  • 代码可以解耦,更好维护
  • 避免命名冲突(全局变量污染)
  • 可异步加载模块,避免发送多个请求
  • 依赖明确,不需要关注依赖引入的顺序问题

#IIFE

// 自执行函数模拟模块化
 
// Person 模块
(() => {
  // 实例个数,模块内部变量,外部无法直接访问,
  let number = 0
  function Person(name, age) {
    number ++
    this.name = name
    this.age = age
  }
 
  Person.prototype.getName = function() {
    return this.name
  }
 
  Person.getInstanceNumber = function() {
    return number
  }
 
  // 对外抛出接口
  window.Person = Person
})();
 
 
// main 模块
(() => {
  // 通过 window 引入模块
  const Person = window.Person
 
  const p1 = new Person('Tom', 20)
  const p2 = new Person('Jake', 20)
  const p3 = new Person('Alex', 20)
 
  p1.getName()
 
  console.log('实例化个数', Person.getInstanceNumber())
})()
 
js

#AMD

AMD ,异步模块定义(Asynchronous Module Definition),它的特点是依赖前置,依赖必须在最先定义好,等到异步加载完成这些依赖后会立刻执行这些依赖代码。

// 依赖必须一开始就写好
define(['./a', './b'], function (a, b) {
  a.doSomething()
  // 此处省去 100 行
  b.doSomething()
})
js

#CMD

CMD ,通用模块定义(Common Module Definition),它的特点是就近依赖,也就是什么时候 require ,就什么时候执行依赖代码。

define(function (require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此处省略 100 行
  var b = require('./b')
  b.doSomething()
  // ...
})
js

#AMD/CMD 区别

1、AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
2、CMD 推崇就近依赖,只有在用到某个模块的时候再去 require

#CommonJS

CommonJS 模块中,通过 require 导入模块,通过 exports/modeule.exports来导出。

// 导入
const sum = require('./add.js')
console.log(sum(2, 3)) // 5
js
// 导出
module.exports = {
  sum
}
// 或
exports.sum = sum
js

其内在机制是将 exports 指向了 module.exports ,而 module.exports 在初始化时是一个空对象。我们可以简单地理解为,CommonJS 在每个模块的首部默认添加了以下代码:

var module = {
  exports: {}
}
var exports = module.exports
js

此外 每个模块加载完成一次之后会被缓存,对于原始类型来说,在模块内修改导入的原始类型,被导入模块中的原始 类型是不会改变的,引用类型除外。也可以理解为每个模块在加载一次之后就会被缓存。

总结一下 CommonJS :
优点:

  • 每个文件都是一个模块实例,代码运行在模块作用域,不会污染全局作用域
  • 文件内通过 require 对象引入指定模块,通过 exports 对象来向外暴漏 API,文件内定义的变量、函数,都是 私有 的,对其他文件不可见
  • 每个模块加载一次之后就会被缓存
  • 所有文件加载均是 同步 完成,加载的顺序,按照其在代码中出现的顺序
  • 模块输出的是一个值的拷贝,修改模块内部的原始类型的值并不会改变模块内原始类型的值

缺点:

  • 发送多个请求,模块同步加载,资源消耗和等待时间 ,适用于服务器编程
  • 引入的 js 文件顺序不能搞错,否则会报错

#ESModule

ESModule 是 ES6 的模块化规范,它的特点是输出的是值的引用,脚本执行时,根据引用,到模块里面取值,若原始值变了,import 加载的值也会跟着变)。基本语法

// export
export { sum, sub, div, mult }
// import
import { sum, sub, div, mult } from './math'
sum(2, 3)
sub(2, 3)
div(2, 3)
mult(2, 3)
js

觉得上面看着太冗余,可以使用这样的写法:

// export
export { sum, sub, div, mult }
// import
import * as myMath from './math'
myMath.sum(2, 3)
myMath.sub(2, 3)
myMath.div(2, 3)
myMath.mult(2, 3)
js

#UMD

UMD 是一个兼容写法,一个开源模块可能会提供给 CommonJS 标准的项目中实现,也可能提供给 AMD 标准的项目使用, UMD 应运而生。

(function(root, factory) {
  if (typeof define === 'function' && define.amd) { // AMD
    define(['person'], factory)
  } else if (typeof define === 'function' && define.cmd) { // CMD
    define(function(require, exports, module) {
      module.exports = factory()
    })
  } else if (typeof exports === 'object') { // CommonJS
    module.exports = factory()
  } else { // global
    root.person = factory()
  }
})(this, function() {
  let number = 0
  function Person(name, age) {
    number++
    this.name = name
    this.age = age
  }
 
  // 对外暴露接口
  Person.prototype.getName = function () {
    return this.name
  }
 
  function getInstanceNumber () {
    return number
  }
 
  return {
    Person,
    getInstanceNumber
  }
})
js

很多开源模块都会采用这种兼容性的写法。

#CommonJS 和 ESModule 区别

CommonJS 模块ESModule
关键字require exportsimport、export、default、as、from
执行方式require 需要代码同步执行 - 不适合浏览器端异步
输出输出的是一个值的拷贝
(一旦输出一个值,模块内部的变化就影响不到这个值)
输出的是值的引用
(动态引用,脚本执行时,再根据引用,到模块里面取值,若原始值变了,import 加载的值也会跟着变)
时机运行时加载,加载的是 整个模块 - 所有接口,只有在 脚本运行 完才会生成编译时输出 接口,可以单独加载某个接口,在代码 静态解析 阶段就会生成
加载原理一个模块就是一个脚本,require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象,以后需要用到这个模块的时候,就会到 exports 属性上面取值。也就是说,不会再次执行该模块,而是到 缓存 之中取值,只会在第一次加载时运行一次

#参考文章

Segmentfault - 前端模块化
blog - 前端模块化
掘金 - 前端模块化
JavaScript 核心进阶 - 第六章模块化6.8