nodejs笔记(系统性学习 Node.js,手写 require)

前言

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

往期链接

Node.js 基础概念

Commonjs

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

导出

// 单个导出
exports.a = 1
exports.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 // 错误用法,导入时根本获取不到1

module.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。

举个例子:

nodejs笔记(系统性学习 Node.js,手写 require)

我现在有这样一个目录,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 打包机制的同学肯定都猜到了,其实每个模块的执行都放入了一个函数中,这样就形成了独立的作用域,类似于这样:

// 模块 a

function (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.js
console.log(1)

exports.a = 111

// index.js
require('./a.js')
require('./a.js')
require('./a.js')

// 只打印一次 1

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

最后

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

版权声明:本文内容由互联网用户投稿发布,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2211788188@qq.com 举报,一经查实,本站将立刻删除。如需转载请注明出处:https://www.wptmall.com/a/article/19098

为您推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注