在上一篇文章 模块化与Webpack属性 library,libraryTarget的关联中提到很多关于模块化的规范。 如什么有些模块需要暴露在module.export
上,而有些需要要暴露为define('XXX', [], function() =>{})
等等。这些都是因为使用不同的规范导致的。可能会有点混乱,这里就好好来总结模块化的到底有哪些规范。
目前的模块化的规范:**CommonJs
,AMD
,CMD
** 和 **ES6模块
**。
首先先来看一张图片,说明这种模式的整体区别:
那么接下来具体讲讲!
CommonJS
CommonJS
是以在浏览器之外构建 Javascript
生态系统为目标而产生的项目,比如在服务器里面。CommonJS
的代表:Node应用中的模块,通俗的来说就是 npm
安装的模块。
基本概念
Node
应用是由模块组成,Node
的模块采用CommonJS
模块规范;那么它的规范有哪些呢?
CommonJS
模块规范:
每一个文件就是一个模块,都有自己的作用域。在一个文件定义的变量,函数,类都是私有的,对其他的文件不可见。
1
2
3
4
5// test.js
var name = "shuliqi";
var getName = function() {
return name;
}上面这个代码表示:变量
x
和函数getName
是当前test.js
私有的,其他文件是不可见的。如果想在多个文件中分享一个变量的话, 必须定义为global
对象的属性;当然,这种写法是不推荐的。每个模块内部,
module
变量代表当前的模块。这个变量是一个对象,它的exports
属性(module.exports
)是对对外的接口1
2
3
4
5
6
7// test.js
var name = "shuliqi";
var getName = function() {
return name;
}
module.exports.name = name;
module.exports.getName = getName;上面的代码通过
module.exports
将变量name
,getName
暴露出来。Node
的每个模块提供一个exports
变量, 指向module.exports
;这等同于在每个模块头部, 都有这样的代码。1
var exports= module.exports;
这个造成的结果是:在对外输出模块接口时,可以向
exports
对象添加方法注意,不能直接将exports变量指向一个值,因为这样等于切断了
exports
与module.exports
的联系。注意:不能直接将
exports
变量指向一个值, 因为这样就等用于切断了exports
和module.exports
之间的联系;如果 一个模块的对外接口是一个一个单一的值,那么也是不能用exports
的、建议还是直接使用module.exports
。不用区分那么多在服务端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理(这个我们下面的例子会具体来讲)
CommonJS模范定义分为三个部分:
模块定义
模块定义其实就是暴露模块,可以把模块暴露在
module.exports
或者exports
上1
2
3
4module.exports.name = "shuliqi";
exports.getName = function() {
return ...
}把对外开放的接口都暴露在
module.exports
对象上。模块标识
模块标识就是
require()
函数的参数,规范是这样的必须是字符串
可以是以./, ../开头的相对路径
可以是绝对路径
可以省略后缀名
require
命令用于加载模块文件,读入并且执行一个Javascript
文件,然后返回该模块的的exports
的对象。如果没有指定模块, 会报错。引用模块
使用
require
加载模块1
2
3
4
5
6
7
8// 绝对路径
const name = require('shuliqi/study/name.js');
// 省略了后缀名, 相对路径
const getName = require('./getName');
// 模块式第三方模块,直接引用模块名
const http = require('http');
服务端使用
首先肯定得下载NodeJS。到这个页面进行下载哦。
项目结构
然后初始化我们的项目结构:
1 | $ mkdir module-example |
添加一些文件夹以及文件;最后的基本结构为:
1 | |--src |
定义模块
module1.js
1 | // 暴露(定义)模块:可以直接在写 module.exports 对象上 |
module2.js
1 | // 暴露(定义)模块:也可以直接在写 exports 对象上 |
加载模块
index.js
1 | // 引用模块: 通过模块表示 require 来引用; |
执行模块
最后我们执行 index.js
文件
1 | // 根目录执行 |
执行结果
1 | 模块1 |
浏览器端使用
添加 index.html 文件
src/commonJS/index.html
1 |
|
我们浏览器打开 HTML 文件, 发现报错: Uncaught ReferenceError: require is not defined
。
为什么呢? 这是因为浏览器缺少Node.Js环境的变量:module
,exports
,require
,global
。所以浏览器是无法加载CommonJs模块(npm模块);
所以: npm
模块的都是 JavaScript
语言写的,但是浏览器用不了, 当然是因为不支持 CommonJS
格式;所以想要浏览器能使用上这些模块, 就要得必须转换格式。
目前最常用的 CommonJS
格式转换的工具是:Browserify; 那我们接下来的例子(基于上面的例子)就使用Browserify 来转换的 CommonJS
。
Browserify的作用: 将在HTML 引用的js文件打包编译,是的其能够在浏览器上运行
安装 Browserify
1 | // 全局安装(本例子采用) |
打包编辑
在跟目录执行:
1 | browserify src/commonJS/index.js -o src/commonJS/bundle.js |
引用编译文件
src/commonJS/index2.html
1 |
|
打开 HTML。控制台能成功能成功打印:
1 | 模块1 |
现在最终的文件结构为:
1 | |--src |
AMD
在 上一节CommonJS
中,我们可以看出 CommonJS
的加载是同步的,如:
1 | const module1 = require('./module1'); |
上面的第二行代码必须在 require('./module1')
之后运行,因此是必须要等 module1.js
加载完,也就是说,如果加载的时间很长的, 整个应用是会卡在那里的。
但是在服务器端,所有的模块都是放在本地磁盘,可以同步加载完成,等待的时间就是硬盘的读取时间。但是对于浏览器来说,等待的时间取决于网速的快慢,可能要等待很长的时间,浏览器处于”假死”状态。
所以浏览器端的模块不能采用“同步加载”, 只能采用“异步加载”。这就是AMD
规范产生的背景
基本概念
AMD
-异步模块加载定义,采用异步方式加载模块, 模块的加载不影响后面语句的运行,所有依赖这个模块的语句都定义在一个回调函数中,等到模块完成之后, 这个回调函数才会运行。
定义暴露模块
1 | // 定义一个没有依赖的模块 |
1 | // 定义一个没有依赖的模块 |
引用模块
如何引用AMD
模块呢?这里先讲讲 RequireJS
和 AMD
规范的关系!
我们知道的大名鼎鼎的 ReuireJS
,实际上AMD
就是ReuireJS
在推广的过程中对模块定义的规范化产出;
RequireJS 是一个工具库,主要是用于客户端的模块管理,它可以让客户端的代码分成一个个模块,实现异步加载和动态加载。从而提高代码的性能和可维护性。重要的是它的模块管理遵守 AMD规范
。 如果我们是可以使用RequireJS来加载 AMD模块
先引入require.js
1 | <script src="js/require.js" defer async="true" ></script> |
引入require.js
之后加载我们自己的代码:
1 | <script src="js/require.js" data-main="js/main"></script> |
data-main属性的作用是:指定网页的主模块。
AMD模块的加载(主模块)
1 | require(['module1', 'module2'], function(module1, module2){ |
更详细的引用方式, 我们看下面的例子
例子🌰
在基于上一节CommonJS
的结构目录, 我们添加新的目录和文件
1 | |--src |
定义(暴露)模块
src/AMD/module1.js
1 | // 定义一个没有依赖的模块 |
src/AMD/module2.js
1 | // 加载有依赖的模块 |
引用模块
可以下载 require.js,到本地直接引用, 也可以网上的 [require.js链接]https://requirejs.org/docs/release/2.3.6/minified/require.js)
1 | <script src="https://requirejs.org/docs/release/2.3.6/minified/require.js" data-main="./main.js"></script> |
src/AMD/index.html
1 |
|
主模块main.js 引入我们的module1
和 module2
src/AMD/main.js
1 | require(['./module1', './mosule2'], function(module1, mosule2) { |
结果
打来我们的index,html文件,结果如下:
结果说明:浏览器能使用我们的AMD模块了
CMD
ReuireJS
在声明依赖的模块时会在第一时间加载并且执行模块内部的代码:
1 | require(['./module1', './mosule2'], function(module1, mosule2) { |
而CMD
则是另外一种 js
优化方案, 它与 AMD
很类似,规范也是专门用于浏览器,模块的加载也是异步的, 但是模块使用的时候才会加载执行。AMD
是推崇依赖前置,提前执行,CMD
推崇依赖就近延迟执行。
CMD
是 Sea.js
推广过程中的产生的;
基本概念
定义暴露模块
在CMD
中规范中, 一个模块就是一个文件, 定义暴露模块的格式如下:
1 | define(factory) |
当factory
为函数时,表示是模块的构造方法,执行这个构造方法就可以得到模块向外提供的接口。
factory
方法在执行的时候,默认会传入三个参数:require
,exports
和module
。
require 是一个方法,用来获取其他模块提供的接口
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选注意:
require
是同步往下执行,require.async
则是异步回调执行。require.async
一般用来加载可延迟异步加载的模块。exports 是一个对象, 用来向外部提供模块接口
module 是一个对象,存储了与当前模块相关联的一些属性和方法
1 | // 定义没有依赖的模块 |
1 | // 定义有依赖的模块 |
引用使用模块
使用 require
来引用模块。上面代码也写过。
1 | define(function (require) { |
举例子🌰
CMD
是 Sea.js
推广过程中的产生的;所有结合 Sea.js
来使用。
在原先的项目结构目录上我们加载CMD
目录, 添加文件
1 | |--CMD |
引入Sea.js
- 官网: seajs.org/
- github : github.com/seajs/seajs
在这里下载 然后将dist
目录下面的sea.js
导入项目
1 | |--CMD |
定义暴露模块
module1.js
1 | // 定义没有依赖的模块 |
module2.js
1 | // 定义有依赖的模块 |
module3.js
1 | // 定义没有依赖的模块 |
main.js
1 | // 定义有依赖的模块 |
浏览器使用模块
1 |
|
结果
控制台能成功打印:
1 | name: shuliqi age: 18 |
ES6 模块
在 ES6
之前,最只要是的模块规范是 CommonJS
和AMD
,CommonJS
主要用于服务端,AMD
用于浏览器端;ES6
实现了模块功能,完全可以取代 CommonJS
和AMD
规范。
ES6
模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,输入和输出的变量。而CommonJS
和AMD
都是只能在运行的时候才能确定这些:
1 | // CommonJS |
这代码实质是加载fs
模块(即加载fs
的所有模块)生成一个对象_fs
, 然后再从这个对象上读取 3 个方法。 这种加载就称为”运行时加载“, 因为只有运行了才能得到这个对象,导致完全没办法在编译时做”静态化“
而ES6
模块不是对象,而是通过export
命令显示的指定输出的代码,再通过import
命令输入:
1 | import { stat, exists, readFile } from 'fs'; |
这代码的实质是从fs
模块加载3 个方法,其他方法不加载,这种加载称为”编译时加载“或者”静态加载“,即 ES6可以在编译时就完成模块加载, 效率要比 CommonJS
模块的加载方式高。
基本概念
定义暴露模块
使用 export
命令, export default
命令来暴露对外的接口。其中export default
命令是为模块指定默认输出
使用
export
暴露对外的接口,import
引用的时候需要知道所要加载的变量名/函数名,否则无法加载,为了给用户提供方便,让他们不需要阅读文档就能加载模块,就可以使用export default
命令来为模块指定默认输出。
1 | // 暴露变量 (export 命令) |
1 | // 为模块指定默认输出 |
1 | // 暴露函数或者类 |
引用模块
使用 import
命令来加载模块。但是暴露模块的方式,import
的使用会有所不同
如果是使用
export
命令定义了模块的对外接口1
2
3
4
5// export.js
// 暴露变量 (export 命令)
const name = 'shuliqi';
const age = 18;
export { name, age }1
2// 引用
const { name, age } from './export.js';如果使用
export default
来加载模块1
2
3
4
5// export-default.js
// 为模块指定默认输出
export default function () {
console.log('shuliqi')
}1
2import customName from './export-default.js';
customName()
例子🌰
在原来的项目结构上,加载ES6
文件夹及其一些文件
1 | |--ES6 |
定义暴露模块
module1.js
1 | // 使用 export 暴露模块 |
module2.js
1 | // 使用 export default 为模块指定默认输出 |
引用模块
main.js
1 | import { name, getName } from './module1.js'; |
安装转换器和编译器
由于目前各大浏览器对ES6
的支持大不相同, 所以通常需要把ES6
代码转换成ES5
的代码,这就需要转换器,如现在广泛使用的 Babel 。转换完之后代码需要再编译一下, 就可以在浏览器使用了。
1 | // 安装babel-cli, babel-preset-es2015和browserify |
在跟目录添加 .babelrc
文件
1 | { |
转码我们写的ES6
代码和编译我们的转码之后的代码
1 | // 转码 |
完成之后我们的ES6文件目录为:
1 | |--ES6 |
html 文件使用
最后在我们的html文件中加载我们转码和编译之后的代码 bundle.js
index.html
1 |
|
打开浏览器,结果: 控制台能正确打印出:
1 | shuliqi |
最后
最后文章所有的示例可以在module-example下载
参考文章: