你知道JS模块化吗?你对模块化是怎么理解的呢?为什么需要模块化?开发中如何构建模块化?require,import 的区别和关系是什么?commonjs,amd,cmd 又是什么呢?如果你概念不是很清楚,那就一起来学习下吧!
模块化:
百度百科定义:是指解决 一个复杂问题 时自顶向下 【逐层把系统划分】成若干模块的过程,有多种属性,分别反映其内部特性。
对模块化的理解:
简单来说就是---为了更好的管理,把复杂的系统分解。
再说的具体点就是----实现一个功能的一系列的【相关状态数据方法等】在一个单独的文件里
为什么需要模块化?
最开始的时候,js是没有模块化的,就是我们知道的<script></script>
标签,但随着网络的发达,用户需求的增长,前端需要做的事情越来越多,程序也随之越来越复杂。复杂随之而来的就是管理复杂。所以为了更好地管理复杂的系统,也为了更好的复用,模块化也就变得越来越重要了。
模块化优点:
- 更好维护
- 复用性更好
- 更好扩展
- 管理依赖
模块化的三个阶段:
阶段一:基于文件划分
这个时期,约定一个文件就是一个模块
缺点:
- 命名容易冲突,不利于多人开发
- 污染全局作用域
- 无法管理模块与模块之间的依赖关系
- index.html中 利用script 引入模块化js文件
html
<!DOCTYPE html>
<html lang="">
<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>模块化</title>
</head>
<body>
<div id="app"></div>
</body>
// 一个script就对应一个模块
<script src="./index.js"></script>
<script>
// 调用模块中的变量,容易造成命名冲突,变量污染
console.log("当前时间", currentTime)
</script>
</html>
- 提取相关功能到独立的index.js文件中
js
const currentTime = new Date().getTime();
const getDate = ()=>{
}
阶段二:命名空间方式
约定每个模块只暴露一个全局的对象,把一系列方法属性,都挂在一个对象上。
缺点:
- 依然没有私有空间,外部可以随意修改内部的方法和属性
- 无法管理模块间依赖关系
js
const module = {
time: 1,
getTime: function(){
return new Date().getTime()
}
}
阶段三:IIFE 立即执行函数
由于函数拥有独立的作用域,所以可以通过闭包来实现 【模块私有作用域 】,对于外部需要的可挂载在window上
优点:
- 拥有独立的作用域
- 不会污染全局作用域
应用:
- jquery框架 就用的这种方式
js
(function() {
// 拥有独立作用域
})()
js
// IIFE module
const module = ((agr1,agr2) => {
let count = agr1;
return {
increase: () => {
count ++;
console.log(count);
},
init: () => {
count = 0;
console.log(count);
}
}
})(5,6);
// 调用
module.increase();
以上三个阶段是早期没有工具和规范的情况下的方案,所以后期出现了比较成熟的模块化加载方案。
模块化规范 :
现在有四种比较成熟的模块加载方案:
👍🏻 值拷贝:
一个值一旦输出,模块内部的改变就影响不到这个值了
1. commonJS
- 引入方式:
require
- 输出方式:
exports.xx
- commonjs2 :多了
module.exports = {xx1,}
- commonjs2 :多了
- 拷贝方式:
值拷贝
- 加载方式:
同步
- 加载时机:
运行时加载
- 应用:
node.js
使用的就是commonjs 规范
1.1 require原理:
💻 require原理:
- 是通过动态创建script脚本异步引入模块的,
- 然后对每个脚本的load事件进行监听,
- 如果每个脚本都加载完成了, 再调用回调函数
require的核心概念:
- 在导出 的文件中:定义
module.exports
,导出的对象类型不予限定(可为任意类型)。 - 在导入 的文件中:
使用require()引入
即可使用。
本质上,是将要导出的对象,赋值给module这个对象的exports属性,在其他文件中通过require这个方法来访问exports这个属性。
如:require(./a.js) = exports
1.2 为什么commonjs 规范不适合浏览器端?
commonjs规范多用于服务端, 因为 nodejs 这种主要用于服务器的编程,加载的模块文件一般都已经存在在本地硬盘 ,所以加载起来比较快 ,所以同步就可以了,不用考虑异步加载方式。
而浏览器主要是通过网络请求等方式,所以使用异步更合适。所以出现了更适合的AMD、CMD异步规范。👇🏻
2. AMD方案
依赖前置
:声明的时候就依赖,2.0后也可以延迟执行- 加载方式:
异步
- 应用:
require.js
js
// 定义:依赖需要提前写好
define('myModule', ['moduleA', 'moduleB'], (moduleA, moduleB) => {
// 使用moduleA.xxx
// 使用moduleB.xxx
})
// 使用
require(['myModule'], (myModule) => {
// 使用myModule
})
js
// ===AMD兼容已有代码=======》
define('module', [], () => {
var moduleA = require(./moduleA);
var moduleB = require(./moduleB);
const func1() {
moduleA.xxx;
}
const func2() {
ttt;
moduleB.xxx;
moduleA.xxx;
}
return { func1, func2 };
})
// ===End==============
2.1 UMD规范
- umd 是AMD和commonjs 规范的糅合
- 可以自己写判断,nodejs环境使用commonjs规范,浏览器使用amd规范
3. CMD方案
- 加载方式:
异步
- 应用:
sea.js
- 特点:
按需加载,就近原则
- 缺点:
依赖打包
,打包的时候才去判断是否需要按需加载- 加载逻辑存在所有模块,
打包体积变大
js
define('module',(require, exports, module)=>{
let a = require(a);
let b = require(b);
})
4. ES6方案提出的方案ESM规范: import 和export
编译时确定依赖关系
,静态加载(commonjs和amd 是运行时确定)- 自动使用严格模式
- es6模块本身无法被引用
- 顶层this指向undefined,
所以不应该在顶层代码中使用this
值引用
:可以实时
取得模块内部的值,commonjs 是值的拷贝,一旦输出,不会改变
import:本质就是是在编译阶段执行的,在代码运行之前
- import具有提升效果,会提升到整个模块头部先执行(早于require)
- 静态的:所以不能使用表达式和变量,不允许运行时改变
- 多次重复只会执行一次
方式一:export
export:可以导出多个,导入时不可以改名字,但是可以as别名
js
const name = "清华";
const age = 12;
export {
name,
age as aliasAge
}
js
import {name,aliasAge} from 'xx.js'
方式二:加载默认模块
export default: 只能导出一个,导入时可以改名字
js
const name = "清华";
const age = 12;
export defalut name;
js
// 可以改名字
import aliasName from 'xx.js'
方式三:整体加载
js
// 可以改名字
import * from 'xx.js'
四种规范表格总结:
1. AMD 和CMD 的区别:
区别 | AMD | CMD |
---|---|---|
模块定义时:对依赖的处理 | 依赖前置 :定义的时候就要声明其依赖的模块 |
就近依赖 :用到的时候才去require |
依赖模块的执行时机 | 加载完成后立即执行执行顺序和书写 顺序不一定一致 |
依赖模块加载完成后不执行, 只下载顺序一致 |
模块执行 | 提前执行 (RequireJs2.0也可以延迟执行) |
延迟 执行 |
主要应用 | require.js | sea.js |
加载方式 | 异步 | 异步 |
CMD 是等所有的依赖都加载完成后,进入回调函数逻辑,遇到require语句的时候才执行对应的模块,这样执行顺序和书写顺序就保持一致了
2. ESM 和CommonJs规范的区别:
对比 | ES6 | CommonJS |
---|---|---|
输出值 | 值引用 |
值拷贝 |
加载时机 | 编译时 输出接口 |
运行时 加载 |
对外接口 | 一种【静态定义】,在代码静态解析阶段就会生成 | 是对象:即在输入时先加载整个模块,生成一个对象,然后从这个对象上取方法 |
输出方式 | export 或 export default |
module.exports |
Interview:
-
对模块化的理解?
模块化主要用于管理复杂的系统,将实现一个功能的一系列方法状态等放在一个模块中,前期约定一个文件就是一个模块、挂在对象上、闭包+函数的私有作用域特点实现模块私有作用域等。后面逐渐出现了成熟的模块化规范,如 commojs,amd,cmd,es6等。
早期前端模块化并没有采取commonjs规范,而是重新设计了规范,AMD 可以说是前端模块化演进道路上的一步,是一种妥协的实现方式,不能算是最终的解决方案。只不过在当时的环境背景下,AMD 还是很有意义的,它毕竟给前端模块化提供了一个标准。除了Requirejs使用的AMD规范,同期还出现了淘宝推出的Sea.js 库,但实现的是另一个标准CMD,类似于Commonjs。但是他们都会有难以接受的问题。
随着技术的发展,js标准逐渐完善、基本已经是Nodejs环境中使用commonjs,浏览器环境中使用ES6的模块化,也就是import 和 export。
最早在ESM这个标准刚推出的时候,所有主流的浏览器基本上都不支持这个特性,但是随着 Webpack / Rollup 等一系列的打包工具的流行,这个规范才开始逐渐的普及。截至目前,ES Modules 基本上可以说是最主流的前端模块化方案了,相比于 AMD 这种社区提出来的开发规范,ES Modules 可以说是在JS语言层面实现了模块化,所以说它更为完善。 -
js 模块化规范有几种?
比较成熟的有4种,amd,cmd,commonjs,es6的import+export
-
amd,cmd,commonjs,es6的import+export 区别?
- commonjs多用于后台语言,是随着nodejs出现的,同步,值拷贝,运行时加载
- amd是浏览器端的规范,异步,主要是requirejs提出的规范,依赖前置
- cmd也是浏览器的规范,异步,主要是sea.js 提出的规范,按需加载
- es6 是值引用,编译时确定依赖
-
require 和import的区别?
- require 是commonjs 规范,同步,值拷贝,运行时加载
- 而import是es6的模块化规范,异步,值引用,编译时加载,import提升,静态,重复引入会执行最后一次
注解:
RequireJS 和 Sea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。 他们都推动了模块化的开发的思想。
区别:
区别 | require.js | sea.js |
---|---|---|
定位 | 想成为浏览器\Rhino\Node等环境的模块加速器 | 专注于web浏览器端 |
规范 | amd | cdm |
推广理念 | 在尝试让第三方类库修改自身来支持 RequireJS | 自主封装的方式来"海纳百川",目前已有较成熟的封装策略。 |
开发调试支持 | 无明显支持 | 支持debug,nocache |
插件机制 | 源码中预留接口,插件类型单一 | 通用事件机制,插件类型更丰富 |
-
RequireJS :是一个JavaScript模块加载器。它非常适合在浏览器中使用,但它也可以用在其他脚本环境,就像 Rhino and Node。使用RequireJS加载模块化脚本将提高代码的加载速度和质量。
-
SeaJS :是一个遵循CMD规范 的JavaScript模块加载框架 ,可以实现JavaScript的模块化开发及加载机制。
SeaJS不会扩展封装语言特性,而只是实现 JavaScript的模块化及按模块加载 。
SeaJS的主要目的 是令JavaScript开发模块化并可以轻松愉悦进行加载 ,将前端工程师从繁重的JavaScript文件及对象依赖处理 中解放出来,可以专注于代码本身的逻辑。SeaJS可以与jQuery这类 框架完美集成 。
使用SeaJS可以提高JavaScript代码的可读性和清晰度,解决 JavaScript编程中普遍存在的依赖关系混乱和代码纠缠等问题 ,方便代码的编写和维护 。
SeaJS的作者是前淘宝UED,现支付宝前端工程师玉伯。