文章摘要:
- 一、概念
- 二、作用
- 三、历史
- 1.全局函数
- 2.命名空间
- 3.巧用闭包
- 4.现代模块化机制
- 四、现代模块化方案
- 1.要解决的问题
- 规避全局污染,变量冲突
- 高内聚
- 模块依赖已经循环引用等边界问题
- 2.常用模块化方案
- commonJS
- 典型实践:nodeJS
- ESModule
- 典型实践:ES6
- commonJS
- 3.commonJS 和 ESModule 差别
- 4.commonJS 和 ESModule 加载机制
- 5.script标签接轨模块化
- 1.要解决的问题
一、概念
本质上模块就是一种对外要提供通信接口,对代码进行切分/组合的管理方式。
二、作用
- 把复杂功能进行拆分(关注点分离)
- 大型软件开发的思想基础,更好的内聚功能、方便复用
- 方便多人协同、专注过程开发即可
三、历史
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能。
发展阶段
- 起初开发,只是一堆函数的堆砌,组织代码靠经验;
- 后来挂载到对象身上,起到分离和内聚的作用。这种方式学名:命名空间;
- 后来利用闭包使得污染的问题得到解决,内聚的更加纯粹;
- 但是上边的都解决不了模块间依赖的问题,于是有了现代模块化机制。
四、现代模块化方案
js这门语言没支持模块化之前,社区制定了一些模块加载方案: CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。现如今 ES6 在语言层面上,实现了模块功能,而且书写简单,完全可替代前两种规范,成为浏览器和服务器通用的模块化解决方案,进而也就取代了UMD。
1.要解决的问题
- 规避全局污染,变量冲突
- 高内聚
- 如何引入其他模块,如何暴露出api给其他模块使用
- 模块间依赖以及可能出现的循环引用等边界问题
2.常用模块化方案
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS,CommonJS 的目标是为浏览器之外的 JavaScript 指定一个生态系统。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export。
它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
2.1 commonJS
是一种思想、经典践行者:nodeJS
文件是一个模块,私有。内置两个变量 module 和 require (exports = module.exports)
一个引入一个导出,就构成了通信的基本结构
使用
m1.js文件:
导出某个方法
exports.fn1 = function(){}
导出一个类、对象
module.exports = Class1
module.exports = {fn1:function(){}}
导入模块
const aModule = require('./m1.js')
注意
exports和module.exports的关系 exports 是 module.exports 的引用! module.exports指向堆的哪一块区域,则 exports 也跟着指向哪里!
console.log(module.exports === exports);//true
true其实就说明它们就是一个东西,其实exports = module.exports,因为他们是引用类型的一个变量名,所以当exports再指向一个新的引用类型的时候,那么他们就不再相等!
exports = [0, 1];
console.log(exports === module.exports);//false
所以就容易理解:
1.exports不进行赋值操作。(赋值后,也就斩断了和module.exports的链接,指向发生改变。)
2.exports常用来挂载一个属性。(方法或是值),exports.name = "utils"。
3.赋值的行为一般都是给module.exports去完成,然后再exports = module.exports
- 当nodejs执行模块中的代码时,它会将模块中的代码,用一个函数进行包裹:
function (exports, require, module, __filename, __dirname) {
// 所以文件中可以直接使用,不报错!
}
2.2 ESModule
原生js也开始对模块化进行官方支撑。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块间的依赖关系,以及输入和输出的变量。
export 命令用于对外提供功能出口。 import 命令用于对内引入其他模块功能的入口。
使用
m1.js文件
导出某个方法
export function fn1(){}
//or
export const fn1 =()=>{}
//or
const fn1=()=>{}
export {fn1}//export fn1 是错误的
对应的导入写法
import {fn1} from './m1.js'//from指定模块文件的位置
关键字 import 导入模块(文件)有2种形式:
import ‘模块名称’ from ‘路径’;
import ‘路径’;//此方式一般用来引入样式文件或预处理文件,因为他们不需要用变量来接收。
模块名称:等同于定义一个变量,用来存即将导入的模块。
路径:可以是绝对路径,也可以是相对路径,甚至是模块名称,文件后缀也可以省略。当你使用该命令时,系统会自动从配置文件中指定的路径中去寻找并加载正确的文件。
import Vue from "vue";//实际解析是 "../node_modules/vue/dist/vue.js";
JS文件可以向外输出变量、函数和对象。导出的含义是向外暴露,暴露多个需要{}包裹,暴露一个不用。 指定模块默认的输出
ps:一个模块仅允许导出一个default对象
export default function fnx() {}
//or
export default{fnx:()=>{}}
则对应的导入则省去 {};同时随便取个名字
import anyName from './m1.js'
//等价写法
import {default as anyName } from './m1.js'
两种导出都存在一个文件中时,对应的导入写法:
import anyName, {fn1} from './m1';
与 export 相比,export default 有以下几点不同:
- 在同一个模块中,export default 只允许向外暴露一次成员;
- 这种方式可以使用任意的名称接收,不像 export 那样有严格的要求;
- export 和 export default 可以在同一模块中同时存在。
注意
- 由于
import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'm1';
// 报错
if (x === 1) {
import { foo } from 'm1';
} else {
import { foo } from 'm2';
}
2.区分import()和import
import()和import主要区别为前者是动态加载,后者静态执行。
import()返回一个Promise对象
import()主要解决:按需加载、条件加载、动态的模块路径
import('./m1.js')
.then(({fn1, fn2}) => {
// ...
});
3. commonJS 和 ESModule 差别
三大差异
-
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- 二者运行机制不一样导致的!
- CommonJS 模块输出的是值的拷贝(给缓存起来了),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
-
CommonJS 模块的
require()是同步(阻塞式)加载模块,ES6 模块的import命令是异步(非阻塞式)加载,有一个独立的模块依赖的解析阶段。 -
CommonJS 模块的顶层
this指向当前模块,ES6 模块之中,顶层的this指向undefined;
第二个差异是因为 CommonJS 加载的是一个实打实的对象实体,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,所以导入的只是一种引入关系而已,在代码静态解析阶段就会生成!
4.commonJS 和 ESModule 加载机制
commonJS
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
ES6 模块
ES6 模块是动态引用,如果使用import从一个模块加载变量(即import fn1 from './m1.js'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
5.script标签接轨模块化
意义
普通的script标签都是全局的,没有隔离的效果。支持ES6模块化后,意味着:这些script标签都是彼此独立的js模块。
实现
把script标签的type属性设为module,浏览器就知道这是一个 ES6 模块。
<script type="module" src="./m1.js"></script>
浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module">
import fn1 from "./m1.js";
//todo something
</script>
参考文章: Module 的语法 Module 的加载实现