nodejs 实践笔记

模块路径解析

require路径解析支持类似foo/bar的写法,按照以下规则解析路径:

1、内置模块

require('fs')

2、node_modules目录

node_modules是特殊目录,如某个模块的绝对路径是 /home/user/hello.js , require('foo/bar')将以下面路径作为解析:

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

3、NODE_PATH环境变量

与PATH环境变量类似,Node通过 NODE_PATH 环境变量来指定额外的模块搜索路径。 NODE_PATH=/home/user/lib:/home/lib

包(package)

其实是一个目录,里面包括多个模块js,其中需包含一个入口模块,当require这个目录时,其实就是require入口模块。入口模块包含了其他模块。 默认入口模块为index.js,如果想自定义入口模块,则在该目录下packge.json里面定义:

// package.json
{
  "name": "cat",
  "main": "./lib/custom.js"
}

命令行程序

Node开发的成品最终要么是一个包,要么是一个命令行程序;而且前者最终也是用于开发后者。所以最好是以命令行的方式来运行模块。 至于如何把js模块配置为系统命令行,请参考这里, 包含Linux和Windows的配置教程。

文件操作

大致介绍与文件操作相关的API:

Buffer(数据块)

Buffer处理二进制数据。不同于字符串修改(字符串任何修改得到的都是一个新字符串),Buffer像做指针操作的C语言数组。 除了读取文件得到Buffer的实例外,下面有更多用法:

// 直接构造
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

// 获取相应位置字节
bin[0]; // => 0x68;

// 转为字符串
var str = bin.toString('utf-8'); // => "hello"

// 转为二进制编码
var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>

.slice方法不同于数组的.slice方法,返回的是指向原Buffer的某个位置的指针,所以对.slice返回的Buffer的修改,会作用于原Buffer

Stream(数据流), File System

Stream对数据边读取边处理。 下面为拷贝大文件的实例程序:

var rs = fs.createReadStream(src); // 创建一个只读数据流
var ws = fs.createWriteStream(dst) // 创建一个只写数据流

rs.on('data', function(chunk){
  if(false === ws.write(chunk)){
    rs.pause() // 先暂停,免得不停触发data应付不过来
  }
})

rs.on('end', function(){
  clearup()
})

rs.on('drain', function(){
  rs.resume()
})

fs提供的API基本有下面3类:

  • 文件属性的读写:fs.stat,fs.chmod,fs.chown等等
  • 文件内容的读写:fs.readFile, fs.readdir, fs.writeFile, fs.mkdir等等
  • 底层文件的操作:fs.open, fs.read, fs.write, fs.close等等

Path

  • path.normalize 解析路径中的...外,还能去除多余的斜杠,可用于比如标准化用户随意输入的路径。 **注意,**标准化后,斜杠在Windows下是\, Linux是/。如果要保证是/,需要再.replace(/\\/g, '/')

  • path.join 将传入的路径拼接为标准路径。

      path.join('foo/', 'bax/', '../bar') // => foo/bar
    
  • path.extname 获取扩展名,可用于比如对不同扩展名的文件进行不同的操作:

      path.join('foo/bar.js') // => '.js'
    

遍历目录

简单同步遍历目录示例:

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)
    }
  })
}

异步遍历示例:

function travel(dir, callback, finish){
  fs.readdir(dir, function(err, files){
    var fileLength = files.length
    (function next(i){
      if(fileLength > i){
        var pathname = path.join(dir, files[i])

        fs.stat(pathname, function(err, stats){
          if(stats.isDirectory()){
            travel(pathname, callback, function(){
              next(i+1)
            })
          }else{
            callback(pathname, function(){
              next(i+1)
            })
          }
        })
      }else{
        finish && finish()
      }
    })(0)
  })
}

文件编码

常用编码为UTF8和GBK,有些UTF8带BOM的。读取不同编码的文本,需转换为JS使用的UTF8编码才能正常处理。

BOM的移除

BOM会在文本文件头部以一个Unicode编码(如UTF16BE, UTF8)的字符来标记文件编码,但通常需要先移除BOM再作node的处理。

以下编码去除UTF8 BOM:

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

GBK to UTF8

node自身不支持GBK,需借助如iconv-lite第三方包转换编码。

var iconv = require('iconv-lite')
function readGBKText(pathname){
  var bin = fs.readFileSync(pathname)
  return iconv.decode(bin, 'gbk')
}

单字节编码

不管源文本文件是什么编码,都用单字节编码(即保存英文字符的那种编码)进行保存和读取。虽然对于中文字符那些会乱码,但以相同的单字节编码进行读写即可。 Node提供binary编码来实现这个方法:

function replace(pathname){
  var str = fs.readFileSync(pathname, 'binary')
  str = str.replace('foo', 'bar')
  fs.writeFileSync(pathname, str, 'binary')
}

网络编程

http模块

此模块主要作用为: 1、作服务器,监听HTTP客户端请求并返回响应 2、作客户端,发起请求,获取服务器响应

可以用request对象获取 请求头数据,同时以读取数据流的方式获取 请求体数据:

http.createServer(function(request, response){
  var bodyarr = [] , body ;
  console.log(request.method)
  console.log(request.headers)
  request.on('data', function(chunk){
    bodyarr.push(chunk)
  })

  request.on('end', function(){
    body = Buffer.concat(bodyarr)
    console.log(body.toString())
  })
}).listen(80)

相对应地,利用response对象直接写入 响应头数据,以数据流的方式写入 响应体数据,只不过是边写边响应的方式:

http.createServer(function(request, response){
  response.write(200, {'Content-Type': 'text/plain'})

  request.on('data', function(chunk){
    response.write(chunk)
  })

  request.on('end', function(){
    response.end()
  })
}).listen(80)

https模块

https模块与http模块极为类似,区别在于https模块需额外处理SSL证书。

var options = {
  key: fs.readFileSync('./ssl/default.key'), // 私钥
  cert: fs.readFileSync('./ssl/default.cer') // 公钥
};

var server = https.createServer(options, function(request, response) {
  // ...
});

node支持同一HTTPS服务器使用多个域名提供服务:

server.addContext('foo.com', {
  key: fs.readFileSync('./ssl/foo.com.key'),
  cert: fs.readFileSync('./ssl/foo.com.cert')
})

server.addContext('bar.com', {
  key: fs.readFileSync('./ssl/bar.com.key'),
  cert: fs.readFileSync('./ssl/bar.com.cer')
})

发起客户端请求:

var options = {
  hostname: 'www.example.com',
  port: 443,
  path: '/',
  method: 'GET'

  /*
  ,
  rejectUnauthorized: false // 禁止对证书有效性的检查,以允许使用自制证书的HTTPS服务器
  */
}

var request = https.request(options, function(response){})

request.end()

URL

parse模块

url.parse方法加上request.url属性是处理HTTP请求时的固定搭配。

url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'http:',
  slashes: true,
  auth: 'user:pass',
  host: 'host.com:8080',
  hostname: 'host.com',
  port: '8080',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' }
*/

.parse方法接受参数时,不一定要完整的url。传进来的url有哪部分,返回的对象就有哪些属性,没有的就为null。 .parse方法还支持第二个和第三个布尔类型可选参数。 第二个参数等于true时,该方法返回的URL对象中,query字段不再是一个字符串,而是一个经过querystring模块转换后的参数对象。 第三个参数等于true时,该方法可以正确解析不带协议头的URL,例如//www.example.com/foo/bar>

format模块

相反,.format将URL对象转换为URL字符串:

url.format({
  protocol: 'http:',
  host: 'www.example.com',
  pathname: '/p/a/t/h',
  search: 'query=string'
});

/* =>
'http://www.example.com/p/a/t/h?query=string'
*/

querystring模块

querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/

querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
/* =>
'foo=bar&baz=qux&baz=quux&corge='
*/

zlib模块

zlib进行数据压缩和解压。可用来压缩请求体数据/响应体数据。

Net模块

Net模块可创建Socket服务器和Socket客户端。Socket服务器与客户端可对HTTP协议做底层操作。

进程管理

示例:使用子进程调用终端命令实现目录拷贝:

var child_process = require('child_process')
var util = require('util')
function copy(src,dst,callback){
  child_process.exec( util.format('cp -r %s/* %s', src, dst ), callback)
}

copy('a', 'b', function(err){})

process

注意 process 不是一个模块,而是一个全局变量,感知和控制node自身进程的方方面面。 一个程序正常退出,退出状态码为0。但如果是发生异常挂了,退出状态码就不为0了,可设置为指定数字:

try{}
catch(e){
  process.exit(1)
}
  • process.stdin标准输入流,只读
  • process.stdout标准输出流,只写
  • process.stderr标准错误流,只写
// console.log的实现
function log(){
  process.stdout.write( util.format.apply(util, arguments) + '\n' )
}

如何降权例子:

在linux下监听1024以下端口需要root权限。完成监听后,继续让程序运行在root权限下存在安全隐患,故需降权。 通过sudo来获取root,运行程序的用户的UID和GID保存在SUDO_UIDSUDO_GID 通过chmod +s获取root权限,用户的UID和GID可通过process.getuidprocess.getgid来获取。

降权必须先降GID再降UID,反之则没权限更改GID了

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)
})

child_process

child_process是一个模块,最核心的API是.spawn。 创建子进程示例:

var child = child_process.spawn('node', ['xxx.js'])

child.stdout.on('data', function(data){
  console.log('stdout: ' + data)
})

child.stderr.on('data', function(data){
  console('stderr: ' + data)
})

child.on('close', function(code){
  console.log('child process exited with code ' + code)
})

.spawn(exec, args, options)方法,该方法支持三个参数。第一个参数是执行文件路径,可以是执行文件的相对或绝对路径,也可以是根据PATH环境变量能找到的执行文件名。第二个参数中,数组中的每个成员都按顺序对应一个命令行参数。第三个参数可选,用于配置子进程的执行环境与行为。 子进程的.stdout.stderr的访问子进程的输入输出,但options.stdio字段的配置可使子进程的输入输出重定向至任何数据流,或父进程的输入输出流,或直接忽略。

进行间的通讯示例:

/* parent.js */
var child = child_process.spawn('node', ['child.js'])
child.kill('SIGTERM')

/* child.js */
process.on('SIGTERM', function(){
  clearup()
  process.exit(0)
})

不要被.kill名字迷惑了,它实际是给进程发送信号的。 如果父子进程都是Nodejs进程,就可以通过IPC(进程间通讯)双向传递数据:

/* parent.js */
var child = child_process.spawn('node', ['child.js'], {
  stdio: [0,1,2,'ipc']
})

child.on('message', function(msg){
  console.log(msg)
})

child.send({hello: 'hello'})

/* child.js */
process.on('message', function(msg){
  msg.hello = msg.hello.toUpperCase()
  process.send(msg)
})

父进程在创建子进程时,在options.stdio字段通过ipc开启了IPC通道,父子进程可相互通讯。

创建守护进程示例:

守护进程在监控工作进程的运行状态,在工作不正常时重启工作进程。

function spawn(mainModule){
  var worker = child_process.spawn('node', [mainModule])

  worker.on('exit', function(code){
    if(0 !== code){
      spawn(mainModule)
    }
  })
}

spawn('main.js')

Cluster

cluster模块是对child_process模块的进一步封装,专用于单线程nodejs web服务器无法利用多核CPU的问题。使用这模块能简化多进程服务器程序的开发,让每个核运行一个工作进程,并统一通过主进程监听端口和分发请求。

node的异步设计模式

通过domain来捕获异常

在一个运行环境中,异常如果没有被捕获,将作为全局异常被抛出。而domain就是一个js的运行环境。 在主运行环境,可通过process.on('uncaughtException', function(){})来捕获异常。

http.createServer(function(request, response){
  var d = domain.create()

  d.on('error', function(){
    response.writeHead(500)
    response.end()
  })

  d.run(function(){
    async(request, function(){
      response.writeHead(200)
      response.end(data)
    })
  })
})

node官方建议处理完异常后立即重启程序,而不是让程序继续运行。虽然js本身的异常处理机制不会导致内存泄露,但nodejs大量API都是通过C/C++实现的,可能会导致C/C++部分代码表现异常,进而导致内存泄露等问题。

在mac下卸载node

  1. 把这段shell脚本保存为sh文件,如uninstallNode.sh:

    lsbom -f -l -s -pf /var/db/receipts/org.nodejs.pkg.bom \
    | while read i; do
      sudo rm /usr/local/${i}
    done
    sudo rm -rf /usr/local/lib/node \
         /usr/local/lib/node_modules \
         /var/db/receipts/org.nodejs.*
    
  2. 更改sh权限: chmod 777 ./uninstallNode.sh

  3. 执行sh:./uninstallNode.sh

**附加:**如果执行了上述脚本后找不到org.nodejs.pkg.bom,然后npm命令是没有了,但node命令还运行。那就把路径/usr/local/bin下的node命令删去,保证不使用原来的node命令即可

npm模块发布:npm publish

发布模块:

基本发布流程:http://segmentfault.com/a/1190000000491188

更新模块:

模块发布出去后,以后的模块更新步骤如下:

  1. 不能手动来更新package.json的版本号,需要调用这个命令:npm version <update_type>,update_type的取值为:patch,minor,major。分别从小到大更新模块版本号。
  2. npm publish发布出去即可
  3. 把最新代码push到git上去

###知识点记录

  • 使用了大淘宝的 npm 镜像安装Node: npm install express --registry=https://registry.npm.taobao.org

  • require.main

    if (require.main === module) {
      // 如果是直接执行 main.js,则进入此处
      // 如果 main.js 被其他文件 require,则此处不会执行。
    }
    
  • 假设你有一个项目A,用到了 mocha 的 version 3,其他人有个项目B,用到了 mocha 的 version 10,那么如果你 npm i mocha -g 装的是 version 3 的话,你用 $ mocha 是不兼容B项目的。因为 mocha 版本改变之后,很可能语法也变了,对吧。 这时,跑测试用例的正确方法,应该是: npm i mocha --save-dev,装个 mocha 到项目目录中去 ./node_modules/.bin/mocha,用刚才安装的这个特定版本的 mocha,来跑项目的测试代码。 ./node_modules/.bin 这个目录下放着我们所有依赖自带的那些可执行文件。

    每次输入这个很麻烦对吧?所以我们要引入 Makefile,让 Makefile 帮我们记住复杂的配置。

    test:
      ./node_modules/.bin/mocha
    
    cov test-cov:
      ./node_modules/.bin/istanbul cover _mocha
    
    .PHONY: test cov test-cov
    

    这时,我们只需要调用 make test 或者 make cov,就可以跑我们相应的测试了。

  • npm install -g without sudo, http://devin-clark.com/global-npm-install-without-sudo/

  • 在jade模板里面以下两个模板写法,不同之处在于对undefined值的处理:

    // person = {name: undefined}
    input(value="#{person.name}")  // undefined
    input(value=person.name)       // ''
    
  • 利用req.session保存状态:req.session.user = user;

  • 在获取req里的参数有以下几种方法:

    var user = req.query.user;
    // or
    var user = req.body.user;
    // or
    var user = req.params.user;
    
    // or,不管是GET还是POST,都可获取
    // 但对于这种方式,面对这样的路由:
    // /userlist/:user?user=xxx,并且request的body也有{user:xxxx}
    // 获取的排序是:params(:user) > body({user}) > query(?user=)
    var user = req.param('user');
    
  • 启动node程序使用特定端口:PORT-400 node app,或者在程序代码中指定port

  • npm list -g --depth 0可以只显示一级的层级的已安装模块

  • process是全局变量,process.argv可获取命令行参数。 命令行参数的格式:argv[0]固定等于node执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,所以第一个命令行参数从argv[2]这个位置开始。 通常这样来传递命令行参数main(process.argv.slice(2)); function main(argv){}

  • 要安装特定版本的npm模块,在包名后面加@<version>

    npm install argv@0.0.1
    

    可用来解决The package xxxx does not satisfy its siblings peerDependencies requirements!的问题,先卸载当前的版本npm uninstall xxxx --save,然后再安装特定版本:npm i xxxx@~1.4 --save

  • 在package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

  • 使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

  • 发起客户端HTTP请求前需要先创建一个客户端。http模块提供了一个全局客户端http.globalAgent,可以让我们使用.request或.get方法时不用手动创建客户端。但是全局客户端默认只允许5个并发Socket连接,当某一个时刻HTTP客户端请求创建过多,超过这个数字时,就会发生socket hang up错误。解决方法也很简单,通过http.globalAgent.maxSockets属性把这个数字改大些即可。 另外,https模块遇到这个问题时也一样通过https.globalAgent.maxSockets属性来处理。

  • 自定义npm命令,以mocha-phantomjs测试举例: 1、在package.json定义:

      "scripts":{
        "test": "./node_modules/.bin/mocha-phantomjs vendor/index.html"
      }
    

    2、将mocha-phantomjs作为依赖: npm i mocha-phantomjs --save-dev 3、这时即可运行:npm test

  • npm init命令帮助我们生成简单的package.json

  • REST-Representational State Transfer

    • 使用特定的HTTP方法:获取数据GET(直接url访问),创建数据POST(通过form提交),修改数据PUT,删除数据DELETE(通过ajax的type:'DELETE'来提交)
    • 无状态:把状态的保存放在客户端
    • 像文件目录式的URI路径
    • 传输xml, json,或者both

转载于:https://my.oschina.net/luozt/blog/500908

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值