大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

随着前端项目的不断复杂,代码日益膨胀,项目的维护难度随之越来越大,此时模块化也就相继的出现了,本篇文章将会介绍如下内容:

  • 模块化的概念以及演变过程
  • 模块化规范
    • CommonJS
    • AMD
    • CMD
    • ES module

模块化的概念以及演变过程

什么是模块化

模块化就是将一个复杂的程序依据一定的规则或者说是规范,将其封装成几个单独的块(这里的块指的就是文件),在使用的时候将其组合在一起。

块内部的数据是私有的,只是向外部暴露一些接口或者说是一些方法,让其与其他模块进行通信。

模块化的演变过程

早期的前端技术标准根本没有预料到前端会有现在一个规模,所以说很多设计上遗留的问题就导致了现在去实现前端模块化的时候会遇到很多的困难。

虽然说模块化现在已经被一些标准或者工具去解决了,但是它的一个演变过程还是值得我们去思考的。

模块化演变的过程其实就是前端领域的实践过程,这个过程大致可以分为四个阶段:

文件划分方式

文件划分方式是最原始的模块系统,具体做法就是将一个功能以及它相关的一些状态单独存在不同的文件中,每一个文件就代表一个模块。使用这个模块就是将这个模块文件引入页面文件中,一个<script>标签对应一个模块。如下代码展示:

<body><!-- 登录模块 --><script src="login.js"></script><!-- 用户模块 --><script src="user.js"></script>
</body>

使用这种方式的缺点很明显,如下:

  • 模块内部的成员都处在全局作用域中,任意位置都可以进行访问和修改,这样就造成了污染全局作用域。
  • 命名容易冲突。
  • 没有办法很好的管理模块间的依赖关系

命名空间方式

命名空间方式就是在第一个阶段的基础上约定每一个模块只暴露一个全局对象,所有模块的成员都挂载到这个对象的下面。

示例代码如下:

component/module_a.js

let moduleA = {name: '一碗周',handle() {console.log(this.name)},
}

component/module_b.js

let moduleB = {name: '一碗粥',handle() {console.log(this.name)},
}

index.html

<body><script src="./component/module_a.js"></script><script src="./component/module_b.js"></script><script>console.log(moduleA.name);console.log(moduleB.name);moduleA.handle()moduleB.handle()</script>
</body>

通过这种方式减少了命名冲突的可能,但是仍然没有私有空间,且模块之间的依赖关系还是没有进行解决。

IIFE模式

所谓的IIFE模式就是使用立即执行函数去创建闭包,这种方式为模块提供了私有空间。

具体的做法就是将模块中每一个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以通过挂载到全局对象上的方式去实现。这种方式实现了私有成员的概念,就是说模块的私有成员只能在模块内部通过闭包的方式去访问而在外部,是没有办法去使用使用的这样就确保了私有成员的安全。

示例代码如下:

component/module_a.js

;(function () {let name = '一碗周'function handle() {console.log(name)}window.moduleA = { handle }
})()

component/module_b.js

;(function () {let name = '一碗粥'function handle() {console.log(name)}window.moduleB = { handle }
})()

index.html

<body><script src="./component/module_a.js"></script><script src="./component/module_b.js"></script><script>console.log(moduleA.name) // undefinedconsole.log(moduleB.name) // undefinedmoduleA.handle()moduleB.handle()</script>
</body>

发展到这个阶段,就已经实现了私有成员的概念了,但是模块间的依赖关系还是没有解决。

IIFE依赖参数

我们通过为立即执行函数添加参数的形式可以实现模块间的依赖,示例代码如下:

component/module_a.js

;(function () {function printName(name) {console.log(name)}// 暴露一个打印的方法window.moduleA = { printName }
})()

component/module_b.js

;(function (m) /* 形参 */ {let name = '一碗周'function sayName() {// 使用其他模块的成员m.printName(name)}window.moduleB = { sayName }
})(moduleA) // 实参

index.html

<body><script src="./component/module_a.js"></script><script src="./component/module_b.js"></script><script>moduleB.sayName() // 一碗周</script>
</body>

以上4个阶段就是早期开发者在没有工具和规范的情况下,对模块下进行的落地方式。

但是这种方式还是存在问题的,如下:

  • 引入多个<script>标签,就需要发送多个请求,请求数量太多
  • 依赖模糊,很难说清每一个模块之间的依赖
  • 难以维护

接下来我们来介绍一下现在开发过程中使用的模块化规范。

CommonJS

CommonJS在Node.js中广泛应该,Node.js是CommonJS的实践者。CommonJS规范指出一个单独的文件就是一个模块,它采用的是同步加载模块,也就是说模块加载的顺序就是代码中编写的顺序是一致的,而加载的文件资源大多数都存储在服务器中,所以说加载速度没有什么问题。但是这种方案不适用与浏览器端,由于网络原因,更合理的方案是采用异步加载(CMD、AMD和ESmodule)。

基本语法

暴露模块使用module.exports,或者直接使用exports,引入模块直接使用require()方法,示例代码如下:

component/module_c.js

let name = '一碗周'
module.exports = {name,getName() {return name},setName(n) {name = n},
}

index.js

// 引入自定义的模块
const person = require('./component/module_c')
// 引入 Node.js 提供的模块
const fs = require('fs')console.log(person.getName()) // 一碗周
person.setName('一碗粥')
console.log(person.name) // 一碗周console.log(person.getName()) // 一碗粥

模块加载机制

我们现在来讲解一下上面代码,讲解上面代码的过程中就了解的CommonJS的模块加载机智。

首先通过module.exports导出一个对象,该对象中包含一个属性两个方法,我们在index.js中引入该模块,通过require()方法引入模块并定义一个变量来接收这个模块。CommonJS的模块加载机制是被输出值得拷贝 ,也就是说一旦输出了某个值,即使模块内的数据变化,也不会影响这个值了。

我们上面的代码中通过setName()重新为name进行赋值,在赋值后拿到的结果还是初始值,这是因为name是一个原始类型的值,它的值会被缓存。

当我们通过getName()方法来方法name的值才可以获取到没有缓存的那个结果。

AMD

AMD是"Asynchronous Module Definition "的缩写。它与CommonJS不同,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD规范的最佳实践者是require.js,现在我们来看一下require.js怎么用。

首选我们通过define()方法定义模块,该方法接受一个函数,该函数的返回值作为暴露给外部的接口。然后通过requirejs()方法引入具体模块,通过回调函数的方式来调用具体内容。示例代码如下:

component/module_d.js

// 通过 define() 方法定义
define(() => {let name = '一碗周'console.log('this is module')return {name,getName() {return name},setName(n) {name = n},}
})

index.html

<body><!-- 借助CDN引入requirejs --><scriptsrc="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script><script>// 通过 requirejs 提供了 requirejs() 方法使用定义的模块,该方法接受两个参数// - 第一个参数接收一个数组,数组中的每一项是一个路径,表示模块地址// - 第二个参数接收一个回调函数,回调函数中的参数表示具体的模块requirejs(['./component/module_d.js'], (person) => {console.log(person.name) // requirejs})</script>
</body>

CMD

CMD规范是在sea.js推广中形成的,与AMD类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

目前CMD已经不再用了,这里就不在介绍了。

ES Module

概述

在ES6之前, JavaScript一直没有一个官方提供的模块化的体系,所使用的都是社区所提供的,例如CommonJS和AMD等。但是在ES6的时候,ECMA提出了ESmodule规范,即原生的模块化体系。但是原生提供的模块化体系的兼容性并不是很好,下图展示了ESModule的浏览器兼容性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJvZjvV3-1639636102254)(image/01_ESmodule%E5%85%BC%E5%AE%B9%E6%80%A7.png)]

从上图中我们可以看到,IE浏览器完全不支持(虽然现在IE几乎已经死掉了),随着时间的推移,想Webpack这种打包工具的流行,慢慢的,ESmodule就进入大家的视野了。

语法特性

如果想要在HTML中使用使用ES Module的话,需要为<script>标签添加一个type="module"的属性,然后就可以执行其中的JS代码。

ES Module有主要以下几个特性:

  • 自动全部采用严格模式,自动忽略'use strict'
  • 每个ES Module都会运行的单独的私有作用域中
  • ES Module是通过CORS的方式请求外部JavaScript模块的
  • ES Module的<script>标签会自动延迟执行脚本,相当于加了defer属性,网页对默认的<script>标签采用的是立即执行的机制,页面的渲染会等待这个脚本执行完成才会往下渲染

导入和导出

导出成员可以通过export导出具体成员,也可以通过export default导出默认成员,示例代码如下:

component/module_e.js

// 导出单个成员
export let name = '一碗周'
// 导出默认成功
export default function sayMe() {console.log('一碗周')
}
// 批量导出成员
// export { name, sayMe }

值得注意的是,批量导出成员的写法并不是导出一个对象,而是固定的语法,导出多个成员必须使用花括号包裹,如果想要导出对象,可以使用默认语法,示例代码如下:

export default { name, sayMe }

使用ES Module导出成员,导出的是值得引用 ,也就是说如果模块内部的成员发生改变,所有引用该模块的地方都会发生改变。

我们也可以对导出成员通过as关键字进行重命名,示例代码:

export { name as e_name, sayMe as e_sayMe }

导入成员使用import关键字导入,如下代码展示了如何导入一个ES Module模块,示例示例代码如下:

// 导入默认成员
// import sayMe from './component/module_e.js'
// 或者通过 as 关键字对导入的默认成员进行重命名
// import { default as sayMe } from './component/module_e.js'
// 导入指定成员
// import { name } from './component/module_e.js'// 也可以将上面两行合并为1行,示例代码如下:
// import { default as sayMe, name } from './component/module_e.js'
// 或者简写如下:
import sayMe, { name } from './component/module_e.js'sayMe()
console.log(name)

值得注意的是,我们无法修改导入的成员的值,如果修改则会抛出异常示例代码如下:

import sayMe, { name } from './component/module_e.js'name = '1'

异常信息为Uncaught TypeError: Assignment to constant variable.

如果我们只想要执行某个模块,并不需要模块内部的成员,可以直接通过import关键字引入即可。

如果我们想要动态的引入某个成员,可以将import()当做一个函数来使用,示例代码如下:

import('./module.js').then(res=>{// res 表示模块的默认导出成员
})

我们可以将导入的模块直接导出,示例代码如下:

export { name } from './module.js'

在Node.js中使用ES Module

如果我们想要在Node.js中使用ES Module,需要将后缀名改为.mjs,然后就可以支持ES Module模块了,或者说是通过在项目的package.json文件中,指定type字段为module。修改完成之后我们可以使用ES Module在Node.js项目中加载模块了,示例代码如下:

// 导入自定义的成员
import { foo, bar } from './module.js'
console.log(foo, bar)// 通过 ES Module 导入内置模块
import fs from 'fs'
fs.writeFileSync('./test.txt', '一碗周')
// 导入模块内的成员
import { writeFileSync } from 'fs'
writeFileSync('./text.txt', '一碗周')
// 导入第三方模块
import _ from 'lodash'
_.camelCase('ES Module')// 有些第三方模块不支持内部成员的导入,因为模块直接导出默认成员
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))

CommonJS的差异

如果在Node中使用ES Module,是不能使用requiremoduleexports__filename__dirname,其实这五个成员其实是 CommonJS把模块包装成一个函数,然后通过参数提供过来的成员,并不是真正的全局对象,如果想要使用的话,需要自己进行封装,示例代码如下:

import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

写在最后

本篇文章介绍了前端模块化的发展过程,以及常用的模块化规范。但是这么多模块规范,有的已经成为了历史,在浏览器端以及全部采用ES Module,而服务端现在用的比较多的还是CommonJS,但是在服务端可是可以使用ES Module。

也就是说模块化已经成为了前端开发者的必备技能了。

往期推荐

  • 《轮子是怎么跑起来的》从0到1教你开发一款脚手架
  • 【建议收藏】总结了42种前端常用布局方案
  • JS高级之必会的5个高阶函数
  • JS高级之图解ES6之前的6种继承方式

前端模块化详解(CommonJS、AMD、CMD、ES Module)相关推荐

  1. 前端模块化详解(完整版)

    前言 在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可:如今CPU.浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax ...

  2. 前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS

    我们知道,在NodeJS之前,由于没有过于复杂的开发场景,前端是不存在模块化的,后端才有模块化.NodeJS诞生之后,它使用CommonJS的模块化规范.从此,js模块化开始快速发展. 模块化的开发方 ...

  3. CommonJS,AMD,CMD,ES6,require 和 import 详解

    CommonJS,AMD,CMD,ES6 commonJS用同步的方式加载模块.在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题.但是在浏览器端,限于网络原因,更合理的方案是使用异步 ...

  4. commonjs是什么_JavaScript模块化标准CommonJS/AMD/CMD/UMD/ES6Module的区别

    JS-模块化进程 随着js技术的不断发展,途中会遇到各种问题,比如模块化. 那什么是模块化呢,他们的目的是什么? 定义:如何把一段代码封装成一个有用的单元,以及如何注册此模块的能力.输出的值依赖引用: ...

  5. 详解CommonJS模块与ES6模块

    详解CommonJS模块与ES6模块 历史上,JS一直没有模块体系,在ES6之前,最主要的是CommonJS和AMD两种.前者用于服务器,后者用于浏览器,ES6在语言标准的层面上实现了模块功能,使用简 ...

  6. JAVA9模块化详解(一)——模块化的定义

    JAVA9模块化详解(一)--模块化的定义 前言 java9已经出来有一段时间了,今天向大家介绍一下java9的一个重要特性--模块化.模块化系统的主要目的如下: 更可靠的配置,通过制定明确的类的依赖 ...

  7. python调用cmd命令释放端口_详解python调用cmd命令三种方法

    目前我使用到的python中执行cmd的方式有三种 使用os.system("cmd") 该方法在调用完shell脚本后,返回一个16位的二进制数,低位为杀死所调用脚本的信号号码, ...

  8. HTML5 多图片上传(前端+后台详解)

    HTML5 多图片上传(前端+后台详解) 1.参考jquery插件库 2.修改代码 3.添加的后台代码 4.删除的后台代码 1.参考jquery插件库 手机端实现多图片上传 2.修改代码 我发现他这里 ...

  9. web前端项目详解:OPPO首页进度条特效(定时轮播)

    web前端项目详解:OPP首页进度条特效(定时轮播) 知识点:布局结构分析,定位运用,页面兼容性问题,Jquery的基础运用(修改盒子样式,动画方法,简单算法,淡入淡出方法,定时器方法)代码结构 效果 ...

最新文章

  1. 2021 线性代数 第五章 习题课
  2. 从企业发展的四个问题,理解OKR的价值所在
  3. PHP自动搜索框post,php搜索框提示(自动完成)实例代码_PHP教程
  4. CodeForces - 1300E Water Balance(贪心)
  5. 本地计算机绑定域名访问
  6. 蛮力写算法_蛮力算法解释
  7. Linux 源码编译安装过程-以安装XZ解压为例
  8. mysql ---- 约束
  9. 使用SQL语句添加和删除约束
  10. 有名无实别占地儿──巧用批处理快删空文件夹
  11. 在没有创建Provision Profile权限的情况下 发布Enterprise inhouse app 的方法
  12. Linux 文件系统(三)---dup和fork函数执行后的文件情况
  13. python中List和Tuple的区别
  14. 模糊pid算法实现(Java)
  15. 基于51单片机的酒精检测仪设计
  16. 关于运行微信小程序报错 [微信小程序开发者工具] Error: read EBADF
  17. CSGO 控制台 准星详细设置
  18. 智慧园区一体化信息管理平台设计方案
  19. JAVA的直接内存介绍
  20. java优化方法_JAVA程序性能优化的10个简单方法

热门文章

  1. Spark GraphX 中的PageRank算法、pregel函数、航班飞行网图分析
  2. 来自波哥大的见闻与思考
  3. Keras中dense层原理及用法解释
  4. 【技术美术】菲涅尔效果的实现
  5. 消息摘要算法---加密学习笔记(二)
  6. GDC - 《幽灵行动:荒野》地形技术和工具(一)
  7. Android SDK安装和配置
  8. Rust学习笔记(9)——Option的几个方法及所有权问题
  9. long journey android,long journey
  10. UML相关工具一览(截止2012年5月)