nodejs中的文件操作的基础,path模块(一)

fs模块之前,有一个很重要的点就是,我要操作文件,文本什么的,我总得要知道文件的路径吧?就像,你要我煮饭,好歹也得给点米吧,巧妇难为无米之炊,所以,在操作文件时,文件路径的操作,就变得异常的重要。

path模块

而在Nodejs中,就有这么一个模块,是专门来处理各种路径问题的,所以,在说关于fs的模块之前,先对来看下path模块如何。

当然啦,如果我们只是关注fs模块中支持的一些属性和方法,那么path模块的很多东西,基本上使用不到的。

看下path模块中包含的属性和方法

直接看下,在path模块中,包含哪些方法和属性,循环把这些方法,保存到文件当前目录下的path.txt文本中,可以如下实现:


var fs = require("fs"),
    path = require("path"),
    pathData = [],
    i;

for(i in path){
    pathData.push(i+"\n");
}

fs.writeFile("path.txt",pathData.join(""));


在当前目录下的path.txt文件中,可以看到如下的内容:


resolve
normalize
isAbsolute
join
relative
_makeLong
dirname
basename
extname
format
parse
sep
delimiter
posix
win32


接下来,就对这些属性和方法,进行一个简单的说明吧。

1:resolve

resolve放在第一个,并不是因为它是最重要的,而是因为,在之前输出的属性中,它是排在第一位的~~~

该方法以应用程序的根目录为起点,根据所有参数值字符串解析出一个绝对路径,使用方法如下:


path.resolve(path1,[path2],[path3]);


resolve方法中,可以任意多个参数,每一个参数值均为一个字符串,如果不为字符串,则会抛出异常,无法继续执行resolve方法。

resolve方法的具体解析过程如下:

1:以应用程序的根目录为起点,解析第一个参数值字符串。

2:如果该参数值字符串不以“..”、“.”、“/”开头,则将数值字符串解析为应用程序根目录下的某个子目录,将当前参考目录定位该子目录的绝对路径。例如:应用程序的根目录为C:\nodejs\fs,第一个参数值为“c”,则通过resolve返回的值就是:C:\nodejs\fs\c。这个返回值与当前执行resolve方法的文件所在的目录是毫无关系的。

看下面的一个示例:


//根目录为:C:\nodejs\fs

//app.js文件
var path = require("path"),
    b = require("./a/a.js");

console.log(path.resolve("c"));
//C:\nodejs\fs\c

//a/a.js文件	
var path = require("path");
	
console.log(path.resolve("c\d"));
//C:\nodejs\fs\c\d


由上述文件,也可以看出,该情况下,不论是在哪一个目录下的文件执行,都是以根目录为基准,进行的这次路径的查找。

3:如果该参数值字符串是以“..”开头,则将“..”解析为应用程序跟目录的上级目录,然后将“..”后的字符串解析为该目录下的某个子目录。

看下面的一个示例:


//根目录为:C:\nodejs\fs

//app.js文件
var path = require("path"),
    b = require("./a/a.js");

console.log(path.resolve("../c"));
//C:\nodejs\c

//a/a.js文件	
var path = require("path");
	
console.log(path.resolve("../c\d"));
//C:\nodejs\c\d


4:如果该参数值字符串以“.”开头,则将“.”解析为应用程序根目录的当前目录,即,应用程序根目录,然后将“.”后的字符串解析为应用程序根目录下的某个子目录。

看下面的一个示例:


//根目录为:C:\nodejs\fs

//app.js文件
var path = require("path"),
    b = require("./a/a.js");
	
console.log(path.resolve("./e"));
console.log(path.resolve("e"));
//C:\nodejs\fs\e

//a/a.js文件	
var path = require("path");
	
console.log(path.resolve("./c/d"));
console.log(path.resolve("c/d"));
//C:\nodejs\fs\c\d


这里让我很意外的是,为什么上面的两种写法获取的结果是相同的呢?有什么意义吗?都不是以当前目录为基础去做的,都是以程序的根目录去做的基础。

5:如果该参数值字符串开头以“/”开头,表示指定了一个绝对路径,即基础路径为所在的盘符。

看下面的一个示例:


//根目录为:C:\nodejs\fs

//app.js文件
var path = require("path"),
    b = require("./a/a.js");
	
console.log(path.resolve("./e"));
//C:\nodejs\fs\e
console.log(path.resolve("e"));
//C:\e

//a/a.js文件	
var path = require("path");
	
console.log(path.resolve("./c/d"));
//C:\nodejs\fs\c\d
console.log(path.resolve("/c/d"));
//C:\c\d


6:如果还有多第二个参数,第三个参数,则重复上述的这些规则,只是把跟路径,改为经过第一个参数变化之后,返回的路径了。

7:如果没有参数,该方法返回的是当前文件根目录的绝对路径。

说到这里,基本上,resolve的用法已经都包含了,下面看下,在源代码中,是如何实现resolve方法的。

path方法的一些方法定义,根据不同的平台,会有不同的实现逻辑,接下来,只以win32平台的基础上,对该部分的源码进行一下说明。


var util = require("util");
var splitDeviceRe =
    /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; 
    //该正则表达式,是用来把输入的每一个元素进行匹配,用于获取对应的信息
    //([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?用于匹配盘符或者一个以双斜杠开头的字符串,匹配0次或1次
    //[a-zA-Z]:匹配盘符加冒号,即:"C:"等
    //[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+
    //双斜杠开头,加上非斜杠的元素(至少一个),
    //再加一个斜杠,再加非斜杠的元素(至少一个)
    //例如:"\\a\b"
    //可以如下的方法,进行验证
    //console.log(win32StatPath("//a/b"))
    //这里之所以使用"//"而不是"\\",因为"\"会被认为是转义字符的
    //console.log("device="+win32StatPath("\\/a/b").device)
    //([\\\/])?,匹配一个非斜杠的元素,0次或者1次
    //([\s\S]*?),最短匹配,任意字符,即优先匹配前面的正则
	
function win32StatPath(path) {
  var result = splitDeviceRe.exec(path),
      device = result[1] || '',
      isUnc = !!device && device[1] !== ':';
    //使用正则匹配,并根据匹配获取到的子元素,返回一个字符串
    //这里的难点,也只有对正则的理解了
    //不过,这里的难点对我来说,就是为什么是这样的正则,代表的含义是什么。
	
  return {
    device: device,
    isUnc: isUnc,
    isAbsolute: isUnc || !!result[2], // UNC paths are always absolute
    tail: result[3]
  };
}

function normalizeArray(parts, allowAboveRoot) {
  var res = [];
  //parts数组,进行一些标准化
  for (var i = 0; i < parts.length; i++) {
    var p = parts[i];

    // ignore empty parts
    //如果当前的元素为空,或者为".",则不做处理
    //可以对比之前的文字部分,第四种情况,以"."开头的情况,
    //对比参考学习	
    if (!p || p === '.')
      continue;

    //如果当前元素为"..",同样对比前文中的第三种情况,
    //即,以当前目录的上级父目录为基础,当然下面也进行了不同的判断。	
    if (p === '..') {
      if (res.length && res[res.length - 1] !== '..') {
        res.pop();
      } else if (allowAboveRoot) {
        res.push('..');
      }
    } else {
      res.push(p);
    }
  }

  return res;
}

function normalizeUNCRoot(device) {
  //标准化格式,以便于以后对路径的处理
  return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
}

win32 = {};
win32.resolve = function() {
  var resolvedDevice = '',
      resolvedTail = '',
      resolvedAbsolute = false;

  for (var i = arguments.length - 1; i >= -1; i--) {
    var path;
    if (i >= 0) {
      //如果当前的参数存在,则取得输入的参数值
      path = arguments[i];
    } else if (!resolvedDevice) {
      //如果没有输入参数值,那么这个for语句,只会被执行一次,
	  //当i执行到-1时,也会执行该方法,也就是最后的path值
      //则path取值是当前的据对路径
      path = process.cwd();
      //path获取为当前目录的绝对路径
    } else {
      // Windows has the concept of drive-specific current working
      // directories. If we've resolved a drive letter but not yet an
      // absolute path, get cwd for that drive. We're sure the device is not
      // an unc path at this points, because unc paths are always absolute.
      //该部分的逻辑,值会在最后一次才会被执行,
      //在最初,如果没有输入值,则执行else if中的逻辑,
      //如果有输入,则执行i>=0的逻辑,所以只有最后一次i=-1和resolvedDevice不为空时,
      //才会执行该部分逻辑
      path = process.env['=' + resolvedDevice];
      //使用env查看是否为env中的一个属性,如果是,则获取该属性的值。
      //env对象中,包含了一些特殊标志或者说文件,或者说,环境变量等的路径值。
	  
      // Verify that a drive-local cwd was found and that it actually points
      // to our drive. If not, default to the drive's root.
      if (!path || path.substr(0, 3).toLowerCase() !==
          resolvedDevice.toLowerCase() + '\\') {
          //如果前面获取到的path为空,或者为undefined
          //或者path得到的前三个字符,和resolvedDevice+"\\"不想等
          //则把path值设置为resolvedDevice
         path = resolvedDevice + '\\';
      }
    }
    //前面的这个if语句,只是为了能保证path存在,
    //根据输入参数个数和resolvedDevice的不同,把path参数,
    //进行不同的赋值

    // Skip empty and invalid entries
    //如果获取到的path不是字符串,则抛出一个类型错误	
    if (!util.isString(path)) {
      throw new TypeError('Arguments to path.resolve must be strings');
    } else if (!path) {
      continue;
    }

    //win32StatPath方法,会根据path的值,返回一个对象。
    //关于win32StatPath的逻辑,请参考前面代码中win32StatPath内部的注释
    var result = win32StatPath(path),
        device = result.device,
        isUnc = result.isUnc,
        isAbsolute = result.isAbsolute,
        tail = result.tail;

    if (device &&
        resolvedDevice &&
        device.toLowerCase() !== resolvedDevice.toLowerCase()) {
      // This path points to another device so it is not applicable
      //如果盘符已经确定,即resolvedDevice = device;
      //而在后面,依然还有盘符,则对于该元素,不做处理
      //例如:console.log(win32.resolve("C:/a/b","d:/d/e"));
      //因为是倒序处理的,所以,当盘符被定为d:时,再遇到C:/就会直接被忽略了。
      //直接进入下一次的for循环
      continue;
    }

    if (!resolvedDevice) {
      //如果之前并没有定义盘符,则把该盘符进行定义	
      resolvedDevice = device;
    }
    if (!resolvedAbsolute) {
      //判断是否已经属于绝对路径了,如果不是,则添加当前的tail到字符串中
      resolvedTail = tail + '\\' + resolvedTail;
      resolvedAbsolute = isAbsolute;
      //并且重新定义,当前是否为绝对路径了。
      //这个地方,可能理解的太准确。
      //对比两个例子:console.log(win32.resolve("c:/a/b","c","d:"));
      //console.log(win32.resolve("c:/a/b","c","d:/d"));
    }

    if (resolvedDevice && resolvedAbsolute) {
      //如果盘符和绝对路径都属于了,那么直接停止循环,不再计算前面剩余的参数
      break;
    }
  }

  // Convert slashes to backslashes when `resolvedDevice` points to an UNC
  // root. Also squash multiple slashes into a single one where appropriate.
  if (isUnc) {
    //把获取到字符串进行标准化,以便于后面的使用
	resolvedDevice = normalizeUNCRoot(resolvedDevice);
  }

  // At this point the path should be resolved to a full absolute path,
  // but handle relative paths to be safe (might happen when process.cwd()
  // fails)

  // Normalize the tail path
  //对标准化之后的数据,进行处理,并返回绝对路径值
  resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/),
                                !resolvedAbsolute).join('\\');

  return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) ||
         '.';
};


上述代码,是我自path.js单独分离出来的,可以用来进行学习,测试,以及验证每一步的执行结果,当然,并以自己的理解以及一些验证,添加了以上的注释。

总结

本来的打算,是在这里把所有的path方法,都说一遍的,但是说完resolve方法之后,已经有这么长的篇幅了,所以,本篇到此为止,下一篇继续path模块其他的属性和方法。

本篇内容主要参考自:“Nodejs”源码和“Nodejs权威指南”

本文地址:http://www.zhangyunling.com/?p=408

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>