零、前言
本文通过Node.js require 的特性,自行Mock了一个 global.$require,主要目的是说明原理,并给出一些代码书写的思路,所有代码整理在 git 仓库 MockRequire 中
一、划分功能模块,定义所需接口
// 缓存模块 require 的结果
var cached = {}
// 初始化的时候就会装载进来
var coreModule = {}
// 获取入口 js
// 用于后续的相对路径转绝对路径
var ENTRYJS = path.resolve(process.cwd(), process.argv[1])
var stack = new Stack(getParent(ENTRYJS))
function $require(pathname) {
// 是否核心模块
if (coreModule[pathname]) { // 核心模块
return coreModule[pathname]
}
// 是否已存在缓存
if (!cached[pathname]) {
// 获取到模块的真实位置
// 1, 如果是绝对路径,则转换相对路径
// 2. 如果是包,则查找是否存在 package.json,并读取 main 属性
var targetModule = getModuleLocation(pathname)
if (-1 === targetModule) {
throw new Error('模块' + pathname + '未找到') // 包未找到
}
// 1. 如果是目录,则添加 /index
// 2. 如果是文件,先检测文件是否存在
// 3. 若不存在,则检测添加后缀名后的文件是否存在, ['', '.js', '.json', '.node']
var targetFile = getRealPath(targetModule)
if (-2 === targetFile) {
throw new Error('模块' + pathname + '未找到') // 文件未找到
}
stack.push(getParent(targetFile))
// 找到具体的文件,并执行,执行完成赋值到缓存中
cached[pathname] = _require(targetFile)
stack.pop()
}
return cached[pathname]
}
// 需要实现的接口
// 数据结构 - 栈
function Stack() {}
// 获取文件的所在目录的绝对路径
function getParent(pathname) {}
// 查找模块的所在位置
function getModuleLocation(pathname) {}
// 从模块的所在位置,获取到真实的 js 路径
function getRealPath(pathname) {}
// 纯粹的模块执行处理函数
function _require(pathname) {}
可以看到,至此已经有一个不可执行的 require 函数了。
在真实的开发中,这一步其实最需要代码功底与最需要功能抽象能力的一步,主要要做好两件事情:
- 在开始编码前,进行尽可能合理的功能模块划分,可以让代码逻辑清晰,减少重复步骤 (DRY),并增强后期的代码可维护性
- 定义你需要哪些接口。如果是比较复杂的功能,且不是独立开发的话,这一环节做的好坏,合理地划分与合理地分配,决定团队合作开发是否可以配合恰当
二、实现接口
这一步骤,主要是对上述过程需要的接口进行实现。其实是一个分而治之的思想,实现一个很复杂的东西可能我们不会,但是实现具体的功能细节,在一定输入输出的条件下,每个人都能轻易的写出符合需求的算法,并进行调优。
1. 数据结构与工具函数类
a. 栈
function Stack(...args) {
this._stack = new Array(...args);
}
Stack.prototype = {
top: function () {
return this._stack .slice(-1)[0]
},
push: function (...args) {
this._stack.push(...args)
},
pop: function () {
this._stack.pop()
},
constructor: Stack
}
需要知识点:
- 栈的概念与常用方法
b. 获取文件所在目录
function getParent(pathname) {
return path.parse(pathname).dir
}
需要知识点:
- path.parse 作用
2. 获取模块所在位置
a. 获取模块所映射到的绝对路径
function getModuleLocation(pathname) {
var moduleType = getModuleType(pathname)
var map = {
[MODULE_TYPE.ABSOLUTE_PATH]: function () {
return pathname
},
[MODULE_TYPE.RELEALITIVE_PATH]: function () {
return path.resolve(stack.top(), pathname)
},
[MODULE_TYPE.IN_NODE_MODULES]: function () {
var parent = stack.top()
while (!fs.lstatSync(path.resolve(parent, 'node_modules', pathname))) {
parent = getParent(pathname)
if (!parent) {
return -1
}
}
// 从包名 到 绝对路径
pathname = path.resolve(stack.top(), 'node_modules', pathname)
// 输出包的 main 文件
pathname = getPackageMain(pathname)
return pathname
}
}
return map[moduleType]()
}
/**************** 分割线 *******************/
// 以下部分,是 getModuleLocation 自身需要实现的接口,往往是开发过程中自行提炼的,其他模块不通用
var MODULE_TYPE = {
"ABSOLUTE_PATH": 1,
"RELEALITIVE_PATH": 2,
"IN_NODE_MODULES": 0
}
function getModuleType(pathname) {
if (path.isAbsolute(pathname)) {
return MODULE_TYPE.ABSOLUTE_PATH
}
if (pathname.MODULE_TYPE('.')) {
return MODULETYPE.RELEALITIVE_PATH
}
return MODULE_TYPE.IN_NODE_MODULES
}
function getPackageMain(pathname) {
try {
var packageJson = fs.readFileSync(path.resolve(pathname, 'package.json')).toString('utf-8')
var json = JSON.parse(packageJson)
if (json.main) {
return path.resolve(pathname, json.main)
}
} catch(e) {
return pathname
}
}
需要知识点:
- 判断是否包的一些逻辑
- package 的查找顺序是就近原则
- package.main 属性作用
b. 对 ‘/index.js’, ‘.js’,’.node’, ‘.json’ 可能有的省略进行补齐
function getRealPath(pathname) {
var list = ['', '.js', '.json', '.node']
var stats
try {
stats = fs.lstatSync(pathname)
} catch (e) {
return -2
}
if (stats) {
if (stats.isFile()) {
return pathname
} else if (stats.isDirectory()) {
pathname = path.join(pathname, 'index')
}
}
for(var i = 0; i < list.length; i++) {
var fullname = pathname + item
try {
fs.lstatSync(fullname)
return fullname
} catch (e) {}
}
return -2
}
需要知识点:
- require 函数后缀格式的补充策略
3. _require 函数
function _require(pathname) {
// 定义一个Module对象
var Module = function() {
this.exports = {}
}
// 引入nodejs 文件模块 下面是nodejs中原生的require方法
var fs = require('fs')
// 同步读取该文件
var sourceCode = fs.readFileSync(pathname, 'utf8')
// 字符串转换成函数
var moduleExportsFunc = new Function('module', 'exports', `${sourceCode}; return module.exports;`)
// 实例化一个Module 里面有一个exports属性
var module = new Module()
// 把module 和 它内部的module.exports都作为参数传进去
// 并得到挂在到module.exports 或 exports上的功能
var res = moduleExportsFunc(module, module.exports)
// 最终我们拿到了path代表的文件模块提供的API
return res
}
需要知识点:
- JS new Function 作用
- module.exports
至此,我们简单粗暴地模拟了 Node.js 中的 require。
当然也遗留着一些问题。比如,当出现异步 require 的时候,由于当前模块已经执行完,用于存储当前模块所在目录的 stack 已经被清空了,会出现相对路径查找失败的问题。比较可取解决方法是,在eval前将当前模块所有的 $requre 函数的参数处理成绝对路径
那就动手也写一写吧,可以拓展下手写一个 commonjs 语法的打包器。
End
本文是我参考一些关于 NodeJS require 特性后,在未参照源码情况下YY出来的方案,主要提供一个思路。若有不恰当的地方,还望指正。 本文中实现的 $require 函数记录在了 github MockRequire