《Node.js高级编程》学习笔记

Node简介

什么是闭包
闭包就是函数,但是它可以继承并访问它自身被声明的那个作用域里的变量。

var clickCount = 0;
document.getElementById('myButton').onclick = function() {
  clickCount += 1;
  alert("clicked " + clickCount + " times.");
};

在JS中,函数是第一类对象

全局变量容易和其他代码冲突,最好用函数包装额外创建闭包避免污染全局作用域:

(function() {
  var clickCount = 0;
  document.getElementById('myButton').onclick = function() {
    clickCount += 1;
    alert("clicked " + clickCount + " times.");
  };
}());

加载模块

导出模块
function Circle(x, y, r) {
  function r_squared() {
    return Math,pow(r, 2);
  }
  function area() {
    return Math,PI * r_squared()
  }
  return {
    area: area
  };
}
module.exports = Circle;

module表示当前模块自身

加载模块

require可以用文件路径也可以用名称,除非是核心模块,否则用名称引用的模块最终都会被映射成路径。

var myModule = require('./myModule');

如果没有找到这个文件,Node会在文件名后加上.js扩展名再次查找路径。

还可以使用文件夹路径:

var myModule = require('./myModuleDir');

Node会假定文件夹是一个包,并试图查找包定义。包定义包含在名为package.json的文件中。

  • 如果文件没有package.json,那么包的入口点会假定为默认值index.js。
  • 如果有,尝试解析并查找main属性,当做入口点。

Node还会尝试查找node_modules文件夹,从pwd一直查到root。

缓存模块
console.log('initializing...');
module.exports = function() {
  console.log('Hi');
};
console.log('initialized.');
var myModuleInstance1 = require('./my_module');
var myModuleInstance2 = require('./my_module');

输出:

initializing...
initialized.

只输出一遍,即只初始化一次

Node取消了JS默认的全局名称空间,而用CommonJS模块系统取代。


缓冲区

创建和设置缓冲区

JS善于处理字符串,但由于它最初是被设计用来处理HTML文档的,因此它不善于处理二进制数据。JavaScript中没有字节类型,也没有结构化类型,甚至没有字节数组类型,只有数值类型和字符串类型。
为了使二进制数据处理任务变得容易些,Node引入了二进制缓冲区实现,以Buffer伪类中的JS API形式暴露给外界。缓冲区长度以字节为计量单位,并且可以随机设置和获取缓冲区数据。

注意:Buffer类的另一个特别之处是数据占用的内存并不是分配在JS VM内存堆中,也就是说这些对象不会被垃圾收集算法处理:它会占据一个不会被修改的永久内存地址,这避免了因缓冲区内容的内存复制所造成的CPU浪费。

var buf = new Buffer('Hello World!');
var buf2 = new Buffer('8b76fde713ce', 'base64');
var buf3 = new Buffer(1024);

可被接受的标识符:

  • ascii
  • utf8
  • base64
    注意:如果将缓冲区中某个位置设置为一个大于255的数,则会用256取模;如果设置为小数100.7,则只保留整数部分100。
var buf = new Buffer(100);
console.log(buf.length);
for (var i=0; i < buf.length; i++){
  buf[i] = i;
}
切分缓冲区
var buffer = new Buffer('this is the content of my buffer');
var smallerBuffer = buffer.slice(8, 19);
console.log(smallerBuffer.toString());

注意:slice只是对原始数据的引用,修改子缓冲区,父缓冲区也被修改。

在用slice创建子缓冲区时,父缓冲区在操作结束后依然继续被保留,并不会被垃圾收集器回收,容易造成内存泄漏。可以使用copy方法替代slice方法。

复制缓冲区
var buffer1 = new Buffer("this is the content of my buffer");
var buffer2 = new Buffer(11);

var targetStart = 0;
var sourceStart = 8;
var sourceEnd = 19;

buffer1.copy(buffer2, targetStart, sourceStart, sourceEnd);
console.log(buffer2.toString());
缓冲区解码
var b64Str = buf.toString("base64");

事件发射器

标准回调模式

后继传递风格(continuation-passing style,CPS)

var fs = require('fs');
fs.readFile('/etc/passwd', function(err, fileContent) {
  if (err) {
    throw err;
  }
  console.log('file content', fileContent.toString());
});
事件发射器模式
var req = http.request(options, function(response)) {
  response.on("data", function(data) {
    console.log("some data from the response", data);
  });
  response.on("end", function(){
    console.log("response ended");
  });
});
req.end();

当需要在请求的操作完成后重新获取控制权时就使用CPS模式,当事件可能发生多次时就使用事件发射器模式。
Node中的大多数事件发射器实现在程序发生错误时都会发射“error”事件。如果不监听该事件,则发生“error”事件时会向上抛出一个未捕获的异常。

事件发生器API
  • .addListener和.on
function receiveData(data) {
  console.log("got data from file read stream: %j", data);
}
readStream.addListener("data", receiveData);
// = readStream.on("data", receiveData);

可以多绑,但在触发事件时依次调用,若前一绑定抛出异常,则后续绑定不会被调用。

  • .once
    实现:
var EventEmitter = require("events").EventEmitter;
EventEmitter.prototype.once = function(type, callback) {
  var that = this;
  this.on(type, function listener() {
    that.removeListener(type, listener);
    callback.apply(that, arguments);
  });
};

注意:function.apply()方法接受一个对象和一个参数数组,并将接受的对象作为一个隐含的this变量。

  • .removeListener
  • .removeAllListener
创建事件发射器
util = require('util');
var EventEmitter = require('events').EventEmitter;
//Here is the MyClass constructor:
var MyClass = function() {
}
util.inherits(MyClass, EventEmitter);

util.inherits建立了一条原型链,使MyClass类实例能够使用EventEmitter类的原型方法。

发射事件
MyClass.prototype.someMethod = function() {
  this.emit("custom event", "argument 1", "argument 2");
};

var myInstance = new MyClass();
myInstance.on('custom event', function (str1, str2) {
  console.log('got a custom event with the str1 %s and str2 %s!', str1, str2);
});
var util = require('util'),
EventEmitter = require('events').EventEmitter;
var Ticker = function(){
    var self = this;
    setInterval(function (){
        self.emit('tick');
    }, 1000);
};
util.inherits(Ticker, EventEmitter);

var ticker = new Ticker();
ticker.on("tick", function(){
    console.log("tick");
});

定时器

process.nextTick

setTimeout使用JS运行时的内部执行队列,而不是事件循环;
用process.nextTick(callback)取代setTimeout(callback, 0),回调函数会在事件队列内的所有事件处理完毕后立刻执行,它要比激活JS的超时队列快得多。

process.nextTick(function() {
  my_expensive_computation_function();
});

Node和JavaScript的运行时采用的是单线程事件循环。

事件循环被阻塞:(不要在事件循环内使用CPU敏感的操作)

process.nextTick(function nextTick1() {
  var a=0;
  while(true) a++;
});
process.nextTick(function nextTick2(){
  console.log("next tick");
});
setTimeout (function timeout(){
  console.log("timeout");
},1000);

通过使用process.nextTick,可以将一个非关键性的任务推迟到事件循环的下一轮在执行,这样可以释放事件循环,让它可以继续执行其他挂起的事件。

下面的例子,如果需要删除一个之前创建的临时文件,但又不想在客户端做出响应之前进行该操作,就可以延迟删除操作:

stream.on("data", function(data){
  stream.end("my response");
  process.nextTick(function() {
    fs.unlink("/path/to/file");
  });
});
setTimeout代替setInterval
var interval = 1000;
setInterval(function() {
  my_async_function(function() {
    console.log('my_async_function finished!');
  });
});

使用setInterval无法保证这些函数不会同时执行。假如my_async_function函数的执行时间比interval变量多1毫秒,它们就会被同时执行,而不是按顺序串行执行。
因此需要指定my_async_function函数执行结束与下个my_async_function函数开始执行之间的时间间隔:

var interval = 1000;
(function schedule() {
  setTimeout(function do_it() {
    my_async_function(function() {
      console.log('async is done!');
      schedule();
    });
  }, interval);
}());

读写文件

三个特殊的文件描述符——1、2和3。它们分别表示标准输入文件、标准输出文件和标准错误文件的描述符。

处理文件路径

规范化路径

var path = require('path');
path.normalize('/foo/bar//baz/asdf/quux/..');
// -> '/foo/bar/baz/asdf'

连接路径

var path = require('path');
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// -> '/foo/bar/baz/asdf'

解析路径
类似挨个进行cd操作,不同在于它只是对路径字符串进行处理,不对路径是否正确进行判断。

var path = require('path');
path.resolve('/foo/bar', './baz');
// -> /foo/bar/baz
path.resolve('/foo/bar', '/tmp/file');
// -> /tmp/file
path.resolve('wwwroot', 'static_files/png/', '../gif/image/gif');
// -> /home/myself/node/ wwwroot/ static_files/gif/image.gif

相对路径

var path = require('path');
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// -> ../../impl/bbb

提取路径的组成部分

var path = require('path');
path.dirname('/foo/bar/baz/asdf/quux.txt');
// -> /foo/bar/baz/asdf
path.basename('/foo/bar/baz/asdf/quux.html')
// -> quux.html
path.basename('/foo/bar/baz/asdf/quux.html', '.html')
// -> quux
path.extname('/a/b/index.html');
// -> '.html'
path.extname('/a/b.c/index');
// -> ''
path.extname('/a/b.c/.');
// -> ''
path.extname('/a/b.c/d.');
// -> '.'

确定路径是否存在

var fs = require('fs');
fs.exists('/etc/passwd', function(exists) {
  console.log('exits:', exists);
  // -> true/false
});
fs模块简介

fs.stat函数查询文件或目录的元信息:

var fs = require('fs');
fs.stat('/etc/passwd', function(err, stats) {
  if (err) { throw err; }
  console.log(stats);
});

fs.stat()函数返回一个stats类,可以用该类继续调用函数:

  • stats.isFile()
  • stats.siDirectory()
  • stats.isBlockDevice()
  • stats.isCharacterDevice()
  • stats.isSymbolicLink()
  • stats.isFifo()
  • stats.isSocket()
打开文件
var fs = require('fs');
fs.open('/path/to/file', 'r', function(err, fd) {
  //获取文件描述符fd
});
读取文件
var fs = require('fs');
fs.open('./my_file.txt', 'r', (err, fd) => {
    if (err) { throw err; }
    var readBuffer = new Buffer(1024),
        bufferOffset = 0,
        bufferLength = readBuffer.length,
        filePosition = 100;
    fs.read(fd,
            readBuffer,
            bufferOffset,
            bufferLength,
            filePosition,
            (err, readBytes) => {
                if (err) { throw err; }
                console.log('just read ' + readBytes + 'bytes');
                if (readBytes > 0) {
                    console.log(readBuffer.slice(0, readBytes));
                }
            });
});
写入文件
var fs = require('fs');
fs.open('./my_file.txt', 'a', (err, fd) => {
    if (err) { throw err; }
    var writeBuffer = new Buffer('writing this string'),
        bufferPosition = 0,
        bufferLength = writeBuffer.length,
        filePosition = null;
    fs.write(fd,
            writeBuffer,
            bufferPosition,
            bufferLength,
            filePosition,
            (err, written) => {
                if(err) { throw err;}
                console.log('wrote ' + written + ' bytes');
            });
});
关闭文件
fs.close(fd [, callback]);

上述都是底层的原语操作来打开、读写和关闭文件。若想并发地写文件,应用WriteStream;读取文件中某个区域,考虑ReadStream。

进程

Node是被设计用来高效处理I/O操作的,但正如你所见,某些类型的程序并不适用于这种模式。比如当用Node处理一个CPU密集型的任务时可能会阻塞事件循环,并会因此降低使用程序的响应能力。替代的方法是,CPU密集型任务应该被分配给另一个进程处理,从而释放事件循环。

执行外部命令
var child_process = require('child_require');
var exec = child_process.exec;
exec(command, callback);

command表示shell命令的字符串表示。

exec('ls', function(err, stdout, stderr) {
  //...
});

还可以传递一个包含若干配置选项的可选参数:

var exec = require('child_require').exec;
var options = {
  timeout: 10000,
  killSignal: 'SIGKILL'
};
exec('cat *.js | wc -l', options, function(err, stdout, stderr) {
  //...
});

详细配置选项看官方文档

var env = process.env,
    varName,
    envCopy = {},
    exec = require('child_process').exec;
//将process.env对象的内容复制到envCopy中
for (varName in env) {
    envCopy[varName] = env[varName];
}
//分配一些自定义变量
envCopy['CUSTOM ENV VAR'] = 'some value';
envCopy['CUSTOM ENV VAR 2'] = 'some other value';
//结合process_env对象和自定义变量执行命令
exec('ls -la', { env: envCopy }, (err, stdout, stderr) => {
    if (err) { throw err; }
    console.log('stdout:', stdout);
    console.log('stderr:', stderr);
});

环境变量是通过操作系统在进城之间传递的,因此所有环境变量值都是以字符串的形式传入子进程的。如数字123,接收到字符串123。

//parent.js
var exec = require('child_process').exec;
exec('node child.js', {env: {number: 123}}, (err, stdout, stderr) => {
    if (err) { throw err; }
    console.log('stdout:\n', stdout);
    console.log('stderr:\n', stderr);
});
//child.js
var number = process.env.number;
console.log(typeof(number));
number = parseInt(number, 10);
console.log(typeof(number));
生成

test test test

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容