带你浅浅的认识JavaScript中的模块化

143 阅读2分钟

模块化


什么是模块化?


在这之前我们先认识一下我们早期开发是怎样使用的

早期在开发过程中通常我们是这样进行引用的

<!DOCTYPE html>
<html lang="en">
<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>Document</title>
</head>
<body>
  
  <script src=".111.js"></script>
  <script src=".222.js"></script>
  <script src=".333.js"></script>

</body>
</html>

通常我们是将在外部的js文件通过 <script>标签放入到HTML结构体中进行引用,其实这样看起来没什么不好,我们只需要把每个需要使用的js文件放入到目标html文件中就行了.

在最开始对于简单的动画实现来说还是可以的,但随着JS的快速发展,JS愈发复杂且功能及需求也越来越多,以往的这种开发模式这就会导致很多问题,比如:命名冲突的问题、作用域的问题...

那么什么是模块化与模块化开发呢?

维基百科定义:

---模组化设计(英语:Modular design),也称模块化设计,是门设计理论与实作方法,其旨在于将一个系统细分为许多小单元,称为模组(module)或模块(block),可以独立的于不同的系统中被建立与使用。模组化设计的特征为将功能切分为抽象的、可扩充的、可重复使用的模组;模组需严格使用定义明确的模块化接口,并以行业间标准作为接口规范。在这种情况下,模组化在组件级别实作,并且具有一个单一、固定且特殊的组件插槽。这种由组件单元组合而成的模组化系统,通常称为使用模组化组件的平台系统。例如个人电脑上的USB

通常使用主程式、子程式子过程等架构将软件的主要结构和流程描述出来,而非一般程式的编写──逐条输入计算机程序和指令。

---模块化(modular)编程,是强调将计算机程序的功能分离成独立的、可相互改变的“模块”(module)的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面(aspect)所必需的所有东西。

以上条条框框的太多,按照我自己的理解来说模块化就是:

  • 模块化开发是将程序划分成一个个小结构;
  • 结构中可以编写自己的代码逻辑且有自己的作用域,并不会影响到其他结构
  • 该结构可以将自己希望对外暴露的东西(函数、变量、对象...)对外暴露给其他结构使用,同时也可以导入另外的结构中的东西(函数、变量、对象...)

这种结构实际上就是模块,那么按照上述过程进行程序开发的过程就是模块化开发

认识一下JS中几种模块化规范


1)CommonJS---(CJS)

其中Node是对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

---在Node中每一个js文件都是一个单独的模块;

---这个模块中包括CommonJS规范的核心变量:exports、module.exports、require

---我们可以使用这些变量来方便的进行模块化开发;

其中:

exports和module.exports可以负责对模块中的内容进行导出;

require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

基本使用:

//index.js
const name = "小明"
const age = 18function sum(num1, num2) {
  return num1 + num2
}
​
// 1.导出方案 module.exports
module.exports = {
  name,
  age,
  sum
}
------------------------------------------
//main.js
//使用另外一个模块导出的对象, 那么就要进行导入 require
const { name, age, sum } = require("./index.js")
​
console.log(name)
console.log(age)
console.log(sum(20, 30))
​

(node)内部原理:类似于对象引用的赋值

exports:(用内存地址指向解释)

exports是一个对象,可以添加多个属性,添加的属性会被导出(node 中使用)

const name = "小明"
const age = 18
function sum(num1, num2) {
  return num1 + num2
}
********************************************
//使用exports导出
exports.name = name
exports.age = age
exports.sum = sum
​
//内部实现源码,最终导出的一用是module.exports
module.exports = {}
exports = module.exports
​
//这种情况下不会被导出
exports = {
  name,
  age,
  sum
}

require(X):

require是一个函数,可以引入一个模块中导出的对象

  • 情况一:X是一个Node核心模块,比如path、http....会直接返回核心模块,并停止查找
  • 情况二:X是以 ./ 或 ../ 或 /(根目录)开头的

第一步:将X当做一个文件在对应的目录下查找;

1.如果有后缀名,按照后缀名的格式查找对应的文件

2.如果没有后缀名,会按照如下顺序:

1> 直接查找文件X

2> 查找X.js文件

3> 查找X.json文件

4> 查找X.node文件

第二步:没有找到对应的文件,将X作为一个目录

查找目录下面的index文件

1> 查找X/index.js文件

2> 查找X/index.json文件

3> 查找X/index.node文件

---如果没有找到,那么报错:not found

3. X不是一个模块,也不是路径

    会以当前文件夹进行逐级往上查找,若上面路径没有找到则会报错

模块加载过程(运行时加载)

  • 结论一:模块在被第一次引入时,模块中的js代码会被运行一次

  • 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次

    为什么只会加载运行一次呢?

    这是因为每个模块对象module都有一个属性:loaded。

    false表示还没有加载,为true表示已经加载;

  • 结论三:如果有循环引入,那么加载顺序是什么?

    如果出现下图模块的引用关系,那么加载顺序是什么呢?

1657355460740.png

---这个其实是一种数据结构:图结构;

图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);

Node采用的是**深度优先**算法:main -> aaa -> ccc -> ddd -> eee ->bbb

2)AMD规范

AMD主要是应用于浏览器的一种模块化规范:

  • AMD是Asynchronous Module Definition(异步模块定义)的缩写;
  • 它采用的是异步加载模块;
  • 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
  • AMD实现的比较常用的库是require.js和curl.js;

3)CMD规范

  • CMD规范也是应用于浏览器的一种模块化规范:
  • CMD 是Common Module Definition(通用模块定义)的缩写;
  • 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
  • AMD实现的比较常用的是SeaJS(sea.js)

AMD和CMD了解一下就行,目前都是用较少

4)ES Module

了解:

  • 它使用了import和export关键字实现模块化

    export负责将模块内的内容导出;

    import负责从其他模块导入内容;

  • 采用编译期的静态分析,并且也加入了动态引用的方式

  • 采用ES Module将自动采用严格模式:use strict

基本使用:

//导出
export const name = "小明"
export const age = 18
//导入
import { name, age } from "./foo.js"
console.log(name)
console.log(age)
//html文件中引用main.js需要在type属性声明为"module"
 <script src="./main.js" type="module"></script>

export与import

分别使用:
// 1.第一种方式: export 声明语句
export const name = "小明"
export const age = 18export function foo() {
  console.log("foo function")
}
​
export class Person {
}
​
// 2.第二种方式: export 导出 和 声明分开
const name = "小明"
const age = 18
function foo() {
  console.log("foo function")
}
​
export {
  name,
  age,
  foo
}
​
// 3.第三种方式: 第二种导出时起别名
export {
  name as fName,
  age as fAge,
  foo as fFoo
}
// 1.导入方式一: 普通的导入
import { name, age, foo } from "./foo.js"
import { fName, fAge, fFoo } from './foo.js'// 2.导入方式二: 起别名
import { name as fName, age as fAge, foo as fFoo } from './foo.js'// 3.导入方式三: 将导出的所有内容放到一个标识符中
import * as foo from './foo.js'
结合使用:

在工具库函数文件夹中设置index.js将所有其他所有希望暴露的接口放入其中

// 1.导出方式一:
import { add, sub } from './math.js'
import { timeFormat, priceFormat } from './format.js'export {
  add,
  sub,
  timeFormat,
  priceFormat
}
​
// 2.导出方式二:
export { add, sub } from './math.js'
export { timeFormat, priceFormat } from './format.js'// 3.导出方式三:
export * from './math.js'
export * from './format.js'
default
const name = "小明"
const age = 18
const foo = "foo value"
//默认导出只能有一个
export default foo
​
//导入语句: 导入的默认的导出,此时xixi导入的是foo
import xixi from './foo.js'
import函数

某些情况下希望动态的加载某一个模块。这个时候需要使用import()函数来进行动态加载

---import()函数返回值是一个Promise

// import函数返回的结果是一个Promise
import("./foo.js").then(res => {
  console.log("res:", res.name)
})
ES Module的解析流程

ESModule的解析过程可以划分为三个阶段:

  • 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(ModuleRecord);

1657359774248.png

  • 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向 对应的内存地址。
  • 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;

1657359828685.png