js 读取json文件_系统性学习 Node.js(2)——手写 require

本文深入探讨Node.js的模块系统,包括CommonJS规范的应用、模块的导出与导入机制,并通过示例说明如何实现模块加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

现在 Node.js 对于前端越来越重要,笔者现在也感受到了学好 Node.js 可以极大的提升前端竞争力。之前也或多或少的接触过,但是没有一个系统的学习过程,所以笔者接下来打算系统的去学习 Node.js,同时建立一个从零到一学习 Node 的一个博客。希望我的学习经验可以帮助到大家,

往期链接

Node.js 基础概念

Commonjs

Node 里面一个文件就是一个模块,每个文件都有独立的作用域,模块的规范遵循 Commonjs 规范。我们通过具体的例子来看一下如何使用 Node 的模块机制。

导出

// 单个导出exports.a = 1exports.b = 2// 多个导出module.exports = {  a: 1,  b: 2}

Node 里可以使用 exports 跟 module.exports 导出,exports 就是 module.exports 的引用。初始状态时 exports === module.exports === {}。实际上内部操作就是下面这样

module.exports = {}exports = module.exports

导入时获取的是 module.exports,不是 exports ,所以 exports 不能重新赋值。

我们来看一下容易错误的示例。

exports = 1 // 错误用法,导入时根本获取不到1module.exprots = {  a: 1}exports.b = 2 // 导入时也获取不到 b,因为 module.exports 的引用变了。

导入

示例1

const a = require('./a')

只能导入 .js 跟 .json 文件,可以省略文件后缀。如果省略优先查找 .js ,然后查找 .json,如果都没找到则会查找同名的文件目录下的 package.json 文件内的 main 字段对应的文件,如果还没找到则查找该文件目录下的 index.js,然后查找 a.json。

举个例子:

5297e8086c7ccdd1aa7d1bde5533fcb8.png

我现在有这样一个目录,package.json 内 main 字段的值是 main.js

如果我在 index.js 目录内 require('./a'),那么文件的查找顺序是

./a.js => ./a.json => ./a/main.js => ./a/a.js => ./a/a.json

如果导入时没有文件标识符,即没有 / 或 ./ 或 ../ ,则代表引入 node 的核心包或第三方模块。优先查看是否为核心模块,如果不是则在 node_modules 内查找第三方模块。

查找 node_modules 时,会从当前目录的 node_modules 目录开始查找,一直到根目录的 node_modules。

关于 Commonjs 详细的描述可以查看 Commonjs 的规范,附规范地址。

实现 Commonjs

基础知识我们了解完了,接下来我们实现一下 require,以助于我们更好的理解 Commonjs。

下面的代码与源码的执行流程一样,以便于更好的理解源码,我们做了一些删减。

首先我们先简单的剖析一下模块导入的原理。

在 Node 里面一个文件就是一个模块,每个模块都有一个独立的作用域。熟悉 webpack 打包机制的同学肯定都猜到了,其实每个模块的执行都放入了一个函数中,这样就形成了独立的作用域,类似于这样:

// 模块 afunction (exports, module, require) {  // to do something...    module.exports = { x: 1 }}

我们在 require 的时候,其实就是调用了这个函数,然后传入我们实现好的 require 与 module。

原理理解了,那我们直接上代码。

先实现一下 require 函数

function require(filename) {  filename = Module._resolveFilename(filename)  const module = new Module(filename)  module.load()  return module.exports}

require 方法接收文件名为参数,方法内调用 Module._resolveFilename 方法得到最终的文件名,因为我们传入的有可能是不带文件后缀的,所以我们要解析一下。

然后 new Module 得到一个 module 对象,通过 module.load 加载 module,最后返回 module.exports,也就说我们最终拿到的就是 module.exports。

我们来看一下 Module 的具体实现

function Module(filename) {  this.filename = filename  // 获取文件目录名  this.dirname = path.dirname(filename)  // 导出对象  this.exports = {}}Module.prototype.load = function() {  const extension = path.extname(this.filename)  Module._extensions[extension](this)}// 可支持加载的文件Module._extensions = {}// json 文件解析Module._extensions['.json'] = function(module) {  const content = fs.readFileSync(module.filename, 'utf-8')  module.exports = JSON.parse(content)}// js 文件解析Module._extensions['.js'] = function(module) {  const content = fs.readFileSync(module.filename, 'utf-8')  // const wraped = Module._wrapper(content)  const fn = new Function(    'exports',    'module',    'require',    '__filename',    '__dirname',    content  )  fn.call(module.exports, module.exports, module, require, module.filename, module.dirname)}// 文件路径解析Module._resolveFilename = function(filename) {  let filepath = path.resolve(__dirname, filename)  let isExists = fs.existsSync(filepath)  if (isExists) {    return filepath  }  const extensions = ['.js', '.json']  for (let i = 0; i < extensions.length; i += 1) {    let path = `${filepath}${extensions[i]}`    isExists = fs.existsSync(path)    if (isExists) {      return path    }  }  throw new Error('module not found')}

代码逻辑不是特别复杂,顺着 require 的调用逻辑捋一捋应该差不多。这里用到了几个核心 api 可能需要讲解一下。

path.resolve // 获取文件的绝对路径

path.extname // 获取文件后缀名

fs.existsSync // 判断文件是否存在

fs.readFileSync // 读取文件内容

__dirname // 当前文件所在的绝对目录

__filename // 当前文件所在的绝对地址

后面我们会专门对核心 api 作讲解,这里先简单了解一下。

这里我们着重讲一下 Module._extensions['.js']

// js 文件解析Module._extensions['.js'] = function(module) {  const content = fs.readFileSync(module.filename, 'utf-8')  // const wraped = Module._wrapper(content)  const fn = new Function(    'exports',    'module',    'require',    '__filename',    '__dirname',    content  )  fn.call(module.exports, module.exports, module, require, module.filename, module.dirname)}

这里读取了引入文件的内容,然后通过 new Funcion 将内容当作函数体放入 Functin 内,同时这个函数接收 requre, module, exports, __filename, __ dirname, 作为参数。我们在调用时也将这些参数传递了进去。这样当函数执行时,也就可以访问到了。

假如我们文件的内容是 module.exports = {a: 1},这是创建的函数就是

function(exports, module, require, __filename, __dirname) {  module.exports = {a: 1}}

函数执行时

// module 就是当前 Module 实例fn.call(module.exports, module.exports, module, require, module.filename, module.dirname)// 

fn 的 this 指向 module.exports,同时将 module.expors, module, require, module.filename, module,dirname 传递进去。

执行时就把 module.exports 的值改变了,所以我们 require 的时候就拿到了导出的值。

以上就是模块导出的核心代码了。但是这里还漏了一点,就是模块无论导入多少次,模块的代码只执行一次。举例:

// a.jsconsole.log(1)exports.a = 111// index.jsrequire('./a.js')require('./a.js')require('./a.js')// 只打印一次 1

那我们如何实现这个功能呢?评论区留下你的答案吧。

最后

如果这篇文章能够对你有帮助,期望得到你的点赞~~~

接下来会持续更新 Node 的学习记录,欢迎关注我的专栏,让我们一起努力,一起进步~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值