前端模块化

433 阅读5分钟

模块化是前端工程化中最重要的一部分,因为目前的前端项目中,单个文件里面的代码已经多到非常臃肿的地步,不利于开发和管理。而模块化开发是目前比较主流的解决方法,它通过将文件代码安装不同的功能来划分为不同的模块区域文件,以此来提高开发效率,降低维护成本

模块化本质上是一种思想,并不是一种具体的实现,需要的用其他的方式来实现代码的模块化

模块化演变流程

第一阶段:文件划分方式

不同模块写在不同的js文件中,使用的时候在html页面用不同的script标签引进来

早期模块化是完全依靠约定,代码量提升后就难以维护

var name = 'modules-a'
function method1 () {
  console.log(name + 'modules-1')
}
function method2 () {
  console.log(name + 'modules-2')
}
<script src="./modules-a.js">
</script><script src="./modules-b.js">
</script>
// 命名冲突
method1()
// 里面的数据可以被外部修改
name = 'foo'

缺点:

  • 全部js中的变量、函数都是同一个全局作用域,污染全局作用域
  • 里面的变量可以被外部随意修改
  • 会发生命名冲突问题
  • 无法管理模块之间的依赖关系

第二阶段:命名空间方式

在第一阶段的基础上,规定每一个文件只导出一个全局对象,文件里面的变量都挂载到这个全局对象上

单个文件里面导出一个对象,其他变量包在这个对象中,把作用域限定在这个对象中

这种方式可以解决命名冲突问题,但是还是没有私有空间,内部变量可以被修改

var moduleA = {
  name: 'modules-a',
  method1: function () {
    console.log(this.name + 'modules-1')
  },
  method2: function () {
    console.log(this.name + 'modules-2')
  }
}
<script src="./modules-a.js"></script>
moduleA.method1()
// 里面的数据可以被外部修改
moduleA.name = 'foo'

缺点:

  • 里面的变量可以被外部随意修改
  • 无法管理模块之间的依赖关系

第三阶段:立即执行函数

将文件里面变量和函数都包在一个立即执行函数里面,把变量和函数限制在这个内部私有作用域,把需要暴露的成员,挂载在window全局对象上,这样可以把内部变量限制在私有作用域内,外部无法访问修改

;(function () {
  var name = 'modules-a'

  function method1 () {
    console.log(name + 'modules-1')
  }
  function method2 () {
    console.log(name + 'modules-2')
  }
  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()
<script src="./modules-a.js"></script>
// 访问不到  undefined
console.log(moduleA.name)

缺点:

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

第四个阶段:通过立即执行函数传入依赖

在立即执行函数调用时,把这个模块里面需要的依赖传入到参数里面,这样可以清晰地知道需要的依赖

;(function () {
  var name = 'modules-a'

  function method1 () {
    console.log(name + 'modules-1')
  }
  function method2 () {
    console.log(name + 'modules-2')
  }
  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()
<script src="./modules-a.js"></script>
// 访问不到  undefined
console.log(moduleA.name)

之前的方式都是依赖约定来实现的,并且模块的导入都是通过script标签引入的,模块的加载不受代码的控制,随着时间会变得越难维护,代码和html之间对于模块的加载没有办法确保可以相互对应起来,为此应该需要通过代码来维护

合适的模式是:模块化标准 和 模块加载器

模块化规范

CommonJS规范:node.js的标准

  • 一个文件就是一个模块

  • 每个模块都有单独的作用域

  • 通过 module.exports 导出成员

  • 通过 require 函数载入模块

    var math = require('math') var path = require('path') // 需要等待模块require后才执行 console.log(path.join(__dirname, 'src')) math.add(2, 3)

CommonJs是以同步模式加载模块,node是在启动时加载模块,运行时不再加载模块,这种规范方式不适合用在浏览器中,避免页面刷新时同步加载模块卡顿

AMD(Asynchronous Module Definition):为浏览器设计的标准

Require.js库,实现了AMD规范

  • 模块需要通过define来定义,通过return导出需要的成员
  • 载入模块需要用require

大部分第三方库都实现了AMD规范

优点:

  • 适合浏览器环境异步加载模块
  • 可以并行加载多个模块

缺点:

AMD规范使用起来相对复杂,需要额外增加代码

不能按需加载,要在提前加载所有依赖后,才能回调

// 异步 导入
require(['math'], function (math) {
    math.add(2, 3)
})
// 不需要等待加载
console.log('hello')

// 第一个参数为 模块标识 可选
define('myModule', ['module1', 'module2'], function (module1, module2) {
    function foo () {}
    return {
        foo: foo
    }
}

CMD(Common Modules Definition)和sea.js:通用模块规范,为浏览器设计的标准

和AMD规范相似,也是通过define定义模块,require导入模块

优点:

  • 适合浏览器环境异步加载模块
  • 模块按需加载

缺点:

  • 模块加载受代码逻辑影响,不直观
  • 依赖SPM打包

CMD和AMD区别:

CMD依赖是延时执行;AMD依赖的模块需要提前执行

CMD推崇依赖就近,用到再按需加载;AMD推崇依赖前置,定义时要先声明依赖

// CMD是按需加载的
define(function (require, exports, module) {
    console.log('不用依赖就先执行的')
    const math = require('math')
    math.add(2, 3)
    var status = true
    if (status) {
        // 动态引入
        var module2 = require('module2')
    }
    // 导出
    exports.foo = function () {
        return 'hello'
    }
}

ES6 模块

目前最佳的模块化方案是

浏览器环境:ES Modules 规范

node环境:CommonJS 规范

ES Modules 是 ES6的标准,一开始的浏览器都不支持这个规范,需要借助webpack等打包工具来实现,后期部分浏览器支持了ES Modules,可以在浏览器中直接使用

ES Modules是从语言规范上设计的标准,目前最主流的浏览器模块化规范