前言
作为一个前端小白,对于常说的模块化,一直不是很理解,当面试官说,说说你对模块化有什么理解时,我的脑子是待机状态……封装?导入?导出?暴露接口?
为了深入理解,有了这篇文巩固知识。
什么是模块化
第一,将复杂的程序封装成文件
第二,内部变量及数据是私有的,只暴露接口供其他模块使用
模块化有哪些好处
- 避免命名冲突
- 更好的分离,按需加载
- 高可复用性
- 高维护性
模块化进程
最开始,是没有模块化的概念的,后来慢慢形成了这个概念。
模块的化一个进程,大致如下。
函数封装模式
使用函数来封装一个模块。
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输出结果。
-
成为浏览器和服务器通用的模块解决方案。
-
本文部分内容参考 前端模块化详解(完整版)