03月26, 2018

node深入笔记(2)

第二篇开始。。。

ecoding编码

我个人觉得可以了解一下编码的发展历史,从最开始的ASCII,然后不够用(这个毫无疑问,毕竟我们有汉字),发展到gb2312(特指我国,其他国家可能用的是其他的编码),因为中国汉字太多,就又扩展了,发展到gbk,再扩展,到了GB18030。

后来ISO 的国际组织废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符 的编码:Unicode ,它是一个很大的集合,现在的规模可以容纳100多万个符号。

而像UTF8、UTF16都是Unicode的一种实现方式。UTF-8就是每次以8个位为单位传输数据,而UTF-16就是每次 16 个位。UTF-8 最大的一个特点,就是它是一种变长的编码方式。Unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节

这里我觉得只要搞清楚下面几个问题,就差不多可以了:

  • 1个字节有几位
  • 进制间如何转换
  • GBK/GB2312占几个字节,UTF8占几个字节

编码规则

alt

有一个有意思的话题,就是联通不如移动,如果我们用记事本(默认保存编码为gbk)来写移动两个字,保存关闭,再重新打开是正常的,但是联通这两个字在保存后,重新打开会变成乱码。原因就是:在打开记事本时,会去判断它的编码,刚好符合上图的utf8的规则。

通过GB2312表:我们可以查到的16进制为:C1AA,转换成二进制的结果为:1100000110101010,结果误中,将gb2312转为utf8,肯定是乱码了。

BOM

N久之前吃过这个的亏,在用记事本转成utf8编码后,它会有一个BOM头产生。后来就用notepad++

alt

在node的require源码中,有这样一段:

alt

alt

所以在读取文件内容时,可以考虑加这样一段函数,来防止前面一个字乱码。

另外在node中,是不支持gbk/gb2312的编码的,所以如果要用node爬gbk的那种网页时,需要使用三方模块:iconv-lite,示例demo如下:

let iconv = require('iconv-lite'); 
let fs = require('fs');
let path = require('path');
let result = fs.readFileSync(path.join(__dirname, './1.txt')); // 假定1.txt是gbk编码
result = iconv.decode(result,'gbk');
console.log(result.toString());

Buffer

  • 缓冲区Buffer是暂时存放输入输出数据的一段内存。 JS语言没有二进制数据类型,而在处理TCP和文件流的时候,必须要处理二进制数据。
  • NodeJS提供了一个Buffer对象来提供对二进制数据的操作
  • 是一个表示固定内存分配的全局对象,也就是说要放到缓存区中的字节数需要提前确定
  • Buffer好比由一个8位字节元素组成的数组,可以有效的在JavasScript中表示二进制数据

声明

有以下几种方式

  • Buffer.alloc
  • Buffer.allocUnsafe
  • Buffer.from(字符串)
  • Buffer.from(数组对象)

其中Buffer.alloc和Buffer.allocUnsafe的区别在于前者内存永远是干净的,声明比较耗时。

常用方法

  • buf.fill(value[, offset[, end]][, encoding])
buffer.fill(0);
  • buf.write(string[, offset[, length]][, encoding])
buffer.write('小',0,3,'utf8');
buffer.write('翼',3,3,'utf8'); //小翼
  • buf.toString([encoding[, start[, end]]]) : 将buffer对象转为字符串

  • buf.slice([start[, end]])

let newBuf = buffer.slice(0,4);

需要注意的是截取可能会导致乱码,这时候可以使用string_decoder这个模块来解决,之前写过一篇文章:node模块之string_decoder

  • buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
let buf5 = Buffer.from('测试文本');
let buf6 = Buffer.alloc(6);
buf5.copy(buf6,0,0,4);
buf5.copy(buf6,3,3,6);
//buf6=测试

这里我们也可以简单实现一个copy方法:

Buffer.prototype.myCopy = function (targetBuffer, offset, sourceStart, sourceEnd) {
    for (let i = sourceStart; i < sourceEnd; i++) {
        targetBuffer[offset++] = this[i];
    }
}
  • Buffer.concat(list[, totalLength])
let buf1 = Buffer.from('小');
let buf2 = Buffer.from('翼');
let buf3 = Buffer.concat([buf1,buf2],3);
console.log(buf3.toString());

简单的concat方法实现:

Buffer.myconcat = function (list, totalLength) {
    if (list.length == 1) {
        return list[0];
    }
    if (typeof totalLength === "undefined") {
        totalLength = list.reduce((prev, next) => {
            return prev + next.length
        }, 0)
    }
    let buf = Buffer.alloc(totalLength); // 创建这么大的buffer
    let pos = 0; // 记忆当前拷贝的位置
    list.forEach(function (buffer, index) { // [[1,2,3],[4,5,6]]
        for (var i = 0; i < buffer.length; i++) {
            buf[pos++] = buffer[i];
        }
    })
    return buf.fill(0, pos)
}

fs模块

在readFile中,有这样的一个参数:

alt

alt

readFile默认编码是null,即 读出来会是buffer的结果。

在writeFile中,option选项多了一个mode参数:

alt

这个表示的是linux权限。

alt

666这个值是表示所有者、所属组、其他用户都拥有读写的权限。读这个操作,默认编码为utf8。

读写文件,直接通过readFile或者writeFile,很容易出现一个问题:即内存12g,读一个20g文件,肯定就挂了(或者说崩了)。所以就有了后面createReadStream和createWriteStream。不过在说这两个之前,得先要了解一下fs.read和fs.write,因为createReadStream和createWriteStream的实现就是基于这两个方法的。

fs.read

alt

它的第一个参数为fd,fd是表示file descriptor(文件描述符),它是从3开始,1和2分别代表标准输出和错误输出。fd是通过fs.open来得到:

alt

这里提供一个简单的demo

let fs = require("fs");
let path = require("path");
let buffer = Buffer.alloc(3);
fs.open(path.join(__dirname,'1.txt'),'r',0o666,function (err,fd) {
    // offset表示的是 buffer从那个开始存储
    // length就是一次想读几个
    // postion 代表的是文件的读取位置,默认可以写null 当前位置从0开始
    // length不能大于buffer的长度
    fs.read(fd,buffer,0,2,0,function(err,bytesRead){
        // bytesRead 读取到个数
        console.log(err,bytesRead);
        console.log(buffer);
    })
});

fs.write

alt

简单demo示例:

let fs = require("fs");
let path = require("path");
fs.open(path.join(__dirname,'2.txt'),'r+',0o666,function(err,fd){
    // offset表示的是 buffer从哪个开始读取(就是下面的小翼)
    // length就是一次想写几个
    // postion 代表的是文件的写位置,默认可以写null 当前位置从0开始
    fs.write(fd,Buffer.from('小翼'),0,3,3,function(err,byteWritten){
        if(err) return console.log(err);
        console.log(byteWritten)
    })
});

当然上面的写法都只是打开,并没有关闭,所以并不是太好。

copy

通过上面的写法就可以引申出copy的实现:

function copy(source,target){
    let size = 3; // 每次来三个
    let buffer = Buffer.alloc(3);
    fs.open(path.join(__dirname,source),'r',function(err,rfd){
        fs.open(path.join(__dirname,target),'w',function(err,wfd){
            function next(){
                fs.read(rfd,buffer,0,size,null,function(err,bytesRead){
                    if(bytesRead>0){ // 读取完毕了 没读到东西就停止了
                        fs.write(wfd,buffer,0,bytesRead,null,function(err,byteWritten){
                            next();
                        })
                    }else{
                        fs.close(rfd,function(){}); // 读取的

                        fs.fsync(wfd,function(){ // 确保内容 写入到文件中 
                            fs.close(wfd,function(){ // 写入的
                                console.log('关闭','拷贝成功')
                            })
                        })
                    }
                })
            }
            next();
        })
    });
}

上面有一个fs.fsync方法,是因为当write方法触发了回调函数 并不是真正的文件被写入了,而是先把内容写入到缓存区。所以需要这个方法来保证强制将缓存区的内容 写入后再关闭文件。

目录操作

fs.mkdir(path[, mode], callback)

判断一个文件是否有权限访问

fs.access('/etc/passwd', fs.constants.R_OK | fs.constants.W_OK, (err) => {
  console.log(err ? 'no access!' : 'can read/write');
});

在目录操作中,有两个东西比较难:

  • 递归创建目录(同步、异步)
  • 删除非空目录

先说创建目录,比如说我们要创建“a/b/c”这样层次的目录,直接通过mkdir是会报错的。

我们可以通过两种方式来写,同步和异步。

// 同步
function makep(dir) {
    let paths = dir.split('/');
    for (let i = 1; i <= paths.length; i++) {
        let newPath = paths.slice(0, i).join('/');
        // 创建目录需要先干一件事:判断能否访问
        try {
            fs.accessSync(newPath, fs.constants.R_OK);
        } catch (e) {
            fs.mkdirSync(newPath)
        }
    }
}
makep("a/b/c")
// 异步
function mkdirSync(dir,callback){
    let paths = dir.split('/');
    function next(index){
        if(index>paths.length) return callback();
        let newPath = paths.slice(0,index).join('/');
        fs.access(newPath,function(err){
            if(err){ // 如果文件不存在就创建这个文件
                fs.mkdir(newPath,function(err){
                    next(index+1);// 创建后 继续创建下一个
                })
            }else{
                next(index+1); //这个文件夹存在了 那就创建下一个文件夹
            }
        })
    }
    next(1);
}
mkdirSync('a/e/w/q/m/n',function(){
    console.log('完成')
});

写异步一般用next函数这种来书写即可。需要知道的是异步写法里面永远不能用for循环。

再来说删除非空文件夹,这里会有几种情况:

  • 删除的是文件(fs.unlink)
  • 删除的是文件夹(fs.rmdir)

判断是文件还是文件夹得用fs.stat方法。

// 同步
function removeDir(dir) {
    let files = fs.readdirSync(dir);// 读取到所有内容
    for (var i = 0; i < files.length; i++) {
        let newPath =path.join(dir,files[i]);
        let stat = fs.statSync(newPath);
        if(stat.isDirectory()){
            // 如果是文件夹 就递归走下去
            removeDir(newPath); // 递归
        }else{
            fs.unlinkSync(newPath);
        }
    }
    fs.rmdirSync(dir); // 如果文件夹是空的就将自己删除掉
}
removeDir('a');
// 异步方案一 promise版
function removePromise(dir){
    return new Promise(function(resolve,reject){
        fs.stat(dir,function(err,stat){
            if(stat.isDirectory()){
                fs.readdir(dir,function(err,files){ 
                    files = files.map(file=>path.join(dir,file)); // [a/b,a/e,a/1.js]
                    files = files.map(file=>removePromise(file))
                    Promise.all([...files]).then(function(){
                        fs.rmdir(dir,resolve);
                    });
                })
            }else{ // 如果是文件 删除文件 直接变成成功态即可
                fs.unlink(dir,resolve)
            }
        })

    })
}
removePromise('a').then(function(){
    console.log('删除')
});
// 异步方案二 常规
function rmdir(dir,callback){
    console.log(dir);
    fs.readdir(dir,function(err,files){
        // 读取到文件
        function next(index){
            if(index===files.length) return fs.rmdir(dir,callback);
            let newPath = path.join(dir,files[index]);
            fs.stat(newPath,function(err,stat){
                if(stat.isDirectory()){ // 如果是文件夹
                    // 要读的是b里的第一个 而不是去读c
                    // 如果b里的内容没有了 应该去遍历c
                    rmdir(newPath,()=>next(index+1));
                }else{
                    // 删除文件后继续遍历即可
                    fs.unlink(newPath,()=>next(index+1))
                }
            })
        }
        next(0);
    });
}
rmdir('a',function(){
    console.log('删除成功');
});

异步的方案,从代码的角度来说,promise写起来更容易一些,不过理解起来,可能还是常规的容易理解。

上面的同步和异步方案都是从上往下递归的,即是深度的。

在算法中,有一个叫广度的,也可以实现上面的需求

alt

我们可以先来看一个广度同步的代码:

function preWide(dir){
    let arrs = [dir]; // 存放目录结构的数组
    let index = 0; // 指针
    let current;
    while(current = arrs[index++]){ // current可能是文件
        let stat = fs.statSync(current);
        if(stat.isDirectory()){
            let files = fs.readdirSync(current); // [b,c]
            // [a,a/b,a/c,a/b/d,a/b/e,a/c/m];
            arrs = [...arrs,...files.map(file=>{
                return path.join(current,file)
            })];
        }
    }
    for(var i = arrs.length-1 ;i>=0;i--){
        let stat = fs.statSync(arrs[i]);
        if(stat.isDirectory()){
            fs.rmdirSync(arrs[i]);
        }else{
            fs.unlinkSync(arrs[i]);
        }
    }
}
preWide('a');

实现思路就是先把要删的目录文件展开放到一个数组中,然后遍历从后往前一一删除。

那么我们也可以来写一个广度异步的代码:

function wide(dir,callback){
    // 目录是不是文件
    let arr = [dir];
    let index = 0;
    function rmdir(){
        function next(){
            let current = arr[--index];
            if(!current) return callback();
            fs.stat(current,function(err,stat){
                if(stat.isDirectory()){
                    fs.rmdir(current,next)
                }else{
                    fs.unlink(current,next)
                }
            })
        }
        next();
    }
    function next(){
        if(index === arr.length) return rmdir()
        let current = arr[index++];
        fs.stat(current,function(err,stat){
            if(stat.isDirectory()){
                fs.readdir(current,function(err,files){ // [b,c]
                    arr = [...arr,...files.map(file=>{
                        return path.join(current,file);
                    })];
                    next();
                });
            }else{
                next();
            }
        })
    }
    next();
}
wide('a',function(){
    console.log('删除成功')
})

学会了这些东西,我们再去看知名的rimraf,就可以看到它使用的是深度异步遍历。

它判断文件夹还是文件,用的是fs.lstat,不过这个并不是太纠结。在源码中,各种判断条件还是挺严谨的。

本文链接:www.my-fe.pub/post/node-deep-note-2.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。