前端工程化(二)之模块化 (精品、面试必备基础)(春招、秋招)

什么是模块化?

事实上模块化开发最终的目的是将程序划分成一个个小的结构
这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;

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实现模块的加载和导出,有助于提高代码的可维护性和复用性。

模块化的核心

导入和导出
导出
exportsmodule.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 当做一个文件在对应的目录下查找

  1. 有后缀名, 按照后缀名查找对应的文件
  2. 没有后缀名 ,按照下面顺序
    ① 直接查找 x 文件 ② 查找 x.js 文件 ③ 查找 x.json 文件 ④ x.node 文件
    二、没有找到文件,就将
    X*当做一个目录
  3. 查找 x/index.js文件
  4. x/index.json 文件
  5. 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),运行代码,计算值,并且将值填充到内存地址中;

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值