javascript模块化

139 阅读4分钟

1、模块化的由来:

1、IE6之前是没有js引擎的,js是由渲染引擎执行。页面中所有的js代码全部写在一个js文件中。

2、由于浏览器的交互越来越多,js逻辑也越来越复杂,导致js代码维护成本加高。

3、这时每一个页面对于一个js文件,这个js文件就处理这个页面的逻辑。结构与行为相分离。

4、如果页面中出现同样的逻辑代码,导致的问题js代码的重复度高

5、把重复的代码抽离出来一个common.js。让每一个页面按需引入,这时某个一个页面只需要common.js的某一些方法,导致的问题是,代码冗余。

6、这时需要把common.js继续抽离分成多js文件按需引入,这样导致的问题

1、如果a.js中用到b.js中的变量C,这时标签的顺序恰恰是:

<script src="./a.js"></script>
<script src="./b.js"></script>

由于<script>标签有阻塞,b.js无法加载,导致a.js找不到变量C报ReferenceError

2、同一个页面所有的<script>共享一个全局作用域,导致的问题:

变量数据类型 全局污染 ——> 变量重名导致 ——> 变量覆盖

这个时模块化就开始闪亮登场了,模块化解决的问题:

1、污染全局
2、js的加载顺序;

1、解决污染全局,并且可以相互依赖

通过立即执行函数创建模块独立的作用域,通过对象的形式把变量包装好通过闭包return抛出去,或者把这个包装好的对象,挂载到window下:

var ModuleA=(function(){
	var a=18
	var b=20
	return{
		a:a,
		b:b
	}
}());
		
;(function(){
	var a=18
	var b=20
	var moduleB={
		a:a,
		b:b
	}
	window.moduleB=moduleB
}());

如果moduleB需要用到moduleA中的变量,通过模块的注入的方式把ModuleA传给moduleB:

var ModuleA=(function(){
	var a=18
	var b=20
	return{
		a:a,
		b:b
	}
}());
		
;(function(ModuleA){
	var a=18
	var b=20
	var module={
		a:a,
		b:b
	}
	window.module=module
}(ModuleA));

模块的名称是在全局的,按模块命名所以而且模块比变量少得太多了,如果出现被覆盖,查找模块一定比查找变量容易。模块注入的好处就是,让模块引入到局部作用域下,就不需要每一次都要到全局里面去找,提高效率。

插件化封装提供模块的复用性:通过配置项,让用户决定插件的行为,是模块化的一部分,。

以上解决模块化,都是基于js语言本身的特点。还不足以解决变量污染,加载顺序问题也依然。

二、模块化解决方案

1、AMD RequireJs Asynchronous Module Definition异步模块定义

1、define(moduleName,[module],factory)定义模块

2、require([module],callback)引入模块

image.png

math.js

define('moduleName',function() {
	var add = function(x, y) {
		return x + y;
	};
	var opt={
		name:'qige',
		age:18
	}
	return {
		opt,
		 add
	};
});

index.js

require.config({
	paths: {
		moduleName: 'js/math',
	}
})
require(['moduleName'], function(moduleName) {
	console.log(123);
console.log(moduleName);
});

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		
	</head>
	<body>
		<script src="./js/require.js"  type="text/javascript" charset="utf-8"></script>
		<script src="./js/index.js" type="text/javascript" charset="utf-8"></script>
	</body>
</html>

2、CMD seajs common Module Definition 通用模块定义(阿里出品)。

1、define(function(require,exports,module){})定义模块;

2、require加载 exports导出/return module操作模块;

seajs.use([module路径],function(moduleA,moduleA,moduleC){})使用模块。

image.png

image.png

image.png

AMD和CMD的异同

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果js体积过大,就会导致浏览器“堵塞”。为了解决这个问题,浏览器允许js脚本异步加载,给<script>标签添加async或者defer属性。

1、async 表示js脚本加载完成之后,渲染引擎停止渲染,立即执行当前脚本。

2、defer 只是开启异步脚本加载,直到页面全部渲染完成之后,再按照标签的顺序执行脚本。

相同之处:

1、所有的文件都加载完成之后,回调函数才执行。

2、加载的本质是通过<script src="./modulePath.js" async></script>

异同之处

CMD:是依赖后置 就近原则,用到的时候才加载,按需加载。

AMD:是依赖前置,不管用不用到,只要出现就全部下载回来才执行。

3、Common.js

Node.js的诞生带来了真正的模块化开发体验:不依赖html页面实现js之间模块依赖。

1、require('.....) 引入模块。

2、module.exports 导出模块exports。

3、commonJs是同步加载文件,主要是运行在Node环境。

//commonJs.js
var obj = { count: 1 };
var qige = 123;

function print() {
	qige=456;
	obj.count++;
	console.log('commonJs::',qige);//456
}
module.exports = {
	print,
	obj,
	qige:qige
}

//index.js
const common = require('./commonJs.js')
console.log('index::',common.qige);//index::123
common.print();
console.log('index::',common.qige);//index::123
setTimeout(()=>{
	const common2 = require('./commonJs.js')
	console.log(common===common2);//true
	console.log(common);
})

common打印如下:

image.png

由于修改commonJs文件内部的原始值类型变量qige=456和内部对象obj.count++之后,再打印响已输出的外部的common.qige依然是123,外部的common.obj.count的值为2。

得出结论1: 引入的是模块exports对象的浅拷贝

由于common===common2为true;

得出结论2 commonJs有缓存机制;

由于common2是在定时器中加载的

得出结论3 commonJs是运行时加载;

4、官方ES6模块化 ES6 在语言标准的层面上,实现了模块化。 ES6中一个文件就是一个独立的模块,模块内所有的变量对其他模块是不可见的,如果当前模块的变量需要对外可见,需要export导出,外部文件需要用到该模块导出的变量需要import导入。

1、export导出对外的变量;

2、import导入其他模块提供的变量;

1、export 变量声明/函数声明/l类的名称

// module_a.js
export var moduleName='qige';
export function gets() {
	return 'hello world'
}
export class Good{
	constructor(arg) {
	  console.log(arguments);
	}
}

import * as moduleA from "./module_a.js"
import {moduleName,gets,Good} from "./module_a.js"

2、export{变量1、变量2、变量3},花括号里面的变量名必须模块中的全局变量,不能是局部变量

// module_a.js
export{moduleName,gets,Good}

//index.js
import * as moduleA from "./module_a.js"
import {moduleName,gets,Good} from "./module_a.js"

3、export default 变量名/函数声明/类声明/{变量1、变量2、变量3},一个模块中只能有一个default

// module_a.js
 function gets() {
	return 'hello world'
}
var moduleName='qige'
var firstName = 'foo',
	lastName = 'bar'
export { firstName, lastName }
export default class Good{
	constructor(arg) {
	  console.log(arguments); 
	}
}
//index.js
import Good, * as moduleA from "./module_a.js"

moduleA是一个对象,包含module_a.js模块导出的变量。

注意:export/import只能在模块的全局作用域中。导出/导入都可以通过as 关键字为变量重命名。

//ES6.js
var obj = { count: 1 };
var qige = 123;

function print() {
	qige = 456;
	obj.count++;
	console.log('ES6::', qige); //456
}
export {print,obj,qige}

//index.js
import * as es6 from './ES6.js'
console.log('index::',es6.qige);//index::123
es6.print();
console.log(es6);
console.log('index::',es6.qige);//index::123

image.png

由于内部修改原始值类型变量,外部获取到也修改了,

得出结论1:ES6 模块输出的是值的引用

三、ES6模块与commonJs模块的区别

1、 commonJs模块是在运行是加载,ES6是在编译时加载;

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成

ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

2、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用 ; CommonJSa加载完就缓存起来

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

CommonJS 模块导入的对象的属性可以随意修改。由于时副本它的修改对内部不影响。

ES6 模块导出的时对外接口,读外部提供只读权限,由于外部拿到的时内部的引用,如果A模块和B模块都引用了该模块,如果允许修改导致数据混乱。

4、CommonJS 模块的require是同步加载模块,ES6 模块的import命令是异步加载

ES6导入:commonJs模块

//commonJs.js
exports.common = {print,obj,qige}
//index.js
import {common} from './commonJs.js'

commonJs导入ES6模块

//ES6.js
export  {print,obj,qige}
//index.js
const es6 = require('./ES6.js')