随着互联网的发展,前端的技术已经发生了巨大的改变,早期的前端技术标准根本没有预料到前端行业会有今天这个规模,所以在设计上存在很多缺陷,导致我们现在去实现前端模块化时会遇到诸多问题。虽然说,如今绝大部分问题都已经被一些标准或者工具解决了,但在这个演进过程中依然有很多东西值得我们思考和学习。

1. 文件划分

约定一个文件就是一个模块

  1. // module-a.js
  2. fucntion foo() {
  3. // xxxx
  4. }
  1. // module-b.js
  2. var zoo = 1;
  1. // index.html
  2. <body>
  3. <script src="module-a.js"></script>
  4. <script src="module-b.js"></script>
  5. <script>
  6. // 直接使用全局成员
  7. foo() // 可能存在命名冲突
  8. console.log(zoo)
  9. zoo = 2 // 数据可能会被修改
  10. </script>
  11. </body>

缺点:

  • 模块直接在全局工作,大量模块成员污染全局作用域。
  • 没有私有空间,所有模块内部的成员都可以在模块外部被访问或者修改。
  • 容易造成命名冲突。
  • 无法管理模块之前的依赖关系。

2. 命名空间

约定一个模块暴露一个全局对象,所有模块成员都挂载到这个全局对象上。

  1. // module-a.js
  2. window.a = () => {
  3. // xxx
  4. }
  1. // module-b.js
  2. window.b = {
  3. data: 1
  4. }
  1. // index.html
  2. <body>
  3. <script src="module-a.js"></script>
  4. <script src="module-b.js"></script>
  5. <script>
  6. // 直接使用全局成员
  7. a();
  8. console.log(window.b.data);
  9. window.b.data = 2 // 数据可能会被修改
  10. </script>
  11. </body>

缺点:

  • 模块直接在全局工作,模块成员污染全局对象。
  • 没有私有空间,模块成员对象之前可以相互修改。
  • 无法管理模块之间的依赖关系。

3. IIFE

约定使用立即执行函数表达式包裹模块。将模块放在立即执行函数的私有作用域中。对于需要向外暴露的成员,挂载全局对象上。

  1. / module-a.js
  2. ;(function() {
  3. function method1 () {
  4. console.log(1)
  5. }
  6. window.moduleA = {
  7. method1: method1
  8. }
  9. })();
  1. / module-b.js
  2. ;(function() {
  3. function method1 () {
  4. console.log(1)
  5. }
  6. window.moduleB = {
  7. method1: method1
  8. }
  9. })();

缺点:

  • 无法管理模块之间的依赖关系。

4. IIFE 依赖参数

在 IIFE 的基础上,利用 IIFE 参数作为依赖声明使用。

  1. // module-a.js
  2. ;(function ($) { // 通过参数明显表明这个模块的依赖
  3. var name = 'module-a'
  4. function method1 () {
  5. console.log(name + '#method1')
  6. $('body').animate({ margin: '200px' })
  7. }
  8. window.moduleA = {
  9. method1: method1
  10. }
  11. })(jQuery)

到这里虽然已经解决了最开始文件划分模块的所有问题,但是还有一个通病没有得到解决,就是模块的加载,通过 script 这种方式直接在页面中加载模块,这意味着模块的加载并不受控制。

5. AMD

异步模块定义规范,AMD 模式比较出门的库 require.js,异步加载模块,模块加载后立即执行,依赖必须提前申明好,依赖前置。

在 AMD 规范中约定每个模块通过 define() 函数定义,这个函数默认可以接收两个参数,第一个参数是一个数组,用于声明此模块的依赖项;第二个参数是一个函数,参数与前面的依赖项一一对应,每一项分别对应依赖项模块的导出成员,这个函数的作用就是为当前模块提供一个私有空间。如果在当前模块中需要向外部导出成员,可以通过 return 的方式实现。除此之外,Require.js 还提供了一个 require() 函数用于自动加载模块,用法与 define() 函数类似,区别在于 require() 只能用来载入模块,而 define() 还可以定义模块。当 Require.js 需要加载一个模块时,内部就会自动创建 script 标签去请求并执行相应模块的代码。

  1. // a.js
  2. define(['./moduleA.js', './moduleB.js'], function(moduleA, moduleB) {
  3. // 导出成员
  4. return {
  5. doing: () => {}
  6. // xxx
  7. };
  8. });
  1. require(['./a.js'], function(a) {
  2. a.doing();
  3. });

缺点:

  • 使用复杂
  • 模块划分过于细致时,一个页面可能会产生对 js 的重复加载,导致效率降低。

    6. CMD

    淘宝 SeaJs,异步加载模块,所有依赖先提前加载,但是不会执行依赖模块的代码,懒执行。等到模块在被调用到时才会去执行代码。可以依赖就近。SeaJs 算是重复造轮子了,随着时代的发展,SeaJs 也被 Require.js兼容了。

  1. define(function(require, exports, module) {
  2. var $ = require('jquery');
  3. module.exports = function () {};
  4. });

7. CommonJS

Node.js 中遵循的模块规范,该规范中一个文件就是一个模块,每一个模块都有自己的作用域,通过 module.exports 导出成员,再通过 require 函数载入模块。CommonJS 约定的是以同步的方式加载模块。

缺点:

不适合在浏览器环境中运行,原因是同步的模块加载机制,导致运行效率低下。

8. UMD

因为 AMD,CommonJS 规范是两种不一致的规范,虽然他们应用的场景也不太一致,但是人们仍然是期望有一种统一的规范来支持这两种规范。于是,UMD(Universal Module Definition,称之为通用模块规范)规范诞生了。客观来说,这个UMD规范看起来的确没有 AMD 和 CommonJS 规范简约。但是它支持 AMD 和 CommonJS 规范,同时还支持古老的全局模块模式。个人觉得 UMD 规范更像一个语法糖。应用 UMD 规范的 js 文件其实就是一个立即执行函数。函数有两个参数,第一个参数是当前运行时环境,第二个参数是模块的定义体。在执行 UMD 规范时,会优先判断是当前环境是否支持 AMD 环境,然后再检验是否支持 CommonJS 环境,否则认为当前环境为浏览器环境(window)。当然具体的判断顺序其实是可以调换的。

9. ES Modules


深入了解 ES Modules