模块化开发-模块化演变过程

508 阅读6分钟

由于早期的前端技术标准根本没有预料到前端行业没有今天这样一个规模,所以说很多设计上的遗留问题就导致了现在去实现前端模块化的时候会遇到很多问题。虽然说现如今一些问题都被一些标准或者工具去解决了,但是它的一个演进过程是值得思考的,这个过程就是模块化思想在前端领域的实践过程。这个过程经历的以下四个阶段:

  • Stage 1 -- 文件划分方式:这是 WEB 当中最原始的模块系统,具体的做法就是将每一个功能以及它相关的一些状态数据单独存放到不同的文件当中并约定每一个文件代表一个模块。使用时就就将这个模块文件引入到页面文件当中,一个 script 标签就对应一个模块。然后在代码当中直接引用模块当中的全局成员,这个成员有可能是一个变量也有可能是一个函数。这个方式的缺点也是很明显的,就是所有的模块都在全局作用域工作它并没有一个独立的私有空间,这样就导致模块当中所以的成员都可以在模块外部被任意的去访问或者修改这样就造成了污染全局作用域;而且模块一旦多了以后很容易产生命名上的冲突;除此之外,也没有办法很好的管理模块与模块之间的依赖关系;总的来说,这种方式完全依赖约定项目一旦上了体量过后就会不行了。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 1</title>
</head>
<body>
  <h1>模块化演变(第一阶段)</h1>
  <h2>基于文件的划分模块的方式</h2>
  <p>
    具体做法就是将每个功能及其相关状态数据各自单独放到不同的文件中,
    约定每个文件就是一个独立的模块,
    使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量 / 函数)
  </p>
  <p>
    缺点十分明显:
    所有模块都直接在全局工作,没有私有空间,所有成员都可以在模块外部被访问或者修改,
    而且模块一段多了过后,容易产生命名冲突,
    另外无法管理模块与模块之间的依赖关系
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    // 命名冲突
    method1()
    // 模块成员可以被修改
    name = 'foo'
  </script>
</body>
</html>
// module-a.js

var name = 'module-a'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}

// module-b.js

var name = 'module-b'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}
  • Stage 2 -- 命名空间:第二个阶段在第一个阶段的基础上约定每一个模块只暴露一个全局的对象,所有的模块成员都挂载到这个对象下面。具体的做法就是在第一个阶段的基础上,通过将每一个模块包裹成为一个全局对象的方式去实现。这样有点类似于在模块内去为每个模块一些成员添加了命名空间,通过命名空间的方式我们就可以减小命名冲突的可能。但是这种方式仍然没有私用空间,所以说模块成员仍然可以在外部被访问、修改,另外模块之间的依赖关系没有得到解决
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 2</title>
</head>
<body>
  <h1>模块化演变(第二阶段)</h1>
  <h2>每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中</h2>
  <p>
    具体做法就是在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,
    有点类似于为模块内的成员添加了「命名空间」的感觉。
  </p>
  <p>
    通过「命名空间」减小了命名冲突的可能,
    但是同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改,
    而且也无法管理模块之间的依赖关系。
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
    // 模块成员可以被修改
    moduleA.name = 'foo'
  </script>
</body>
</html>
// module-a.js

var moduleA = {
  name: 'module-a',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}

// module-b.js

var moduleB = {
  name: 'module-b',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}
  • Stage 3 -- IIFE:在第三阶段当中使用立即执行函数这种方式,去为模块提供私有空间。具体的做法就是将模块中每一个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以通过挂载到全局对象上的方式去实现。这种方式实现了私有成员的概念,就是说模块的私有成员只能在模块内部通过闭包的方式去访问而在外部,是没有办法去使用使用的这样就确保了私有成员的安全。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 3</title>
</head>
<body>
  <h1>模块化演变(第三阶段)</h1>
  <h2>使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供私有空间</h2>
  <p>
    具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,
    对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现
  </p>
  <p>
    有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。
  </p>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
    // 模块私有成员无法访问
    console.log(moduleA.name) // => undefined
  </script>
</body>
</html>
// module-a.js

;(function () {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()

// module-b.js

;(function () {
  var name = 'module-b'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()

  • Stage 4 -- IIFE 依赖参数:可以利用自执行函数的参数去作为依赖声明去使用,这样的话就使得每一个模块之间的依赖关系就变得明显。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 4</title>
</head>
<body>
  <h1>模块化演变(第四阶段)</h1>
  <h2>利用 IIFE 参数作为依赖声明使用</h2>
  <p>
    具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项。
  </p>
  <p>
    这使得每一个模块之间的关系变得更加明显。
  </p>
  <script src="https://unpkg.com/jquery"></script>
  <script src="module-a.js"></script>
  <script src="module-b.js"></script>
  <script>
    moduleA.method1()
    moduleB.method1()
  </script>
</body>
</html>
// module-a.js

;(function ($) {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})(jQuery)

// module-b.js

;(function () {
  var name = 'module-b'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()

总结:以上四个阶段是早期的开发者在没有工具和规范的情况下,对模块化的落地方式。这些方式却是解决了前端领域的模块化的问题,这只是仅仅解决了组织问题但模块化的加载问题。

  • Stage 5 -- AMD & CMD: AMD 和 CMD 是模块化规范初期落地实现的两个方案。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Modular evolution stage 5</title>
</head>
<body>
  <h1>模块化规范的出现</h1>
  <h2>Require.js 提供了 AMD 模块化规范,以及一个自动化模块加载器</h2>
  <script src="lib/require.js" data-main="main"></script>
</body>
</html>
// main.js
require.config({
  paths: {
    // 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
    // 所以使用时必须通过 'jquery' 这个名称获取这个模块
    // 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
    jquery: './lib/jquery'
  }
})

require(['./modules/module1'], function (module1) {
  module1.start()
})
// ./modules/module1.js
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
// ./modules/module2.js
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
	// 通过 require 引入依赖
  var $ = require('jquery')
  // 通过 exports 或者 module.exports 对外暴露成员
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})