前端模块化理解

185 阅读10分钟

前言

作为一个前端小白,对于常说的模块化,一直不是很理解,当面试官说,说说你对模块化有什么理解时,我的脑子是待机状态……封装?导入?导出?暴露接口?

为了深入理解,有了这篇文巩固知识。

什么是模块化

第一,将复杂的程序封装成文件

第二,内部变量及数据是私有的,只暴露接口供其他模块使用

模块化有哪些好处

  • 避免命名冲突
  • 更好的分离,按需加载
  • 高可复用性
  • 高维护性

模块化进程

最开始,是没有模块化的概念的,后来慢慢形成了这个概念。

模块的化一个进程,大致如下。

函数封装模式

使用函数来封装一个模块。

function fn1(){
    // ....
}
function fn2(){
    // ...
}

在函数内部,我们可以实现一些功能,当需要的时候,就调用函数。

但这种方法缺点极为明显,容易引起命名冲突,污染全局命名空间,而且成员之间(fn1,fn2之间)也看不出有什么样的联系。

对象封装模式

后来,又有了一个方式,使用对象来封装一个模块:

let obj = {
    name:'obj',
    fn1:function(){
        // ...
    },
    fn2:function(){
        // ...
    }
}
// 使用时
obj.fn1()
obj.fn2()

当需要使用时,调用对象的属性即可。

但这种也有一个缺点,就是外部能访问对象中的属性,并且改变它。

obj.name = 'string'

这样做,是很不安全的,我们并不希望内部变量轻易的被别人改变。

闭包(立即执行函数)封装模式

我们使用立即执行函数来封装:

let obj = (function(){
    let name = 'obj'
    let fn1 = function(){
        // ...
    }
    let fn2 = function(){
        // ...
    }
    return {
        fn1:fn1,
        fn2:fn2
    }
})()

console.log(obj.name)// undefined
obj.fn1()
obj.fn2()

这样我们能保证,内部变量obj.name不会被外部访问到,数据是私有的,外部只能访问暴露的方法。

如果这个模块里,需要新增一个方法,我们又无法改动源码?

引入依赖封装模式

我们先将模块封装到一个文件里:

// module.js 文件
(function(window){
  let name = 'obj'
  let fn1 = function(){}
  let fn2 = function(){}
  window.module = { fn1, fn2}// 暴露接口
})(window)

然后在页面中引入:

<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
let module = (function(module){
    module.fn3 = function(){
        // ...
    }
    return module
})(window.module || {})
</script>

如上,将一个模块当做参数传入,新增一个方法,返回新的模块。

另外,为了确保传入的模块不为空,需要简单的判断。

如果需要用到另一个模块的方法或变量呢?

// module.js 文件
(function(window, $){
  let name = 'obj'
  let fn1 = function(){
      $('body').css('background', 'red')
  }
  let fn2 = function(){}
  window.module = { fn1, fn2}// 暴露接口
})(window,jquery)
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
 module.fn2()
</script>

值得注意的是,因为,module.js需要依赖jquery,所以jQuery的引入必须放在前面。

虽然通过script引入,解决了模块变量私有,及命名问题,但如果一个页面中引入了过多的script,也会有问题。

  • 请求过多,script的资源,是需要发送请求的。
  • 依赖模糊,我们很难知道每个模块之间的具体依赖关系,无法预先知道加载顺序。
  • 难以维护,以上两种原因导致难以维护。

为了解决这些问题,很快,又多了模块化规范。

模块化规范

约束模块。

CommonJs

是为node应用服务的模块规范。

定义模块

根据CommonJs规范,一个单独的文件就是一个模块,每个模块都是一个单独的作用域,模块中的变量,对其他模块不可见。

输出

模块只有一个出口,module.exports,它接受一个对象。

//module.js
let name = 'yang'
let getName = function(){
    return name
}
let changeName = function(val){
    name = val
}
module.exports = {
    getName:getName,
    changeName:changeName
}

// 也可以简写
module.exports = {
  getName,
  changeName
}

// 也可以单个
exports.getName = getName
exports.changeName = changeName

module.exports.getName = getName
module.exports.changeName = changeName

加载模块

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

//newMod.js
let newMod = require('./module.js')
newMod.getName()

如果是第三方模块,不需要`./`路径。例如:`require('http')`

使用命令运行newMod.js文件

node newMod.js

打印出'yang'。

模块中的this

模块中的this,指向模块module.exports对象。

在没有module.exports导出之前,this是一个空对象。

let name = 'yang'
console.log(this) // {}
let changeName = function(val){
  console.log(this)// { changeName:Function changeName}
  name = val
}
module.exports = {
  changeName
}

总结

CommonJS暴露的模块到底是什么?

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性是对外的接口。

加载某个模块,其实是加载该模块的module.exports属性。

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

比如:

// module.js
let name = 'yang'
let changeName = function(val){
  name = val
}
module.exports = {
  name,
  changeName
}
//newMod.js
let newMod = require('./module')
console.log(newMod.name) // yang
newMod.changeName('li')
console.log(newMod.name) // yang

所以,我们如果想要获取name的值,只能把它写成一个函数的返回值,再将函数导出。

let name = 'yang'
let changeName = function(val){
  name = val
}
let getName = function(){
    return name
}
module.exports = {
  getName,
  changeName
}

AMD

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思,它是一个在浏览器端模块化开发的规范。

从上commonjs规范可以看出,commonjs是同步的,只有当模块加载完成,才能执行后面的操作。

AMD则是异步加载模块,允许指定回调函数。

node环境采用commonjs

浏览器环境采用AMD。

requireJS库

使用AMD规范,需要用到requireJS库。它主要解决两个问题:

  • 多个js文件之间可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
  • js加载时,浏览器会阻止页面渲染,加载文件越多,页面响应越慢,requireJS使用异步加载模块,不影响后续代码的执行。

可去官网下载require 官方网址

定义模块及输出

define接受三个参数

  • module-name 可省略,模块标识,省略之后,模块将成为匿名函数,调用时,使用文件名,如module1.js,module1就是文件名,也是模块名。
  • 依赖 可省略,所依赖的模块
  • 函数或对象 实现模块功能

当define函数执行时,它首先会异步的去调用第二个参数中列出的依赖模块,当所有的模块被载入完成之后,如果第三个参数是一个回调函数则执行,然后告诉系统模块可用,也就通知了依赖于自己的模块自己已经可用。

// module1.js
//定义没有依赖的模块
define(function(){
   let name = 'yang'
   let getName = function(){
       return name
   }
   return { getName } // 暴露模块
})

main.js文件

(function(){
  require.config({
      baseUrl:'js/',// 基本路径
      paths:{
        // 映射 模块标志名 : 路径 (重写模块名)
          module1:'./module1' // 不能写成module1.js,会报错
      }
  })
})()

页面中引入模块

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>

</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/require.js"></script>
<script>
// 使用模块
 require(['module1'],function(module1){
     console.log(module1.getName()) // yang
  })
</script>
</body>
</html>

模块中引入第三方模块

//定义有依赖的模块
define(['jquery'], function($){
   let name = 'yang'
   let getName = function(){
       return name + module2.getName()
   }
   $('body').css('background','green')
   return { getName } // 暴露模块
})

main.js中

(function(){
  require.config({
      baseUrl:'js/',// 基本路径
      paths:{
        // 映射 模块标志名 : 路径
          module1:'./module1', // 不能写成module1.js,会报错
          jquery:'./jquery.js'
      }
  })
})()

CMD

CMD 即Common Module Definition通用模块定义。

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

SeaJS库

定义模块及输出

CMD推崇一个模块一个文件,常用文件名作为模块id。

函数中有三个参数。

  • require 接受 模块标识 作为唯一参数,用来获取其他模块提供的接口
  • exports 一个对象,用来向外提供模块接口;
  • module 一个对象,上面存储了与当前模块相关联的一些属性和方法
// module1.js
//定义没有依赖的模块
define(function(require, exports, module){
   let name = 'yang'
   let getName = function(){
       return name
   }
   let changeName = function(val){
       name = val
   }
   exports.getName = getName // 暴露模块
   
   // 或者 只能二选一,不可同时导出
   module.exports = {
       changeName,
       getName
   }
})

main.js文件

main.js是一个所有模块集合的入口。

define(function(require,exports,module){
  let module1 = require('./module1')
  module.exports = {
    module1
  }
})

页面中引入模块

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>

</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script type="text/javascript" src="js/seaJS.js"></script>
<script>
// 可修改别名
seajs.config({
  alias:{
    module1:'./js/module1.js',
    main:'./js/main.js'
  }
})
// 使用模块
seajs.use(['main'],function(my){
  console.log(my.module1)
  console.log(my.module1.getName())
  my.module1.changeName('li')
})

</script>
</body>
</html>

定义有依赖的模块及异步依赖模块

//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
  })
  //暴露模块
  exports.xxx = value
})

ES6模块化

定义模块及输入输出

export命令用于规定模块的对外接口

import命令用于输入其他模块提供的功能。

  • 导出对象
//module1.js
export let module = {
  name:'yang',
  getName:function(){
    return this.name
  },
  changeName:function(val){
    this.name = val
  }
}

使用导出的对象:

import module1 from './module1.js'
console.log(module1.getName())
console.log('name',module1.name)
module1.changeName('li')
console.log(module1.getName())
console.log('name',module1.name)
  • 导出函数
let name = 'yang'
export function getName(){
    return name
}
export function changeName(val){
    name = val
}

或者合并导出

//module1.js
   let name = 'yang'
   let getName = function(){
       return name
   }
   let changeName = function(val){
       name = val
   }
   export { getName, changeName }

使用导出的对象:

import {getName,changeName} from './module1.js'
  • 默认导出

export default可以不需要大括号{}

//module1.js
   let name = 'yang'
   export default name

使用export default命令,为模块指定默认输出,这样就不需要知道所要加载模块的变量名。

默认导出后,引入时允许任意命名:

import a from '/module1.js'
import b from '/module1.js'
console.log(a) // yang
console.log(b) // yang

html中的模块引入

Module 的加载实现的是es6语法,所以在浏览器加载html文件时,需要在script 标签中加入type="module"属性。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script type="module">
    import { getName, changeName } from './js/module1.js'
    console.log(getName()) // yang
  </script>
</head>
<body>
</body>
</html>

重命名

默认导出不可重命名。

函数导出重命名:

import {getName as get,changeName as change} from './js/module1.js'
console.log(get())
change('li')
console.log(get())

导出一整个模块:

    import * as newModule from './js/module1.js'
    console.log(newModule.getName())
    newModule.changeName('li')
    console.log(newModule.getName())

总结

commonjs/AMD/CMD/ES6模块化的区别?

  • commonjs

    • node端的模块规范。

    • 使用module.exports输出模块,使用require()引入。

    • 同步加载,运行时加载。

    • 缺点:它的输出,是一个对象,即module.exports,是一个拷贝后的值。

  • AMD

    • 浏览器端模块规范,使用requireJS库,

    • 异步加载方式。

    • 使用define函数定义模块,使用return输出结果。使用require引入模块。

    • 依赖前置,在定义模块时声明依赖,在所有依赖加载完成后,执行模块回调。

    • 缺点:依赖是提前执行,如果某个依赖并没有在回调中用到,它仍然还是加载了。

  • CMD

    • 浏览器端模块规范,使用SeaJS库,

    • 异步加载,按需加载。

    • 使用define函数定义模块,使用module.exports输出结果。使用seajs.use()引入模块。

    • 就近依赖,延迟执行,所有依赖模块加载完成后,进入主线程,只有当require模块时,才会执行模块内容。

    • 缺点:打包困难,依赖 SPM 打包,模块的加载逻辑偏重。

  • ES6模块化

    • 相当简单的实现了模块功能。

    • 使用export导出,import输出结果。

    • 成为浏览器和服务器通用的模块解决方案。

本文部分内容参考 前端模块化详解(完整版)