CommonJS 模块化简易实现

2018-10-11 admin

在这里插入图片描述

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、我们在虚拟机中执行构建的闭包函数时利用执行上/下文 callthis 指向了模块实例的 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

<hr/>

CommonJS 模块查找规范

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

在这里插入图片描述

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

原文链接:https://segmentfault.com/a/1190000016654114

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-42447.html

文章标题: CommonJS 模块化简易实现

相关文章
js实现鼠标左右移动,图片也跟着移动效果
效果:鼠标往左移,图片对应右移,鼠标往右移,图片就左移动。图片距离越远偏移距离越大。 思路:首先获取图片原先的距离。设置一个变化值,图片的最终距离等于原先的距离加上变化值 布局:大盒子里面是图片,大盒子position:relative;图...
2017-02-17
js实现文字向上轮播功能
话不多说,请看实现代码: &lt;!DOCTYPE html&gt; &lt;html lang=&quot;en&quot;&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt;...
2017-03-10
纯css实现窗户玻璃雨滴逼真效果
这里仅是用CSS技术来演示这样的一个场景,可能并不太实用。然而这是一个探索CSS新功能的最佳机会。可以让你尝试使用一些新特性和新工具。并且逐渐将在工作中实践。在制作窗口雨滴效果,将使用到HAML和Sass。 案例效果 看到上面的效果是不是...
2017-03-29
canvas实现流星雨的背景效果
看到一个很棒的流星雨效果。修改一下样式就可以作为网页背景了。。! &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;utf-8&quot...
2017-03-09
js实现的tab标签切换效果代码分享
这是一款基于js实现的tab标签切换效果,是一款无需jQuery,原生javascript制作的tab切换效果源码。点击上面的标题即可实现对应页面的切换功能,非常具有实用价值。 为大家分享的js实现的tab标签切换效果代码如下 &lt;!...
2017-03-31
JavaScript实现简单的数字倒计时
这里是一个JavaScript中显示倒计时的方法,从10一秒一秒地往下减直到计时结束(即0),代码如下: runCount(10); function runCount(t){ if(t&gt;0){ document.getE...
2017-03-23
Bootstrap显示与隐藏简单实现代码
本文实例为大家分享了bootstrap显示隐藏的具体代码,供大家参考,具体内容如下 &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;utf-8&quot;&gt; &lt;meta http...
2017-03-14
Node.js实现Excel转JSON
一直在做一个关于网上选课的系统,选用了时下比较流行的node.js。今天在想怎么把学生或者老师的信息导入进去,涉及数量比较多一点,我手边又正好有一部分excel的表格。就想把excel转成json然后倒入到mongodb中去。 搜了下网上的...
2017-03-23
Javascript实现数组中的元素上下移动
交换数组可以实现元素上下移动了,这个效果我们在表格或以前排序算法中都会用到,下面来看一个JavaScript下实现交换数组元素上下移动例子 在写项目的时候,要实现一个数组记录上下移动的示例。写起来也没有没麻烦,无非是交换数组元素。最终实现代...
2017-05-02
JS实现的通用表单验证插件完整实例
本文实例讲述了JS实现的通用表单验证插件。分享给大家供大家参考。具体如下: 这里演示一个通用的JS表单验证插件代码。使用方法:第一步:需设定表单项数据类型,第二步:实例表单验证,验证错误提示说明(程序有自带相关错误提示,可自定义每项验证错误...
2017-03-29
回到顶部