由于JavaScript自己并无模块的概念,不支持封闭的做用域和依赖管理,传统的文件引入方式又会污染变量,甚至文件引入的前后顺序都会影响整个项目的运行。同时也没有一个相对标准的文件引入规范和包管理系统,这个时候CommonJS规范就出现了。node
下面就是本文的重头戏部分了,经过手写一个CommonJS规范,更加清晰和认识模块化的含义及如何实现的。另外本文中的示例代码须要在node.js环境中方可正常运行,不然将出现错误。事实上ES6已经出现了模块规范,若是使用ES6的模块规范是无需node.js环境的。所以,须要将commonJS规范和ES6的模块规范区分开来。正则表达式
咱们先写一段简单的代码,在node环境下运行,来看看commonJS是如何处理的:npm
(function (exports, require, module, __filename, __dirname) {});
复制代码
咱们不难发现,其实这是一个自执行函数,那么为何要加上这样一段看似多余的代码呐,这就是咱们说得CommonJS规范一个好处,它将要执行的函数封装了起来,全部的变量和方法均可以理解为是私有的了,保证了命名空间。json
前面咱们已经了解到在node中,每一个文件均可以被当作是一个模块,那么node中对于模块的导出,都是使用的相同的方法module.exports。缓存
var str='hello World';
module.exports=str;
复制代码
####3.文件导入 为了方便的使用模块,咱们可使用require方法对模块进行导入,相似于这样:bash
var a=require('./a.js');
复制代码
值的注意的是:在文件引入的过程当中,是否使用相对或者绝对路径,若是
a.js
前添加./
或者../
是证实是第三方模块,不写绝对和相对路径为内置模块,例如:fs
。app
咱们写一个简单的模块引入,经过断点,分析它的代码,并以此为来完善咱们本身的commonJS规范模块化
Module._load
,从名字中能够看出这应该是一个加载的方法,此方法传递三个参数,第一个是路径,第二个是this的指向,第三个是一个布尔值,表示为是否为必要的。断点继续运行,走到下一个方法Module._resolveFilename,这个方法是用来解析文件名称的,将相对路径解析成绝对路径。函数
var filename = Module._resolveFilename(request, parent, isMain);
复制代码
node中会对已经加载过的模块进行缓存,供下次引入时候使用,这个方法就是:Module._cache
ui
var cachedModule = Module._cache[filename];
复制代码
没有缓存的时候,node会新建一个模块,用来存放这个正在加载的模块:
var module = new Module(filename, parent);
Module._cache[filename] = module;
复制代码
而后尝试加载这个模块
tryModuleLoad(module, filename);
复制代码
而后继续回到load方法中,执行下面的代码,对扩展名进行完善:
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
复制代码
有了文件名以后就就能够拿到对应的文件内容,下面就对文件内容进行处理,咱们称这个方法为文件包裹方法:
var wrapper = Module.wrap(content);
复制代码
进入这个方法以后你会看到咱们熟悉的自执行函数,经过字符串拼接的形式进行包裹。
首先得有一个方法或者类实现这样一个规范,而后这个方法接受一个参数path(路径)
let fs = require('fs');//文件模块,用来读取文件
let path = require('path');//用来完善文件路径
let vm=require('vm');//将字符串看成JavaScript执行
function req(path) {
}
function module() { //模块相关
}
复制代码
第一步加载,传入参数路径,进入到方法中会有一个Module._resolveFilename
,用来解析文件名,咱们的代码就变成了:
let fs = require('fs');//文件模块,用来读取文件
let path = require('path');//用来完善文件路径
let vm=require('vm');//将字符串看成JavaScript执行
function req(path) {
module._load(path);//尝试加载模块
}
function module() { //模块相关
}
module._load = function (path) { //
let fileName=module._resolveFilename(path)//解析文件名
}
module._resolveFilename = function (path) {
}
复制代码
在进入这个_resolveFilename
方法的时候,传入的参数可能没有后缀,多是一个相对路径,继续完善module._resolveFilename
方法:
咱们利用正则表达式来对文件名后缀进行分析,这里只考虑是js文件仍是json文件,而后利用path模块完善文件后缀
module._resolveFilename = function (p) {
if ((/\.js$|\.json$/).test(p)) {
// 以js或者json结尾的
return path.resolve(__dirname, p);
}else{
// 没有后后缀 自动拼后缀
}
}
复制代码
若是没有文件后缀名,咱们须要补全后缀名,就调用了Module._extensions
module._extensions = {
'.js':function (module) {},
'.json':function (module) {}
}
复制代码
module._resolveFilename
方法中对_extensions
这个对象进行遍历,而后将后缀名加上继续尝试,而后经过fs模块的accessSync
方法对拼接好的路径进行判断,代码以下:
Module._resolveFilename = function (p) {
if((/\.js$|\.json$/).test(p)){
// 以js或者json结尾的
return path.resolve(__dirname, p);
}else{
// 没有后后缀 自动拼后缀
let exts = Object.keys(Module._extensions);
let realPath;
for (let i = 0; i < exts.length; i++) {
let temp = path.resolve(__dirname, p + exts[i]);
try {
fs.accessSync(temp); // 存在的
realPath = temp
break;
} catch (e) {
}
}
if(!realPath){
throw new Error('module not exists');
}
return realPath
}
}
复制代码
到如今咱们已经能够拿到完整的绝对路径和后缀名了,根据上面的分析,咱们就要去缓存中查看是否有缓存,若是有,就是用缓存的,若是没有,加入缓存中。
首先去Module._cache这个对象中查找是否有,若是有就直接返回模块中的exports,也就是cache.exports,若是没有,就新建立一个模块。并将模块的绝对路径做为module的id属性
Module._cache = {};
Module._load = function (p) { // 相对路径,可能这个文件没有后缀,尝试加后缀
let filename = Module._resolveFilename(p); // 获取到绝对路径
let cache = Module._cache[filename];
if(cache){ // 第一次没有缓存 不会进来
}
let module = new Module(filename); // 没有模块就建立模块
Module._cache[filename] = module;// 每一个模块都有exports对象 {}
//尝试加载模块
tryModuleLoad(module);
return module.exports
}
复制代码
下面就开始尝试加载这个模块,并将module.exports返回。
经过模块的id咱们能够很方便的拿到文件的扩展名,而后利用path.extname
方法来获取文件的扩展名,并调用对应扩展名下面的处理方法:
function tryModuleLoad(module){
let ext = path.extname(module.id);//扩展名
// 若是扩展名是js,调用js处理器.若是是json,调用json处理器
Module._extensions[ext](module);
}
复制代码
若是这个文件是一个json文件。由于读文件返回的是一个字符串,因此要用JSON.parse
转换读到的文件,至此对于json文件的引入就所有搞定了,因此要将module.exports赋值,这样外面return才有内容。 若是是一个js文件,用获取到的绝对路径也就是 module的id属性进行文件读取,而后调用Module.wrap对文件内容进行包裹,也就是加在对应的自执行函数,而后执行这个函数。 Module._extensions完善以下:
Module._extensions = {
'.js':function (module) {
let content = fs.readFileSync(module.id, 'utf8');
let funcStr = Module.wrap(content);
let fn = vm.runInThisContext(funcStr);
fn.call(module.exports,module.exports,req,module);
},
'.json':function (module) {
module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8'));
}
}
复制代码
咱们用俩个字符串将文件内容进行包裹并返回新的字符串
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
Module.wrap = function (script) {
return Module.wrapper[0] + script+ Module.wrapper[1];
}
复制代码
到如今咱们的代码已经基本完成了,可是如今出现的问题是每次require的代码都会被执行,咱们但愿的是有这个模块的时候要直接使用exports中的值,因此代码能够这样完善:
if(cache){ // 第一次没有缓存 不会进来
return cache.exports;
}
复制代码
上面的代码不少状况的处理我并无给出,好比path的处理等等。和真正的commonJS规范代码仍是有不少不足的地方,可是我但愿经过这样的方式能够加深你对commonJS规范的理解和使用,特此说明。