# 前端模块化历程

在 ES6 之前,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。

那么什么是模块化呢?模块就是实现特定功能的相互独立的一组方法,块内的数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。

所以模块化的存在可以让我们对代码进行高度复用,提高代码的可维护性、减少全局污染,同时能更好的管理网页的业务逻辑。

# Object Literal pattern

JavaScript 开发人员针对模块化提出了一系列的解决方案,比如对内置的对象进行封装:

// person.js
var person = {
  name: 'Kisstar',
}

// hello.js
person.sayHello = function() {
  console.log('Hello ' + this.name)
}
<!DOCTYPE html>
<html>
  <head>
    <script src="person.js"></script>
    <script src="hello.js"></script>
  </head>
  <body>
    <script>
      person.sayHello()
    </script>
  </body>
  <script>
    // destroy internal data
    person.name = 'xxx'
    // shared scope means other code can inadvertently destroy ours
    var person = 'all gone!'
  </script>
</html>

利用对象我们可以很方便的实现一个模块,但显然模块内的数据很不安全,外部可以进行任意的修改。

同时,我们依赖了一个全局变量(person),如果其它的 JavaScript 代码也使用了这个变量,我们很可能会失去的原本需要的功能。

# IIFE

通过立即调用函数表达式,我们可以将数据和行为封装到一个函数内部, 通过给指定的对象(比如:全局对象)添加属性来向外暴露接口。

// person.js
var person = (function(name) {
  var insertName = name || 'Kisstar'

  return {
    getName: function() {
      return insertName
    },
  }
})(outerName)

// hello.js
;(function(module) {
  module.sayHello = function() {
    console.log('Hello ' + this.getName())
  }
})(person)
<!DOCTYPE html>
<html>
  <head>
    <script>
      // set init values
      var outerName = 'Sharon'
    </script>
    <script src="person.js"></script>
    <script src="author.js"></script>
  </head>
  <body>
    <script>
      person.sayHello()
    </script>
  </body>
  <script>
    // shared scope means other code can inadvertently destroy ours
    var person = 'all gone!'
  </script>
</html>

使用 IIFE 之后我们将一些数据进行了私有化(name),外部将不能对其进行直接修改,而且 hello.js 的代码也可以得到复用,如果我们传入不用的模块,他们都会得到一个 sayHello 的方法。

遗憾的是,这种方式依然依赖全局变量,而且实现上并不优雅。

# CommonJS

随着 Node.js 项目的诞生,JavaScript 语言被带到了服务器。为了应对复杂的服务端开发,Node.js 参照 CommonJS 规范实现了自己的模块化系统。

根据 CommonJS 规范,一个单独的文件就是一个模块。加载模块使用 require 方法,该方法读取一个文件并执行,最后返回文件内部的 exports 对象。

// person.js
module.exports = function(name) {
  var insertName = name || 'Kisstar'

  return {
    getName: function() {
      return insertName
    },
  }
}

// hello.js
module.exports = function(module) {
  module.sayHello = function() {
    console.log('Hello ' + this.getName())
  }
}

// app.js
var person = require('./person')('Sharon')
var withHello = require('./hello')

withHello(person)
person.sayHello()

通过一些编译工具,我们也可以让复合 CommonJS 规范的代码运行在浏览器上,比如使用 Browserify:

npx browserify app.js -o dist/bundle.js
<!DOCTYPE html>
<html>
  <head>
    <script src="dist/bundle.js"></script>
  </head>
  <body></body>
</html>

现在模块的代码都运行在自己的作用域内,不会污染全局作用域,整个语法看起来也更加整洁和明朗。

缺点是,CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。这对于浏览器环境来说很不友好,可能导致很糟糕的用户体验。

# AMD

AMD 规范通过一个 define 方法来定义模块,它可以接受三个参数(只有函数是必须的):

  • 模块名;
  • 第二个参数是一个数组,指明了此模块的依赖;
  • 模块的具体实现。

当我们需要使用一个模块时,可以使用配对的 require 方法,它可以接受两个参数:

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

  return {
    getName: function() {
      return insertName
    },
  }
})

// hello.js
define(function() {
  return function(module) {
    module.sayHello = function() {
      console.log('Hello ' + this.getName())
    }
  }
})
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script>
  </head>
  <body>
    <script>
      require(['person', 'hello'], function(person, withHello) {
        withHello(person)
        person.sayHello()
      })
    </script>
  </body>
</html>

目前,AMD 规范运行的很好,但严重依赖于一个较为丑陋的语法格式,并且由于浏览器所要求的异步特性导致它不能被静态分析。

# UMD

UMD 试图将 AMD 和 CJS 结合在一起,下面是一个简单的例子(了解更多 (opens new window)):

// if the module has no dependencies, the above pattern can be simplified to
;(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define([], factory)
  } else if (typeof module === 'object' && module.exports) {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory()
  } else {
    // Browser globals (root is window)
    root.returnExports = factory()
  }
})(typeof self !== 'undefined' ? self : this, function() {
  // Just return a value to define the module export.
  // This example returns an object, but the module
  // can return a function as the exported value.
  return {}
})

所以,UMD 具备两者的优点,同时也包含两者的缺点,不过它确实朝着同构 JavaScript 的方向迈出了第一步,它可以在服务器和客户端上运行。

# ES6 modules

ES6 在语言标准的层面上,实现了模块功能,其设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

除了静态加载带来的各种好处,ES6 模块还有以下好处:

  • 不再需要 UMD 模块,将来服务器和浏览器都会支持 ES6 模块格式。
  • 不再需要将对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性。
// person.js
export default function(name) {
  var insertName = name || 'Kisstar'

  return {
    getName: function() {
      return insertName
    },
  }
}

// hello.js
export default function(module) {
  module.sayHello = function() {
    console.log('Hello ' + this.getName())
  }
}
<!DOCTYPE html>
<html>
  <body>
    <script type="module">
      import getPerson from './person.js'
      import withHello from './hello.js'

      var person = getPerson('Sharon')
      withHello(person)
      person.sayHello()
    </script>
  </body>
</html>

查看更多关于 ES6 modules 的信息,请点击查看 Module 的语法 - ECMAScript 6 入门 (opens new window)

# 常见问题

  • CommonJS 规法和 ES Module 规范的区别?

# 参考