写在前面

为什么会出现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. IT兄弟连 Java语法教程 Java平台的版本划分

    自从Sun公司推出Java以来,就力图使之无所不能.Java发展至今,按应用范围划分为3个版本,即Java SE.Java EE和Java ME,也就是SunOne(Open Net Environm ...

  2. jdk及tomcat的安装

    Tomcat和JDK安装指南 1  JDK的安装 要运行JAVA程序,必须安装JDK(JAVA 开发包)的支持. 1.1  安装 1.J2SDK的安装比较简单,在安装盘目录下寻找“JDK安装程序”文件 ...

  3. IP服务-8-WCCP

    WCCP(网页缓存通信协议) 内容引擎负责将频繁访问的数据收集到本地,通常是HTTP流量,当主机访问相同页面时,可以直接通过内容引擎为主机提供相应内容,而无需通过WAN进行访问.WCCP与网页代理并不 ...

  4. safari不支持new Date函数

    最近在做移动Web的时候,在PC上用Chrome调试都成功了,但是在iPhone上真机一测就出现了奇怪的问题.经过一系列调试发现是日期相关的地方出现了问题.起初怀疑是生产环境的问题,但用Mac版的sa ...

  5. Net Core 2.0 Redis

    Net Core 2.0 Redis配置.封装帮助类RedisHelper及使用实例 https://www.cnblogs.com/oorz/p/9052498.html 本文目录 摘要 Redis ...

  6. RabbitMQ使用教程(一)RabbitMQ环境安装配置及Hello World示例

    你是否听说过或者使用过队列? 你是否听说过或者使用过消息队列? 你是否听说过或者使用过RabbitMQ? 提到这几个词,用过的人,也许觉得很简单,没用过的人,也许觉得很复杂,至少在我没使用消息队列之前 ...

  7. 路径方案数(mod)

    路径方案数(mod) [题目描述] 给一张无向图,n 个点和 m 条边,cyb 在 1 号点,他要去 2 号点, cyb 可以从 a 走到 b,当且仅当a到2的最短路,比b 到2的最短路长. 求 cy ...

  8. winform代码生成器(三)

    代码下载 地址 http://pan.baidu.com/s/1nuZjyat 接上面的两篇. 用户有时对 从表的 排版不喜欢,可以因某些字太长,需要拉长一些,有些则需要隐藏. 有什么办法呢? 我的思 ...

  9. Android 下的 SQLite 操作封装 —— DatabaseUtil

    看到别人写的代码不错,对自己目前的开发很有用,所以转载一下,希望也能帮助到其他人: 1.DatabaseUtil.java(封装的类) package com.dbexample; import an ...

  10. 实现memcopy函数

    实现memcopy函数: void * memcpy(void *dest, const void *src, unsigned int count); { if ((src == NULL) || ...