0%

Node.js 踩坑

Node.js 现在已经发展成一个很强大的框架了,像 Hexo 这样的博客系统、Visual Studio Code 这样的文本编辑器、甚至Steam客户端(?)等等都是用这个写的。还有一堆,哪哪都是 Node。

很早就想学一下这玩意,结果各种事情一直拖着。

考完试终于有时间可以瞎折腾了。

记一下学 Node 的时候遇到的一些坑点。


Learning


我的学习路线大概是这样,找个 Node.js 写的爬虫的示例照着写,写完就学会了怎么写爬虫,顺便就把 JavaScript 和 TypeScript 的基本用法学会了。(理想情况

实际情况大概是其中会遇到一些问题,那么再针对性地解决好了。(例如 Node.js 里面的异步逻辑)

然后,被异步这块绕的不行,所以干脆找了个对 Node.js 底层实现的分析。了解整个运行原理之后应该就好了。

参考资料:

下面记的好多东西都来源于上面的 深入理解Node.js


Node.js 的整体架构:

底层是 C/C++ 写的,所以 Node.js 的代码跑起来会比 Python 这样的脚本语言要快很多。


异步


Node.js 是一个单线程事件驱动异步系统。

Node.js 本身的 API 里面有同步函数也有异步函数,简单地说整个调度过程是这样的:

  1. JavaScript 线程启动,宿主环境创建。堆用于存储 JavaScript 对象,栈用于存储执行上下文,当然每个异步函数都有一个对应的执行上下文,可以理解成一个函数要执行就要进栈。
  2. 栈内执行任务的顺序是同步的,跟 C/C++ 等等其他非异步的完全一样,自顶向下按顺序来,执行完退栈。当异步任务要执行时,异步任务不入栈,而是把相关消息通知线程(大概是把自己的信息注册到线程上去),然后进入等待状态。
  3. 当(前面注册过的异步任务对应的)事件被触发或者异步响应返回时,线程向消息队列插入一条事件消息。
  4. 栈内的同步任务先全部执行完,然后线程从消息队列取出一个事件消息,事件消息对应的异步任务入栈,如果消息上面绑了回调函数那就执行它。
  5. 当执行栈空了,就再从消息队列取出下一个事件消息,然后继续执行。这个就是事件循环

关于单线程,从 深入理解Node.js 里面对事件部分的源码分析也可以很清楚地看到,事件循环的核心部分代码就是个简单的 do-while 循环。

异步则是因为 Node.js 底层用了 libuv,下面的事件消息通知是异步驱动的。(所以相当于整个调度是单线程的,而每个异步IO实际上是多线程进行的?)Linux 下用了 epoll。

举个栗子:(发现各种教程里面举异步的例子都喜欢用 setTimeout …)

1
2
3
4
5
6
7
8
9
10
console.log("start")

for (var i=0;i<=5;i++)
{
setTimeout(function() {
console.log(i);
}, 0);
}

console.log("end");

这段代码除了 setTimeout 这个函数,其他的 API 包括这个 for 循环都是同步的,所以同步的部分先执行了。

6 个 setTimeout 任务进入等待状态。

在执行同步任务的过程中,setTimeout 的计数器到时间了(因为本身就设的是 0),消息队列里面会插进去 6 条事件,分别是触发对应的那条 setTimeout。

然后同步部分执行完毕,startend 都输出了。开始从消息队列中取消息,每取一条 setTimeout,就执行它的回调函数 console.log(i)

!!然后要注意这里的 i 是个全局变量,i 在 for 循环执行完毕的时候变成了 6,所以后面每一次输出的 i 都是 6。

实际执行的结果是:

1
2
3
4
5
6
7
8
start
end
6
6
6
6
6
6

这里如果把 for 循环中的 var 换成 let,最后得到会是这样的结果:

1
2
3
4
5
6
7
start
end
0
1
2
3
5

再改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test() {
console.log("123");
}

console.log("start")

test();

for (let i=0;i<=5;i++)
{
setTimeout(function() {
setTimeout(function() {
console.log(i);
}, 0);
console.log(i);
}, 0);
}

console.log("end");

输出结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
start
123
end
0
1
2
3
4
5
0
1
2
3
4
5

从上面可以看出来的是:

  1. 普通的函数是同步的!…… 之前一直以为这里面的函数全是异步的,看来只要不涉及到异步的 API 就都是同步的
  2. 因为消息队列是个队列,所以 i=0 那条触发的 setTimeout 中增加的 setTimeout 事件只会被插到队列的再后面去。

爬虫


爬虫的基本思路就是,把页面的 html 拿下来,然后用解析的工具从里面匹配出需要的信息来。

最简单的两个包是 superagentcheeriosuperagent 用于获取网页的 html 源码,cheerio 用于解析 html 信息。

这段代码是简单地抓了一下博客首页:

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
31
let cheerio = require('cheerio');
let superagent = require('superagent');

export const doit = async function () {
superagent.get('http://jcf94.com')
.end(function (err, sres) {
if (err) {
return err;
}
let $ = cheerio.load(sres.text);
let items = [];
$('#posts .post-title-link').each(function (idx, element) {
let $element = $(element);
//console.log($element);
items.push({
title: $element.text(),
href: $element.attr('href')
});
});
console.log(items);

let pics = [];
$('#posts img').each(function (idx, element) {
let $element = $(element);
pics.push({
src: $element.attr('src')
});
});
console.log(pics);
});
}

然后是 request 包,比 superagent 更常用一些,用于请求网页等等各种资源。

简单的爬虫学会了,然后我就想试试能不能把网易云上歌曲的评论数抓下来,,这里遇到个坑点。

网易云音乐的页面用了 iframe 框架来动态加载内容,打开页面之后直接获取 html 的源码会发现抓下来的只有框架,没有内容。

这就抓瞎了。。。


然后就上了 selenium-webdriver 这个包。

selenium 原本是用于页面的自动化测试的,具体使用起来就是 Node.js 代码会控制一个浏览器完成点击、输入等等操作(按键精灵有木有?)。

简单的抓页面的代码是这样,这里关键在于这个 driver.switchTo().frame('contentFrame'),用于切换到 iframe 加载出来之后的页面。

切过去之后才能正常抓到页面内容。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
let webdriver = require('selenium-webdriver'),
By = webdriver.By,
until = webdriver.until;

let driver = new webdriver.Builder()
.forBrowser('chrome')
.build();

let fs = require('fs');

const getComments = async (url: string) => {
const promise = new Promise<number>((resolve, reject) => {
driver.get(url);
driver.switchTo().frame('contentFrame');
driver.findElement(By.xpath('//*/div[1]/span/span')).getAttribute('innerHTML').then(result => {
if (result) resolve(result);
else reject(0);
});
});
return promise;
}

export const doit = async () => {
driver.get('http://music.163.com/#/discover/toplist?id=3778678');

driver.switchTo().frame('contentFrame');
//driver.getPageSource().then(res=>console.log(res));
let urls = await driver.findElements(By.xpath('//*/td[2]/div/div/div/span/a'));
let titles = await driver.findElements(By.xpath('//*/td[2]/div/div/div/span/a/b'));
let total = urls.length;
console.log("Total " + total + " Songs find.");

let list = [];

for (let i=0; i<total; i++) {
let ur:string = await urls[i].getAttribute('href');
let ti:string = await titles[i].getAttribute('title');
list.push({
no: i,
title: ti,
url: ur,
comments: 0
})
}

for (let i=0;i<total;i++) {
let co:number = await getComments(list[i].url);
console.log("Get song " + i + " .");
list[i].comments = co;
}

driver.quit();

//------------------

fs.writeFileSync('netease.json', JSON.stringify(list));

console.log("Finish");
}