模块化解析

更新时间: 2023-03-11 21:18:26

# 什么是模块

随着程序规模的扩大,以及引入各种第三方库,共享全局作用域会带来很多问题。首先是命名冲突问题,为了解决命名冲突问题,主流编程语言都提供了语言层面的方案,JavaScript社区选择了模块方案。一个合格的模块方案需要满足以下特性:

  • 独立性——能够独立完成某个功能,隔绝外部环境的影响
  • 完整性——能够完成某个特定功能
  • 可依赖——可以依赖其他模块
  • 被依赖——可以被其他模块依赖

# 原始模块

如果仅从定义层面来看,一个函数即可称为一个模块,而我们早就开始使用这种模块了:

// 最简单的函数,可以称作一个模块
function add(x,y) {
  return x + y
}
1
2
3
4

ECMAScript2015(即es6)之前,只有函数能够创建作用域。下面是JavaScript社区中原始模块的定义代码:

(function (mod, $) {
  function clone(source) {
    //此处省略代码
  }
  mod.clone = clone
})((window.clone = window.clone || {}), jQuery)
1
2
3
4
5
6

上面的mod模块不会被重复定义,依赖通过函数参数注入。这种实现其实并不完美,仍需要手动维护依赖的顺序,典型的场景就是其中的jQuery必须先于代码被引用,否则会报引用错误。随着模块数量的增加,这种问题很快变得不可维护,这显然不是我们想要的。

一般的库都会提供对这种模块的支持,因为这种模块可以直接通过script标签引入,使用script标签引入库的方式依然存在使用场景,如古老的前端系统、简单的活动页面、简单的测试页面等。

# AMD

AMD是一种异步模块加载规范,专为浏览器端设计,其全称是Asynchronous Module Definition,中文名称是异步模块定义。AMD规范定义模块的方式如下:

define(id?, dependencies?, factory)
1

浏览器并不支持AMD模块,在浏览器端,需要借助RequireJs才能加载AMD模块。RequireJS是使用最广泛的AMD模块加载器,但目前的新系统基本不再使用RequireJS,因为大部分库都会提供对AMD模块的支持。

// 匿名,无依赖模块,文件名就是模块名
define(function() {
  function clone(source) {
    //此处省略代码
  }

  return clone
})
1
2
3
4
5
6
7
8

上面的代码定义了一个匿名AMD模块,假设代码位于clone.js文件中,那么在index.js文件中可以像下面这样使用上面定义的模块

define(['clone'], function(clone) {
  const a = {a:1}
  const b = clone(a)
})
1
2
3
4

# CommonJS

CommonJS是一种同步模块加载规范,目前主要用于Node.js环境中(Sea.js使用的也是CommonJS规范)。CommonJS规范中定义模块的方式如下:

define(function(require, exports, module) {
  //此处省略代码
})
1
2
3

Node.js中,外面的define包裹函数是系统自动生成的,不需要开发者自己书写。

// 匿名,无依赖模块,文件名就是模块名
function clone(source) {
  //此处省略代码
}

module.exports = clone
1
2
3
4
5
6

Node.js环境下,假设上面的代码位于clone.js文件中,那么在index.js文件中可以像下面代码这样使用上面代码定义的模块

const clone = require('./clone.js')
const a = {a:1}
const b = clone(a)
1
2
3

# UMD

UMD是一种通用模块加载规范,其全称是Universal Module Definition,中文名称是通用模块定义。UMD想要解决的问题和其名称所传递的意思是一致的,它并不是一种新的规范,而是对前面介绍的3种模块规范(原始模块、AMD,CommonJS)的整合,支持UMD规范的库可以在任何模块环境中工作。

(function (root, factory) {
  var clone = factory(root)
  if(typeof define === 'function' && define.amd) {
    //AMD
    define('clone', function() {
      return clone
    })
  } else if (typeof exports === 'object') {
    //CommonJS
    module.exports = clone
  } else {
    // 原始模块
    var _clone = root.clone

    clone.noConflict = function() {
      if(root.clone === clone) {
        root.clone = _clone
      }
      return clone;
    }
  }
})(this, function(root) {
  function clone(source) {
    // 此处省略代码
  }
  return clone
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

可以看到,UMD规范只是对不同模块规范的简单整合,稍微不同的是,代码中给原始模块增加了noConflict方法,使用noConflict方法可以解决全局名称冲突的问题。

# ES Module

ECMAScript 2015带来了原生的模块系统——ES Module。目前,部分浏览器已经支持直接使用ES Module,而不兼容的浏览器则可以通过构建工具来使用。

ES Module的语法更加简单,只需要在函数前面加上关键字export即可。

export function clone(source) {
  //此处省略代码
}
1
2
3

假设上面的代码位于clone.js文件中,那么在index.js文件中可以像下面代码这样引用clone.js文件中的clone函数

import {clone} from './clone.js'
const a = {a:1}
const b = clone(a)
1
2
3

# 总结

对于开源库来说,为了满足各种模块使用者的需求,需要对每种模块提供支持。开源库可以提供两个入口文件,这两个入口文件及其支持的模块如下:

入口文件 支持的模块
index.js 原始模块,AMD模块,CommonJS模块,UMD模块
index.esm.js ES Module