Node.js 踩坑

栏目: Node.js · 发布时间: 6年前

内容简介:Node.js 踩坑

Node.js 踩坑

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

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

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

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

Learning

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

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

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

参考资料:

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

Node.js 的整体架构:

Node.js 踩坑

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

异步

Node.js 踩坑

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

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。

实际执行的结果是:

start
end
6
6
6
6
6
6

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

start
end
0
1
2
3
5

再改一下:

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");

输出结果是:

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 信息。

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

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 加载出来之后的页面。

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

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

以上所述就是小编给大家介绍的《Node.js 踩坑》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Beginning iPhone and iPad Web Apps

Beginning iPhone and iPad Web Apps

Chris Apers、Daniel Paterson / Apress / 2010-12-15 / USD 39.99

It seems that everyone and her sister has developed an iPhone App—everyone except you, the hard-working web professional. And now with the introduction of the iPad, you may even feel farther behind. B......一起来看看 《Beginning iPhone and iPad Web Apps》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具