背景
javascript最初主要是两个方面的作用。一方面是做表单的验证,另一个方面是做一些特效来提供友好的人机交互体验。经历了漫长的发展后,javascript天然缺乏像java的类文件,python的import机制,没有模块系统。想要用javascript开发大型项目,文件的组织就成为一个很大的问题。我们可能都写过如下的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>example</title>
</head>
<body>
// 往往这些js文件还有顺序关系
<script src="jquery.js"></script>
<script src="xxxa.js"></script>
<script src="xxxb.js"></script>
<script src="xxxc.js"></script>
</body>
</html>
复制代码
直到有了CommonJS规范,javascript大放光彩。node就是借鉴CommonJS的modules规范实现了一套易用的模块系统,接下来,我们就聊一聊node是怎么实现的。
node中模块的使用
在聊使用之前,我们先聊聊node中模块的使用方法。
require函数的使用
require函数用于在当前模块中加载和使用别的模块例如:
// 加载node自己的模块和三方模块
const path = require('path')
// 加载自己写的模块
const foo = require('./foo')
// 在node中支持.js,.json,.node为扩展名的文件。
const mock = require('./goodList.json')
const nodeDem = require('./nodeDem.node')
复制代码
exports 和module.exports 的使用
实质上exports是module.exports的一个引用,至于为什么下面我们会说到,这里我们谈谈他的用法。 我们创建一个exports-demo.js的文件。打下如下代码:
exports.a = 'a'
exports.name = function () {
console.log('my name is xiaopang')
}
复制代码
我们创建另外一个文件user.js。打下如下代码:
var ExportsDemo = require('./exports-demo.js')
ExportsDemo.a // a
ExportsDemo.name() // my name is xiaopang
复制代码
那上面的代码用module.exports怎么写呢?同样我们创建一个module-exports-demo.js文件。他的代码如下:
var f = {
a: 'a',
name: function () {
console.log('my name is xiaopang')
}
}
module.exports = f
复制代码
在user.js中调用代码如下:
var ModuleExportsDemo = require('./module-exports-demo.js')
ModuleExportsDemo.a // a
ModuleExportsDemo.name() // my name is xiaopang
复制代码
那么问题来啦什么时候用exports,什么时候用module.exports呢?
如果你想让你的模块返回一个特殊的对象类型,比如构造函数,那么你得使用 module.exports ;如果你只想模块作为一个典型的模块实例(module instance),那么就用exports。这个仅仅是一些经验之谈。
node的模块实现即require函数的实现
前面我们聊了聊node中模块的使用,并留下了一个坑那就是exports和module.exports到底是什么关系,现在我们就慢慢填坑。在填坑之前,我们先聊一聊require都干了啥?聊完这个之后呢,我们一步一步实现就好啦。大体分为一下四步:
- 路径分析
- 首先require会根据我们传的url,来找到我们文件的路径。
- 模块加载
- 根据之前找到的路径来读取文件,然后讲字符串函数,转换为函数并执行。
- 解析文件名
- node中支持以.js、.node、.json为后缀的文件,有时候我们可能并不会输入文件的后缀名例如 var foo = require('foo')
- 缓存
- 在使用的时候你会发现,同一个模块你引用多次,node并不会执行多次。 说完上面这些,我们先搭个大体的架子,随后我们一步步实现。
// require 函数
function $require (path) {
Module._load(path)
}
// 熟悉的module函数
function Module (id) {
this.id = id;
this.exports = {}
}
Module_load = function (path) {
let filename = Module._resolveFilename(path)
var module = new Module(path)
resolveAndLoadFile(module)
}
// 加载模块
Module._resolveFilename = function (name) {}
// 不同的文件扩展名对应不同的解析函数,这里不对.node文件进行处理
Module._extension = {
'.js': function () {},
'.json': funciton () {}
}
// 缓存
Module._cache = {}
复制代码
模块的加载
Module._resolveFilename = function (path) {
// 如果输入的URL带有.js或者.json后缀的话,我们直接解析就是
if ((/.js$|.json$/).test(path)) {
return path.resolve(__dirname, path)
} esle {
// 如果没有后缀,我们就需要给他拼接上
let exts = Object.keys(Module._extension);
let realPath;
for (let i = 0; i < exts.length; i++) {
let temp = path.resolve(__dirname, path + exts[i])
// 判断文件是否真的存在
try {
fs.accessSync(temp)
realPath = temp
} cache (e) {
throw new Error(e)
}
}
if (!realPath) {
throw new Error('module is not exists')
}
}
}
复制代码
解析文件名
function resolveAndLoadFile (module) {
const ext = path.extname(module.id)
Module._extension[ext](module)
}
对于json处理很简单,只需要把json字符串转换为json就可以啦
Module._extension = {
'.json': function (module) {
Module.exports = JSON.parse(fs.readFileSync(module.id), 'utf8')
}
}
对于js的处理,比较复杂,主要是要将字符串函数转为函数即: "console.log('hello,world')"
进行转化。在这里我们不深究,实际上node给我们提供了一个vm的沙箱,它会帮我们处理这件事情。
Module._extension = {
'.js': function (module) {
const content = fs.readFileSync(module.id, 'utf8')
const funStr = Module.wrap(content)
const fn = vm.runInThisContext(funStr)
fn.call(module.exports, module.exports, $require, module)
}
}
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
Module.wrap = function (script) {
return Module.wrapper[0] + script + Module.wrapper[1]
}
复制代码
缓存
Module._load = function (path) {
// 解析出绝对路径
let filename = Module._resolveFilename(path)
// 解析出绝对路径后,匹配对应的文件名后缀,对应不同的解析方法
let module = new Module(filename)
// 缓存处理
if (!Module._cache[filename]) {
Module._cache[filename] = module
} else {
return Module._cache[filename].exports
}
// 尝试解析模块
resolveAndLoadFile(module)
}
复制代码
好啦,以上就是全部啦,欢迎大家拍砖。