前端模块化发展

285 阅读6分钟

所有的大型应用都会使用模块化开发,模块化就是将复杂系统分解成多个独立模块,便于之后维护。

  1. 高可维护性:很长一段时间里,前端通过
  2. 命名冲突:js所创建的对象都会变成全局对象,这样没有命名空间的方式,会造成很多问题,引入第三方库还有可能命名的覆盖
  3. 高复用性

上面都是非模块化存在问题,所以我们如何解决这些问题,使前端支持模块化开发

1.命名空间模式

简单的来说,我们要通过js对象来模拟命名空间的概念

  • 这样我们把模拟的namespace 看作是一个 模块module 通过对象的方式减少了全局变量,解决命名冲突问题

  • **但是没有解决外部还是可以解决内部变量的问题,name 也暴露出来可以随意改变,这不是模块化想要的

    var namespace = {
        name: 'namespace',
        getName: function() {
            return this.name
        }
    }
    namespace.name
    namespace.getName()
    

    **

2.IIFE

  • js 为了避免全局变量污染,提供了闭包的方式提供模块化方案,我们通过自执行函数来避免变量泄露到全局作用域中

  • 数据是私有作用域的,只通过方法来操作私有变量

    (function(window) {
      let name = 'private'
      var module = {
        getName: function() {
          return name
        },
        setName: function() {
          return name + 'Set'
        }
      }
      window.module = module
    })(window)
    

如上 将module 挂载在window下,这个模块只暴露了getName, setName 方法,内部的name不能修改

  • 但是这种模式也面临一个问题,如果我想引入其他依赖的库,还是不能引用全局的依赖库,比如我们想引入jQuery,这个时候我们要向模块中注入依赖

  • 这种模式也叫IIFE模式增强版

  • 这样做实现了模块化,并使得模块的依赖关系变得清楚一些

    (function(window, jQuery) {
      let name = 'private'
      var module = {
        getName: function() {
          return name
        },
        setName: function() {
          return name + 'Set'
        }
      }
      window.module = module
    })(window, jQuery)
    

3. CommonJS规范

随着js在服务端的应用,急需标准化的模块化方案,CommonJS 规范提出后Node.js就是应用了这个规范,

  • 每个文件就是一个模块,都有自己的作用域
  • 一个文件里的变量,函数,类都是私有的,对其他文件不可见
  • node中模块加载是运行时候同步加载的

1.用法是 用 module 暴露对象,require  引入模块

//定义模块  module.js
module.exports = {
    a: 1
}
// 加载模块
var moduleA = require('./module.js')

2.模块的加载机制

require到的对象是对 module.exports 暴露对象的拷贝,但是观察到一个现象,如果两个模块共同引入了一个 module,如果其中一个模块扩展了当前模块,其他模块引入时候除了module 初始化定义的key的值不变,其他新加的key ,也会在各个模块间相互影响

3. 在浏览器端使用CommonJS规范,可以用 Browserify ,gulp, webpack等解析后引入到页面中去,这里主要介绍 Browserify

//定义模块A moduleA.js
module.exports = {
    a: 1
}
// 定义模块B moduleB.js
var moduleB = require('./moduleA.js')
console.log(moduleB)
module.exports = {
    b: 2
}

定义好之后安装Browserify

npm install --save-dev Browserify

打包命令 将打包命名放到 package.json 的scripts 中

"scripts":{
    "build": "browserify moduleB.js -o build.js"
}

npm run build

将打包好的build.js 文件引入到html文件中就是使用了

4.AMD (Asynchronous Module Definition)

这也是一种js模块化规范,CommonJS采用同步加载方式,只有模块全部加载完成才能进行之后的操作,AMD主要提供了异步加载功能,需要RequireJS来实现模块化编写,浏览器一般需要从服务端加载模块,所有采用异步加载会比较好

1.基本语法

定义一个模块: 通过define 方法将代码定义成模块

// moduleA.js
define(function(){
    return {
        ...module
    }
})

引用一个模块:通过require方法加载一个模块

require(['moduleA'], function(moduleA){
    // 这里拿到 moduleA 的返回
})

2. 浏览器使用AMD的好处

  • 模块定义清晰,不用再写自执行函数了, 并且不会污染全局环境
  • 清楚的展示依赖关系

5.CMD( Common Module Definition)

CMD 规范专门用于浏览器端,模块加载是异步的,模块使用时候才会加载执行,CMD整合了CommonJS 和AMD规范的特点,对应的库是SeaJS,它跟requireJS一样,都是解决异步加载问题,只是在使用方法和加载时机上不同

  • CMD 加载完某个模块依赖后并不执行,只是下载,在所有依赖模块加载完成后进入主逻辑
  • 遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序完全一致,如果使用require.async()方法,可以实现模块的懒加载

1.基本使用

//引入Sea.js
// 定义 模块A, moduleA.js
define(function(require, exports, module) {
    let a = 'aaa'
    module.exports = a
})

//定义模块B, moduleB.js
define(function(require, exports, module){
    var a = require('./a') // 同步引入
    var aa = require.async('./a', function(MA) {
        
    })// 异步引入
    var b = 'bbb'
    console.log(a, '我是B模块引入的A模块')
    module.exports = b
})

//定义入口 main.js
define(function(require){
    var b = require('./moduleB.js')
})

// sea.js引入入口文件
seajs.use('./main')

6.UMD

上面说了CommonJS常用在node端,AMD用在浏览器端,都是针对特定平台,如果想要跨平台方案要引入UMD方案(Universal Nodule Definition),能够很好的兼容AMD, CommonJS,很多JS框架和类库都会打包成这种形式,UMD的实现如下

  • 首先判断是否支持CommonJs模块(exports是否存在),存在则使用CommonJs模块化格式
  • 在判断是否支持AMD(define)是否存在,存在则用AMD方式加载
  • 判断define(factory)是一个全局函数,用来定义模块,存在就是AMD或者CMD环境
  • 都不存在的话就将模块挂在到全局(window或者global)

1.基本语法

(
function(name, factory){
    // 检测 是否包含 AMD 和 CMD
    var hasDefine = typeof define === 'function'
    var hasExports = typeof module != 'undefined' && module.exports
    if(hasDefine) {
        define(factory)
    }else if(hasExports) {
        module.exports = factory()
    }else{
        this[name] = factory()
    }
}
)('xxx',function(){})

7.ES6 Module

有了ES6之后,不必再用闭包和函数进行模块化,需要一个转义工具Babel等工具进行编译,

它的思想就是尽量静态化,编译阶段就能确定这些东西,而CommonJS和AMD,都只能再运行时候确定

1.基本语法

// export 用于定义模块,import 用于引入模块 // ModuleA.js
var a = 'a'
export {a: a}

import {a} from './ModuleA.js'
  • ES6使用基于文件的模块,必须一个文件一个模块,不能将多个模块合并到单个文件中
  • ES6模块API是静态的,一旦模块导入后,就不能在程序中增添方法
  • ES6采用引用绑定(指针),这个和CommonJS中的值不用,如果你的模块在运行过程中修改了导出的变量值,就会反映到使用模块的代码中去
  • ES6模块采用的是单例模式,每次对同一个模块的导入其实指向同一个实例