浅谈node的模块机制的实现

本文介绍了Node.js中模块系统的背景及发展历程,详细讲解了CommonJS规范下如何使用require函数加载模块,以及exports与module.exports的区别。并通过代码示例展示了模块加载、文件名解析和缓存机制的具体实现。

背景

javascript最初主要是两个方面的作用。一方面是做表单的验证,另一个方面是做一些特效来提供友好的人机交互体验。经历了漫长的发展后,javascript天然缺乏像java的类文件,python的import机制,没有模块系统。想要用javascript开发大型项目,文件的组织就成为一个很大的问题。我们可能都写过如下的代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>example</title>
</head>
<body>
// 往往这些js文件还有顺序关系
<script src="jquery.js"></script>
<script src="xxxa.js"></script>
<script src="xxxb.js"></script>
<script src="xxxc.js"></script>
</body>
</html>
复制代码

直到有了CommonJS规范,javascript大放光彩。node就是借鉴CommonJS的modules规范实现了一套易用的模块系统,接下来,我们就聊一聊node是怎么实现的。

node中模块的使用

在聊使用之前,我们先聊聊node中模块的使用方法。

require函数的使用

require函数用于在当前模块中加载和使用别的模块例如:

// 加载node自己的模块和三方模块
const path = require('path')
// 加载自己写的模块
const foo = require('./foo')
// 在node中支持.js,.json,.node为扩展名的文件。
const mock = require('./goodList.json')
const nodeDem = require('./nodeDem.node')
复制代码
exports 和module.exports 的使用

实质上exports是module.exports的一个引用,至于为什么下面我们会说到,这里我们谈谈他的用法。 我们创建一个exports-demo.js的文件。打下如下代码:

exports.a = 'a'
exports.name = function () {
    console.log('my name is xiaopang')
}
复制代码

我们创建另外一个文件user.js。打下如下代码:

var ExportsDemo = require('./exports-demo.js')
ExportsDemo.a // a
ExportsDemo.name() // my name is xiaopang
复制代码

那上面的代码用module.exports怎么写呢?同样我们创建一个module-exports-demo.js文件。他的代码如下:

var f = {
    a: 'a',
    name: function () {
        console.log('my name is xiaopang')
    }
}
module.exports = f
复制代码

在user.js中调用代码如下:

var ModuleExportsDemo = require('./module-exports-demo.js')
ModuleExportsDemo.a // a
ModuleExportsDemo.name() // my name is xiaopang
复制代码

那么问题来啦什么时候用exports,什么时候用module.exports呢?
如果你想让你的模块返回一个特殊的对象类型,比如构造函数,那么你得使用 module.exports ;如果你只想模块作为一个典型的模块实例(module instance),那么就用exports。这个仅仅是一些经验之谈。

node的模块实现即require函数的实现

前面我们聊了聊node中模块的使用,并留下了一个坑那就是exports和module.exports到底是什么关系,现在我们就慢慢填坑。在填坑之前,我们先聊一聊require都干了啥?聊完这个之后呢,我们一步一步实现就好啦。大体分为一下四步:

  • 路径分析
    • 首先require会根据我们传的url,来找到我们文件的路径。
  • 模块加载
    • 根据之前找到的路径来读取文件,然后讲字符串函数,转换为函数并执行。
  • 解析文件名
    • node中支持以.js、.node、.json为后缀的文件,有时候我们可能并不会输入文件的后缀名例如 var foo = require('foo')
  • 缓存
    • 在使用的时候你会发现,同一个模块你引用多次,node并不会执行多次。 说完上面这些,我们先搭个大体的架子,随后我们一步步实现。
// require 函数
function $require (path) {
    Module._load(path)
}
// 熟悉的module函数
function Module (id) {
    this.id = id;
    this.exports = {}
}
Module_load = function (path) {
    let filename = Module._resolveFilename(path)
    var module = new Module(path)
    resolveAndLoadFile(module)
}
// 加载模块
Module._resolveFilename = function (name) {}
// 不同的文件扩展名对应不同的解析函数,这里不对.node文件进行处理
Module._extension = {
    '.js': function () {},
    '.json': funciton () {}
}
// 缓存
Module._cache = {}
复制代码
模块的加载
    Module._resolveFilename = function (path) {
        // 如果输入的URL带有.js或者.json后缀的话,我们直接解析就是
        if ((/.js$|.json$/).test(path)) {
            return path.resolve(__dirname, path)
        } esle {
            // 如果没有后缀,我们就需要给他拼接上
            let exts = Object.keys(Module._extension);
            let realPath;
            for (let i = 0; i < exts.length; i++) {
                let temp = path.resolve(__dirname, path + exts[i])
                // 判断文件是否真的存在
                try {
                    fs.accessSync(temp)
                    realPath = temp
                } cache (e) {
                    throw new Error(e)
                }
            }
            if (!realPath) {
                throw new Error('module is not exists')
            }
        }
    }
复制代码
解析文件名
function resolveAndLoadFile (module) {
    const ext = path.extname(module.id)
    Module._extension[ext](module)
}
对于json处理很简单,只需要把json字符串转换为json就可以啦
Module._extension = {
    '.json': function (module) {
        Module.exports = JSON.parse(fs.readFileSync(module.id), 'utf8')
    }
}
对于js的处理,比较复杂,主要是要将字符串函数转为函数即: "console.log('hello,world')"  
进行转化。在这里我们不深究,实际上node给我们提供了一个vm的沙箱,它会帮我们处理这件事情。
Module._extension = {
    '.js': function (module) {
        const content = fs.readFileSync(module.id, 'utf8')
        const funStr = Module.wrap(content)
        const fn = vm.runInThisContext(funStr)
        fn.call(module.exports, module.exports, $require, module)
    }
}

Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]

Module.wrap = function (script) {
    return Module.wrapper[0] + script + Module.wrapper[1]
}
复制代码
缓存
Module._load = function (path) {
    // 解析出绝对路径
    let filename = Module._resolveFilename(path)
     // 解析出绝对路径后,匹配对应的文件名后缀,对应不同的解析方法
     let module = new Module(filename)
     // 缓存处理
    if (!Module._cache[filename]) {
        Module._cache[filename] = module
    } else {
        return Module._cache[filename].exports
    }
    // 尝试解析模块
    resolveAndLoadFile(module)
}
复制代码

好啦,以上就是全部啦,欢迎大家拍砖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值