模块化考古

589 阅读5分钟

前言

前端模块化的概念在技术论坛是个老生常谈的话题。什么CommonJS、AMD、CMD、UMD、ES6模块化等都能扯一点,但大多囫囵吞枣。

本文围绕前端模块化的目的、历史时间线、应用三方面来叙述。

文中案例均测试执行过。如果文字看累了💕,代码直接复制粘贴,更容易理解。

模块化的目的

前端开发初期,Javascript只承担简单的交互动画、表单校验等功能实现。随着Ajax技术得以广泛使用,Web进入2.0时代,浏览器也因此承载了更多的内容与逻辑实现。

随着工程越来越庞大,越来越复杂,项目需要一个团队进行分工协作、进度管理等,开发人员不得不采取模块化的方法去管控项目开发、迭代。从而避免命名冲突代码冗余代码高耦合、低内聚等问题。

模块化历史时间线

无模块化规范

原始写法

将不同函数归置在一起,就算是一个模块。
缺陷:污染全局变量,存在命名冲突,模块成员之间无明显依赖关系。

function m1() {
    //...
}
function m2() {
    //...
}

对象/命名空间 写法

利用对象包裹模块,降低命名冲突的风险,有一定的模块封装和隔离。
缺陷:没有解决命名冲突的根本问题,比如模块名称相同🤦‍♂️;暴露私有内部属性和方法,且可被改写。

var m1 = new Object({
    a: 0,
    b: function() {
        //...
    }
})

m1.a = 1 // 内部a属性被改写

立即调用函数表达式(IIFE)

利用立即执行函数,创建私有的命名空间,解决暴露内部属性和方法的问题。

var m1 = (function() {
    let a = 0  //函数作用域下,a变量私有化 
    let b = function() { console.log('a': a) }
    return {
        b: b
    }
})()
m1.b() // 0

引入依赖

显式地传入模块依赖的全局变量。既保证模块的独立性,且使模块之间的依赖关系变的明显,依赖模块 (Jquery) 需要先于当前模块引入。这种写法奠定了模块化规范的基础。

<script type="text/javascript" src="jquery.js"></script>
<script>
//m1.js
var m1 = (function(window,$) {
    let a = 0
    let b = function() {
        $('body').text(a)
    }
    return {
        b: b
    }
})(window,jQuery)
m1.b() // 页面上显示0 
</script>

模块化规范

CommonJs [09- 10]

CommonJs规范下,每个js文件都被视为独立的模块,拥有私有的变量和方法,不会污染全局环境;模块可加载多次,但仅在首次加载时运行,后续加载均读取缓存文件,需清理缓存后可再次读取文件;模块加载按照代码顺序同步执行;导出的值为拷贝;
NodeJs即基于CommonJs规范下编写。

案例

//helloworld.js
module.exports.helloworld = function () {
  console.log('hello world')
}
//main.js
let helloworld = require('./hello.js').helloworld
helloworld() // node main.js 输出 hello wolrd

CommonJs 在服务端取得了不错的实践,但在浏览器端却无法得以推广,主要因为如下原因。

  1. 没有 moduleexportsrequireglobal变量。
  2. 在服务端require一个模块,只会有磁盘I/O,所以同步加载机制没什么问题;但若是浏览器加载,一是会产生开销更大的网络I/O,存在严重阻塞问题;二是异步,会产生时序上的错误。

目前,borwserify可转换CommonJS格式,使其运行在浏览器端。但无法根本解决上述所说的阻塞问题。

AMD [10]

为了制订新的适用浏览器的标准规范,衍生出三大流派(保守派、激进派、中间派),其中的激进派思想的产物即AMD规范。AMD规范是异步加载模块,允许指定回调函数。
require.Js是该规范下的产物。官网直通车

define(id?: String, dependencies?: String[], factory: Function|Object)

id 是模块名字,?代表可选参数。
dependencies 指定依赖的模块列表,是个数组,也是可选的参数。每个依赖的模块的输出将作为参数传入factory。如果没有指定dependencies,默认值是['require', 'exports', 'module']
factory是最后一个参数,包裹了模块的具体实现。

案例

//定义模块
//helloworld.js
define(function () {
  let msg = 'hello world'
  let helloworld = function () {
    console.log(msg)
  }
  return { helloworld }
})
//main.js
(function () {
  require.config({
    baseUrl: '', //加载模块根路径
    paths: {
      helloworld: './helloworld' //基于根路径下的相对路径
    }
  })
  require(['helloworld'], function (helloworld) {
    helloworld.helloworld()
  })
})()
<!DOCTYPE html>
<html>
  <head>
    <title>AMD Demo</title>
  </head>
  <body>
    <!-- require.js可从官网直接下载 -->
    <script data-main="./main.js" src="./require.js"></script>
    <!-- 输出hello world -->
  </body>
</html>

当依赖大量模块时,AMD规范存在性能问题,需等待所有依赖模块加载完毕才可执行模块内的程序。

CMD[11]

CMD 规范与AMD规范相似,且保留CommonJS中的延迟加载、就近声明特点。
sea.js基于当前规范编写,由玉伯提出。官网直通车

案例

//helloworld.js
define(function (require, exports, module) {
  let msg = 'hello world'
  function helloworld() {
    console.log(msg)
  }
  exports.helloworld = helloworld
})
//main.js
define(function (require, exports, module) {
  let helloworld = require('./hello').helloworld //就近原则
  helloworld()
})
<!DOCTYPE html>
<html>
  <head>
    <title>CMD Demo</title>
  </head>
  <body>
    <!-- 引入sea.js -->
    <script type="text/javascript" src="./sea.js"></script>
    <script>
        seajs.use('./main')
    </script>
  </body>
</html>

UMD[14]

UMD规范主要通过if-elseif-else达到通用的目的。

  • 先判断是否存在 exports 方法,如果存在,采用 CommonJS 方式加载模块。
  • 后判断是否存在 define 方法,如果存在,采用 AMD 方式加载模块。
  • 均没有,直接挂在window上

ES6[15-17]

喜大普奔!!!庆幸生存在ES6模块化的时代。

ES6模块化思想:尽量静态化。使得在编译阶段就能确定模块的依赖关系、输入和输出的变量。这种差异主要因为ES6模块对外接口是一种静态定义而非对象

案例

// helloworld.js
let helloWorld = function() {
   console.log('hello world')
}
export { helloWorld }
// main.js
import { helloWorld } from './helloworld.js'
helloWorld() //node main.js hello world

案例需借助babel-clibabel-preset-es2015将ES6语法转为ES5语法才可执行。

步骤

  1. npm init初始化生成package.json
  2. npm i babel-cli -gnpm install babel-preset-es2015 --save-dev
  3. 生成.babelrc文件。
{ "presets": ["es2015"] }
  1. 新建文件夹src,移动helloworld.js以及main.js文件至src文件夹。
  2. 在根目录下执行babel src -d lib
  3. lib文件夹下执行node main.js输出hello world

ES6模块化有如下特点

  • ES6模块输出的是值的引用,而非拷贝。
  • ES6模块是编译时输出接口,import命令会被JS引擎静态分析,优先于其他模块内容执行,export模块命令会有变量提升的效果。因此,importexport的位置不影响程序的输出。

参考

Javascript模块化编程(一)
深入了解JavaScript模块化编程
《编程时间简史系列》JavaScript 模块化的历史进程
深入浅出javaScript模块化