目录
什么是模块化?
事实上模块化开发最终的目的是将程序划分成一个个小的结构;
这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;
CommonJS规范和Node关系
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它
的广泛性,修改为CommonJS,平时我们也会简称为CJS。
Node 是 CommonJS在服务器端一个具有代表性的实现;
Browserify是CommonJS在浏览器中的一种实现;
webpack 打包工具具备对CommonJS的支持和转换;
什么是 CommonJS
CommonJS是一种JavaScript模块化规范,主要用于服务器端编程,如Node.js环境。CommonJS的核心思想是每个文件都是一个独立的模块,拥有自己的命名空间,不会与其他模块的变量或函数产生冲突。在CommonJS中,
module
是一个特殊的变量,代表当前模块。模块通过module.exports
对外提供接口,而其他程序可以通过require()
函数来同步加载并使用这个模块。CommonJS规范的出现主要是为了解决JavaScript在浏览器环境外,特别是在服务器端运行时的模块化问题。它提供了一个标准化的方法来定义模块及其依赖关系,使得代码更加模块化,更易于维护和复用。此外,与CommonJS相对应的,还有AMD (Asynchronous Module Definition)和CMD (Common Module Definition)等模块化标准,它们主要用于浏览器端。AMD和CMD都支持异步加载模块,但它们的加载方式有所不同。AMD采用预先加载的方式,而CMD则是按需加载。总的来说,CommonJS为JavaScript提供了一种在非浏览器环境中模块化开发的规范,通过require()
和module.exports
实现模块的加载和导出,有助于提高代码的可维护性和复用性。
模块化的核心
导入和导出
导出
exports 和 module.exports 可以负责对模块中的内容进行导出
require 函数可以帮助我们导入其他模块 (自定义、系统、第三方库模块)
exports 导出 & require 导入
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
导出
let FriendName = "康康"
exports.FriendName = FriendName
// 两秒后, 修改一下 FriendName
setTimeout(()=>{
exports.FriendName = "芜湖"
},2000)
// 函数
function add(a,b) {
return a+b
}
exports.add = add
导入
const bar = require('./bar.js');
console.log(bar.FriendName)
console.log(bar.add) // 函数
// 使用函数
console.log(bar.add(1,2)) // 3
setTimeout(() => console.log(bar.FriendName),5000)
结果
康康
芜湖
说明
也就是require通过各种查找方式,最终找到了exports这个对象;
并且将这个exports对象赋值给了bar变量;
bar变量就是exports对象了;
所以后面
setTimeout(()=>{
exports.FriendName = “芜湖”
},2000) 一定要 exports.FriendName 才可以修改 FriendName
module.exports 导出
module.exports 和 exports 有什么关系或者区别呢?
从维基百科可以了解到
CommonJS 中没有 module.exports 的概念
但是为了实现模块的导出, Node 中使用 Module 的类, 每一个模块都是 Module 的实例, 也就是 module
所以在Node 中真正用于导出的其实根本不是 exports , 而是 module.exprots;
因为 module 才是真正的实现者
但是 为什么 exports 也可以导出?
因为 module对象的 exports属性是exports对象的一个引用
也就是说 module.exports = exports = [const bar = require(‘./bar.js’);]
补充
还不是很理解 这一句话 “因为 module对象的 exports属性是exports对象的一个引用”
下面我画了一个图解
简单解释就是 module对象中 exports 属性 和 exports 对象指向是同一个对象(同一个地址)
图
验证代码
console.log(exports === module.exports) // true
为什么说 module 才是真正导出实现者
由上面我们可以知道,默认情况下, module.exports 和 exprots 地址是相同的
其实,const bar = require(‘./bar.js’) 这个语句找到是 module类中exprots对象
然后把其地址赋值给bar
下面通过一个简答代码验证一下
bar.js ( 导出 )
let FriendName = "康康"
let age = 12
function add(a,b) {
return a+b
}
console.log(exports === module.exports)
// 导出
module.exports = {
FriendName,
age,
add
}
console.log(exports === module.exports)
// 打印一下 module 类型
console.log(module)
console.log(typeof(module)) // Object
console.log(module.exports)
// 通过 exprots 修改一下 (验证 require 到底是找那个地址 )
exports.FriendName = "爱敲代码的小黑" // 进行修改
bar.js 运行结果
test.js
const bar = require('./bar.js');
console.log(bar.FriendName) // 如果不是 "爱敲代码的小黑" 就说明, require找的就是module中exprots属性的引用
console.log(bar.add) // 函数
// 使用函数
console.log(bar.add(1,2)) // 3
bar.js运行结果
补充
可能有一些小伙伴会对这个写法有点疑问
下面我进行解答一下
module.exports = {
FriendName,
age,
add
}
其实上面代码是使用了对象字面量
它允许我们使用简写方式来定义对象的属性和值。
在对象字面量中,我们可以省略属性名和冒号之间的引号,直接使用变量名作为属性名。这样可以避免重复书写相同的属性名。
在ES6中,如果对象的方法是一个函数,我们可以直接使用方法名作为属性名,而不需要写出完整的function关键字和冒号。
例子
let name = "John";
let age = 25;
let city = "New York";
// 函数
function greet() {
console.log("Hello, world!");
}
// 使用对象字面量简写语法
let person = {
name,
age,
city,
greet // 函数
};
console.log(person); // 输出: { name: 'John', age: 25, city: 'New York', greet: function greet(){console.log("Hello, world!");}}
注意:
在上面的示例中,我们定义了三个变量 name、age 和 city,并使用对象字面量的简写语法将它们作为属性和值赋给 person 对象。通过这种方式,我们可以直接使用变量名作为属性名,而无需重复书写相同的属性名。
需要注意的是,对象字面量的简写语法只适用于属性名为合法的标识符(即符合 JavaScript 变量命名规则的字符串)的情况。如果属性名包含特殊字符或空格等非法字符,仍然需要使用常规的对象字面量语法。
require 细节
// 导入格式
require(X) // X 可以是 path 或者 http
查找规则
情况一: X为 path 或者 http 等等
const http = require('http')
const path = require('path')
console.log(path)
console.log(http)
运行结果
情况二:
一、将 X 当做一个文件在对应的目录下查找
- 有后缀名, 按照后缀名查找对应的文件
- 没有后缀名 ,按照下面顺序
① 直接查找 x 文件 ② 查找 x.js 文件 ③ 查找 x.json 文件 ④ x.node 文件
二、没有找到文件,就将X*当做一个目录- 查找 x/index.js文件
- x/index.json 文件
- x/index.node 文件
三、上面都找不到, 就报错: not found
const bar = require('./bar'); // 这里不一定需要写完整,可以参考上面的规则来进一步解释
const index = rquire('./util') // 先找 './util/index.js' 找不到就找 './util/index.json'....
console.log(bar.FriendName)
console.log(bar.add) // 函数
// // 使用函数
console.log(bar.add(1,2)) // 3
情况三: 名字不是路径 也不是内置模块(http、path…)
node_module/why/index.js
module.exports = {
foo: function() {
return "why 模块被调用"
}
}
test.js
const why = require("why")
本质: 在 test.js 当前文件夹 中找到 node_modules 文件夹, 如果没有,就去上一层目录找node_module, 直到找到为止。如果到达根目录还是找不到就报错
接着在 node_modules 文件夹下面找why文件夹
然后通过查找 index.js , 如果找不到就按照上面的规则进行寻找
模块加载
结论
① 模块被第一次引用时,模块中的 js 代码会执行一次
② 模块被多次引用, 会缓存,最终只加载一次
a. 因为每一个模块对象module都有一个属性: loaded。当 loaded 为 false 时, 表示没有加载, 为true时, 表示加载
③ 如果循环引入, 那么加载顺序是什么?
如上图所示
Node采用的是深度优先算法 ( 图相关知识 ):main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS 规范缺点
CommonJS加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:
当然在webpack中使用CommonJS是另外一回事;
因为它会将我们的代码转成浏览器可以直接执行的代码;
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES
Module代码的转换;
AMD和CMD已经使用非常少了
补充
AMD 、 CMD 是什么?
两者都采用异步加载模块
AMD 即 Asyncrounous Module Definition,是异步模块定义的缩写;CMD 即 Common Module Definition,是通用模块定义的缩写。它们都是前端模块化的开发规范,都采用异步加载模块的方式,用于解决浏览器端的模块化问题。
对比
执行时机不同:AMD 推崇的是依赖前置和提前执行,即在定义模块时就声明所有的依赖项,并且在代码执行到 define 函数之前,这些依赖已经被加载并执行完毕。而 CMD 推崇的是依赖就近和延迟执行,依赖的加载和执行会推迟到需要它们的时候才进行。
模块化标准不同:AMD 有一个广泛使用的实现库叫做 RequireJS,它遵循 AMD 规范,强调模块化的预加载。CMD 则有一个对应的实现库叫做 SeaJS,它遵循 CMD 规范,强调模块化的懒加载。
开发文化不同:AMD 和 CMD 的产生背后反映了不同的开发文化和优化理念。AMD 更加符合传统的同步加载思维,而 CMD 则更符合现代的异步加载和执行的理念,尽量延后模块的加载和执行,减少页面初始加载时间,提高用户体验。
通俗理解
AMD 和 CMD 是两种不同的 JavaScript 模块化加载方案,它们的主要区别在于处理模块依赖的方式不同。
首先,让我们来理解一下 AMD。想象你在做饭,你需要一些调料,比如盐和糖。在 AMD 的世界里,就像你提前把需要的调料都准备好,放在厨房里,然后再开始做菜。这样,当你需要使用某个调料时,它已经在那里等着你了。AMD 就是这样,它会在你做菜之前(也就是你的代码运行之前),就把你可能需要的所有模块都加载好。
然后我们再来看看 CMD。同样以做饭为例,这次你可能是等到需要用盐的时候才去拿,而不是一开始就把所有东西都准备好。CMD 就是采取这种策略,它不会一开始就加载所有模块,而是等你真正需要用到某个模块的时候,它才会去加载。
总的来说,AMD 更像你提前准备所有东西,而 CMD 更像是随用随拿。这两种方式各有优缺点,AMD 可以让所有的模块都快速加载完成,但可能会浪费一些资源;而 CMD 则是按需加载,可以节省资源,但可能会稍微慢一点。
问题: AMD推崇依赖前置 和 异步加载模块 冲突吗?
想象一下,你正在组装一个玩具模型。有些部件是一开始就需要准备好的,比如主体结构,你需要先把它拼好,才能继续添加其他部分。这就像AMD规范中的依赖前置和提前执行,你需要先声明并加载一些基本的模块,这些模块会在你的程序运行之前就准备好。
然后,考虑一些特殊的部件,比如一些小装饰或者配件,你可能不需要一开始就把它们全部准备好,而是在组装过程中,当你需要它们的时候再去找。这就是异步加载模块的概念,即模块不是一开始就全部加载,而是在需要的时候才加载。
虽然AMD规范推崇依赖前置和提前执行,但这并不意味着它不支持异步加载。实际上,AMD是一种实现异步加载的方法
通过使用AMD规范,你可以在声明模块时指定其依赖的其他模块,并确保这些依赖在主程序运行之前已经加载完成。这样,当主程序需要使用某个模块时,这个模块已经准备好了,可以直接使用。
因此,尽管依赖前置和提前执行看起来像是同步加载的方式,但AMD实际上是通过异步加载来实现的。这种方式可以提高页面的加载速度和性能,因为它不需要等待所有模块都加载完成才能开始运行主程序。
认识 ES Module
ES Module和CommonJS的模块化有一些不同之处:
一方面它使用了import和export关键字;
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
import 导入 exprot导出
采用ES Module将自动采用严格模式:use strict
improt & export 关键字
export 关键字( 导出 )
使用方式
① 在语句声明前面加上 exprot 关键字
② 将所有需要导出标识符, 放在 exprot 后面 { } (注意这里的{} 并不是前面提到对象字面量增强写法, {} 不是表示一个对象)
③ 导出时, 给标识符起一个别名
通过as 关键字起别名
import 关键字 ( 导入 )
使用方式
① import{标识符列表 } from ‘模块’
② 导入时 给标识符起别名
③ 通过 * 将模块功能放在一个模块功能对象 {a module object} 上
例子
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 注意, 在我们打开相对于的html时, 如果html 中有使用 模块化代码, 那么必须
开启一个服务来打开 -->
<script src="./bar.js" type="module"></script>
<script src="./main.js" type="module"></script>
</body>
</html>
注意
直接运行, 浏览器会报错
MDN给出解释
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
你需要通过一个服务器来测试;
可以使用 VsCode 插件: Live Server
具体步骤: 安装插件 - 右键点击 index.html - open with live server
bar.js
// 导出方式一:
export const name = "爱敲代码的小黑"
export const age = 19
// 方式二
const hight = 170
export {
height
}
// 也可以通过 as 对变量起别名导出, 但是用得不多
const weight = 70
export {
weight as myweight
}
main.js
// 导入
// 方式一:
// import {name, age, height,myweight} from "./bar.js"
// 如果导入的变量和 main.js 定义的变量名冲突
// 可以在导入时改别名
import {name as myname, age, height, myweight} from "./bar.js"
// 导入方式三
// import * as bar from "./bar.js"
let name = "康康"
console.log(name)
console.log(myname)
// 导入方式三 通过 bar.xx 进行取值
// console.log(bar.name)
运行
挖坑
直接运行 index.html 为什么会直接报错? COSR 又是什么? 后面我会单独出一期讲跨域问题, 大家可以关注一下, Thanks♪(・ω・)ノ
exprot 和 import 结合使用( 常用的思想 )
说明一下
parse.js 以及 format.js 通过export 实现导出
在 index.js 统一导入 然后 统一导出
在这里, format.js 、index.js、parse.js都是在 util 工具文件夹中
import {funcOne, funcTwo, funcThree } from './parse.js'
import {funcFour} from './format.js'
// 统一导出
exprot {
funcOne,
funcTwo,
funcThree,
funcFour
}
这样子做的好处就是其他地方想要引入工具类时
可以直接写
import {funcOne, funcTwo, funcThree,funcFour} from './util/index.js'
// 如果在浏览器的话,是不支持省略写法(不要后面index.js), 记得通过"open with live server"
简洁写法( 优化 )
// 导入以及导出结合写法
// 写法一:
exprot {funcOne, funcTwo, funcThree } from './parse.js'
// 写法二:
exprot * from '/parse.js'
总结
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
这样方便指定统一的接口规范,也方便阅读;
这个时候,我们就可以使用export和import结合使用, export * from '… ( 路径) ’
default 用法 ( 一个 js文件只有一个默认导出 )
format.js
export default function sayHello() {
return ['hello', 'world']
}
**在inde.js默认导入 format.js **
import [随便请名字并且不用大括号] from ‘…(路径)’
// 导入 format.js
import say from './format.js';
console.log(say)
console.log(say())
**同上,在index.html 中运行可以得到: **
import 函数加载问题
通过import加载一个模块,是不可以在其放到逻辑代码中的
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
如果确实是逻辑成立时, 才需要导入某一个模块, 可以使用导入函数 import
这个时候我们需要使用 import() 函数来动态加载;
import函数返回一个Promise,等待模块加载完成后, 然后通过 then 回调函数打印 res 结果
Code
main.js
const importPromise = import("./util/parse.js") // 这里是异步的, 不影响下面代码执行
importPromise.then(res=>{
console.log(res)
console.log(res.func)
console.log(res.func())
})
console.log("=======") // 用来验证上面 import 函数是不是异步
parse.js
function func() {
return "import函数"
}
export {
func
}
ES Module 的解析流程
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
参考文档
总结: 三个阶段
阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;