C++老司机的Node.js学习记录

对于写惯C++这种强类型语言的人来说,js代码简直天马行空任性的要死啊有木有,很多写法完全无厘头啊有木有。还好有Typescript来让我缓一缓。

安装配置

  1. 安装Node.js, 改成国内镜像: npm install -g cnpm --registry=https://registry.npm.taobao.org
  2. 安装Typescript
    cnpm install -g typescript
  3. 安装Visual Studio Code,安装下面的扩展
    1. Node.js Modules Intellisense
    2. TSLint
  4. 建立一个文件夹当工程根目录
  5. 用VS Code打开,在终端里输入下面的命令:
    1. npm init 建立package.json文件
    2. cnpm install --save @types/node Node.js的类型库
    3. tsc --init 建立tsconfig.json文件
    4. cnpm install --save tslint 在工程里装个tslint
    5. 修改tsconfig.json, 把 "outDir": "./js/", "sourceMap": true, 这两条反注释,outDir写输出的js路径。这样才能在VS Code里调试。
  6. 按快捷键Ctrl+Shift+B,建立一个launch.json,添加一个配置,如下设置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "type": "node",
    "request": "launch",
    "name": "Launch TS",
    "program": "${file}",
    "outFiles": [
    "${workspaceFolder}/**/*.js"
    ]
    },
  7. 再次按Ctrl+Shift+B,选择tsc watch那条,这样写ts文件就会自动编译成js了。
  8. 对照菜鸟教程学起来http://www.runoob.com/nodejs/nodejs-tutorial.html

事件驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
import {EventEmitter} from 'events';

var eventEmitter = new EventEmitter();
eventEmitter.on('fire', (a, b)=>{
console.log(a, b);
});

eventEmitter.on('fire', (a, b)=>{
console.log(typeof(a), typeof(b));
});

eventEmitter.emit('fire', "hello", "world");
console.log('Done');

输出:

1
2
3
hello world
string string
Done

on注册事件,emit触发。

Node.js几乎所有类型都实现了EventEmitter,javascript是单线程异步操作的,工作原理是在程序内把事件处理函数都挂好,异步操作走起。最后进入一个消息循环,完成啦出错啦啥的来了就触发事件。

缓冲区

缓冲区Buffer有点像内存块,支持复制、比较、裁剪。

1
2
3
4
5
6
let buf = Buffer.alloc(10);
for(let i=0; i<buf.length; i++)
buf[i] = i*10;

for(let i=0; i<buf.length; i++)
console.log(buf[i]);

Node.js里几乎所有的IO操作都抽象成了流操作。
流分四种,可读Readable、可写Writable、双向Duplex、转换Transform

读操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as fs from 'fs';

let ifstream = fs.createReadStream('index.html');
ifstream.on('data', (chunk: Buffer)=>{
console.log('data:', chunk.toString('utf8'));
});

ifstream.on('end', ()=>{
console.log('end');
});

ifstream.on('error', (err)=>{
console.log('error:', err);
});

console.log('Done!');

常用的事件有data, end, error, finish。error比较特殊,如果不处理会当作异常抛出。

写操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import * as fs from 'fs';

let ofstream = fs.createWriteStream('out.txt');

for(let i=0; i<100; i++)
{
ofstream.write("Hello\n", ()=>{
console.log('writting...');
});
}

ofstream.end(()=>{
console.log('end');
});

ofstream.on('error', (err)=>{
console.log(err);
});

console.log('Done!');

输出:

1
2
3
4
5
Done!
writting...
writting...
...
end

注意看程序输出的先后顺序

管道

1
2
3
4
5
6
7
8
9
import * as fs from 'fs';

let ifstream = fs.createReadStream('index.html');
let ofstream = fs.createWriteStream('out.txt');
ifstream.pipe(ofstream);
ifstream.on('close', ()=>{
console.log('OK!');
});
console.log('Done!');

输出:

1
2
Done!
OK!

管道原理,以及流控制,从http://taobaofed.org/blog/2017/08/31/nodejs-stream/抄来的一个模拟pipe的代码可以看出一二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Readable.prototype.pipe = function(writable, options) {
this.on('data', (chunk) => {
let ok = writable.write(chunk);
// 背压,暂停
!ok && this.pause();
});
writable.on('drain', () => {
// 恢复
this.resume();
});
// 告诉 writable 有流要导入
writable.emit('pipe', this);
// 支持链式调用
return writable;
};

上面做了五件事情:

  • emit(pipe),通知写入
  • .write(),新数据过来,写入
  • .pause(),消费者消费速度慢,暂停写入
  • .resume(),消费者完成消费,继续写入
  • return writable,支持链式调用

模块

Typescript的模块系统和ES6的差不多,直接export就可以了

1
2
3
4
5
6
7
8
9
10
11
12
//mod.ts的代码
export class Cat {
Say( s:string) {
console.log(s);
}
};

export enum MyFunc {F1, F2, F3};

export function NewCat() {
return new Cat;
}
1
2
3
4
5
6
7
//调用mod.ts
import * as mod from './mod'

let a = new mod.Cat();
a.Say("Hi");

console.log(mod.MyFunc.F3);

小坑

this

Javascript的this和C++的不一样,它是在调用的时候才确定的。比如下面抄来的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function

setTimeout里的函数在100ms后调用,那时候调用它的是全局值,所以这时this就是全局值。解决办法是用bind。

类?对象?函数?

Javascript除了基本类型,其它什么都是对象。包括类型(class)、函数什么的也是对象。这就容易搞晕,用Typescript还好一些。
我是这么理解的,不知道对不对:

  1. 给类(class)加属性、方法类似于C++里的静态属性、方法。
  2. 类(class)有个prototype属性,在prototype上添加的属性方法可以被类实例调用。
  3. 实例有个__proto__属性,指向了所属类的prototype。