前端模块化的演进过程

1,458 阅读4分钟

前端模块化的演进过程

前言:模块化的作用

  • 模块能为你提供了一个更加好的方式来组织这些变量和方法
  • 模块能会将这些函数和变量放入一个模块作用域当中。模块作用域使得模块中的不同函数能够共享这些变量。
  • 模块化能清晰的表达模块与模块间的关系

stage 1 文件划分方式

做法:通过文件来区分模块,然后用script标签去引入

// 目录结构
|__ pageA
    |__moduleA.js
    |__moduleB.js
    |__index.html
// moduleA.js
function func() {
  console.log(123)
}
​
// moduleB.js
var func = 222// index.html
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
​
<script>
console.log(func) // 222
</script>

缺点:

  • 健壮性不强;作用于全局作用域,容易造成变量名冲突与覆盖
  • 不安全;没有私有空间,所有模块都可以在模块外部被访问和修改
  • 不好维护;模块与模块间的依赖关系不清晰,对文件引入顺序有严格的要求

stage 2 命名空间方式

做法:在文件划分方式的基础上,加一个命名空间,一个文件规定只导出一个模块

// moduleA.js
window.moduleA = {
  a: 1,
  func: function() {
    console.log(this.a)
  },
}

// moduleB.js
// moduleA.js
window.moduleB = {
  a: 2,
  func: function() {
    console.log(this.a)
  },
}

// index.html
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>

<script>
moduleA.func() // 1
moduleB.func() // 2
</script>

优点:

  • 解决部分命名冲突问题(导出的模块名还是需要保持唯一)

缺点:

  • 不安全;没有私有空间,所有模块都可以在模块外部被访问和修改
  • 不好维护;模块与模块间的依赖关系不清晰,模块加载顺序问题,对文件引入顺序有严格的要求

state3 IFFE 依赖参数

做法:使用IIFE(立即执行函数) 形成私有作用域,为模块提供私有变量,外部通过闭包访问模块抛出的变量。

// moduleA.js
;(function($){
  var a = 1
  function func() {
    console.log(a)
  }
  
  window.moduleA = {
    func: func
  }
})(jQuery)

优点:

  • 解决部分命名冲突问题(导出的模块名还是需要保持唯一)
  • 可以声明私有变量,外部无法更改/访问私有变量
  • 通过依赖参数可以知道依赖哪些模块

缺点:

  • 模块加载顺序问题;对文件引入顺序有严格的要求

state 4 模块化规范的演进

1. CommonJS规范

commonjs是Node.js所遵循的规范,通过module.exports导出模块,require函数导入模块 特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
  • 输入的是被输出值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值了。
  • 同步的方式加载模块;启动时加载模块,执行过程中去使用模块;

因为是同步的加载,所以该规范不适用于浏览器,比如说:模块A依赖模块B,当浏览器执行到模块A导入模块B的代码时,才会去请求模块B的代码并等待加载完成,整个应用就会停着一直等待,影响应用性能等。

// moduleB.js
const a = 1
function func() {
  console.log(a)
}
module.exports = {
  func: func
}
​
// moduleA.js
const moduleB = require('moduleB')
moduleB.func()
2.AMD规范

AMD(Asynchronous Moudle Definition) 异步模块定义规范

AMD规范主要用于浏览器的模块化加载,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。由于AMD不是浏览器支持的,所以还需引入相关实现库。

AMD规范相关实现库:Require.js、curl.js

  • 定义模块

    define(['jquery'], function($){
      return {
        func: function() {
          console.log('moduleA')
        }
      }
    })
    
  • 加载模块

    require(['./moduleA', function(moduleA){
      moduleA.func() // 'moduleA'
    }])
    

优点:可以以模块化的方式组织代码,通过异步的方式去加载模块,以回调的形式执行请求成功后的代码,不会阻塞应用的运行。

缺点:这种引入方式相对复杂,并且模块一多,请求资源的请求也会随之增多。

3.cmd规范

Common Module Definition,即通用模块定义

cmd规范提供了模块定义和按需加载执行模块。它解决的问题和AMD规范是一样的,只不过在模块定义方式和模块加载时机上不同,CMD也需要额外的引入第三方的库文件,SeaJS

  • 定义模块:

    // cmd1.js
    define(function(require,exports,module){
        // ...
        module.exports={
            // ..
        }
    })
    ​
    // cmd2.js
    define(function(require,exports,module){    
        var cmd2 = require('./cmd1') 
        // cmd2.xxx 依赖就近书写
        module.exports={
            // ...
        }
    })
    
  • 加载模块

    seajs.use(['cmd2.js','cmd1.js'],function(cmd2,cmd1){
      // ....
    })
    

CMD与AMD的区别

  • 区别:

    • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

    • CMD 推崇依赖就近,AMD 推崇依赖前置

      // CMD 默认推荐的是
      define(function (require, exports, module) {
        var a = require('./a') 
        a.doSomething() // 此处略去 100 行   
        var b = require('./b') 
        // 依赖可以就近书写   
        b.doSomething()   
        // ... 
      })
            
      // AMD 默认推荐的是
      define(['./a', './b'], function(a, b) {
        // 依赖必须一开始就写好
        a.doSomething() 
        // 此处略去 100 行
        b.doSomething()
      }
      
4. ES module(最佳实践)

ES Modules是浏览器原生支持(es6)的模块系统,它通过export(导出)、import(导入)来管理模块。

特性:

  • 自动采用严格模式,忽略“use strict”

    <script type="javascript">
      console.log(this)// => windows
    </script><script type="module">
      console.log(this)// => undefined
    </script>
    
  • 每个ESM模块都是单独的自有作用域

    <script type="javascript">
      let a  = 10
      console.log(a)// => 10
    </script><script type="module">
      console.log(a)// => 直接报错未定义  这样就解决了作用域污染的问题
    </script>
    
  • ESM是通过CORS去请求外部的JS模块

    外部js文件通过cors请求的,如果请求的地址不在同源的话,那么服务端的响应头包含提供有效cors标头,
    如果没有的话直接出现跨域问题
    ​
    cors不支持文件的形式访问,必须是http Sever方式的请求,在实际开发过程中肯定都是http Sever方式的
    
  • ESM是script标签会延迟执行脚本

  • 与COMMONJS导出值的拷贝不同,ESModule导出的是值的引用

问题:

  1. 由于esmodule是ES6才实现的,低版本的浏览器是没有实现这一规范,使用ESModule会存在兼容性问题,webpack按es module的规范,在内部实现了一套兼容性更好的模块加载系统。
  2. 模块化的方式划分出来的文件过多,会导致浏览器频繁的发送请求,影响应用的工作效率
5. UMD规范

UMD (Universal Module Definition) 通用模块定义规范

随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了。

写法:

((root, factory) => {
    if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        //CommonJS
        var $ = requie('jquery');
        module.exports = factory($);
    } else {
        root.testModule = factory(root.jQuery);
    }
})(this, ($) => {
    //todo
});