1. 引言

最近在研究前端框架,但发现好多JavaScript知识不是很了解,很是苦恼,下面就来研究一下JavaScript的模块化,先理解几个概念和模块化的进化过程。

2. 模块化

2.1 什么是模块?

模块就是将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起,块的内部数据和实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。

一个模块的组成由两部分组成: 数据(内部的属性)、操作数据的行为(内部的函数)

2.2 模块化的进化过程

2.2.1 全局function模式

模块一:module1.js

// 数据
let data = 'module1'
// 操作数据的函数
function foo() {
  console.log(`foo() ${data}`)
}
function bar() {
  console.log(`bar() ${data}`)
}

模块二:module2.js

let data2 = 'module2'
function foo() {  //与另一个模块中的函数冲突了
	console.log(`foo() ${data2}`)
}

页面引入:test.html

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
  let data = "修改后的数据"
  foo() // 冲突
  bar()
</script>

说明:全局函数模式是将不同的功能函数封装成不同的全局函数,这样会有一些问题, 比如上面两个模块中都有foo函数,如果同时引入,将不知道调用的是哪一个,很容易引起命名冲突,污染全局命名空间,导致数据不安全。(上面的两个模块中的函数会成为window对象中的函数)

2.2.2 namespace模式

模块一:module1.js

let myModule1 = {
  data: 'module1',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}

模块二:module2.js

let myModule2 = {
  data: 'module2',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}

页面引入:test.html

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
  // module.js模块
  myModule1.foo()
  myModule1.bar()
  // module2.js模块
  myModule2.foo()
  myModule2.bar()
  
  myModule1.data = 'other data' //能直接修改模块内部的数据
  myModule1.foo()
</script>

说明:namespace模式就是简单对象封装,它虽然解决了命名冲突的问题,减少了全局变量,但依然存在问题,我们可以在外部可以直接修改模块内部的数据,导致数据不安全。

2.2.3 IIFE模式/增强

模块:module1.js

(function (window) {
  //数据
  let data = 'module1'
  //操作数据的函数
  function foo() { //用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {//用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() { //内部私有的函数
    console.log('otherFun()')
  }
  // 暴露foo,bar,并没有暴露otherFun,所以外界无法访问
  window.myModule = {foo, bar}
})(window)

页面引入:test.html

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
  myModule.foo()
  myModule.bar()
  //myModule.otherFun()  //myModule.otherFun is not a function,没有暴露
  console.log(myModule.data) //undefined 不能访问模块内部数据
  myModule.data = 'xxxx' //不是修改的模块内部的data
  myModule.foo() //没有改变
</script>

说明:IIFE : immediately-invoked function expression(立即调用函数表达式),就是匿名函数自调用(闭包),它通过将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口,它的作用是,数据是私有的, 外部只能通过暴露的方法操作。但此时有个问题: 如果当前这个模块依赖另一个模块怎么办?

下面拿jQuery举例,把jQuery引入到项目中
模块:module1.js

  (function (window, $) {
      //数据
      let data = 'module1'
      //操作数据的函数
      function foo() { //用于暴露有函数
        console.log(`foo() ${data}`)
        $('body').css('background', 'red')
      }
      function bar() {//用于暴露有函数
        console.log(`bar() ${data}`)
        otherFun() //内部调用
      }
      function otherFun() { //内部私有的函数
        console.log('otherFun()')
      }
      //暴露行为
      window.myModule = {foo, bar}
    })(window, jQuery)

页面引入:test.html

<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>

引入依赖,是IIFE模式的增强,也是现代模块实现的基石。

3. 模块化规范

模块分的规范有如下四种:CommonJSAMDCMDES6,下面逐一介绍一下。

3.1 CommonJS

遵循CommonJS规范的有:Node.js(服务器端)、 Browserify (浏览器端),Browserify 也称为js的打包工具 。 暴露的本质就是暴露exports对象

基本语法:定义暴露模块 : exports

exports.xxx = value
module.exports = value

引入模块 : require

var module = require('模块名') // 引入第三方模块
var module = require('模块相对路径')

说明

  • 一个文件就是一个模块,require方法用来加载模块,该方法读取一个文件并执行,最后返回文件内部的module.exports对象
  • require是默认读取.js文件的,所以require(模块名)可以不写后缀
  • module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量;为了方便也可以用exportsexports指向module.exports;即exports = module.exports = {}exports.xxx相当于在导出的对象上添加属性,该属性对调用模块可见
  • exports = 相当于给exports重新赋值,这样就切断了和module.exports的关联,调用模块就不能访问exports的对象及其属性;

3.2 AMD (浏览器端)

AMD是requireJS倡导的一种模块化规范,推崇依赖前置;在requireJS中模块是通过define来进行定义的,如果模块之间相互依赖,需要先将依赖模块导入进来,待导入完毕之后,在通过回掉函数的方式执行后面的代码,有效的解决了模块依赖的问题。

定义暴露模块

define([依赖模块名], function(){return 模块对象})

引入模块

require(['模块1', '模块2', '模块3'], function(m1, m2){//使用模块对象})

配置:

require.config({
	// 基本路径
    baseUrl: "js/lib",
     // 标识名称与路径的映射
    paths: {
     "jquery": "jquery.min",
     "underscore": "underscore.min",
     "backbone": "backbone.min"
  	},
  	//非AMD的模块
    shim: {
       'underscore':{
            exports: '_'
  		},
	  'backbone': {
	    deps: ['underscore', 'jquery'],
	    exports: 'Backbone'
	  }
 	}
});

3.3 CMD (浏览器端)

CMD则是seaJS倡导的一种解决模块之间相互依赖规范,推崇依赖就近,在seaJS中一个脚本文件就是一个模块,所有的模块代码写在define的回调函数中,传递三个参数requireexportsmodule,通过使用 module.exports(exports) 对象向外暴露。

require 函数加载模块的时候,会自动拿到模块内部的 module.exports 对象。定义暴露模块:

define(function (require, exports, module) {
	// 通过require引入依赖模块
    var moduleA = require('add.js') // 等待add.js下载、执行完
    console.log(moduleA.add(10,20))
})

// add.js
define(function (require, exports, module) {
  function add(a, b){
    return a+b;
  }
  // 通过module/exports来暴露模块
  module.exports.add = add;
})
  • AMDCMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块
  • AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块

3. 4 ES6

ES6在语言标准上面实现了模块功能。设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入输出变量,CommonJS以及AMD都只能在运行时确定,ES6的模块并不是对象,而是通过export显示指定输出的代码,再通过import命令导入。

可以使用ES6的模块暴露和引入来实现模块化编程,ES6暴露模块的方式有三种:分别暴露、统一暴露(前两者也称为常规暴露)和默认暴露。

3.4.1 分别暴露

模块一:module1.js

// 分别暴露:也叫多行暴露,每个方法逐一暴露,这种方式在引入时需要用对象来引入
export function foo() {
    console.log('foo() moudle1');
}
export function bar() {
    console.log('bar() moudle1')
}

3.4.2 统一暴露

模块二:module2.js

function fun1() {
    console.log('fun1() module2')
}
function fun2() {
    console.log('fun2() module2')
}
// 统一暴露: 暴露的是一个对象,引入时也必须是个对象
export {foo,bar} 

需要注意的是:以上两种向外暴露方式在主文件引入时必须使用对象的解构赋值引用,不能使用变量接收的方式来映入(注意和默认暴露的区别)

主模块:main.js

import {foo,bar} from '.js/src/module1.js'
import {fun1,fun2} from './js/src/module2.js'

3.4.3 默认暴露

export default {
    foo() {
        console.log('默认暴露方式')
    },
    bar() {
        console.log('默认暴露')
    }
}

默认暴露的方式只允许有一个: export default {}且在主模块引入时可以使用定义变量来接收的方式!

// 引入模块3
import module3 from '.js/src/module3.js'
// 使用模块
module3.foo()
module3.bar()
注意:在使用`ES6`时,会出现一些问题,现在由于并不是所有浏览器都能直接识别`ES6`模块化的语法,所有在不能识别`ES6`语法的浏览器上,就不能执行`ES6`代码,这时就需要用到`Babel`,将`ES6`语法转化为`ES5`(使用了`CommonJS`) ,但此时浏览器还不能直接执行,还需要再使用`Browserify`对`ES5`语法的代码进行打包处理,最后得到的文件,浏览器可以运行。
Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐