前言
现在 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。
举个例子:
我现在有这样一个目录,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
那我们如何实现这个功能呢?评论区留下你的答案吧。
最后
如果这篇文章能够对你有帮助,期望得到你的点赞~~~