什么是模块化
模块化顾名思义就是模块,其目的就是将程序划分成一个个小的结构; 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;并将暴露导出自己的变量、函数、对象等给其他结构使用。
在阐述如何在Node.js中使用模块化之前,我们先来了解一下普通的JavaScript如何实现模块化。
普通JS实现模块化
这里举个栗子,一共有三个 .js 文件, 需要导入到index.html 中使用;
第一个文件 bar.js
通常我们会使用常见的 立即执行函数(IIFE) 实现块级作用域来模拟模块化
var moduleBar = (function () {
var name = "巫师";
console.log('bar中的name:' + name);
return {
name
}
})()
定义了一个 moduleBar 的变量, 里面封装了一个立即执行函数,并且声明了name与age属性,最后将他们返回
第二个文件 foo.js
var name = "骑士";
console.log('foo中的name:' + name);
立即执行函数,声明一个name变量,值为 "骑士"
第三个文件 baz.js 单独的打印了 name 与 moduleBar.name
console.log(name);
console.log(moduleBar.name);
最后导入index.html,在 index.html 文件中通过 <script>标签 将他们分别引入:
// index.html
<script src="./bar.js"></script>
<script src="./foo.js"></script>
<script src="./baz.js"></script>
最后打印结果为:
`bar.js` bar中的name:巫师
`foo.js` foo中的name:骑士
`baz.js` 骑士
`baz.js` 巫师
由上可见,我相信有写同学已经意识到单独引用的问题了,如果index.html中将baz.js个文件位置换了呢,那 moduleBar是肯定无法访问得到的;
缺点
因此总结了以下3个缺点:
- 必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
- 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
- 在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;
因此,即使实现了模块化,但是过于简单且没有规范,我们需要有一种核心功能包括可以让模块本身可以导出暴露的属性,并且模块又可以导入自己需要的属性的一种规范,即 CommonJs 规范
CommonJS 规范
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS,平时我们也会简称为 CJS;而NodeJS是CommonJS在服务器端一个具有代表性的实现,所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node.js中,每一个 .js 文件就是一个单独的模块
- 这个模块中包括 CommonJS 规范的核心变量:
exports、module.exports、require - 我们可以使用这些变量来方便的进行模块化开发
使用CommonJS规范进行模块化开发
首先我们简单的实现一个导入导出
我们需要两个文件 a.js 与 b.js
-
在 a.js 文件中使用上述所说的
export进行导出指定数据const name = "shrimpsss"; const age = 22; // 使用 exports 将 name 属性与 age 属性暴露出去 exports.name = name; exports.age = age;exports是一个对象,我们可以在这个对象中添加需要导出的属性
-
在 b.js 文件中使用
require来接收数据// 使用 require 导入当前目录下的 a.js 文件中暴露的数据 const data = require('./a.js') data // { name: 'shrimpsss', age: 22 }当然,通常我们使用解构赋值来接收
const { name, age } = require('./a'); name // shrimpsss age // 22 // *注意:属性名要与所接收的属性一致,否则接收不到, 如下 // 错误示范 const { name, ages } = require('./a'); name // shrimpsss ages // undefined
现在我们就简单的了使用CommonJs规范进行模块化开发了
CommonJS规范缺点
但其实CommonJS规范也是有缺点的;
因为CommonJS加载模块是同步的,同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;虽然这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;但是如果在浏览器上就不一样了,因为浏览器加载 js 文件需要先从服务器将文件下载下来之后再加载运行,那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
因为它会将我们的代码转成浏览器可以直接执行的代码等等,所以在浏览器中我们通常不使用CommonJS规范:
在早期为了可以在浏览器中使用模块化,通常会采用 AMD 或 CMD ,而现在主要使用的是ES Modules,简单列一下各自的特性与区别
AMD 规范与 CMD 规范
AMD(Asynchronous Module Definition) 主要是应用于浏览器的一种模块化规范,它采用的是异步加载模块;
AMD实现的比较常用的库是 RequireJS 是一个 JavaScript 文件和模块加载器 和 curl.js 一个小型且非常快速的 AMD 兼容 异步执行器;
CMD(Common Module Definition) 规范也是应用于浏览器的一种模块化规范,它也采用了异步加载模块;
较好的实现方案:Sea.js 追求简单、自然的代码书写和组织方式
感兴趣的话可以自行去了解一下,这里不花太多时间讲述
ES Module规范
ES Modules(ESM)是用于处理模块的ECMAScript标准 ,它和CommonJS的模块化有一些不同之处:
一方面它采用编译期的静态分析,并且也加入了动态引用的方式,另一方面采用ES Module将自动采用严格模式;并且ES Module模块采用 export(将模块内的内容导出)和 import(从其他模块导入内容)关键字来实现模块化
ES Module 与 CommonJS 的区别
环境的区别: CommonJS 模块是 Node.js 专用的,与 ES Module 模块不兼容,但ES Module模块在浏览器和Node.js中都可以用
语法的区别: CommonJS 模块使用 require() 和 module.exports,ES Module模块使用import和export。
文件的区别: 在NodeJS使用模块化,需要将 CommonJS 脚本的后缀名都改成.cjs,ES Module模块采用.mjs后缀文件名或者修改package.json里面的文件,type字段为module或commonjs
导入的区别: CommonJS 模块的 require() 是同步加载模块,而 ES Module 模块的import命令是异步加载,它有一个独立模块依赖的解析阶段
输出的区别:CommonJS 模块输出的是一个值的拷贝,其他模块内部的变化就影响不到这个值;而ES Module模块输出的是值的引用,不会缓存值,所以模块里面的变量绑定其所在的模块
CommonJs 与 ES Module 的加载过程
CommonJS模块加载js文件的过程是运行时加载的,并且是同步的:
同步则意味着: js 引擎在执行 js 代码的过程中加载的模块,并且一个文件没有加载结束之前,后面的代码都不会执行;
ES Module模块加载js文件的过程是编译时加载的,并且是异步的:
异步则意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行;也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行;
即 ES Module可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高;但因为是在编译时完成加载,就导致了没法引用 ES Module 模块本身,因为那时它不是对象,因此它也被称为是静态编译的
并且大多数场景是可以使用ES Module 搭配 CommonJS的 ,因为 ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用,但在Node中却无法使用ES Module
总结
普通情况下建议使用 ES Module 规范进行模块化开发,若是在Node中也可以使用CommonJS规范去进行开发的,但如果你硬要在浏览器中使用 CommonJS 规范的话,可以参考 阮一峰:浏览器加载 CommonJS 模块的原理与实现
最后如果本文对于本文有疑惑,还请指导勘正 (●'◡'●)