早期的 JavaScript 作为一种脚本语言,其产生的目的只是为了简单的表单验证或动画实现。
并且不需要分离多个 js 文件来写,功能相对简单。只需要内嵌一个 script 标签即可。
ajax 的出现,前后端也分离,前端更新数据接口。
SPA 的出现,前端页面复杂化,前端路由、状态管理。
Node 的出现, JavaScript 编写复杂的后端程序。模块化的需要日趋明显。
在 JavaScript 还没有推出模块化的解决方案之前,社区涌现出许多的模块化规范。如:AMD、CMD、CommonJS 等。
没有模块化带来的很多问题, 比如命名冲突的问题。
1. 命名冲突问题。
A 同事的代码:
const name = 'A 同事';
B 同事的代码(是不知道 A 同事内部代码的):
console.log(name); // 'A 同事'
const name = 'B 同事'; // 报错
我们可以看出来,因为 B 同事并不知道 A 同事代码中具体使用了什么变量,而同样创建一个 name 属性,将会造成命名冲突。
2. 不利于开发大型项目。
面对成千上百万行代码,没有模块化,将会产生很大的危害,大家都只是在一个作用域中进行操作。
3. 维护成本高。
早期是没有模块作用域的,只能利用函数作用域,来使得每一个 js 文件都看作为一个模块的思想。
foo.js
(function () {
var name = 'A 同事';
return {
name
}
})()
bar.js
(function () {
var name = 'B 同事';
return {
name
}
})();
必须记住每一个模块中返回对象的命名,才能够再其他模块中使用正确。
并且写起来混乱,都得包裹在 IIFE 中。
在没有合适的规范下,每个人、公司的 的 模块名字可能出现相同的情况。
虽然 IIFE 简单的实现了模块化,但却是没有规则可言的。
我们真正需要的是指定一定的规范来约束每个人都按照这个规范去编写模块化的代码。
模块本身可以导出暴漏出去的东西,也可以导入自己需要的属性方法。
js 社区为了解决上述问题,涌现出一系列好用的规范。
CommonJS 是一个规范,最初提出来实在浏览器以外的地方使用,并且当时被命名为 ServerJS。我们从名字就可以直观的看出来,一开始诞生的目的就是用在服务器上。
后来为了体现广泛性,修改为 CommonJS ,也叫做 CJS。
所谓规范,就是制定了一系列的规则,但却偏于理论方面。CommonJS 在服务器端的一个具有代表性的实现就是 Node.Js 。
Node 中对 CommonJS 进行了支持和实现,让我们在开发 node 过程中可以方便的进行模块化开发。
每一个 js 文件可以看作是一个模块,在这个模块中包含 CommonJS 规范的核心变量。也是 Node 中的特殊全局对象(模块变量)。exports 、module.exports 、require
exports 和 module.exports 可以负责模块中的内容进行导出。
require 函数可以帮助我们 导入其他的模块 (自定义模块、系统模块、第三方模块)中的内容。
foo.js 文件
const name = 'A 同事';
// 导出 name 变量
exports.name = name;
index.js 文件
// 导入 foo.js 的 name
const { name } = require('foo.js');
name; // 'A 同事'
exports 是一个模块全局对象,既然是对象,我们只需要给对象添加属性就可以挂载属性,从而导出变量。
require('引入对应的foo.js 文件'),就可以配合解构赋值引入 name 属性。
require 是一个函数,可以帮助我们导入一个文件(模块)中导入的对象。
1. 情况一:
导入的是一个核心模块,比如 http path,直接返回核心模块,并且停止查找。
2. 情况二:是以 ./ 或 ../ 或 / 根目录开头的
将它当作一个文件在对应的目录下查找。如果有后缀名,按照后缀名的格式查找对应的文件
如果没有后缀名,会按照顺序:
a. 直接查找文件。
b. .js 文件
c. .json 文件
d. 查找 .node 文件
没有找到对应的文件,就将它当作为一个目录: 查找文件下面的 index 文件
a. index.js 文件
b. index.json 文件
c. index.node 文件
d. 没有就报错 not found
3. 情况三: 不是一个路径,也不是核心模块。 require('hello')
a. Users/xxx/Desktop/learn/node_modules
b. /Users/xxx/Desktop/node_modules
c. /Users/xxx/node_modules
d. /Users/node_modules
e. /node_modules
f. 没有找到,就报错 not found
并且这个过程是同步加载的,所以需要等到 require 加载完之后才能执行后面的代码。
既然是同步加载的,就会引发下列的问题。
1. 当同一个模块被引入多次,会加载几次?
index.js 文件
require('foo.js');
require('bar.js');
bar.js 文件
require('foo.js');
执行 index.js 文件,相当于会引入两次 foo.js 文件,但是并不是会加载两次 foo.js 文件,而是将第一次加载的 foo.js 文件进行了缓存,在此加载时,将不会在加载 foo.js 文件。
Node 是如何做到的,在加载模块的时候,每一个模块上会有一个 loaded 属性,代表是否已经加载过此模块。false 表示没有加载, true 代表已经加载过。
这么做有什么好处呢?
1. 可以避免重新加载同一个模块,可以节约资源。
2. 如果存在循环引用,不会造成卡死。加载过后,即使还存在引用关系,但也不会加载。
index.js 文件
require('a.js');
require('b.js');
a.js 文件
require('c.js');
c.js 文件
require('e.js');
e.js 文件
require('b'.js);
b.js 文件
require('e.js');
我们可以看出来,上面 index.js 开始,造成一个闭环。为一个图结构。
Node 采用的是深度优先算法。所以加载顺序为 index -> a -> c -> d -> e -> b,index -> b ,所以在 执行完 e -> b ,开始执行 require('b.js') ,因为 b.js 文件已经加载过,loaded 为 true,所以就不会继续加载。
exports 并不是 Node 自身的,而是 commonJS 的规范中要求必须有一个 exports 作为导出,所以 Node 为了迎合 commonJS ,所以才新增了一个 exports 。
foo.js 文件
const name = 'heihei';
const age = 21;
const sayHello = function () {
console.log('hello ', name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
index.js 文件
const foo = require('foo.js'); // {name: 'heihei', age: 21, sayHello: Function}
既然 module.exports 和 exports 都能够用于导出,那么二者的区别是什么?
按照上面的代码,我们来画一个图。
在 Node 中在是将 module.exports = exports 的,所以,二者属于同一个对象的引用。指向一块地址 0x100。
我们可以看出来这个时候 exports 导出的话,是在堆内存中存开辟了一块 0x100 的地址,exports 导出的对象指向 0x100 这块地址,index.js 文件在 require 导入的时候,同样引用了 0x100 这块地址。
如果是 module.exports 导出呢?因为 module.exports 是赋值给一个新的对象。
foo.js 文件
const name = 'heihei';
const age = 21;
const sayHello = function () {
console.log('hello ', name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
module.exports = {
name: 'heihei2',
age: 18,
sayHello
}
index.js 文件
const foo = require('foo.js'); // {name: 'heihei2', age: 18, sayHello: Function}
所以 index.js 导入的 foo 对象是引用了 0x200 这个地址,但是 exports 还是引用的 0x100 地址。这个时候 exports 就不会对当前的改动。
Node 中导出是依赖着 module.exports 为导出的,exports 只是为了迎合 CommonJs ,才让 module.exports = exports 的,如果 module.exports 指向一个新的对象,将断开于 exports 的连接,所以最终导出的还是 module.exports 来控制的。
1. 同步加载模块,只有等到对应的模块加载完毕,当前模块中的内容才能被运行
2. 服务器不会有什么问题,服务器加载的js文件都是本地文件,加载速度非常快。
3. 浏览器加载 js 文件需要先从服务器将文件下载下来,之后在加载运行,那么采用同步的意味着后续的 js 代码都无法正常运行,即使是一些简单的 DOM 操作。
早期的话,会使用 AMD 和 CMD 来解决。
AMD 是 异步模块定义。规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用。具体的实现的库是 require.js 和 curl.js 。
运用于浏览器的模块化规范,采用异步加载。
具体实现:
index.html
// 引入 request.js 库 并且定义主入口 data-main
index.js 文件
(function () {
// 配置,不是在加载模块阶段
require.config({
// 基础路径
baseUrl: '',
paths: {
// 读取的时候,会自动加上后缀名
// 两个模块
'foo': './modules/foo',
'bar': './modules/bar'
}
})
// 加载 foo 模块
require(['bar'], function (bar) {});
})();
foo.js 文件
define(function () {
const name = 'heihei';
const age = 19;
const sayHello = function () {
console.log('你好 ' + name);
}
// 导出
return {
name,
age,
sayHello
}
});
bar.js 文件
// 导入使用 需要依赖一些东西
define([
'foo'
], function(foo) {
// bar 就是依赖的模块
console.log(foo.name);
console.log(foo.age);
foo.sayHello('kobe');
});
我们可以看出来,主要还是运用的是函数作用域来解决的模块化,使用回调来异步模块。
CMD (通用模块定义) ,CMD 相对于使用的更少了,也是用来实现浏览器的异步模块。而且 CMD 吸收了 CommonJS 的优点。
index.html
// 核心代码 主入口 index.js
index.js 文件
define(function (require, exports, module) {
const foo = require('./modules/foo');
console.log(foo.name);
console.log(foo.age);
foo.sayHello();
});
foo.js 文件
define(function (require, exports, module) {
// 导出东西
const name = 'heihei';
const age = 18;
const sayHello = function (name) {
console.log('你好 ' + name);
}
// 导出模块
module.exports = {
name,
age,
sayHello
}
});
CMD 也是使用 module.exports 、exports、require 来实现模块导入导出的。
虽然社区涌现出不少的模块规范,但是 JavaScript 一直没有模块化是一大痛点。
ES Module 使用 import 和 export 关键字。它采用编译期的静态分析,并且也加入了动态引用的方式。
使用 ES Module 将会自动开启严格模式。
导入方式
导出一:
export const name = 'heihei';
export const age = 21;
export const sayHello = function () {
console.log('hello ', name);
}
export 作为关键字,只需要在导出的语句前加上即可导出。
导出二:
const name = 'heihei';
const age = 21;
const sayHello = function () {
console.log('hello ', name);
}
export {
name,
age,
sayHello
}
注意:export 后面的 {} 并不是对象,只能算是一个导出列表,并且是不能够写入对象形式的。
导出三:
const name = 'heihei';
const age = 21;
const sayHello = function () {
console.log('hello ', name);
}
export {
name as fName,
age as fAge,
sayHello as fSayHello
}
可以采用别名的方式,最终导出的是 fName、fAge、fSayHello
导出四:
export default function () {
console.log('默认导出');
}
这种导出是不需要制定名字的,我们在导入的时候也不需要 {} 来制定名字。
导入方式
导入一:
import {fName, fAge, fSayHello} from 'foo.js'
导入二:
import {fName as mName, fAge mAge, fSayHello as mSayHello} from 'foo.js'
导入三:使用 * as 通配,放到统一的对象
import foo from '.foo.js';
export 和 import 综合使用。
export { name, age, sayHello } from './foo.js';
export {age} from './bar.js'
export 和 from 结合使用,我们可以当作导入也是导出。当我们开发一个功能库的时候,通常我们需要将希望暴漏的所有接口放到一个文件中。这样方便制定统一的接口规范。
我们来画一个图。
es module 有一个模块环境记录,用来保存导出文件的环境变量,会在模块记录中创建 const 常量,当我们在 foo.js 文件中修改 foo.js 文件的变量值,在模块环境记录中会重新创建一个新的常量。在 import 文件中只能用来读最新的值,不可以修改。
在 es module 中采用编译期的静态分析,并且也加入了动态引用的方式。
所以,在解析阶段是会分析 import 关键字的,从而找出依赖,但是 import 加载时不能放在代码逻辑中的。
let flag = true;
if (flag) {
import foo from './foo.js';
}
ES Module 在 被 js引擎解析的时候就要知道依赖关系。和 CommonJS 不一样,require 是一个函数,在运行阶段才会加载依赖。可以使用 import() 函数来动态的来加载某一个模块。
let flag = true;
if (flag) {
import('./foo.js') // 异步加载
.then(res => {
console.log(res.name);
}).catch(err => {
console.log('err', err);
});
}
node环境下如何加载 es 模块
通常情况下, CommonJS 不能加载 ES Module.
因为 CommonJS 是同步加载的,但是 ES Module 必须要经过静态分析。无法在这个时候执行 javaScript 代码。
但是这个并非绝对性,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持。
Node 当中是不支持的。
ES Module 来加载 CommonJS.
ES Module 在加载 CommonJS 时,会将 module.exports 导出的内容作为 default 导出来方式来使用。