CommonJS 模块化简易实现

前端Javascript模块化 CommonJS与ES Module区别 ES Module 的静态结构支持编译时删除未使用代码(如 Webpack 的 Tree Shaking),而 CommonJS 因动态加载无法实现此优化。ES Module的普及促使npm生态向ESM迁移,工具链(如Webpack、Rollup、Vite)全面支持ESM,形成从开发到构建的完整解决方案。从“脚本堆砌”到“工程化设计”,模块化推动了组件化开发、Monorepo架构和微前端实践,例如通过Webpack和Vite实现模块联邦与即时编译。模块化通过独立作用域隔离变量,避免全局命名冲突。 阅读详情

在这里插入图片描述


阅读原文


CommonJS 概述

CommonJS 是一种模块化的标准,而 NodeJS 是这种标准的实现,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。


NodeJS 模块化的简易实现

在实现模块加载之前,我们需要清除模块的加载过程:

  • 假设 A 文件夹下有一个 a.js,我们要解析出一个绝对路径来;
  • 我们写的路径可能没有后缀名 .js、.json;
  • 得到一个真实的加载路径(模块会被缓存)先去缓存中看一下这个文件是否存在,如果存在返回缓存 没有则创建一个模块;
  • 得到对应文件的内容,加一个闭包,把内容塞进去,之后执行即可。

1、提前加载需要用到的模块

因为我们只是实现 CommonJS 的模块加载方法,并不会去实现整个 Node,在这里我们需要依赖一些 Node 的模块,所以我们就 “不要脸” 的使用 Node 自带的 require 方法把模块加载进来。

// 依赖模块
// 操作文件的模块
const fs = require("fs");

// 处理路径的模块
const path = require("path");

// 虚拟机,帮我们创建一个黑箱执行代码,防止变量污染
const vm = require("vm");

2、创建 Module 构造函数

其实 CommonJS 中引入的每一个模块我们都需要通过 Module 构造函数创建一个实例。

// 创建 Module 构造函数
/*
* @param {String} p
*/
function Module(p) {
    this.id = p; // 当前文件的表示(绝对路径)
    this.exports = {}; // 每个模块都有一个 exports 属性,用来存储模块的内容
    this.loaded = false; // 标记是否被加载过
}

3、定义静态属性存储我们需要使用的一些值

// Module 静态变量
// 函数后面需要使用的闭包的字符串
Module.wrapper = [
    "(function (exports, require, module, __dirname, __filename) {",
    "\n})"
];

// 根据绝对路径进行缓存的模块的对象
Module._cacheModule = {};

// 处理不同文件后缀名的方法
Module._extensions = {
    ".js": function() {},
    ".json": function() {}
};

4、创建引入模块的 req 方法

为了防止和 Node 自带的 require 方法重名,我们将模拟的方法重命名为 req。

// 引入模块方法 req
/*
* @param {String} moduleId
*/
function req(moduleId) {
    // 将 req 传入的参数处理成绝对路径
    let p = Module._resolveFileName(moduleId);

    // 生成一个新的模块
    let module = new Module(p);
}

在上面代码中,我们先把传入的参数通过 Module._resolveFileName 处理成了一个绝对路径,并创建模块实例把绝对路径作为参数传入,我们现在实现一下 Module._resolveFileName 方法。

5、返回文件绝对路径 Module._resolveFileName 方法的实现

这个方法的功能就是将 req 方法的参数根据是否有后缀名两种方式处理成带后缀名的文件绝对路径,如果 req 的参数没有后缀名,会去按照 Module._extensions 的键的后缀名顺序进行查找文件,直到找到后缀名对应文件的绝对路径,优先 .js,然后是 .json,这里我们只实现这两种文件类型的处理。

// 处理绝对路径 _resolveFileName 方法
/*
* @param {String} moduleId
*/
Module._resolveFileName = function(moduleId) {
    // 将参数拼接成绝对路径
    let p = path.resolve(moduleId);

    // 判断是否含有后缀名
    if (!/\.\w+$/.test(p)) {
        // 创建规范规定查找文件后缀名顺序的数组 .js .json
        let arr = Object.keys(Module._extensions);

        // 循环查找
        for (let i = 0; i < arr.length; i++) {
            // 将绝对路径与后缀名进行拼接
            let file = p + arr[i];
            // 查找不到文件时捕获异常
            try {
                // 并通过 fs 模块同步查找文件的方法对改路径进行查找,文件未找到会直接进入 catch 语句
                fs.accessSync(file);

                // 如果找到文件将该文件绝对路径返回
                return file;
            } catch (e) {
                // 当后缀名循环完毕都没有找到对应文件时,抛出异常
                if (i >= arr.length) throw new Error("not found module");
            }
        }
    } else {
        // 有后缀名直接返回该绝对路径
        return p;
    }
};

6、加载模块的 load 方法

// 完善 req 方法
/*
* @param {String} moduleId
*/
function req(moduleId) {
    // 将 req 传入的参数处理成绝对路径
    let p = Module._resolveFileName(moduleId);

    // 生成一个新的模块
    let module = new Module(p);

    // ********** 下面为新增代码 **********
    // 加载模块
    let content = module.load(p);

    // 将加载后返回的内容赋值给模块实例的 exports 属性上
    module.exports = content;

    // 最后返回 模块实例的 exports 属性,即加载模块的内容
    return module.exports;
    // ********** 上面为新增代码 **********
}

上面代码实现了一个实例方法 load,传入文件的绝对路径,为模块加载文件的内容,在加载后将值存入模块实例的 exports 属性上最后返回,其实 req 函数返回的就是模块加载回来的内容。

// load 方法
// 模块加载的方法
Module.prototype.load = function(filepath) {
    // 判断加载的文件是什么后缀名
    let ext = path.extname(filepath);

    // 根据不同的后缀名处理文件内容,参数是当前实例
    let content = Moudule._extensions[ext](this);

    // 将处理后的结果返回
    return content;
};

7、实现加载 .js 文件和 .json 文件的方法

还记得前面准备的静态属性中有 Module._extensions 就是用来存储这两个方法的,下面我们来完善这两个方法。

// 处理后缀名方法的 _extensions 对象
Module._extensions = {
    ".js": function(module) {
        // 读取 js 文件,返回文件的内容
        let script = fs.readFileSync(module.id, "utf8");

        // 给 js 文件的内容增加一个闭包环境
        let fn = Module.wrap(script);

        // 创建虚拟机,将我们创建的 js 函数执行,将 this 指向模块实例的 exports 属性
        vm.runInThisContext(fn).call(
            module.exports,
            module.exports,
            req,
            module
        );

        // 返回模块实例上的 exports 属性(即模块的内容)
        return module.exports;
    },
    ".json": function(module) {
        // .json 文件的处理相对简单,将读出的字符串转换成对象即可
        return JSON.parse(fs.readFileSync(module.id, "utf8"));
    }
};

我们这里使用了 Module.wrap 方法,代码如下,其实帮助我们加了一个闭包环境(即套了一层函数并传入了我们需要的参数),里面所有的变量都是私有的。

// 创建闭包 wrap 方法
Module.wrap = function(content) {
    return Module.wrapper[0] + content + Module.wrapper[1];
};

Module.wrapper 的两个值其实就是我们需要在外层包了一个函数的前半段和后半段。

这里我们要划重点了,非常重要:
1、我们在虚拟机中执行构建的闭包函数时利用执行上/下文 call 将 this 指向了模块实例的 exports 属性上,所以这也是为什么我们用 Node 启动一个 js 文件,打印 this 时,不是全局对象 global,而是一个空对象,这个空对象就是我们的 module.exports,即当前模块实例的 exports 属性。
2、还是第一条的函数执行,我们传入的第一个参数是改变 this 指向,那第二个参数是 module.exports,所以在每个模块导出的时候,使用 module.exports = xxx,其实直接替换了模块实例的值,即直接把模块的内容存放在了模块实例的 exports 属性上,而 req 最后返回的就是我们模块导出的内容。
3、第三个参数之所以传入 req 是因为我们还可能在一个模块中导入其他模块,而 req 会返回其他模块的导出在当前模块使用,这样整个 CommonJS 的规则就这样建立起来了。

8、对加载过的模块进行缓存

我们现在的程序是有问题的,当重复加载了一个已经加载过得模块,当执行 req 方法的时候会发现,又创建了一个新的模块实例,这是不合理的,所以我们下面来实现一下缓存机制。

还记得之前的一个静态属性 Module._cacheModule,它的值是一个空对象,我们会把所有加载过的模块的实例存储到这个对象上。

// 完善 req 方法(处理缓存)
/*
* @param {String} moduleId
*/
function req(moduleId) {
    // 将 req 传入的参数处理成绝对路径
    let p = Module._resolveFileName(moduleId);

    // ********** 下面为新增代码 **********
    // 判断是否已经加载过
    if (Module._cacheModule[p]) {
        // 模块存在,如果有直接把 exports 对象返回即可
        return Module._cacheModule[p].exprots;
    }
    // ********** 上面为新增代码 **********

    // 生成一个新的模块
    let module = new Module(p);

    // 加载模块
    let content = module.load(p);

    // ********** 下面为新增代码 **********
    // 存储时是拿模块的绝对路径作为键与模块内容相对应的
    Module._cacheModule[p] = module;

    // 是否缓存表示改为 true
    module.loaded = true;
    // ********** 上面为新增代码 **********

    // 将加载后返回的内容赋值给模块实例的 exports 属性上
    module.exports = content;

    // 最后返回 模块实例的 exports 属性,即加载模块的内容
    return module.exports;
}

9、试用 req 加载模块

在同级目录下新建一个文件 a.js,使用 module.exports 随便导出一些内容,在我们实现模块加载的最下方尝试引入并打印内容。

// 导出自定义模块
// a.js
module.exports = "Hello world";
// 检测 req 方法
const a = req("./a");
console.log(a); // Hello world

CommonJS 模块查找规范

其实我们只实现了 CommonJS 规范的一部分,即自定义模块的加载,其实在 CommonJS 的规范当中关于模块查找的规则还有很多,具体的我们就用下面的流程图来表示。

在这里插入图片描述

这篇文章让我们了解了 CommonJS 是什么,主要目的在于理解 Node 模块化的实现思路,想要更深入的了解 CommonJS 的实现细节,建议看一看 NodeJS 源码对应的部分,如果觉得源码比较多,不容易找到模块化实现的代码,也可以在 VSCode 中通过调用 require 方法引入模块时,打断点调试,一步一步的跟进到 Node 源码中查看。


工程化 1:CommonJS标准实现模块化 math.JS 文件这个文文件呢,它是提供一些数学相关的函数啊,我们要编写的就是模拟我们在真实项目里边儿,把很多事情呢拆分成不同的文件去处理。进入终端,然后呢,我们去运行一下这个 Node 命令,后边跟上我们的文件名 JS文件名就是 index . js。因此呢,可以看到,我们 node 环境里边是不需要浏览器参与的,是直接运行。模块中的所有全局定义的,变量、函数,都不会污染到其他模块,【如该JS文件中定义var a=1,只能运行在该文件中,其他文件无法调用该文件定义的变量】我们也可以运行,它怎么运行呢? 阅读详情

相关推荐

webpack系列 —— 模块化原理-CommonJS

webpack是基于node.js开发的,而node.js开发的模块化规范是commonJS. webpack npm node之间关系? webpack是npm生态中的一个模块,我们可以通过全局安装webpack来使用webpack对项目进行打包; webpack的运行依赖于node的环境,没有node是不能打包的,但是webpack打包后的项目本身只是前端静态资源和后台没有关系,也就是说不依赖与node,只要有后台能力的都可以部署项目 npm是于Node社区中产生的,是nodejs的官..

七月 1406

【Nodejs】nodejs模块化规范:CommonJS

nodejs模块化规范:CommonJS

DomCode 596

前端八股文-Node.js模块化规范,概念解读,自定义模块CMD,AMD,exports 和 module.exports 的区别CommonJS模块暴露和引入,基于浏览器端的实现举例

前端入门教程

VAN 885

nodejs-commonjs模块化、异步、核心库

node模块化:common.js。异步编程风格:回调、promise、async/await。node核心模块:events、fs、stream、http。

qq_37987033的博客 397

FPGA设计中的扇出约束实战:从XDC到代码优化的全流程解析

本文全面解析FPGA设计中扇出约束的重要性与应用,详细介绍了从XDC约束文件设置到HDL代码优化的全流程实战技巧。通过MAX_FANOUT约束控制信号负载,结合寄存器复制和物理优化策略,有效解决高扇出导致的时序违例和布线拥塞问题,提升设计性能和可靠性。

nept的博客 797

生信实战:用bcftools玩转VCF文件处理(从压缩到注释全流程)

本文详细介绍了使用bcftools处理VCF文件的全流程实战指南。从数据准备、压缩索引、信息提取、质量统计到核心的变异过滤与功能注释,手把手教你高效管理基因组变异数据。重点阐述了如何利用bcftools进行VCF文件注释,整合外部数据库信息,为变异位点添加关键的生物学标签,是生物信息学数据分析的必备技能。

rgv23456789的博客 341

NodeJs(一):初识nodejs、模块化、CommonJS、ESModule等

模块化开发是将程序划分为一个个小的模块结构,在各自的模块中编写自己的逻辑结构,有自己的作用域,不会造成全局变量的污染。在这个模块结构中,可以将自己的变量、函数、对象等导出给其他模块使用,也可以引入其他模块的变量、函数、对象等为自己所用。在早期js开发中,对于怎么样导入导出没有具体的规范,大家自发的建立起CommonJS、amd/cmd等模块开发规范,直到ES6的ESModule给出了官方的模块化规范。

DogEgg_001的博客 1902

车机破解避坑指南:为什么你的CarPlay盒子不识别?从USB设备ID解析兼容性问题

本文深入解析车机USB设备兼容性问题,特别是CarPlay盒子不识别的原因,从硬件ID到识别机制全面剖析。通过ADB工具和root权限操作,提供详细的破解方案和兼容性测试结果,帮助用户解决车机与第三方设备的连接问题。

weixin_29175469的博客 602

深入理解 Node.js 模块化(CommonJS):原理、用法与避坑指南

虽然现在 Node.js 也支持 ES Modules,但是掌握 CommonJS 仍然是 Node.js 开发者的必备技能!

weixin_44732078的博客 1361

单细胞测序流程(八)单细胞的marker基因转化和​GO富集分析

系列文章目录 单细胞测序流程(一)简介与数据下载 单细胞测序流程(二)数据整理 单细胞测序流程(三)质控和数据过滤——Seurat包分析,小提琴图和基因离差散点图 单细胞测序流程(四)主成分分析——PCA 单细胞测序流程(五)t-sne聚类分析和寻找marker基因 单细胞测序流程(六)单细胞的细胞类型的注释 单细胞测序流程(七)单细胞的细胞类型轨迹分析———————————————— 本期主讲内容——单细胞的maeker基因转化和​富集分析 marker基因转化是为了将基因的id和基.

qq_45478665的博客 1万+

Cadence Sigrity

目录 一、PowerSI 1、PowerSI仿真前设置 2、PowerSI提取S参数 (1)检查叠层 (2)选择需要提取的走线和GND (3)设置端口 (4)设置提取频率和提取方式 (5)运行仿真 一、PowerSI PowerSI是一个交流分析工具,适用于频域仿真。 S参数提取 电源阻抗提取 全波参数提取 阻抗、耦合检查 谐振分析 1、PowerSI仿真前设置 打开PCB文件: 打开后如图: 设置为单色后: 仿真前的设置:

fang_yang_wa的博客 3万+

NodeJS 模块化的简易实现(commonJS)

CommonJS 概述CommonJS 是一种模块化的标准,而 NodeJS 是这种标准的实现,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。NodeJS 模块化的简易实现在实现模块加载之前,我们需要清除模块的加载过程:假设 A 文件夹下有一个 a.js,我们要解析出一个绝对路径来;我们写的路径可能没有后缀名 .js、.json;得到一个真...

weixin_33725272的博客 362

从HPPC测试到Simulink仿真:锂电池二阶RC模型参数辨识完整流程

本文详细介绍了从HPPC测试到Simulink仿真的锂电池二阶RC模型参数辨识完整流程。通过HPPC测试获取实验数据,利用Simulink的Parameter Estimation工具进行参数辨识和模型验证,为BMS算法开发提供精确的电池模型。文章涵盖测试设计、数据预处理、模型搭建及工程应用等关键步骤,助力新能源电池研发。

whisky的博客 683

JavaScript模块化原理浅析

01 写在前面 模块化简单来说就是是指把一个复杂的系统分解到多个模块以方便编码。JS模块化的大致流程为:CommonJS(服务端) -> AMD(浏览器端)-> UMD(兼容了CommonJS和AMD) -> ES Module(ES6标准)。本文将从它们的用法进行介绍,简单实现其原理。并简易实现一个模块化打包工具webpack。 本文将从以下几部分进行总结: ...

tanzhidan1994的博客 636

ImportError: DLL load failed while importing _pydantic_core: 找不到指定的程序对应解决方案

本文主要介绍了ImportError: DLL load failed while importing _pydantic_core: 找不到指定的程序对应解决方案,希望能对使用Python的同学们有所帮助。 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 19万+

具身智能:原理、算法与系统 第18章 模仿学习与人类示范

与基于试错机制的强化学习不同,模仿学习假设存在能够提供高质量轨迹示范的专家策略,学习者的目标是在不直接访问专家内部决策机制的情况下,复现或泛化专家的行为模式。此外,选项 GAIL(Option-GAIL)将分层强化学习与对抗模仿结合,学习选项(Options)级别的策略,能够捕捉专家示范中的多模态行为与高层意图。X-Sim 框架提出从真实到模拟再到真实的迁移路径:首先从人类视频中重建物理仿真环境,在仿真中训练状态级策略,随后通过策略蒸馏获得基于视觉的策略,最后通过在线领域自适应对齐真实与仿真观测。

VectorShift的博客 125

波比-JS模块化详解

1.什么是模块化 模块化是一种代码管理、组织和通信的模式 2.为什么要用模块化? 随着项目复杂度的提高,项目变得越来越难以维护,js模块化也油然而生,所有的模块都处于全局域下,容易造成命名冲突的问题,而且依赖关系不明显。引入js模块化的作用是: 为了将复杂的问题分解成更小的子问题(关注点分离) 更好地进行代码管理,方便代码替换、复用、拓展 方便多人协同开发 3.模块化发展 最开始模块化混沌时期,靠经验组织代码 使用闭包 当前阶段,CommonJS、ES6 4.循环引用 当a模块引用了b模块,b模块又

m0_70846476的博客 200

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

js模块化的历史 | 特点 | 简单实现

1.模块化的历史 概括来讲,以下是演变史 最先是将变量,方法定义在全局,我想用啥就定义啥。 其次是将变量,方法封装在一个函数里面,然后对外暴露一个接口的方式提供服务。(此时已经有模块化的雏形了) 最终比如common.js的模块化,你有一个变量,把它挂在module上面,然后把它export出去,其实和函数,对外抛出一个return,其实是相似的一个东西。 其实目前模块化处在的交错的时代,比如引入有import,require,目前还没有一个统一的规范化标准。 2.各模块化的特点 AMD特点:依赖前置

qq_43011103的博客 223

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

hey,你的CommonJS规范

CommonJS规范概述 一个文件就是一个模块,拥有单独的作用域 普通方式定义的变量,函数,对象都属于该模块内的私有属性 通过require来加载其他模块通过module.exports导出的内容 通过exports和module.exports来导出模块中要暴露的内容 简单使用一下CommonJS A文件 let str = 'hello world'; module.exports =...

weixin_34015566的博客 126

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

JavaScript模块化演进:从CommonJS到ES Module的全面解析

JavaScript模块化演进:从CommonJS到ES Module的全面解析

嗨,欢迎来到我的 优快云 博客小天地!一名深耕多年的技术发烧友。在这里,我将把日常工作中积累的宝贵经验,从复杂架构设计的精妙之处,到代码优化的实战技巧,毫无保留地分享给大家。 3984

JS模块化总结 | CommonJS、ES6

JS模块化总结 | CommonJS、ES6

yavlgloss的博客 2331

ES6的模块化和commonJS的模块化有什么区别?

ES6模块更适合于现代JavaScript开发,具有静态分析、支持异步加载、模块化和更清晰的语法。CommonJS模块主要用于Node.js环境,特点是同步加载、动态模块解析,适用于服务器端开发,但不太适合浏览器环境,除非通过打包工具进行转换。两者各有优缺点,通常在浏览器环境中使用 ES6 模块,在Node.js中则更常用 CommonJS,但在现代应用中,尤其是配合构建工具(如 Webpack、Babel)时,ES6 模块正逐渐成为标准。

dzhnb的博客 1592

前端模块化(commonJs、ES Module)

模块化开发是我们开发当中用于组织和管理代码的方法,它的目的是将复杂的应用程序去拆分为更小和更好管理的模块单元,从而提高代码的复用性和可维护性。在早期的前端开发中,javascript代码通常以脚本的形式直接嵌在html文件中,或者通过多个脚本文件呢去进行一个处置,那这种方式存在一些问题,比如全局命名冲突,代码复用困难,依赖关系难以管理等等,特别是在多人协作的场景下,这种情况尤为常见,直到模块化的出现才从根本上去解决这些问题,这也正是js为什么需要模块的原因。那js的模块化是什么?怎么用?

weijia_kmy的博客 2831

javascript模块化之CommonJS、AMD、CMD、UMD、ES6

这是一篇关于js模块化编程的总结记录 javascript模块化之CommonJS、AMD、CMD、UMD、ES6

皖林的博客 2万+

Node.js:深入探秘 CommonJS 模块化的奥秘

在Node.js上下文环境中提供了一个module对象和一个exports对象。module代表当前模块,exports是当前模块的一个属性,代表要导出的一些API。一个文件就是一个模块,把方法或者变量作为属性挂载在exports对象上即可将其作为模块的一部分进行导出。// add.js在另一个文件中,我们可以通过requireadd(1, 2);// 输出 3Node.js的模块系统基于CommonJS规范,但在实现上进行了优化和扩展。

2301_79793626的博客 1517
上一篇: 高阶函数应用 —— 柯里化与反柯里化
下一篇: EventEmitter 源码分析与简易实现
Panda Shen
博客等级 码龄10年 18粉丝 50原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值