前言
现在 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。
举个例子:

我现在有这样一个目录,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 的学习记录,欢迎关注我的专栏,让我们一起努力,一起进步~~~