Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS

前端模块化

6 分钟阅读 · Note

目录树 578 篇

          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗00 JavaScript学习地图共同主题↗01 JavaScript基础共同主题↗02 类型系统与类型转换共同主题↗03 Number与String共同主题↗04 Array数组共同主题↗05 Object对象共同主题
  • 前端模块化

前端模块化

在vite(rollup)的打包配置里有一个format的选项,可以指定最终打包产物的格式,默认情况下是ES module,但是不同的场景可能会有所区别,本文就想通过这个来讲讲前端模块化。

IIFE

自执行函数 > 英文全称:immediately invoked function expressions

适用场景 浏览器中带有<script>标签包裹的代码块,简单来说就是普通的业务应用都可,没啥兼容问题

优点

  • 函数作用域避免了全局变量的污染 iife也算是比较早期的模块化方案了,只需对外暴露一个全局变量,最知名的就是jQuery

CJS

真正的模块化解决方案,最早从Node应用开始 > 全称:CommonJS

关键词:module,exports,global,require

特点

  • 模块加载require就是代码执行
  • 所有代码运行在模块作用域中,不污染全局
  • 模块可以多次加载,但是只在第一次加载运行,后面是缓存

适用场景 Nodejs,浏览器端需要用其他打包工具支持

CommonJS (CJS) 的实现核心在于 Node.js 运行时的 require() 函数。它并非语言层面的特性,而是一个运行时概念,通过一系列步骤来加载和执行模块。

简单来说,require() 函数的工作流程可以概括为以下几个关键步骤:

  1. 路径解析 (Resolve) 首先,require() 会接收一个模块路径(如 './module' 或 'lodash'),并将其解析为一个绝对的文件路径。这个过程会处理相对路径、绝对路径以及在 node_modules 目录中的查找。

  2. 缓存检查 (Cache Check) Node.js 会维护一个 require.cache 对象来缓存已加载的模块。在加载前,require() 会先检查这个缓存。如果模块已经被加载过,它会直接返回缓存中 module.exports 的值,而不会重新执行模块代码。

  3. 创建模块对象 (Create Module Object) 如果模块未被缓存,Node.js 会创建一个新的 Module 对象。这个对象包含了模块的元信息,如 id (绝对路径)、exports (初始为空对象 {})、loaded (初始为 false) 等。

  4. 提前放入缓存 (Cache Before Execution) 这是一个非常关键的设计。在模块代码执行之前,新创建的 Module 对象就会被放入 require.cache 中。这个机制是处理循环依赖的核心,它确保了当一个模块在加载过程中又去 require 另一个正在加载的模块时,能拿到一个不完整的但可用的导出对象,而不是陷入死循环。

  5. 读取与包装 (Read and Wrap) Node.js 会读取模块文件的内容,然后将其包装在一个函数中。这个包装器函数为模块提供了独立的作用域,并注入了 exports, require, module, __filename, __dirname 等变量。其结构类似于:

    (function (exports, require, module, __filename, __dirname) {
      // 模块的实际代码在这里
    });
    
  6. 编译与执行 (Compile and Execute) 包装后的函数会被编译并立即执行。执行时,this 指向和传入的参数都是 module.exports 对象。模块代码中的所有变量和函数都在这个独立的函数作用域内,从而实现了模块的私有性。

  7. 返回导出值 (Return Exports) 模块代码执行完毕后,require() 函数会返回 module.exports 对象。此时,模块的 loaded 属性会被标记为 true。

CJS 实现带来的核心特性

  • 同步加载 require() 必须同步地返回 module.exports。这意味着在执行 require('./module') 时,程序会阻塞,直到该模块被完全加载和执行完毕。这也是为什么 CJS 不支持顶层 await。

  • 值的拷贝 require() 返回的是 module.exports 的一个值拷贝。

    • 对于原始类型(如 number, string),传递的是值的拷贝。
    • 对于对象类型(如 object, array),传递的是引用的拷贝(即浅拷贝,指向同一个对象)。

    这意味着,如果一个模块导出了一个原始类型的值,后续模块即使修改了自己作用域内的变量,也不会影响 require 进来的值。但如果导出的是一个对象,修改这个对象的属性,所有引用这个对象的地方都会看到变化。

  • 循环依赖处理 得益于“提前放入缓存”的机制,CJS 能够处理循环依赖。当一个模块A加载模块B,而模块B又反过来加载模块A时,B拿到的是A尚未执行完成的 module.exports 对象。这可能导致B只能访问到A的部分导出内容。

AMD

异步模块定义

> 全拼:Asynchronous module definition

amd可以理解是CommonJs在浏览器端的解决方案,cjs在服务端是同步加载依赖代码的,因为服务端都是本地磁盘读取文件,没有网络开销,速度很快,浏览器端要是也这么干,很有可能因为依赖在远程,因网络时间开销而导致出现“假死”,所以浏览器端采用异步加载模块的方式。

关键词:require,define

require的第一个参数是需要异步加载的依赖列表,第二个是模块异步加载后的执行代码,define第一个参数是模块的依赖模块,第二个是模块代码

优点

  • 体积更小,代码按需加载
  • 不堵塞js线程运行

UMD

联合模块定义

> 全拼:Universal Module Definition

准确来说并不是一个独立的模块标准,而是集合了cjs,amd,iife等一体的打包模式,会自动判断当前可用环境。

优点:

  • 一套代码,多端使用,模块化兼容性好

一般会在webpack中成为备用模块

ESM

ES6标准中的模块规则,一统浏览器和服务端标准的解决方案

> 全拼:ES6 Module

关键字:import,export

适用场景: 结合其他打包工具(webpack)使用或浏览器<script type="module">标签包裹

特点:

  • 浏览器端会异步加载,延续AMD的优势,但利用了浏览器原生的解析能力,代码体积更小
  • 模块内自动采用严格模式
  • 模块中的this指向并不是window或global,而是undefined

和cjs的三个差异:

  • cjs 模块输出的是一个==值的拷贝(浅拷贝)==,esm输出的是==值的引用==。
  • cjs 模块是运行时加载,esm是编译时输出接口。
  • cjs==同步==加载模块,esm==异步==加载,有一个独立的模块依赖的解析阶段。

在cjs中,如果多个地方都用了同一个模块,对这个模块进行修改,其实不会有影响,但是在esm中,如果你用incCounter修改了counter值,其他业务模块调用counter的时候就会有变化

原因是esm中模块加载的时候还没完全去取值,实际业务代码执行到了才会去拿真正的值,import的时候只是建了个具体地址,留着后面调用时候方便。