7-days-nodejs读书笔记

前言,本书书名有点夸张,任何一门编程语言都无法在短期内学会,必须坚持不懈进行学习,才能够学有所成。但这本书是挺适合入门的,通过这本书能够了解到node.js的一些基本知识,了解node.js能够实现什么场景下的业务。同时推荐另一本node.js入门书籍,名字就叫做《node.js入门》(The Node Beginner Book)。基本入门之后,那么必须要拜读下朴灵大神的著作《深入浅出Node.js》,这是进阶学习node.js的必读之书!

模块

require函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象
exports对象是当前模块的导出对象,用于导出模块公有方法和属性。

1
2
3
exports.hello = function () {
console.log('Hello World!');
};

通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。

模块路径解析规则
require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。
require函数支持第三种形式的路径,写法类似于foo/bar,并依次按照以下规则解析路径,直到找到模块位置:

/home/user/node_modules/foo/bar
/home/node_modules/foo/bar
/node_modules/foo/bar

JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了便于管理和使用,我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目录里。
在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对象。

当入口模块的文件名为index.js时,加载模块可以使用模块所在目录的路径代替模块文件路径:

1
2
var cat = require('/home/user/lib/cat');
var cat = require('/home/user/lib/cat/index');

以上两条语句等价。因此采用第一种写法,感觉整个目录被当做单个 模块使用,更有整体感。

若想自定义入口模块的文件名和存放位置,可以在包目录下包含一个 package.json 文件,在其中配置入口模块的路径:

1
2
3
4
{
    "name":"cat",
    "main":"./lib/main.js"
}

这样便可以使用require(‘home/lib/user/cat’)加载模块。

小文件拷贝,直接使用fs的读写方法:readFileSync()和writeFileSync()
大文件拷贝,考虑到内存有限,因此应采取流的方法进行读写,读一点,写一点,使用fs的可读流和可写流:createReadStream()和createWriteStream()

Buffer与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于Buffer,更像是可以做指针操作的C语言数组。

文件系统

fs模块提供的API基本上可以分为以下三类:
文件属性读写:常用的有fs.stat、fs.chmod、fs.chown等

文件内容读写:fs.readFile、fs.readdir、fs.writeFile、fs.close等

底层文件操作:fs.open、fs.read、fs.write、fs.close等

nodeJS最精华的异步IO模型在fs模块里有着充分的体现,例如通过回调函数传递结果:

1
2
3
4
5
6
7
fs.readFile(pathname,function(err,data){
    if(err){
        //deal with error
    }else{
        //deal with data
    }
})

此外,fs模块的所有异步API都有对应的同步版本,同步API除了方法名的末尾多了一个Sync之外,异常对象与执行结果的传递方式也有相应变化。

1
2
3
4
5
6
try{
    var data = fs.readFileSynv(pathname)
    //deal with data
}catch(err){
    //deal with error
}

Path

nodeJS提供了path内置模块来简化路径相关操作,并提高代码可读性。
常用API:
path.normalize
将传入的路径转换为标准路径,具体讲,就是除了解析路径中的.和..外,还能去除多余的斜杠。
如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。

1
2
3
4
5
6
7
8
9
10
var cache = {}
function store(key,value){
    cache[path.normalize(key)] = value
}
store('foo/bar',1)
store('foo//baz//../bar',2)
console.log(cache)

注意:标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\/g, ‘/‘)再替换一下标准路径。

path.join
将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。

1
path.join('foo/','baz/','../bar').replace(/\\/g, '/')

path.extname
当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。

1
path.extname('foo/bar.js')

遍历目录

递归算法
遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法通过不断缩小问题的规模来解决问题。

1
2
3
4
5
6
7
function factorial(n){
    if(n === 1){
        return 1
    }else{
        return n*factorial(n-1)
    }
}

注意:使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。

遍历算法
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。
深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。
A
/ \
B C
/ \ \
D E F

同步遍历

1
2
3
4
5
6
7
8
9
10
11
function travel(dir,callback){
    fs.readdirSync(dir).forEach(function(file){
        var pathname = path.join(dir,file)
        if(fs.statSync(pathname).isDirectory()){
            travel(pathname,callback)
        }else{
            callback(pathname)
        }
    })
}

异步遍历
如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function travel(dir,callback,finish){
    fs.readdir(dir,function(err,files){
        (function(i){
            if(i < files.length){
                var pathname = path.join(dir,files[i])
                fs.stat(pathname,function(err,stats){
                    if(stats.isDirectory()){
                        travel(pathname,callback,function(){
                            next(i+1)
                        })
                    }
                })
            }else{
                finish && finish()
            }
        })(0)
    })
}

文件编码

BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(‘\uFEFF’),位于文本文件头部。
可通过BOM标记来判断文件的编码格式,但在读取文件时,如果不去掉BOM,则在某些情况下会有问题,如合并几个JS文件,若中间有BOM标记,则会导致报错。
BOM的移除:

1
2
3
4
5
6
7
8
9
function readText(pathname){
    var bin = fs.readFileSync(pathname)
    if(bin[0] === 0xEF && bin[1] === 0xBB &&0xBF){
        bin = bin.slice(3)
    }
    return bin.toString('utf-8')
}

网络操作

HTTP
http模块提供两种使用方式:
作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应;
作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。

HTTP请求本质上是一个数据流,由请求头和请求体组成。
HTTP响应本质上也是一个数据流,由响应头和响应体组成。

request对象可以当作一个只读数据流来访问请求数据;
response对象可以当作一个只写流来写入响应数据。

URL
parse()方法将一个URL字符串解析为URL对象;
format()方法允许将一个URL对象转换为URL字符串;
resolve()方法用于拼接URL。

Query String
querystring模块用于实现URL参数字符串与参数对象的互相转换。
querystring.parse()将URL参数字符串转换为URL参数对象;
querystring.stringify()将URL参数对象转换为URL参数字符串.

Zlib
zlib模块提供了数据压缩和解压功能。
zlib.gzip()压缩数据;
zlib.gunzip()解压数据;

Net
net模块可用于创建Socket服务器或Socket客户端。

进程管理

NodeJS可以创建子进程并与其协同工作,把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。

process
process不是内置模块,是一个全局对象,可以感知和控制NodeJS自身进程的方方面面。

Child Process
child_process模块可以创建和控制子进程。

Cluster
cluster模块是对child_process模块的进一步封装,专用于解决单进程NodeJS web服务器无法充分利用多核CPU的问题。

应用场景:
获取命令行参数
通过process.argv获取命令行参数,但node执行程序路径和主模块文件路径固定占据了argv[0]和argv[1]两个位置,而第一个命令行参数从argv[2]开始。

如何退出程序

1
2
3
4
5
try{
    //...
}catch(err){
    process.exit(1);
}

如何控制输入输出
标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr):process.stdin、process.stdout和process.stderr,第一个是只读流,后两个是只写流。

如何降权
在Linux系统下,我们知道需要使用root权限才能监听1024以下端口。但是一旦完成端口监听后,继续让程序运行在root权限下存在安全隐患,因此最好能把权限降下来。

1
2
3
4
5
6
7
8
http.createServer(callback).listen(80,function(){
    var env = process.env,
        uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
        gid = parseInt(env['SUDO_GID'] || process.getgid(), 10)
    process.setgid(gid)
    process.setuid(uid)
})

Tips:

  1. 如果是通过sudo获取root权限的,运行程序的用户的UID和GID保存在环境变量SUDO_UID和SUDO_GID里边。如果是通过chmod +s方式获取root权限的,运行程序的用户的UID和GID可直接通过process.getuid和process.getgid方法获取。
  2. process.setuid和process.setgid方法只接受number类型的参数。
  3. 降权时必须先降GID再降UID,否则顺序反过来的话就没权限更改程序的GID了。

如何创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var child_process = require('child_process')
var child = child_process.spawn('node',['net_server.js'])
child.stdout.on('data',function(data){
    console.log('stdout' + data)
})
child.stderr.on('data',function(data){
    console.log('stderr:' + data)
})
child.on('close',function(code){
    console.log('child process exited with code ' + code)
})

spawn(exec, args, options)方法,该方法支持三个参数。第一个参数是执行文件路径,可以是执行文件的相对或绝对路径,也可以是根据PATH环境变量能找到的执行文件名。第二个参数中,数组中的每个成员都按顺序对应一个命令行参数。第三个参数可选,用于配置子进程的执行环境与行为。

进程间如何通讯
在Linux系统下,进程之间可以通过信号互相通信。

1
2
3
4
5
6
7
8
9
10
11
/* parent.js */
var child_process = require('child_process')
var child = child_process.spawn('node',['child.js'])
child.kill('SIGTERM')
/* child.js */
process.on('SIGTERM',function(){
    cleanUp()
    process.exit(0)
})

kill方法本质上是用来给进程发送信号的,并不是关闭进程。进程收到信号后具体要做啥,完全取决于信号的种类和进程自身的代码。

如何守护子进程
守护进程一般用于监控工作进程的运行状态,在工作进程不正常退出时重启工程进程,保障工程进程不间断运行。
监听‘exit’事件,当退出状态码不等于0时,重新启用进程。

1
2
3
4
5
6
7
8
9
10
11
function spawn(mainModule){
    var worker = child_process.spawn('node',[mainModule])
    worker.on('exit',function(code){
        if(code !== 0){
            spawn(mainModule);
        }
    })
}
spawn('worker.js')

异步编程

NodeJS最大的卖点——事件机制和异步IO。而异步编程是NodeJS最大的特点,没有掌握异步编程就不能说是真正学会了NodeJS。

回调
异步编程的直接体现就是回调。
JS是单线程运行,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,在平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程有空闲时才能开始执行。

代码设计模式
1、函数返回值
同步方式下:

1
var output = fn1(fn2('input'))

而在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递:

1
2
3
4
5
fn2('input',function(output2){
    fn1(output2,function(output1){
    
    })
})

多个回调嵌套,代码结构会比较混乱,阅读性差。

2、遍历数组

3、异常处理
JS自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。

4、域
NodeJS提供了domain模块,可以简化异步代码的异常处理。
域的概念:一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。NodeJS通过process对象提供了捕获全局异常的方法。

1
2
3
process.on('uncaughtException',function(err){
    console.log('Error: %s',err.message)
})

域的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var http = require('http')
function async(request,callback){
    asyncA(request,function(data){
        asyncB(request,function(data){
            asyncC(request,function(data){
                callback(data)
            })
        })
    })
}
http.createServer(function(request,response){
    var d = domain.create()
    d.on('error',function(){
        response.writeHead(500)
        response.end()
    })
    d.run(function(){
        async(request,function(data){
            response.writeHead(200)
            response.end(data)
        })
    })
})

通过create()方法创建子域对象,并通过run()方法进入需要在子域中运行的代码的入口点。

陷阱
无论是通过process对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档中都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。

总结

本书讲得相对浅显易懂,能够让新手快速入门node.js,基本了解node.js,但要想学好,并且更好地应用于实践中,还需要继续学习专研,再次推荐下《深入浅出Node.js》!

坚持原创技术分享,您的支持将鼓励我继续创作!