JavaScript 高级深入浅出:四种模块化规范 您所在的位置:网站首页 js常见的选择结构 JavaScript 高级深入浅出:四种模块化规范

JavaScript 高级深入浅出:四种模块化规范

2024-01-05 08:34| 来源: 网络整理| 查看: 265

介绍

本文是 JavaScript 高级深入浅出的第 19 篇,本文将会介绍 JS 中的模块化

正文 1. 什么是模块化

所以到底是什么模块化、以及什么是模块化开发呢?

模块化开发的目的就是将代码分割为多个小单元 每个单元编写自己的逻辑代码,有自己的作用域,不会影响其他的代码 每个单元可以导入或暴露变量、函数、对象等,用于调用其他模块/供其他模块调用

每一个单元都可以视为一个模块,按照这种结构划分开发程序的过程,就是模块化开发的过程

1.1 模块化的历史

在 Brendan Eich 开发最初版 JS 的时候,仅仅是将这门语言作为一种脚本语言开发,做一些简单的表单验证或者动画,所以在很早期的时候,直接将 JS 代码通过 script 内嵌到 HTML 中,甚至流行在 script 中只写一行代码。

因此在早期 JS 就没有模块化的概念,但是随着前端和 JS 的高速发展,代码越来越复杂:

ajax 的出现,前后端开发分离,意味着后端返回数据,我们需要通过 JS 来渲染前端页面 SPA 的出现,前端页面越来越复杂:包括前端路由、状态管理等等一系列的复杂功能需要通过 JS 来处理 包括 Node 的出现,JS 也能写复杂的后端程序,没有模块化是硬伤

所以模块化是 JS 的一个迫切的需求:

但是 JS 本身,在 ES6 才推出自己的模块化方案 在此之前,为了支持模块化,出现很多不同的模块化规范:AMD、CMD、CommonJS 等 1.2 没有模块化的弊端 命名冲突 // index.js Alex 开发 var aa = '123' setTimeout(() => { console.log(aa) // 发现打印的不是 123 而是 456 }, 0) // demo.js John 开发 var aa = '456'

解决方案:IIFE(立即调用函数表达式)再 IIFE 中可以创建一块作用域,CDN 引用 Vue 的 production 文件也是这样做的

(function () { // 这里会创建一块函数作用域 // 不会污染到全局环境 })()

一个模块想要调用另一个模块可以这样:

var moduleA = (function () { var a = 123 })() var moduleB = (function () { moduleA.a })()

当然这也是有弊端的,我们需要时刻记住每一个模块的名字,模块的名字不能改动,模块加载的顺序不能变等等

2. CommonJS 规范

CommonJS 是一种模块化规范,最初提出来是在浏览器以外的地方使用,并且当时命名为 ServerJS,后来为了体现它的广泛性,更名为 CommonJS,也可以简称为 CJS

Node 是 CommonJS 在服务端一个具有代表性的实现 Browserify 是 CommonJS 在浏览器端的一种实现 webpack 具备对 CommonJS 的支持与转换

所以 Node 种对于 CommonJS 进行了支持和实现,让我们在开发 Node 应用时可以使用模块化:

在 Node 种每一个文件都是一个模块 这个模块中包括了 CommonJS 规范的核心变量:exports、module.exports、require 我们可以使用这些变量来进行模块化开发

模块化的核心就是导入和导出,Node 中也对其进行了实现:

module.exports 和 exports 负责模块的导出 require负责模块的导入 // index.js const fooModule = require('./foo') console.log(fooModule.foo) // foo // foo.js exports.foo = 'foo' 2.1 CommonJS 的原理 // util.js const info = { name: 'alex' } module.exports = info // main.js const moduleInfo = require('./util.js') 此时 util.js 中的 info、module.exports 和 main.js 中的 moduleInfo 其实都指向了一个内存地址,所以这三者指向到的是同一块内容 所以就实现了导入导出,导入导出的其实是引用 2.2 module.exports 和 exports

在 CommonJS 中存在两种导出模块的方式:

module.exports = {} export.name = "name"

在具体引擎实现的源码中:

module.exports = {} exports = module.exports

所以使用 exports.xx = 'xxx' 其实就是往 module.exports = {} 这个对象中添加属性

一些问题 // 大家要知道一点,只有 module.exports 才会真正暴露变量 // 但是这里强行逆转了 exports 的引用 // 而上文中我们已经知道原本 exports 的引用指向的是 module.exports 的引用 // 所以使用这种方式是无法暴露变量的 exports = { name: '123' } // 这种方式 name 也无法被暴露 // 这是因为 exports.name 后 // JS 引擎又强行修改了 exports 的引用 // 此时 name 在原 exports 指向的对象中,肯定是无法暴露的 exports.name = "123" module.exports = {}

那么为什么有了 module.exports 后还需要一个 exports 呢?其实根据 CommonJS 的规范来说是要通过 exports 来导出的,但是由于 Node 本身实现是通过 module.exports 来实现的,所以在 Node 开发中经常会使用 module.exports 反而不经常使用 exports 了

2.3 require 细节

require 是一个函数,可以帮助我们引入一个模块导出的对象:

require 查找细节

以下是常见的查找规则(由 coderwhy 老师所总结)导入格式: require(X)

X 是 Node 的核心模块,比如 http、path 等 直接返回核心模块,并停止查找 X 是以 ./、../、/ 开头的 将 X 作为一个文件在对应的目录下查找: 如果有后缀名,就按照后缀名的格式去查找 如果没有后缀名,会按照下面的顺序去查找 直接查找文件本身 查找 X.js 查找 X.json 查找 X.node X 并不是路径也不是核心模块 会根据 module.path 一级一级的查找 node_modules 中是否包含 X 找不到会抛出异常 not found 2.4 模块的加载过程

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

// foo.js console.log('foo module execute') // main.js require('./foo') // 会运行一遍 foo 模块中的代码

结论二:模块在被多次加载后,代码也会只运行一次:

这是因为每个模块对象有一个 loaded 属性 加载了 loaded 就变为 true 了 require('./foo') // 也只会运行一次 foo 模块 require('./foo') require('./foo')

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

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

模块加载顺序.png

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

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

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

2.5 CommonJS 规范的缺点

CommonJS 模块的加载是同步的:

同步意味着需要等模块中加载完毕后,后面的逻辑才会执行 这个在服务器不会有什么问题,因为服务器加载的是本地 JS 文件,速度会很快

那么如果将 CommonJS 规范用于客户端(浏览器)呢?

浏览器加载 js 文件需要先从服务端下载下来,之后再加载运行 那么采用同步就意味着后续的 js 代码都无法正常运行,即使是一些简单的 DOM 操作

所以如果面向的是客户端,我们将不再采用 CommonJS 规范,在早期浏览器中如果使用模块化开发,通常会采用 AMD 和 CMD 规范。

但是由于目前 ES 已经支持模块化 ESM(ES Module),另一方面借助于 webpack 等打包工具也可以将 CommonJS 和 ESM 模块的转换 所以 AMD 和 CMD 用的已经非常少了,下面我们就简单的了解一下 3. AMD 规范

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

AMD 是 Asynchronous Module Definition(异步模块定义)的缩写

它采用的是异步加载模块

事实上 AMD 的规范早于 CommonJS,但是现在 CommonJS 仍被使用,但 AMD 已经很少用了

实现 AMD 规范的库主要是 require.js 和 curl.js

3.1 requirejs 的使用

下载 requirejs

在 html 中引入 requirejs

如果想要在加载完 requirejs 后再加载入口文件,需要这样,这样就会在加载 require 后再加载 index 引入模块 // /index.js // 先配置每个模块的路径 require.config({ paths: { main: './src/main', foo: './src/foo', }, }) // 这里再引入模块 require(['foo'], function (foo) { // 加载完成 foo 后触发回调 并传入加载的数据 console.log(foo.name) console.log(foo.age) console.log(foo.sum(1, 2)) }) 导出模块 // /src/foo.js // 在 define 中写逻辑 define(function () { const name = 'foo' const age = 18 function sum(num1, num2) { return num1 + num2 } // 这里的 return 就导出了 return { name, age, sum, } }) 4. CMD 规范

CMD 也是应用于浏览器的一种模块化规范:

CMD 是 Common Module Definition(通用模块定义)的缩写

他也是采用了异步加载模块,但是它将 CommonJS 的优点吸收了过来

这个目前也很少使用了

SeaJS 实现了 CMD 规范

4.1 seajs 的使用

在使用时需要这样:

// 入口文件 seajs.use('./index.js') 导入模块 // /index.js // define 函数接收一个回调函数,参数 require、exports、module define(function (require, exports, module) { // 通过 require 导入模块 const foo = require('./src/foo.js') console.log(foo) }) 导出模块 // /src/foo.js define(function (require, exports, module) { const name = 'foo' const age = 18 function sum(num1, num2) { return num1 + num2 } // 可以通过 exports.name = name // 和 module.exports 两种 module.exports = { name, age, sum, } })

所以可以看到 CMD 和 CommonJS 可以说是非常像的

5. ES Module 规范

ES Module 规范是 ES 提出的,所以是官方的模块化规范

ESM 和 CommonJS 的模块化有一些不同:

ESM 使用了 import (导入)和 export(导出) ESM 采用编译期的静态分析,并且也加入了动态引用的方式 采用 ESM 将自动采用严格模式 use strict

如果在 html 中引入 ESM 模块,需要这样:加上 type="module"

// 可以使用这种方式来导出 export const name = 'foo' // 这种方式来导入 import { name } from './foo.js'

值得注意的是,如果在本地实验这种方式的话,直接通过浏览器访问 index.html 文件是会报错的,MDN 有具体解释

5.1 导出的几种方式 第一种 export const name = "123" export function foo() {} export class Person {} 第二种 // 这是一个固定的语法,可不是一个对象 export { name, foo } // 也可以给导入的成员起别名 export { name as fName, foo as fFoo } 第三种

从另一个模块中导出

export { add } from './foo' export * from './bar' 5.2 导入的几种方式 第一种 import { name, age } from './foo' // 也可以给导入的成员起别名 import { fName as name, fAge as age } from './foo' 第二种

直接拿到所有的导出内容,放入到一个标识符中

import * as foo from './foo' 5.3 default 的用法

default 用于默认导入和默认导出

// 如果想要让 foo 作为默认导出 const foo = "foo value" // 可以这样 export { // 起别名就叫做 default foo as default } // 也可以这样 export default foo

默认导出后

import foo from './foo' // 这样导入拿到的就是默认导出

注意:

默认导出只能有一个 在导入时不需要 {},并可以自己指定名字 它也方便和 CommonJS 等规范相互操作 5.4 import 函数 - ES11 及以上

我们在导入的时候

import { name } from './foo' // 在导入 name 之前,后续的代码是会阻塞的

如果想要异步的导入一个模块,可以使用 import() 函数

// 返回 Promise import("./foo").then(module => {}) // 后面的代码不受到影响

import 有一个 meta 属性,包含了一个 url 属性,是当前的 js 文件的 url

console.log(importa.url) 5.5 ESM 的实现原理

ES Module 是如何被浏览器所解析并且让模块之间相互引用的呢?

文档地址

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

阶段一:构建(Construction),根据地址查找 JS 文件,并且下载,将其解析为模块记录(Module Record) 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址 阶段三:运行(Evaluation),运行代码,计算值,并将值填充到内存地址中

ESM模块.png

具体每个阶段的工作原理,请看第 20 篇。

总结

在本文中,我们介绍了什么是模块化,以及介绍了社区的三种模块化规范以及 ES 官方的模块化规范。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有