JavaScript模块化开发
CommonJS和Node
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
-
Node是CommonJS在服务器端一个具有代表性的实现;
-
Browserify是CommonJS在浏览器中的一种实现;
-
webpack打包工具具备对CommonJS的支持和转换;
所以, Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
-
在Node中每一个js文件都是一个单独的模块;
-
这个模块中包括CommonJS规范的核心变量: exports、 module.exports、 require;
-
我们可以使用这些变量来方便的进行模块化开发;
前面我们提到过模块化的核心是导出和导入, Node中对其进行了实现:
-
exports和module.exports可以负责对模块中的内容进行导出;
-
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;3
exports导出
注意: exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出:
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
另外一个文件中可以导入:
const { name, age, sayHello } = require("./bar");
它们实际上是一个浅层拷贝
为了进一步论证, bar和exports是同一个对象:
-
所以, bar对象是exports对象的浅拷贝(引用赋值);
-
浅拷贝的本质就是一种引用的赋值而已;
module.exports又是什么
我们追根溯源,通过维基百科中对CommonJS规范的解析:
-
CommonJS中是没有module.exports的概念的;
-
但是为了实现模块的导出, Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module;
-
所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
-
因为module才是导出的真正实现者;
node源码里做了module.exports=exports处理
require细节
require(x)几个常见的查找规则
一、 X是一个核心模块,比如path、http
- 直接返回核心模块,停止查找
二、 X是以./或../或/(根目录)开头的
-
第一步:将X当做一个文件在对应的目录下查找;
-
1.如果有后缀名,按照后缀名的格式查找对应的文件
-
2.如果没有后缀名,会按照如下顺序:
-
1> 直接查找文件X
-
2> 查找X.js文件
-
3> 查找X.json文件
-
4> 查找X.node文件
-
-
-
第二步:没有找到对应的文件,将X作为一个目录
-
查找目录下面的index文件
-
1> 查找X/index.js文件
-
2> 查找X/index.json文件
-
3> 查找X/index.node文件
-
-
如果没有找到,那么报错:not found
-
-
情况三:直接是一个X(没有路径),并且X不是一个核心模块
- 会从当前目录逐级往上级查找 node_modules目录
模块的加载过程
-
结论一:模块在被第一次引入时,模块中的js代码会被运行一次
-
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
-
为什么只会加载运行一次呢?
-
这是因为每个模块对象module都有一个属性:loaded。
-
为false表示还没有加载,为true表示已经加载;
-
-
结论三:如果有循环引入,那么加载顺序是什么?
- 如果出现上图模块的引用关系,那么加载顺序是什么呢?
- 这个其实是一种数据结构:图结构;
- 图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
- Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS规范缺点
-
CommonJS加载模块是同步的:
-
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
-
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
-
-
如果将它应用于浏览器呢?
-
浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;
-
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
-
-
所以在浏览器中,我们通常不使用CommonJS规范:
-
当然在webpack中使用CommonJS是另外一回事;
-
因为它会将我们的代码转成浏览器可以直接执行的代码;
-
-
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
-
但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者 ES Module代码的转换;
-
AMD和CMD已经使用非常少了
-
AMD规范
-
AMD主要是应用于浏览器的一种模块化规范:
-
AMD是Asynchronous Module Definition(异步模块定义)的缩写;
-
它采用的是异步加载模块;
-
事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
-
-
我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:
- AMD实现的比较常用的库是require.js和curl.js;
require.js
-
第一步:下载require.js
-
找到其中的require.js文件;
-
第二步:定义HTML的script标签引入require.js和定义入口文件:
- data-main属性的作用是在加载完src的文件后会加载执行该文件
- bar.js
define(function () {
const name = "Jay";
const age = 25;
const sayHello = function (name) {
console.log("你好" + name);
};
return {
name,
age,
sayHello,
};
});
- foo.js
define(["bar"], function (bar) {
console.log(bar.name);
console.log(bar.age);
bar.sayHello("Jay");
});
- index.js
(function () {
require.config({
baseUrl: "",
paths: {
bar: "./modules/bar",
foo: "./modules/foo",
},
});
require(["foo"], function (foo) {});
})();
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./lib/require.js" data-main="./index.js"></script>
</body>
</html>
最终输出:
CMD规范
-
CMD规范也是应用于浏览器的一种模块化规范:
-
CMD 是Common Module Definition(通用模块定义)的缩写;
-
它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
-
但是目前CMD使用也非常少了;
-
-
CMD也有自己比较优秀的实现方案:
- SeaJS
SeaJS的使用
-
第一步:下载SeaJS
-
找到dist文件夹下的sea.js
-
第二步:引入sea.js和使用主入口文件
- seajs是指定主入口文件的
- foo.js
define(function (require, exports, module) {
module.exports = {
name: "Jay",
age: 28,
sayHello: function name(params) {
console.log("你好" + params);
},
};
});
- index.js
define(function (require, exports, module) {
const foo = require("./modules/foo");
console.log(foo.name);
console.log(foo.age);
foo.sayHello("Jay");
});
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./lib/sea.js"></script>
<script>
seajs.use("./index.js");
</script>
</body>
</html>
最终输出:
认识 ES Module
JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等, 所以在ES推出自己的模块化系统时,大家也是兴奋异常。
-
ES Module和CommonJS的模块化有一些不同之处:
-
一方面它使用了import和export关键字;
-
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
-
-
ES Module模块采用export和import关键字来实现模块化:
-
export负责将模块内的内容导出;
-
import负责从其他模块导入内容;
-
-
了解:采用ES Module将自动采用严格模式:use strict
-
如果你不熟悉严格模式可以简单看一下MDN上的解析;
-
export关键字
export关键字将一个模块中的变量、函数、类等导出
-
方式一:在语句声明的前面直接加上export关键字
-
方式二:将所有需要导出的标识符,放到export后面的 {}中
-
注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
-
所以: export {name: name},是错误的写法;
-
-
方式三:导出时给标识符起一个别名
import关键字
导入内容的方式也有多种:
-
方式一:import {标识符列表} from '模块';
- 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
-
方式二:导入时给标识符起别名
-
方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上
default用法
-
还有一种导出叫做默认导出(default export)
-
默认导出export时可以不需要指定名字;
-
在导入时不需要使用 {},并且可以自己来指定名字;
-
它也方便我们和现有的CommonJS等规范相互操作;
-
在一个模块中,只能有一个默认导出(default export)
import函数
-
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if(true){ import { name } from ("./index.js"); }-
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
-
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
-
-
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
-
如果根据不同的条件,动态来选择加载模块的路径;
-
这个时候我们需要使用 import() 函数来动态加载;
-
import("./modules/foo.js).then(aaa=>{
aaa.foo();
})