模块化
1.什么是模块
为了更好程度的实现对已有功能的复用,同时避免过多的js加载和全局污染,前端引入了模块机制。
模块是将一个复杂的程序依据一定的规则封装成几个文件,并将这些文件组合在一起,每一个文件内部的数据和实现是私有的,只是向外部暴露一些接口方法以对外部其他模块进行通信,将Javascript程序拆分成可按需导入的单独模块的机制。
优点:避免了函数名和变量名的冲突,而且对于浏览器而言也就只识别被包裹的那一层代码,减少了服务端的请求次数,解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。
2.模块的实现进程
- 全局function模式:将不同的功能封装成不同的全局函数,这样的话容易污染全局命名空间,而且每个模块之间的关联并不能通过肉眼看出来
- 命名空间模式:它进行了简单的对象封装,将关联的模块作为对象的属性封装在一起,这样的话能减少全局变量,但对于对象而言,外部可以随时修改模块内部数据
- IIFE模式(立即执行函数):它其实是对匿名函数的自调用,数据是私有的,在外部只能通过暴露的方法进行操作,但是当前的模块要依赖其他模块的时候就遇到问题了
- 引入依赖的IIFE模式:将依赖作为一个入参传入到立即执行函数中,这个也是现代模块实现的一个基础
3.模块化规范
当一个页面需要引用多个模块的时候,可能会出现依赖模糊的情况,我们不知道模块之间的依赖关系,这样的话模块也会变得难维护,很容易出现两个依赖加载顺序失败,导致项目出现严重的问题。这些问题可以通过模块化的规范来解决。在服务端,模块的加载时运行时同步加载的,而在浏览器端,模块需要提前编译打包处理。常见的模块化规范包括commonjs,amd,cmd,umd和es6module几个部分,接下来依次对这几部分进行介绍:
3.1 Commonjs
commonjs为服务器提供了一种采用同步加载模块的模块优化,commonjs的出现为js在服务器上运行创造了条件,nodejs就是其中一个应用的范例。
原理:在编写CommonJs模块的时候,首先会使用require来加载模块,使用exports来做模块输出,向一个立即执行函数提供 module 和 exports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载。
-
输出:module.exports和exports,前者是全部输出,后者是module.exports的一个变量
var exports = module.exports
-
引用:require进行模块加载:require命令的基本功能是,读入并执行一个Javascript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
// export.js var add = function(a,b) { return a+b; }; module.exports= {add} // page.js var add = require('export').add;
nodejs中的commmonjs
Node在解析JS模块的时候,会先按文本读取内容,然后将模块内容进行包裹,在外层裹了一个function,传入变量,再通过 vm.runInThisContext
将字符串转成 Function
形成作用域,避免全局污染,所以module,__filename,__dirname
这些变量不需要引用就能使用。
Node.js有四个环境变量:module,exports,require和global,所以能使用commonjs,浏览器没有,所以浏览器不能兼容。换句话说,只要有方法能提供这四个变量,在浏览器加载commonjs模块就变成了可能(browerify可以解决这个问题)。
彩蛋:CommonJS的特性
- 数据是复制和浅拷贝,所以对模块的修改会影响另一个模块。
- Commonjs在
运行时
加载,因为运行时才能得到对应的值,所以当使用require命令加载某个模块时,就会运行整个模块的代码,导致在编译时没有办法做静态优化。因为Commonjs在加载的时候会先执行对应require模块,得到一份代码拷贝,再获取对应的属性或方法。 - 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
3.2 ES6module
- 这是ECMA官方的模块化方法,用export命令指定输入的代码,通过import将代码输入,主要使用export /export default进行输出,export变量声明提升。使用import进行输入,import支持按需输入,也支持异步加载。
- import是做一份指针引用对应的属性和方法,因此不能实现动态的计算和加载。它会被js引擎静态执行,优先于导入模块内的其他内容执行(不管写在什么地方都会在最开始的时候执行),实现的是编译时加载,可以做静态分析,因此能够进行tree shaking。
- export是具名导出,可以以对象的形式导出多个,导入时必须一样的名称才匹配,export default是匿名导出,导入时取什么名字都可以。
- ES6模块中的值属于动态只读引用,也就是说不允许改变引入变量的值
3.3 AMD
AMD (Asynchronous Module Definition,异步模块定义) 指定一种机制,在该机制下模块和依赖可以异步加载。这对浏览器端的异步加载尤其适用。
原理:采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行
-
输出:define()
-
引用:require
// math.js define(['add'],function(a,b){ return a+b }); require(['math'],function(add){ math.add() })
3.4 CMD
是seajs推崇的规范,CMD使用的是依赖就近的原则,用的时候再require,AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块,AMD是预先加载,CMD是用的时候再加载
3.5 UMD
UMD是AMD和Commonjs的结合,目的是解决跨平台的解决方案,它会先判断是否有Node.js模块的存在,有就用它的模块模式,然后判断有没有AMD,有就用AMD
4. babel编译es6 module
Es6中的export通过babel编译后Es5中代码是以exports方式进行导出的,而Es6中的import导入模块通过babel编译后是通过转变为require的方式引入的:
babel 转换 es6 的模块输出逻辑非常简单,即将所有输出都赋值给 exports,并带上一个标志 __esModule
表明这是个由 es6 转换来的 commonjs 输出。
// before
const add=(a,b)=>a+b
export {add}
// after
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.add = void 0;
var add = function add(a, b) {
return a + b;
};
exports.add = add;
// export default
export default (a,b)=>a+b
// after
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _default = function _default(a, b) {
return a + b;
};
exports["default"] = _default;
default 输出会赋值给导出对象的default属性,所以 babel 加了个 help _interopRequireDefault
函数