目录
前言:
在公司的学习论坛上看到了关于模块化的帖子,写个学习笔记,加深印象。
一.什么是模块化?为什么要模块化?
1.模块化:
将一个复杂的程序依据一定的规则(依据)封装成几个块(文件),并进行组合在一起,这叫模块化。
块的内部数据和实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信。
2.思路:
按功能拆分;数据私有;方便查找;可以复用;
模块内部数据私有了,但是也需要与别的模块建立通讯,所以需要暴露一些接口(方法/api)。
二.模块化的进化过程
1.第一阶段:全局function阶段模式
编码:将不同的功能封装成不同的全局函数;
问题:污染全局命名空间,容易引起命名冲突或者数据不安全,而且模块成员之间看不出直接关系
2.第二阶段:namespace模式
简单对象封装
作用:减少了全局变量,解决命名冲突;
问题:数据不安全(外部可以直接修改模块内部的数据)
3.第三阶段:IIFE模式
匿名函数自调用(闭包)
作用:数据是私有的,外部只能通过暴露的方法操作;
编码:将数据和行为封装到一个函数内部,通过给window添加属性来向外暴露接口
问题:如果当前这个模块依赖另一个模块怎么办?
4.第四阶段:IIFE模式增强
这是现代模块化实现的基石
三.模块化的好处&问题
1.好处
- 避免命名冲突(减少命名空间污染);
- 更好的分离,按需加载;
- 更高可复用性;
- 高可维护性
2.问题
- 请求过多:首先我们要依赖多个模块,引入多个<script>,这样就会发送多个请求,导致请求过多。
- 依赖模糊:我们不知道他们具体的依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。
- 难以维护:以上两个原因导致了难以维护,很可能出现牵一发而动全身的情况导致项目出现严重问题。
四.模块化的规范(requireJS)
1.CommonJs
(1)概述
NodeJs应用又模块组成,采用CommonJs模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类都是私有的,对其他文件不可见。在服务端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
(2)特点
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次运行时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存的结果。想要模块再次运行,必须清除缓存。
模块的加载顺序,按照其在代码中出现的顺序。
(3)基本语法
- 暴露模块:module.exports = value 或者exports.xxx = value
- 引入模块:require("xxx"),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径。require命令的基本功能是,读入并执行一个js文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
疑问?CommonJs暴露的模块到底是什么?CommonJs规范规定。每个模块内部,module变量代表当前模块。每个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载module.exports属性。
2.AMD规范
CommonJs规范加载模块是同步的,也就是说,只有加载完成了才会执行接下来的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于NodeJS主要用于服务端编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJs比较适用。但是如果是在浏览器环境上,要从服务的加载模块,这是就必须采用非同步的加载方式,因此浏览器一般采用AMD规范。此外,AMD规范比CommonJs规范在浏览器端实现要早。
(1)基本语法
- 定义没有依赖的模块:
define(function(){
return 模块
})
- 定义有依赖的模块:
define(['module1','module2'],function(m1,m2){
return 模块
})
- 引入使用模块
require(['module1','module2'],function(m1,m2){
// 使用m1,m2
})
(2)AMD规范在浏览器端的实现
AMD规范的代表:require.js是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,reqiureJs的基本思想是,通过define方法将代码定义为模块;通过reqiure方法,实现代码模块的加载。
AMD规范在浏览器实现的步骤:
1.下载reqiure.js;
2.创建项目结构;
3.定义reqiure.js的模块代码
//dataServe.js文件
//定义没有依赖的模块
define(function(){
let msg = "www.baidu.com"
function getMsg(){
return msg.toUpperCase()
}
return {getMsg}//暴露模块
})
// alert.js
// 定义有依赖的模块
define(['dataServe'],function(dataServe){
let name = "Tom"
function showMsg(){
alert(dataServe.getMsg()+","+name)
}
return {showMsg} //暴露模块
})
4.页面引入reqiure.js模块:
在index.html引入<script data-mian="js/mian" src="js/libs/require.js"></script>
AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚的显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步的加载模块,也可以根据需要动态加载模块。
3.CMD规范
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJs和AMD规范的特点。在Sea.js中,所有js模块都遵循CMD模块定义规范。
CMD由国内的玉伯提出,其与AMD规范的主要区别在于定义模块,和依赖引入部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递到模块内部,而CMD相当于按需加载定义一个模块时不需要立即指定依赖模块,在需要的时候再reqiure就可以了,比较方便。
// 定义没有依赖的模块
define(function(reqiure,exports,module){
export.XXX = value;
module.export = value
})
// 定义有依赖的模块
define(function(reqiure,exports,module){
// 引入依赖模块(同步)
var module2 = require("./module2")
// 引入依赖模块(异步)
require.async("./module3",function(){
function m3(){
}
})
export.xxx = value
})
CMD规范的使用:SeaJS
4.ES6模块化设计
ES6的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量。CommonJs和AMD模块都只是在运行时确定这些东西。比如,CommonJs模块就是对象,输入时必须查找对象属性。
ES6模块化规范中定义:
- 每个js文件都是一个独立的模块
- 导入模块成员使用import关键字
- 暴露模块成员使用export关键字
(1)ES6模块化语法
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
//定义math.js
var basicNum = 0;
var add = function(a,b){
return a+b
};
export {basicNum, add}
// 引入模块
import {basicNum,add} from "./math";
function test(ele){
ele.textContent = add(99+basicNum);
}
上例中使用import引入模块时需要知道所加载的变量名和函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能够加载模块。就要用到 export default命令,为模块指定默认输出。
// export default
export default function(){
console.log("foo")
}
模块默认输出,其他模块加载该模块时,import命令可以为该匿名函数指定任意名称。
import customName from "./export-default.js";
customName(); // foo
(2)ES6模块与CommonJs模块的差异:
两个重大差异:
- CommonJs模块输出的是一个值的拷贝,ES6输出的是值的引用。
- CommonJs模块是运行时加载,ES6模块是编译时输出接口。
第二个差异是因为CommonJs加载的是一个对象(即module.exports属性),该对象只在脚本运行完才生成。而ES6模块不是对象,它的对外接口是一种静态定义,在代码静态解析阶段就会生成。
重点解释第一个差异:
ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
//lib.js
export let counter = 3;
export function incCounter(){
counter ++
}
//main.js
import {counter, incCounter} from "./lib.js";
console.log(counter); // 3
incCounter();
console.log(counter); //4
CommonJs一旦输出一个值,模块内部的变化就影响不到这个值,比如上例中的:counteru一旦输出过后,lib.js模块内部的变化就影响不到 counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
CommonJs模块的加载机制是,输入的是被输出的值的拷贝。
// lib.js
var counter = 3;
function incCounter(){
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter
}
在lib.js中输出内部变量counter和改写这个变量的内部方法incCounter.
//mian.js
var counter = require("./lib").conuter;
var incCounter = require("./lib").incCounter;
console.log(counter); //3
incCounter();
console.log(counter); //4