模拟Node.js中的require
发布于 3 个月前 作者 ImHype 463 次浏览 来自 分享

零、前言

本文通过Node.js require 的特性,自行Mock了一个 global.$require,主要目的是说明原理,并给出一些代码书写的思路,所有代码整理在 git 仓库 MockRequire

一、划分功能模块,定义所需接口

// 缓存模块 require 的结果
var cached = {}
// 初始化的时候就会装载进来
var coreModule = {}

// 获取入口 js
// 用于后续的相对路径转绝对路径
var ENTRYJS = path.resolve(process.cwd(), process.argv[1])
var stack = new Stack(getParent(ENTRYJS))

function $require(pathname) {
    // 是否核心模块
    if (coreModule[pathname]) { // 核心模块
        return coreModule[pathname]
    }

    // 是否已存在缓存
    if (!cached[pathname]) {
        // 获取到模块的真实位置
        // 1, 如果是绝对路径,则转换相对路径
        // 2. 如果是包,则查找是否存在 package.json,并读取 main 属性
        var targetModule = getModuleLocation(pathname)

        if (-1 === targetModule) {
            throw new Error('模块' + pathname + '未找到') // 包未找到
        }

        // 1. 如果是目录,则添加 /index
        // 2. 如果是文件,先检测文件是否存在
        // 3. 若不存在,则检测添加后缀名后的文件是否存在, ['', '.js', '.json', '.node']
        var targetFile = getRealPath(targetModule)
        if (-2 === targetFile) {
            throw new Error('模块' + pathname + '未找到') // 文件未找到
        }

        stack.push(getParent(targetFile))
    
        // 找到具体的文件,并执行,执行完成赋值到缓存中
        cached[pathname] = _require(targetFile)
        stack.pop()
    }
        
    return cached[pathname]
}


// 需要实现的接口
// 数据结构 - 栈
function Stack() {}
// 获取文件的所在目录的绝对路径
function getParent(pathname) {}


// 查找模块的所在位置
function getModuleLocation(pathname) {}
// 从模块的所在位置,获取到真实的 js 路径
function getRealPath(pathname) {}


// 纯粹的模块执行处理函数
function _require(pathname) {}

可以看到,至此已经有一个不可执行的 require 函数了。

在真实的开发中,这一步其实最需要代码功底与最需要功能抽象能力的一步,主要要做好两件事情:

  1. 在开始编码前,进行尽可能合理的功能模块划分,可以让代码逻辑清晰,减少重复步骤 (DRY),并增强后期的代码可维护性
  2. 定义你需要哪些接口。如果是比较复杂的功能,且不是独立开发的话,这一环节做的好坏,合理地划分与合理地分配,决定团队合作开发是否可以配合恰当

二、实现接口

这一步骤,主要是对上述过程需要的接口进行实现。其实是一个分而治之的思想,实现一个很复杂的东西可能我们不会,但是实现具体的功能细节,在一定输入输出的条件下,每个人都能轻易的写出符合需求的算法,并进行调优。

1. 数据结构与工具函数类

a. 栈

function Stack(...args) {
    this._stack = new Array(...args);
}
Stack.prototype = {
    top: function () {
        return this._stack .slice(-1)[0]
    },
    push: function (...args) {
        this._stack.push(...args)
    },
    pop: function () {
        this._stack.pop()
    },
    constructor: Stack
}

需要知识点:

  1. 栈的概念与常用方法

b. 获取文件所在目录

function getParent(pathname) {
    return path.parse(pathname).dir
}

需要知识点:

  1. path.parse 作用

2. 获取模块所在位置

a. 获取模块所映射到的绝对路径

function getModuleLocation(pathname) {
    var moduleType = getModuleType(pathname)
    var map = {
        [MODULE_TYPE.ABSOLUTE_PATH]: function () {
            return pathname
        },
        [MODULE_TYPE.RELEALITIVE_PATH]: function () {
            return path.resolve(stack.top(), pathname)
        },
        [MODULE_TYPE.IN_NODE_MODULES]: function () {
            var parent = stack.top()
            while (!fs.lstatSync(path.resolve(parent, 'node_modules', pathname))) {
                parent = getParent(pathname)
                if (!parent) {
                    return -1
                }
            }
            // 从包名 到 绝对路径
            pathname = path.resolve(stack.top(), 'node_modules', pathname)
            
            // 输出包的 main 文件
            pathname = getPackageMain(pathname)
            
            return pathname
        }
    }

    return map[moduleType]()
}
/**************** 分割线 *******************/
// 以下部分,是 getModuleLocation 自身需要实现的接口,往往是开发过程中自行提炼的,其他模块不通用
var MODULE_TYPE = {
    "ABSOLUTE_PATH": 1,
    "RELEALITIVE_PATH": 2,
    "IN_NODE_MODULES": 0
}

function getModuleType(pathname) {
    if (path.isAbsolute(pathname)) {
        return MODULE_TYPE.ABSOLUTE_PATH
    }

    if (pathname.MODULE_TYPE('.')) {
        return MODULETYPE.RELEALITIVE_PATH
    }

    return MODULE_TYPE.IN_NODE_MODULES
}

function getPackageMain(pathname) {
    try {
        var packageJson = fs.readFileSync(path.resolve(pathname, 'package.json')).toString('utf-8')
        var json = JSON.parse(packageJson)    
        if (json.main) {
            return path.resolve(pathname, json.main)
        }
    } catch(e) {
        return pathname
    }
}

需要知识点:

  1. 判断是否包的一些逻辑
  2. package 的查找顺序是就近原则
  3. package.main 属性作用

b. 对 ‘/index.js’, ‘.js’,’.node’, ‘.json’ 可能有的省略进行补齐

function getRealPath(pathname) {
    var list = ['', '.js', '.json', '.node']
    var stats

    try {
        stats = fs.lstatSync(pathname)    
    } catch (e) {
        return -2
    }
    

    if (stats) {
        if (stats.isFile()) {
            return pathname    
        } else if (stats.isDirectory()) {
            pathname = path.join(pathname, 'index')
        }
    }

    for(var i = 0; i < list.length; i++) {
        var fullname = pathname + item
        try {
            fs.lstatSync(fullname)
            return fullname
        } catch (e) {}
    }

    return -2
}

需要知识点:

  1. require 函数后缀格式的补充策略

3. _require 函数

function _require(pathname) {
    // 定义一个Module对象
    var Module = function() {
        this.exports = {}
    }
    
    // 引入nodejs 文件模块 下面是nodejs中原生的require方法
    var fs = require('fs')

    // 同步读取该文件
    var sourceCode = fs.readFileSync(pathname, 'utf8')

    // 字符串转换成函数
    var moduleExportsFunc = new Function('module', 'exports', `${sourceCode}; return module.exports;`)

    // 实例化一个Module 里面有一个exports属性
    var module = new Module()

    // 把module 和 它内部的module.exports都作为参数传进去 
    // 并得到挂在到module.exports 或 exports上的功能
    var res = moduleExportsFunc(module, module.exports)

    // 最终我们拿到了path代表的文件模块提供的API
    return res 
}

需要知识点:

  1. JS new Function 作用
  2. module.exports

至此,我们简单粗暴地模拟了 Node.js 中的 require。

当然也遗留着一些问题。比如,当出现异步 require 的时候,由于当前模块已经执行完,用于存储当前模块所在目录的 stack 已经被清空了,会出现相对路径查找失败的问题。比较可取解决方法是,在eval前将当前模块所有的 $requre 函数的参数处理成绝对路径

那就动手也写一写吧,可以拓展下手写一个 commonjs 语法的打包器。

End

本文是我参考一些关于 NodeJS require 特性后,在未参照源码情况下YY出来的方案,主要提供一个思路。若有不恰当的地方,还望指正。 本文中实现的 $require 函数记录在了 github MockRequire

回到顶部