写在前面

为什么会出现CommonJS规范?

因为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方法中,分析代码:

  • assert方法用来进行断言,那么第一行代码的含义就是判断一下这个路径的参数path是否存在,如果不存在就报错
  • 同理第二行代码检查路径参数是不是一个字符串格式,如果不是也报错
  • 第三返回一个函数Module._load,从名字中可以看出这应该是一个加载的方法,此方法传递三个参数,第一个是路径,第二个是this的指向,第三个是一个布尔值,表示为是否为必要的。

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规范了解多少?的更多相关文章

  1. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  2. 关于CommonJS规范摘录

    CommonJS规范 1. 概述 为什么要用commonjs 模块化的目的: 减少循环依赖 减少耦合,提高了模块的复用率 有利于多人开发,提高开发的效率. 规避命名的冲突.全局变量的污染.有利于代码的 ...

  3. 内置模块加载器(commonjs规范)的使用

    index9.html <html><head> <title>模块加载器</title> <script src="jquery-1. ...

  4. 该如何理解AMD ,CMD,CommonJS规范--javascript模块化加载学习总结

    是一篇关于javascript模块化AMD,CMD,CommonJS的学习总结,作为记录也给同样对三种方式有疑问的童鞋们,有不对或者偏差之处,望各位大神指出,不胜感激. 本篇默认读者大概知道requi ...

  5. CommonJS规范(转)

    概述 CommonJS是服务器端模块的规范,Node.js采用了这个规范. 根据CommonJS规范,一个单独的文件就是一个模块.加载模块使用require方法,该方法读取一个文件并执行,最后返回文件 ...

  6. commonJS规范基本机构

    commonJS规范:使用 module.exports 和 require ,基本结构如下: // foo.js 输出模块 module.exports = function(x) { consol ...

  7. Javascript模块规范(CommonJS规范&&AMD规范)

    Javascript模块化编程(AMD&CommonJS) 前端模块化开发的价值:https://github.com/seajs/seajs/issues/547 模块的写法 查看 AMD规 ...

  8. NodeJS学习笔记—1.CommonJS规范

    由于现在web开发,越来越重视代码的复用和抽象的封装,为了解决代码的组织结构.管理.复用和部署等问题,现在普遍采用的机制是模块机制(module).CommonJS约定桌面应用程序和服务器应用程序需要 ...

  9. Commonjs规范及Node模块实现

    前面的话 Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于java ...

  10. AMD、CMD、CommonJs规范

    AMD.CMD.CommonJs规范 将js代码分割成不同功能的小块进行模块化的概念是在一些三方规范中流行起来的,比如CommonJS.AMD和CMD.接下来我们看一下这几种规范. 一.模块化规范 C ...

随机推荐

  1. Centos 自动更新git

    首先,要先配置好自己的Git,然后在某一处进行脚本的编写. 比如项目目录为:/home/project,那参考如下来进行 vim /home/project/automatic_git.sh #/bi ...

  2. NSSM把.Net Core部署至 Windows 服务

    NSSM把.Net Core部署至 Windows 服务 https://www.cnblogs.com/emrys5/p/nssm-netcore.html 为什么部署至Windows Servic ...

  3. High waits on control file sequential read

    High waits on control file sequential read (文档 ID 2277867.1) In case we run into an issue where cont ...

  4. 开启 PHP 错误提示配置步骤详解

    PHP编码出错不提示,这对于开发来说,是很不方便的.下面讲解如何开启错误提示步骤: 1. 打开php.ini文件. 以我的ubuntu为例,这个文件在: /etc/php5/apache2 目录下. ...

  5. vue.js数据绑定语法

    原始高清大图下载 1.数据绑定 html代码: <div id="first" class="first">msg:{{msg}}</div& ...

  6. Unity注入

    [此文引用别人,作为随笔自己看.]今天写<WCF技术剖析(卷2)>关于<WCF扩展>一章,举了“如何通过WCF扩展实现与IoC框架(以Unity为例)集成”(<通过自定义 ...

  7. Eclipse Debug模式和断点调试

    1行号上双击,打断点:再双击,取消断点.一般想调试哪一句代码,就在哪一句和下一句打上断点. 2在要执行的class文件上(有main方法的),右键--Debug As 然后程序正常走,当走到断点时,会 ...

  8. Java基础语法(方法)

    Java基础语法 今日内容介绍 u 方法 第1章 方法 1.1 方法概述 在我们的日常生活中,方法可以理解为要做某件事情,而采取的解决办法. 如:小明同学在路边准备坐车来学校学习.这就面临着一件事情( ...

  9. spring data jpa封装specification实现简单风格的动态查询

    github:https://github.com/peterowang/spring-data-jpa-demo 单一实体的动态查询: @Servicepublic class AdvancedUs ...

  10. 【迷你微信】基于MINA、Hibernate、Spring、Protobuf的即时聊天系统:2.技术简介之MinaFilter(1)

    欢迎阅读我的开源项目<迷你微信>服务器与<迷你微信>客户端 Filter filter:过滤器?(不知道是不是这么翻译,算了知道意思就好了╮(╯▽╰)╭),这种东西在很多语言中 ...