Life and freedom Ge Lin ——— Draw by Razzh

JS 模块化

Aug 7 · 11 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())
})()
 

#AMD

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

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

#CMD

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

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

#AMD/CMD 区别

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

#CommonJS

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

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

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

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

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

总结一下 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)

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

// 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)

#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
  }
})

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

#CommonJS 和 ESModule 区别

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

#参考文章

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

浙ICP备2024129591号-1
春秋(Live) - 张敬轩
--:-- / --:--
  1. 1春秋(Live)张敬轩
  2. 2不吐不快(live)张敬轩
  3. 3男孩最痛(live)张敬轩
  4. 4粤语残片(live)陈奕迅
  5. 5几分之几(live)卢广仲
  6. 6地球很危险古巨基
  7. 7樱花树下(live)张敬轩

春秋 (Live) - 张敬轩 (Hins Cheung)

词:林夕

曲:Edmond Tsang

那夜谁将酒喝掉

因此我讲得多了

然后你摇着我手拒绝我

动人像友情深了

我没权终止见面

只因你友善依然

仍用接近甜蜜那种字眼通电

没人应该 怨地怨天

得到这结局

难道怪罪神没有更伪善的祝福

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者 玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志

爱若能堪称伟大 再难挨照样开怀

如令你发现为你而活到失败

令人不安 我品性坏

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志 太没意思

若自觉这叫痛苦未免过份容易

我没有被你改写一生怎配有心事

我没有被你害过恨过

写成情史 变废纸

春秋只转载要事

如果爱你欠意义

这眼泪 无从安置

我没有运气放大自私的失意

更没有道理在这日

你得到真爱制造恨意

想心酸 还可以 想心底 留根刺

至少要见面上万次