内容简介:因为JavaScript本身并没有模块的概念,不支持封闭的作用域和依赖管理,传统的文件引入方式又会污染变量,甚至文件引入的先后顺序都会影响整个项目的运行。同时也没有一个相对标准的文件引入规范和包管理系统,这个时候CommonJS规范就出现了。下面就是本文的重头戏部分了,通过手写一个CommonJS规范,更加清晰和认识模块化的含义及如何实现的。另外本文中的示例代码需要在node.js环境中方可正常运行,否则将出现错误。事实上ES6已经出现了模块规范,如果使用ES6的模块规范是无需node.js环境的。因此,
因为JavaScript本身并没有模块的概念,不支持封闭的作用域和依赖管理,传统的文件引入方式又会污染变量,甚至文件引入的先后顺序都会影响整个项目的运行。同时也没有一个相对标准的文件引入规范和包管理系统,这个时候CommonJS规范就出现了。
CommonJS规范的优点有哪些?
- 首先要说的就是它的封装功能,模块化可以隐藏私有的属性和方法,这样不需要别人在重新造轮子。
- 第二就是它能够封装作用域,保证了命名空间不会出现命名冲突的问题。
- 第三nodejs中npm包管理有20万以上的包并且被全球的开发人员不断更新维护,开发效率几何倍增。
模块化的定义
下面就是本文的重头戏部分了,通过手写一个CommonJS规范,更加清晰和认识模块化的含义及如何实现的。另外本文中的示例代码需要在node.js环境中方可正常运行,否则将出现错误。事实上ES6已经出现了模块规范,如果使用ES6的模块规范是无需node.js环境的。因此,需要将commonJS规范和ES6的模块规范区分开来。
1.自执行函数
我们先写一段简单的代码,在node环境下运行,来看看commonJS是如何处理的:
一段非常简单的函数,调用时候传递参数name,将一段字符串返回。但是通过断点调试我们发现在node环境下,node本身自动给sayHello函数加了一层外衣,就是下面的内容:
(function (exports, require, module, __filename, __dirname) {}); 复制代码
我们不难发现,其实这是一个自执行函数,那么为什么要加上这样一段看似多余的代码呐,这就是我们说得CommonJS规范一个好处,它将要执行的函数封装了起来,所有的变量和方法都可以理解为是私有的了,保证了命名空间。
2.文件导出
前面我们已经了解到在node中,每个文件都可以被看成是一个模块,那么node中对于模块的导出,都是使用的相同的方法module.exports。
var str='hello World'; module.exports=str; 复制代码
####3.文件导入 为了方便的使用模块,我们可以使用require方法对模块进行导入,类似于这样:
var a=require('./a.js'); 复制代码
值的注意的是:在文件引入的过程中,是否使用相对或者绝对路径,如果 a.js
前添加 ./
或者 ../
是证明是第三方模块,不写绝对和相对路径为内置模块,例如: fs
。
分析commonJS规范源码
我们写一个简单的模块引入,通过断点,分析它的代码,并以此为来完善我们自己的commonJS规范
Module._load
首先我们能看到第一次进入是require方法中,分析代码:
Module._load
Module._resolveFilename
断点继续运行,走到下一个方法Module._resolveFilename,这个方法是用来解析文件名称的,将相对路径解析成绝对路径。
var filename = Module._resolveFilename(request, parent, isMain); 复制代码
Module._cache
node中会对已经加载过的模块进行缓存,供下次引入时候使用,这个方法就是: Module._cache
var cachedModule = Module._cache[filename]; 复制代码
new modal
没有缓存的时候,node会新建一个模块,用来存放这个正在加载的模块:
var module = new Module(filename, parent); Module._cache[filename] = module; 复制代码
tryModuleLoad
然后尝试加载这个模块
tryModuleLoad(module, filename); 复制代码
Module._extensions
然后继续回到load方法中,执行下面的代码,对扩展名进行完善:
var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); 复制代码
Module.wrap
有了文件名之后就就可以拿到对应的文件内容,下面就对文件内容进行处理,我们称这个方法为文件包裹方法:
var wrapper = Module.wrap(content); 复制代码
进入这个方法之后你会看到我们熟悉的自执行函数,通过字符串拼接的形式进行包裹。
然后让这个函数执行
手写commonJS规范
初始化
首先得有一个方法或者类实现这样一个规范,然后这个方法接受一个参数path(路径)
let fs = require('fs');//文件模块,用来读取文件 let path = require('path');//用来完善文件路径 let vm=require('vm');//将字符串当作JavaScript执行 function req(path) { } function module() { //模块相关 } 复制代码
Module._load
第一步加载,传入参数路径,进入到方法中会有一个 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
方法:
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
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
首先去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返回。
tryModuleLoad
通过模块的id我们可以很方便的拿到文件的扩展名,然后利用 path.extname
方法来获取文件的扩展名,并调用对应扩展名下面的处理方法:
function tryModuleLoad(module){ let ext = path.extname(module.id);//扩展名 // 如果扩展名是js,调用js处理器.如果是json,调用json处理器 Module._extensions[ext](module); } 复制代码
完善Module._extensions
如果这个文件是一个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.wrap
我们用俩个字符串将文件内容进行包裹并返回新的字符串
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规范的理解和使用,特此说明。
以上所述就是小编给大家介绍的《你对CommonJS规范了解多少?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 你了解HTTPS,但你可能不了解X.509
- 你真的了解Mybatis的${}和#{}吗?是否了解应用场景?
- 你所了解的 array_diff_uassoc 真的是你了解的那样吗?
- 图文了解 Kubernetes
- 深入了解 JSONP
- 一文了解 Kubernetes
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。