浅析前端的模块化

咱们可能常常听到一些模块化的概念,譬如 AMDCommonJSES Modules。这些又是什么概念呢?它们为何而存在,做用又是什么呢?本文将对模块化的概念进行逐一分析。node

为何须要模块化

在了解模块化的概念前,首先先解决一个问题 - 为何须要模块化?webpack

先从实际问题出发,在相似 require.jssea.jsbrowserifywebpack 等工具出现以前,咱们可能会遇到以下一些问题:web

  • 咱们在引用不一样的库或者 js 文件时,可能会出现命名冲突。
  • 引用的库或者文件都会有个先来后到,那如何去决定与维护这些顺序呢?
  • 库与库之间、文件与文件之间可能存在循环引用,即在 a.js 中引用了 b.js,而 b.js 中也引用了 a.js;二者相互依赖。那咱们如何决定先引用哪一个件呢?

咱们再从一个生活中的例子出发,简要了解一下模块化的优势,即为何须要模块化的缘由。npm

假设你有一套修理工具箱,里面包含了属于这套修理工具箱的各类型号的螺丝批、钳子和锤子等等。每次家里水管破了,灯泡坏了什么的,你均可以拿这套修理工具箱进行修理。每次修理可能都会形成一些工具的损耗或损坏,损坏以后咱们就应该去买相同型号的工具进行补充;其余不对头的工具不会回收回这套修理工具箱中,一样这套修理工具箱中的工具也不会随意扔出工具箱中。设计模式

咱们把上述例子转化为模块化来看看。首先,修理工具箱就是 模块,里面的工具就是 模块 中的各类变量或函数。工具出现损坏等于 模块 内出了什么问题,这时候咱们只须要修复 模块 内的 bug 就行了。其余不对头的工具不会回收回工具箱中,反之工具箱中的工具不会随意被扔出表示 模块 内的变量、函数等不会污染外部的变量、函数等等,反之亦然。这套工具箱能够重复利用也就是 模块 的复用性很强。数组

总结模块化有三大优势:浏览器

  • 可维护性强,更新或修复模块内的逻辑不会影响外部逻辑
  • 独立的命名空间,模块内的变量不会污染外部的变量,即便它们拥有相同的变量名
  • 可复用性强,咱们须要在不一样的地方用到某个模块,只须要在对应的地方引入它就好了,无需重复地拷贝复制。

什么是模块化

接下来将会经过一些常见的例子与概念来解释什么是模块化。注意,闭包在模块化中有着重要的应用,这里假设你对闭包概念已有所了解。安全

IIFE - 当即执行函数表达式

顾名思义,当即执行函数表达式就是一个函数在定义时就会当即执行。服务器

var global = "I'm global"

(function () {
  var foo = 'foo'

  function bar () {
    console.log('bar')
  }

  console.log(foo)

  bar()

  console.log(global)
})()
// foo
// bar
// I'm global

var foo = 'global foo'
console.log(foo)  // global foo
bar() // Uncaught ReferenceError: bar is not defined
复制代码

能够看到,咱们在当即执行函数表达式的外部访问其变量会抛出错误,而在当即执行函数表达式的内部能够随时访问外部变量。外部与当即执行函数表达式一样有命名为 foo 的变量,但这二者互不影响。其实这种行为就相似于 C++ 等语言中类的私有变量、私有方法。闭包

固然咱们还可让当即执行函数表达式放回一些东西,相似类的暴露公共方法、变量的概念。

var module = (function () {
  var _privateCnt = 0
  var _privateProperty = 'I am private property'

  function _privateCnter () {
    return _privateCnt += 1
  }

  function publicCnter () {
    return _privateCnter()
  }

  return {
    property: _privateProperty,
    publicCnter: publicCnter
  }

})()

console.log(module.property) // I am private property
console.log(module.publicCnter()) // 1
console.log(module.publicCnter()) // 2
console.log(module.publicCnter()) // 3
复制代码

这个例子展现了经过当即执行函数表达式将一些变量、方法暴露出去,并防止外部直接修改一些咱们不但愿修改的变量、方法。这样作还有一个好处,就是咱们能够快速地了解到这个当即执行函数为咱们提供了哪些公共属性及方法,而不须要阅读全部逻辑代码。这种方式在设计模式中也称做 模块模式(Module Pattern)

CommonJS

CommonJS 主要是为服务端定义的模块规范,它一开始的名字为 ServerJSnpm 生态系统基本都是基于 CommonJS 规范所创建起来的。

// 在 foo.js 中,咱们导出了变量 foo
module.exports = {
  foo: 'foo'
}

// 在 bar.js,咱们经过 require 引入了变量 foo
var module = require('foo')
console.log(module.foo) // foo
复制代码

看起来很简单是吧。可能有人会问了,这个 module 是什么东西呢?其实 module 是 Node 中的一个内置对象。咱们能够在 node 环境下打印看看

咱们能够看到 module 有好几个属性,其中 id 是为了让 node 知道这个模块在哪里,是啥;exports 就是咱们要导出的对象了。

在确保 foo.jsbar.js 在同一目录下,咱们再将例子稍加修改:

// foo,js
module.exports = {
  foo: 'foo'
}
console.log('module: ', module)

// bar.js
var module = require('./foo')
console.log(module.foo)
复制代码

运行 node bar.js 能够获得如下信息:

经过 CommonJS 规范定义的模块一样有一开始说到的模块的三大优势,其实咱们只须要把这些模块文件看出一个个当即执行函数,也就会很好理解了。

CommonJS 里模块都是同步加载的,在浏览器中若是同步去加载模块的话会形成阻塞,致使页面性能降低;而在服务端中,由于文件都存在于同一个硬盘上,因此即便是同步加载都不会有什么影响。

再补充一个小细节,你可能时不时能看到 var exports = module.exports 这样的代码。或许你会问为何要怎么作,难道有什么技巧吗?其实这只是简单的引用而已。即变量 exports 一样指向了 module.exports 的内存地址,也就是二者指向的对象是彻底同样的。咱们想在 module.exports 里添加导出的东西时,只须要在 exports 里加就好了。就是这么简单,只不过被一些说法搞得高深莫测了而已。

var exports = module.exports

exports.foo = 'foo''
复制代码

AMD - Asynchronous Module Definition

刚刚咱们说到 CommonJS 主要是用于服务端的规范,而客户端是没法使用它的,而且 CommonJS 是同步加载模块的。因此咱们又有了叫作 AMD 规范的东西,也就是异步模块定义规范。顾名思义,咱们能够利用这个规范来作到模块与模块的依赖能够经过异步的方式来加载;这也是浏览器(客户端)所但愿的。

AMD 中的核心就是 define 这个方法。

define(
  module_id,
  [dependencies],
  definition
)
复制代码

其中 define 中的 module_iddependencies 为可选参数。

首先 module_id 它是一个字符串,指的是定义的模块的名字,这个名字必须是惟一的。第二个参数 dependencies 是模块所依赖的模块组成的数组,并做为参数传入给第三个参数 definition 工厂方法中。第三个参数 definition 就是为模块初始化要执行的函数或对象。若是为函数,它应该只被执行一次。若是是对象,此对象应该为模块的输出值。

// dep1
define('dep1', [], function () {
  return {
    doSomething: function () {
      console.log('do something')
    }
  }
})

define('dep2', [], function () {
  return {
    doOtherThing: function () {
      console.log('do other thing')
    }
  }
})

define('module', ['dep1', 'dep2'], function (dep1, dep2) {
  dep1.doSomething()
  dep2.doOtherThing()
})
复制代码

虽然 AMD 规范提供了异步加载模块的方案,可是给个人感受就是逻辑不如 CommonJS 直观。所以在 ES6 中也就有了原生的模块化: ESM - ES Modules

ES Modules

CommonJS 在服务端中应用普遍,但因为它是同步加载模块的,它在客户端不太合适;而 AMD 支持浏览器异步加载模块,但在服务端却显得没有必要,所以 ES Modules 出现了。咱们先来看看 ES Modules 是如何工做的。

ES ModulesCommonJS 很类似,较新的浏览器均已支持 ES ModulesNode.js 正在慢慢支持相关规范。

ES Modules 的核心为 exportimport,分别对应导出模块与导入模块。

导出模块:

// CommonJS
module.exports = foo () {
  console.log('here is foo')
}

// ES Modules
export default function bar () {
  console.log('here is bar')
}
复制代码

导入模块:

// CommonJS
var foo = require('./foo')

foo() // here is foo

// ES Modules
import bar from './bar'

bar() // here is bar
复制代码

这二者这么类似,它们在实际的表现上有什么不一样呢?

我分别在 FirefoxEdgeChrome 上测试(Chrome 因为自身的安全策略没法直接经过本地文件进行测试,因此利用插件 Web Server for Chrome 起了个本地服务器。

测试代码以下图(注意咱们在使用 ES Modules 时要给 script 标签加上 type="module"

测试的结果显示为:

开始执行 bar.js
开始执行 foo.js
here is foo
here is bar
复制代码

若是咱们使用 CommonJS 又会有什么结果呢?

开始执行 foo.js
here is foo
开始执行 bar.js
here is bar
复制代码

很显然 CommonJS 是同步加载模块的,因此代码的执行也是顺序的。而 ES Modules 是异步加载模块的,且 ES Modules 是编译时加载模块,在运行时(执行代码时)再根据相关的引用去被加载的模块中取值。再详细一点来讲的话,整个过程分以下三个步骤:

  • 构建 - 查找、下载并将文件解析到模块记录中
  • 实例化 - 在内存中找到全部导出(export)的值的位置,但暂不对这些值进行赋值;而后在内存中建立 exportsimports 的空间。这一步称为连接
  • 运行 - 运行代码并将实际的值赋予给实例化中导出的值

ES Modules 解析的相关参考文章

所以在编译期间,编译器先找到了 foo.js 的依赖 bar.js,先编译 bar.js 而后才是 foo.js。因此你才会先看到 开始执行 bar.js

总结

  • 模块化简单来讲就是将相关的逻辑代码独立出来,独立的形式有不少种,能够是单纯的一个函数,亦能够是单独的一个文件。
  • 模块化能够更好地组织代码结构,加强其可维护性,可复用性强。
  • CommonJS 工做原理为同步加载模块,在 Node.js 中有着普遍的使用,对客户端不友好。
  • AMD 工做原理为异步加载模块。
  • ES ModulesES6 推出的规范,客户端的支持比较好,Node.js 将会慢慢全面支持。它与 AMD 同样,也是异步加载模块。
相关文章
相关标签/搜索