一、前端模块化
什么是模块化?为什么前端需要模块化?
js代码量激增,放在同一个文件里面,不容易维护,而且牵一发而动全身。
这时候就需要将代码按照逻辑放在不同的文件里面,按照一定的语法规则,遵循特定的规范将一个庞大的文件拆分若干个相互依赖的文件。这些文件对外暴露数据或接口,在需要的时候导入引用。这就是前端模块化。
说的很官方,举个简单的栗子来通俗的理解。就像是社会的分工合作,彼此依赖,彼此独立,每个社会的部门可以理解为一个模块,按照某种规则负责特定的功能,组装起来形成一个整体,从而完成整个社会系统所要求的功能。
前端模块化的三个阶段
(1)早期“伪”模块化时代
借助函数的作用域来实现伪模块化,不同的功能封装成不同的函数。
变化过程如下:
①第一个阶段—函数模块化
JS函数有独立的作用域,在函数中可以放任何代码,只需要在使用的地方调用它,实现代码分离组织,视觉上看起来也很清晰。
局限性:如果代码量巨大,无法保证模块之间不发生冲突,各个函数在同一个文件中,没有前后逻辑的依赖关系,混乱的调用,而且存在命名冲突和变量污染问题。
function fn1() {
//...
}
function fn2() {
//...
}
function fn3() {
fn1()
fn2()
}
②第二个阶段—对象模块化
对象可以有属性,而且它的属性可以是数据,也可以是方法。对象的属性可以通过对象名字来访问,相当于设定了一个命名空间,于是对象模块化也叫做命名空间模式
局限性:数据安全性低,内部属性是赤裸暴露的,对象的内部成员可以随意修改。
const module1 = {
data1: 'date1',
fn1: function () {
//...
},
};
const module2 = {
data2: 'data2',
fn2: function () {
//...
},
};
module2.data2 = 'data1'; //可以随意修改,安全系数低
解决方案:IIFE立即执行函数+闭包+对外暴露数据和接口。闭包可以解决数据访问安全,立即执行函数创建一个私有的作用域。意思就是利用函数闭包的特性来实现私有数据和共享方法。
//只能通过调用模块暴露给外界(window)的函数修改data值
(function (window) {
var data = "data";
function showData() {
console.log(`data is ${data}`);
}
function updateData() {
data = "newData";
console.log(`data is ${data} `);
}
window.module1 = {
showData,
updateData
};
})(window);//自执行函数传入window使window是全局变量也是局部变量,当内部代码访问window对象时,不用往上逐层查找
module1.showData()
module1.updateData()
var myModule = (function () {
var name = 'gaby'
function getName() {
console.log(name)
}
return {
getName
}
})()
myModule.getName() //获取name实现属性私有化
myModule.name //外部调用不到
如果myModule需要依赖其他的函数(模块)怎么办呢,这时候就要给函数传入参数(引入依赖)
var yourModule = (function () {
return {
a: 1,
b: 2,
c: 3,
d: 4
}
})()
var myModule = (function () {
var name = 'gaby'
function getName() {
console.log(name)
}
return {
getName
}
})(yourModule)
传参的形式也是现代模块规范的思想来源
依赖注入
三大框架之一的Angular,核心思想就是依赖注入
// 模块fnA
let fnA = function(){
return {name: '我是fnA'}
}
// 模块fnB
let fnB = function(){
return {name: '我是fnB'}
}
//模块fnC 调用fnA、fnB,依赖函数作为显式参数传入
let fnC = function(){
let a = fnA()
let b = fnB()
console.log(a, b)
}
(2)多种规范标准时代
CommonJS:node.js应用模块,一个文件就是一个模块,拥有自己独立的作用域,变量和方法都在独立的作用域内。
AMD规范:CommonJS是同步加载,在服务端没有问题。但是在浏览器端不合适,会导致页面失去响应,同步加载的时间越长,用户体验就越差。所以AMD出现就是浏览器端的异步加载。运用require.js库定义define模块、加载require模块。
CMD规范:综合CommonJS和AMD的优点,可以同步加载也可以异步加载。运用Sea.js库实现模块化。
UMD规范:通用模块定义规范Universal Module Definition。通过运行编译时让同一个代码模块在使用CommonJS、CMD、AMD的项目中运行。js可以运行在服务器端、浏览器端、移动端,遵循同一个语法规范就行.
二、CommonJS规范(服务器端)
(1)什么是CommonJS
CommonJS是服务器端规范,Nodejs采用了这个规范,首先采用js模块化的概念。
CommonJS用同步的方法加载模块,在服务端,模块文件都存在本地磁盘,读取速度快。服务器端同步加载不会有问题,但是在浏览器端的AMD、CMD还是异步加载方案更合理。
(2)CommonJS规范
定义模块分为:module模块标识、exports模块定义、require模块引用
定义当前模块对外输出的接口(不推荐,直接用exports)
module.exports与exports的区别
- module.exports
表示当前模块对外输出的接口,当其他文件加载,实际上是读取module.exports这个属性。
- exports
node为每一个模块提供了一个exports对象。这个exports对象的引用指向module.exports。
相当于var exports = module.exports。可以在这个变量上直接添加属性方法:exports.xxx = function(){}
但是不能写成直接指向一个值赋值,因为这样会改变exports的引用地址,这就使得exports和module.exports没有关系
//定义模块math.js
var number = 0;
function add(x, y) {
return x + y
}
module.exports = { //对外暴露的数据和接口函数
number: number,
add: add
}
//引用自定义模块时,包含参数路径,可以省略js
var math = require('./math.js');
math.add(1, 2)
//引用核心模块,不需要路径
var http = require('http');
http.createService().listen();
(3)CommonJS规范特点
①所有的代码都在独立的模块作用域中,不会污染全局作用域
②模块加载的顺序,按照其在代码中的引入顺序加载。
③模块可以多次加载,但是只会在第一次加载时运行一次,运行结果被缓存。之后加载从缓存中直接读取,清空缓存重新运行。
④module.exports属性输出是值拷贝。一旦操作完成,模块内发生的任何变化不会影响到已经输出的值。
三、AMD规范
(1)什么是AMD
AMD(Asynchronous Module Definition)异步模块定义指定一种机制,在该机制下模块和依赖可以异步加载,这对浏览器端的异步加载尤其适用。AMD是在RequireJS在推广过程中对模块化定义的规范化产出。
(2)AMD可以解决什么问题
①多个js文件存在依赖,被依赖的文件需要早于依赖它的文件加载到浏览器
②js加载的时候浏览器会停止渲染页面,加载的文件越多,页面失去响应的时间就会越长。AMD可以异步加载
(3)AMD规范
require.js实现AMD规范的模块化:用require.config()指定引用路径、define()定义模块、require()调用模块
①定义模块define(id,dependencies,factory),全局变量
id:定义模块的名字,如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定的脚本的名字
dependcies:当前模块的一个依赖,已被模块定义的模块标识的数组字面量。依赖参数可选,忽略默认的是[‘require’,‘exports’,‘module’]
factory:模块初始化要执行的函数或者对象。如果是函数,一般只执行一次。如果是对象,此对象应该为模块的输出值。
②调用模块require([dependencies],function(){})
dependencies:第一个参数是数组,表示依赖的模块
function(){}:第二个参数是回调函数,当前指定的模块都加载成功后,它将被调用。加载的模块会以参数的形式传入函数,因此在回调函数的内部就可以使用这些模块。
require()函数在加载模块的时候是异步加载,浏览器不会失去响应。回调函数只有前面的模块加载成功后,才会运行,解决依赖性问题。
//定义模块myModule.js
define(['dependency'], function () {
var name = 'gaby';
function printName() {
console.log(name)
}
return {
printName: printName
};
});
//加载模块
require(['myModule'], function (my) {
my.printName();
});
define(['dependency'], function () {
var name = 'gaby'
function printName() {
console.log(name);
}
return {
printName: printName //对外暴露的接口
};
});
//加载模块
require(['myModule'], function (my) {
my.printName()
});
四、CMD规范
(1)什么是CMD
CMD是Common Module Definition通用模块定义,CMD规范国内发展出来的。CMD规范中,一个模块就是一个文件。CMD是在SeaJS在推广过程中对模块化定义的规范化产出。
(2)CMD规范
①define(id,d,factory)
- CMD推崇一个文件一个模块,经常用文件名作为id
- CMD推崇就近依赖,一般不在define中写依赖
- factory中写三个参数
②function(require,exports,module)
- require是一个方法,用来获取其他模块提供的接口
- exports是一个对象,用来向外提供模块接口
- module是一个对象,上面存储模块相关联的一些属性和方法
// 定义模块 myModule.js
define(function(require,exports,module) {
var $ = require('jquery.js')
$('div').addClass('active');
});
// 加载模块
seajs.use(['myModule.js'],function(my){
});
//math.js
define(function (require, exports, module) {
exports.add = function () {
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
});
// module1.js
define(function (require, exports, module) {
var add = require('math').add;
exports.module1 = function (val) {
return add(val, 1);
};
});
// module2.js
define(function (require, exports, module) {
var inc = require('module1').module1;
var a = 1;
ml1(a); //2
module.id == 'module2';
});
五、ES6原生模块
(1)ES6模块化的两个特点
①ES6模块化规范中模块输出的是值的引用
ES 模块化规范中导出的值是引用,所以不论何时修改模块中的变量,在外部都会有体现。
②静态化,编译的时候就确定模块之间的关系,每个模块的输入和输出变量也是确定的。
静态化是为了实现tree shaking提升运行性能。
tree shaking
减少web项目中js的无用代码,以达到减少用户打开页面的等待的时间,缩短渲染的时间,提升响应的用户体验。
DCE
减少无用代码的操作有一个名字叫做DCE(dead code elemination),无用代码的减少意味着更小的体积,缩减bundle size,从而获得更好的用户体验。
(2)ES6模块化静态性的局限性
①import依赖必须在文件的顶部
②export导出的变量类型严格限制
③依赖不能动态确定
(3)ES6导出export VS export default
ES6模块化导出有export
和export default
,建议用export
,因为
- export default导出整体对象,不利于减少无用代码tree shaking
- export default导出的结果可以随意命名,不利于代码的管理
(4)ES6模块的两个命令,export
和import
①export:规定模块对外的接口
②import:输入其他模块提供的功能
//定义模块math.js
var number = 0;
var add = function (x, y) {
return x + y
};
export {
number,
add
} //暴露对外数据和接口
//引用模块
import {
number,
add
} from './math';
function test(ts) {
ts.textContent = add(9999999 + number)
}
模块的两个命令,export
和import
①export:规定模块对外的接口
②import:输入其他模块提供的功能
//定义模块math.js
var number = 0;
var add = function (x, y) {
return x + y
};
export {
number,
add
} //暴露对外数据和接口
//引用模块
import {
number,
add
} from './math';
function test(ts) {
ts.textContent = add(9999999 + number)
}