前端模块化
# 前端模块化
JavaScript 在早期的设计中就没有模块、包、类的概念,开发者需要模拟出类似的功能,来隔离、组织复杂的 JavaScript 代码,我们称为模块化。
模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块。
# 模块化优点
避免命名冲突(减少命名空间污染)
更好的分离, 按需加载
提高代码复用率
提高了可维护性
# 模块化规范
常见的的 JavaScript 模块规范有:CommonJS、AMD、CMD、UMD、原生模块化,ES6 模块化
# CommonJS
CommonJs 是服务器端模块的规范,Node.js 采用了这个规范。
根据 CommonJS 规范,一个单独的文件就是一个模块。加载模块使用 require 方法,该方法读取一个文件并执行,最后返回文件内部的 exports 对象。
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像 Node.js 主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以 CommonJS 规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
Commonjs 循环引用问题
如何解决循环加载的原理:循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行,然后在内存中生成该模块的一个说明对象。当再次执行 require 命令,下次会直接读取缓存。
一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出(解决原理)
# AMD
AMD:(Asynchromous Module Definition) 异步模块定义
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
AMD 异步加载模块。它的模块支持对象 函数 构造器 字符串 JSON 等各种类型的模块。
AMD 推崇依赖前置
对于依赖的模块,AMD 是提前执行
# CMD
CMD(Common Module Definition)公共模块定义
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出
CMD 推崇依赖就近
对于依赖的模块,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)
# UMD
UMD (Universal Module Definition) 通用模块定义
UMD 是 AMD 和 CommonJS 的综合产物。
AMD 浏览器第一的原则发展 异步加载模块。
CommonJS 模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式 UMD (Universal Module Definition)。希望解决跨平台的解决方案。
UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。
在判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。
# ES6 模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。而 CommonJS 和 CMD,都只能在运行时确定依赖。
可以进行可靠的静态分析,进而进行 tree-shaking
ES6 模块化特点:
只能作为模块顶层的语句出现
import 的模块名只能是字符串常量
import binding 是 immutable 的
ES6 模块化 循环引用问题
不论是基本数据类型还是复杂数据类型。当模块遇到 import 命令时,就会生成一个只读引用,脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
循环加载时,ES6 模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行(解决原理)
# 原生 JS 模块化(Native JS)
上述的模块都不是原生 JavaScript 模块。它们只不过是我们用模块模式(module pattern)、CommonJS 或 AMD 模仿的模块系统。
JavaScript 标准制定者在 TC39(该标准定义了 ECMAScript 的语法与语义)已经为 ECMAScript 6(ES6)引入内置的模块系统了。
相对于 CommonJS 或 AMD,ES6 模块如何设法提供两全其美的实现方案:简洁紧凑的声明式语法和异步加载,另外能更好地支持循环依赖。
# 模块化对比
# AMD 和 CMD 的区别
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
AMD 推崇依赖前置。 CMD 推崇依赖就近,
对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)
AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹
# ES6 模块与 CommonJS 模块的差异
- 导入导出不同
① CommonJS 使用 require ,exports 或者 module.exports
② ES6 使用 import, export
- 输出不同
① CommonJS 模块输出的是一个值的拷贝
对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
② ES6 模块输出的是值的引用,ES6 模块中的值属于【动态只读引用】
对于只读来说,即不允许修改引入变量的值,import 的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
对于动态来说,原始值发生变化,import 加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
- 加载时机不同
① CommonJS 模块是运行时加载,CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。
② ES6 模块是编译时加载(静态加载)。 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
参考:https://www.cnblogs.com/unclekeith/archive/2017/10/17/7679503.html (opens new window)
# RequireJS 和 SeaJS 区别
两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端
两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者 API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。
两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS 无这方面的支持。
两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致:开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。
# 模块化导入
# require
require 前端和后端 node 都可使用,用于引入模块、 JSON、或本地文件。
require 是 CommonJs,AMD,CMD 规范中定义的模块请求方式
require 是同步请求,require 支持条件导入、动态导入等
require 模块查找机制
require 判断加载标识,优先加载缓存的模块 (加载过的文件会缓存起来,不会重复加载)
如果是加载核心模块,直接从内存中加载,并缓存
如果是相对路径,则根据路径加载自定义模块(不存在对应的文件,就将这个路径作为文件夹加载),并缓存
如果不是自定义模块,也不是核心模块,则加载第三方模块
(1)node 会去本级 node_modules 目录中找 xxx.js---->xxx.json ----> xxx.node(找到一个即返回),找到并缓存。
(2)如果找不到,node 则取上一级目录下找 node_modules/xxx 目录,规则同上
(3)如果一直找到代码文件的文件系统的根目录还找不到,则报错:模块没有找到。
第三方模块不会和核心模块加载冲突?不可能有第三方模块名会和核心模块名一样(规定)
require 避免重复加载
require 会接收一个参数——文件标识符,然后分析定位文件,接下来会从 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容。如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。借此完成模块加载流程。
require 避免循环引用
如何解决循环加载的原理:循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行,然后在内存中生成该模块的一个说明对象。当再次执行 require 命令,下次会直接读取缓存。
一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出(解决原理)
require 主要支持三种文件类型
- .js:.js 文件是我们最常用的文件类型,加载的时候会先运行整个 JS 文件,然后将前面说的 module.exports 作为 require 的返回值。
- .json:.json 文件是一个普通的文本文件,直接用 JSON.parse 将其转化为对象返回就行。
- .node:.node 文件是 C++编译后的二进制文件,纯前端一般很少接触这个类型。
require 加载文件夹
加载文件夹的顺序:
先看看这个文件夹下面有没有 package.json,如果有就找里面的 main 字段,main 字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他 package.json 里面的 main 字段吧,比如 jquery 的 main 字段就是这样:"main": "dist/jquery.js"。
如果没有 package.json 或者 package.json 里面没有 main 就找 index 文件。index.js / index.json / index.node
# import
import 是 es6 的一个语法标准,如果要兼容浏览器的话必须转化成 es5 的语法
注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行。
由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
# import 和 require 的区别
- 遵循规范
require 是 AMD 规范引入方式
import 是 es6 的一个语法标准,如果要兼容浏览器的话必须转化成 es5 的语法
- 调用时间
require 是运行时调用,所以 require 理论上可以运用在代码的任何地方
import 是编译时调用,所以必须放在文件开头
- 本质
require 是赋值过程,其实 require 的结果就是对象、数字、字符串、函数等,再把 require 的结果赋值给某个变量
import 是解构过程,但是目前所有的引擎都还没有实现 import,我们在 node 中使用 babel 支持 ES6,也仅仅是将 ES6 转码为 ES5 再执行,import 语法会被转码为 require
# import()和 import
import()是动态加载。
import()返回一个 Promise 对象,import()加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数。
import()类似于 Node 的 require 方法,区别主要是前者是异步加载,后者是同步加载
import()通常用于按需加载、条件加载、动态的模块路径
# 模块化导出
# modules.export
nodeJS 采用 commonJs 规范,当前文件是一个模块(module)私有域,通过 exports 属性导出,通过 require()引入模块,通过.xx 去获取值,从而了解到加载某个模块,其实是加载该模块的 exports 属性
Node 内部提供一个 Module 构建函数。所有模块都是 Module 的实例
modules.export 给 module 实例中的 exports 对象添加的属性或者方法;
module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports 变量。
// 导出:example.js
module.exports.a = "111";
module.exports.fun = function() {};
// 导出:或者写成对象形式
module.exports = {
a: "111",
fun: function() {},
};
// 导入
let test = require("./example.js");
console.log(test.a);
console.log(test.fun);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# export
export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
// 引入模块的某些变量(方法、类),配合4、5使用
import { xxx, xxxx } from "xxx";
// 输出模块的变量(方法、类),对应引入方法为1
let xxx = "x";
export { xxx };
2
3
4
5
# export default
// 默认输出一个函数
export default function() {
console.log("foo");
}
// 引用并指定名字
import customName from "./export-default";
2
3
4
5
6
# export 和 module.export 的区别
exports 是 module.exports 的引用
exports:
直接打印 exports 是个空对象,且不会报错
因为内部执行了 var module = new Module(); var exports = module.exports
# export default 和 export 的区别
在一个文件或模块中,export、import 可以有多个,export default 仅有一个
通过 export 方式导出,在导入时要加{ },export default 则不需要
共同点:export 与 export default 均可用于导出常量、函数、文件、模块