什么是模块化?
模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分成许多部分单独开发,然后再组装起来,每一个部分即为模块。当使用模块化开发的时候可以避免刚刚的问题,并且让开发的效率变高,以及方便后期的维护。
模块化主要解决:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。
一、ES5及ES5之前如何模块化?
要回答这个问题,首先先了解几个概念。
通行的JavaScript模块规范主要有三种:CommonJS、AMD和CMD。
(1)CommonJS
2009年,美国软件工程师Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。NodeJS是CommonJS规范的实现,webpack 也是以CommonJS的形式来书写。
Common.js优点在于:
1.CommonJS模块规范很好地解决变量污染问题,每个模块具有独立空间,互不干扰,命名空间等方案与之相比相形见绌。
2.CommonJS规范定义模块十分简单,接口十分简洁。导入是通过require,导出通过module.exports或者exports。
require一个模块一般会有两种情况:
require的模块第一次被加载。这时首先会执行县该模块,然后导出内容。
require的模块非首次加载。这时模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
我们可以简单理解为commonjs在每个导出的模块首部添加了如下代码:
var module = {
exports: {}
}
var exports = module.exports;
在使用exports时要注意,不要直接给exports赋值,负责会导致其失效。
但是CommonJS规范不适用于浏览器环境。原因是缺少四个Node.js环境的变量:module/exports/require/global,并且是同步的(意味着在浏览器中会阻塞),CommonJS是主要为了JS在后端的表现制定的,它是不适合前端的。
只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。Browserify 是目前最常用的 CommonJS 格式转换的工具,虽然 Browserify 很强大,但不能在浏览器里操作。
解决思路:
一是开发一个服务器端组件,对模块代码作静态分析,将模块与它的依赖列表一起返回给浏览器端。 但需要服务器安装额外的组件,并因此要调整一系列底层架构。
二是用一套标准模板来封装模块定义,但是对于模块应该怎么定义和怎么加载,又产生的分歧,分成了两个思路:AMD和CMD。
目前node主要编译工具统一为GYP工具,好处主要有两点:一是node的源码就是通过GYP编译的;二是可以跨平台编译。
(2)AMD
AMD即Asynchronous Module Definition(AMD wiki中文版),中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范。由于AMD不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
本人只用过require.js,所以简单可以谈谈require.js。
它的优势主要在于异步模块化加载js文件,并且能很好的管理js文件的依赖关系。主要思想就是将页面用到的js全部异步加载。
1.实现js文件的异步加载,避免网页失去响应;
2.管理模块之间的依赖性,便于代码的编写和维护。
require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。虽然require.js是基于AMD规范的,但是也可以加载非AMD规范的js文件。
(3)CMD
CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个require.js,CMD有个浏览器的实现sea.js,sea.js要解决的问题和require.js一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。
阿里大神玉伯写了sea.js,就是遵循他提出的CMD规范,与AMD非常相近,核心思想就是按需加载,不会等待较长时间。腾讯阅文集团的张鑫旭大神维护的seajs文档。
豆瓣上面这个答案说的比较好:SeaJS对模块的态度是懒执行, 而RequireJS对模块的态度是预执行。
二、ES6模块化
EcmaScript6 标准增加了JavaScript语言层面的模块体系定义。
在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。
1、导出
- 命名导出
- 默认导出
先看模块命名导出的两种写法
//写法1
export const name = 'name';
export const add = function(a, b) { return a + b; };
//写法2
const name = 'name';
const add = function(a, b) { return a + b; };
export {name, add};
模块的默认导出只能导出一个
export default {
name: 'name',
add: function(a, b){
return a + b;
}
}
2、导入
ES6 Module中使用import导入模块
整体导入
import * as name from <myModule>;
混合导入---导入一个默认导入和一个命名导入
目前很少JS引擎能直接支持 ES6 标准,因此 Babel 的做法实际上是将不被支持的import翻译成目前已被支持的require。
三、Commonjs和ES6 Module 区别
1、动态和静态
Commonjs和ES6 Module 最本质的区别是Commonjs对于模块依赖的解决是“动态的”,而ES6Module是“静态的”。
“动态”的含义是:模块依赖关系的建立发生在运行时。“静态”的含义是模块依赖关系的建立发生在代码编译阶段。
ES6Module相对于Commonjs来说具备以下优势:
(1)死代码检测和排除。使用静态工具检测那些代码未被调用,提前清除。
(2)模块变量检查。有利于模块间传递的接口或者变量是准确的。
(3)编译器优化。Commonjs等模块传出的都是一个对象,但是ES6传出的是一个变量,减少了引用层级,程序效率更高。
2、值拷贝和动态映射
在导入一个模块时,对于Commonjs来说获取的是一份导出值的拷贝,ES6中则是值得动态映射,并且这个映射是只读的。另一方面来说,Commonjs支持我们对于导出的值修改,但是ES6Module不支持修改,修改会抛出SyntaxError。
3、循环依赖
一般我们应该是要极力避免代码产生循环依赖,但有时实际工程项目中我们会遇到模块间循环依赖的问题。
下面是循环依赖的一个实例:
//b.js
const a = require('./a.js')
console.log('value of a:', a);
module.exports = 'This is b.js';
//a.js
const b = require('./b.js')
console.log('value of b:', b);
module.exports = ''This is a.js;
//index.js
require('./b.js')
这段代码是有循环依赖的,foo和bar两个模块相互依赖,我们预期输出的是:
value of a: This is a.js
value of b: This is b.js
实际输出:
value of b: {}
value of a: This is a.js
原因是当我们在执行了index.js之后,进入b.js后,执行完第一句就跳到a.js中,这时b.js就不会往下执行,而是进入a.js,但a.js中有循环依赖,
最硬核最关键的点的来了,由于我们知道require如果导出过一次,第二次就不会再导入,所以直接导出b.js的值,这时b.js的值为{},继续执行a.js,所以结果就是我们看到的实际输出。
我们用ES6Module改写来解决这个循环引用的问题:
//b.js
import a from './a.js';
console.log('value of a:', a);
exports default 'This is b.js';
//a.js
import b from './b.js';
console.log('value of b:', b);
exports default 'This is a.js;
//index.js
import b from './b.js';
实际输出:
value of b: undefined
value of a: This is a.js
我们如果想要实现想要的结果必须在内部重新指定导出值
//b.js
import a from './a.js';
function b(cur){
console.log(cur + ' is b.js');
a('b.js')
}
exports default b;
//a.js
import b from './b.js';
function a(cur){
if(!invoked){
cur = true;
console.log(cur + ' is a.js');
b('a.js')
}
}
exports default a;
//index.js
import b from './b.js';
b('index.js');
因为ES6代码import发生在编译期,所以我们import时并没有代码运行,编译器将我们所有的函数都绑定。等到运行期后,直接运行b() -> a()->b()
执行结果如下:
index.js is b.js
b.js is a.js
a.js is b.js