模块化
js中, 一个文件就是一个模块,模块化可以提高代码利用率而减少重复代码,将相同的逻辑维护在一个模块中,分而治之,提高代码的可维护性;另外,模块化的出现可以在工程中引入第三方模块,极大提高开发效率。
在模块化出现前,只能通过<script>标签在html中导入,并根据依赖控制导入顺序。CommonJS出现后,模块化技术不断发展,目前主流的js模块化规范有CommonJS, AMD/CMD以及ES6等模块系统,按照模块化的发展可排序为: CommonJS -> AMD/CMD -> ES6.
CommonJS用于服务端,AMD/CMD用于浏览器端,ES6是服务端和浏览器端通用的模块解决方案。
1.CommonJS规范
CommonJS规范是NodeJs默认的模块化的规范,适用于服务器端,不适用于浏览器端(理由在后面介绍).
在CommonJS中, 每个js文件就是一个模块,有自己的作用域:文件内定义的变量、函数、类都是私有的,其他模块不可见;可以通过module.exports和require组合的方式实现跨模块访问。
1.1 module对象
js
module.exports = {
// 可以导出内部的变量、方法等
}
module变量是一个对象,表示当前模块;有一个exports属性,表示模块对外输出的值。
module对象除了这个exports属性外,还有以下属性:
[1] id 模块的标识符,带有绝对路径的模块文件名;
[2] filename: 模块的文件名,带有绝对路径;
[3] parent: 返回一个对象,调用该模块的对象;
[4] loaded:boolean,表示该模块是否加载完成;
[5] children:数组,该模块内用到的其他模块;
1.2 全局函数require
一般而言,导出是为了导入,不然导出就没有了意义。CommonJS规范通过require方法进行模块的导入.
js
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
// 引用核心模块时,不需要带路径
var http = require('http');
require是加载模块,并给导入的模块定义名称。
require基于缓存加载模块,所有加载的模块都会缓存在require.cache中,之后再次加载会直接从缓存中读取(每个模块只会被加载一次)。require加载返回的对象为module.exports属性对象。如果被加载的模块没有给exports属性对象赋值,则加载后返回的对象为空对象{}。
1.3 案例
js
// entry.js
var myModule = require("./module.js")
console.log(myModule)
---
// module.js
var num = 1
function addNum(val){
return num + val
}
module.exports = {
num,
addNum
}
运行结果如下:
{num: 1, addNum: ƒ}
表示myModule对象包含一个属性值num=1,和一个addNum函数。
js
// entry.js
var myModule = require("./module.js")
console.log(myModule)
---
// module.js
var num = 1
function addNum(val){
return num + val
}
运行结果如下:
{}
表示myModule对象为空对象。
1.4 特点
CommonJs同步加载模块,即后面的模块需要等待前面的模块加载完成。对于服务器端,模块文件存在与本地文件系统,不存在问题。浏览器需要从服务器下载模块并加载,由于渲染是顺序进行的,当script标签阻塞时浏览器不会继续向下渲染DOM,因此页面会失去响应。因此,浏览器端只能采用异步方式加载模块。
2.AMD
AMD模块规范中,模块的加载是异步的,专用于浏览器端;使用时需要依赖第三方库RequireJs.
2.1 引入主模块
在html中通过<script>标签引入require.js库文件时,通过data-main属性指定主模块的路径:
html
<!--在html中-->
<script data-main="./index.js" src="./js/require.js"></script>
当require.js加载并执行后,会根据data-main属性指定的URL加载并运行主模块(此时为index.js).
2.2 定义模块
AMD使用define定义模块的信息、模块的依赖、函数。
第一个参数为模块的名称(不常使用,一般用后面两个参数),第二个参数为数组类型,依赖的模块,第三个参数为函数(用于返回定义的模块信息)。
js
// 定义没有依赖的模块
define(functioin(){
return 模块对象
})
// 定义有依赖的模块
define(['module1','module2'], function(m1, m2){
// m1对应导入的module1对象, m2对应导入的module2对象
return 模块对象
})
2.3 引入模块
js
// 主模块中用过requirejs.config指定模块的路径
requirejs.config({
baseUrl: 'js/',
paths: {
module1: './modules/module1',
module2: './modules/module2'
}
})
requirejs(['module1'], function (m1) {
// 操作m1模块对象
})
通过requirejs函数引入模块,第一个参数为依赖的模块组数,第二个参数为回调函数。
另外,在主模块中通过requirejs.config指定模块的路径,baseUrl指定一个路径,后续配置路径时以此为相对路径(可选属性;paths中配置各个模块与模块文件路径的映射关系。
2.4 案例
案例使用的文件路径为:
shell
-lib
-require.js #依赖库
-js
-module1.js
-module2.js
-index.html
-index.js
index.js文件
javascript
(function (){
requirejs.config({
paths: {
baseUrl: './js/',
module1: './module1',
module2: './module2'
}
})
requirejs(['module1'], function (m1) {
m1.print1("hello world")
})
})()
module1.js文件
javascript
define(['module2'], function (m2) {
function print1 (param) {
m2.print2(" -m1- "+param);
}
return { print1 }
})
module2.js文件
javascript
define(function () {
function print2 (param) {
console.log(" -m2- "+param)
}
return { print2 }
})
index.html文件
html
<!DOCTYPE html>
<html>
<head>
<script src="./lib/require.js" data-main="./index.js"></script>
</head>
<body>
</body>
</html>
浏览器console运行结果如下:
-m2- -m1- hello world
注意:本地测试------直接通过浏览器访问html文件时,Chrome浏览器会因为跨域报错,可以进行如下参数设置:
shell
--allow-file-access-from-files --user-data-dir="空文件夹" --disable-web-security
3.CMD
CMD与AMD类似使用异步加载模块的机制,作为浏览器端的模块规范,区别在于CMD依赖提前下载,而CMD延迟下载,CMD的实现依赖于SeaJS。
3.1 引入主模块
html
<script src="./lib/sea.js"></script>
<script> seajs.use('./index.js')</script>
3.2 定义和加载模块
使用define定义模块,如下所示:
js
define(function(require, exports, module) {
//...
})
其中:[1] require函数用于加载其他模块; [2] exports对象用于向外提供接口,导出对象或者方法;[3]存储当前模块的属性和方法。
require用法同AMD:
js
define(function(require, exports, module) {
const m1 = require("./module1");
m1.print1("hello world");
})
SeaJs也有use功能(在3.1章节中使用到),在需要先引入锁依赖的模块:
js
seajs.use(['module1', 'module2'], function(m1, m2){
//...
});
3.3 案例
案例使用的文件路径为:
shell
-lib
-sea.js #依赖库
-js
-module1.js
-module2.js
-index.html
-index.js
index.js文件
javascript
define(function(require, exports, module) {
const m1 = require("./js/module1");
m1.print1("hello world");
})
module1.js文件
javascript
define(function(require, exports, module) {
const m2 = require("./module2");
exports.print1 = function(param) {
m2.print2(" -m1- "+param)
}
})
module2.js文件
javascript
define(function(require, exports, module) {
exports.print2 = function(param) {
console.log(" -m2- "+param)
}
})
index.html文件
html
<!DOCTYPE html>
<html>
<head>
<script src="./lib/sea.js"></script>
<script> seajs.use('./index.js')</script>
</head>
<body>
</body>
</html>
浏览器console运行结果如下:
-m2- -m1- hello world
4.ES6
ES6使用export和import关键词进行模块的导入和导出。
4.1 export和import
可以使用export指定本模块需要导出的对象(变量、方法、类等信息);在其他模块中通过import导入。
export:
export有两种使用方式,推荐 export { }形式。
javascript
// module1.js
export var test1 = "1";
export var fun1 = function() {
console.log("hello world")
}
等价于:
javascript
// module1.js
var test1 = "1";
var fun1 = function(){
console.log("hello world");
}
export {
test1,
fun1
}
另外,导出时也可以执行别名, 如下所示:
javascript
// ...
export {
test1,
fun1 as fun1,
fun1 as fun2
}
import:
使用import {...} from 模块
形式或使用import * as 别名 from 模块
形式导入模块。
javascript
// index.js
import {test1, fun1} from "./module1";
console.log(test1); //1
fun1(); // hello world
或者:
javascript
// index.js
import * as m1 from "./module1";
console.log(m1.test1); //1
m1.fun1(); // hello world
导入时也可以指定别名:
javascript
import {test1, fun1 ad fun} from "./module1";
注意:使用 import{}和import * 不包括export default导出的对象。
4.2 export default和import
一个模块可以使用export default导出一个对象(变量、函数、类),import导入deault时不需要使用大括号。
在module1.js中使用export default导出一个默认输出:
javascript
// module1.js
export default function test1(){
console.log("hello world")
}
在index.js中使用import导入:
javascript
// index.js
import test from "module1";
test();
导入默认导出项时,需要指定别名,此时为test.