背景
javascript最初主要是两个方面的作用。一方面是做表单的验证,另一个方面是做一些特效来提供友好的人机交互体验。经历了漫长的发展后,javascript天然缺乏像java的类文件,python的import机制,没有模块系统。想要用javascript开发大型项目,文件的组织就成为一个很大的问题。我们可能都写过如下的代码:
example // 往往这些js文件还有顺序关系复制代码
直到有了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 // aExportsDemo.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 // aModuleExportsDemo.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)}复制代码
好啦,以上就是全部啦,欢迎大家拍砖。