node學習之路(一)—— 網路請求

匠心發表於2019-02-16

文章來源:小青年原創
釋出時間:2016-09-29
關鍵詞:JavaScript,nodejs,http,url ,Query String,爬蟲
轉載需標註本文原始地址: http://zhaomenghuan.github.io…

前言

一直以來想學習一下node,一來是自己目前也沒有什麼時間去學習伺服器端語言,但是有時候又想自己擼一下伺服器端,本著愛折騰的精神開始寫一寫關於node的文章記錄學習心得。本系列文章不會過多去講解node安裝、基本API等內容,而是通過一些例項去總結常用用法。本文主要講解node網路操作的相關內容,node中的網路操作依賴於http模組,http模組提供了兩種使用方式:

  • 作為伺服器端使用,建立一個http伺服器,監聽http客戶端請求並返回響應;
  • 作為客戶端使用,發起一個http客戶端請求,獲取伺服器端響應。

node http模組建立伺服器

node 處理 get 請求例項

畢竟作為一個前端,我們經常需要自己搭建一個伺服器做測試,這裡我們先來講一下node http模組作為伺服器端使用。首先我們需要,使用createServer建立一個服務,然後通過listen監聽客服端http請求。

我們可以建立一個最簡單的伺服器,在頁面輸出hello world,我們可以建立helloworld.js,內容如下:

var http = require(`http`);

http.createServer(function(request, response){
    response.writeHead(200, { `Content-Type`: `text-plain` });
    response.end(`hello world!`)
}).listen(8888);

在命令列輸入node helloworld.js即可,我們開啟在瀏覽器開啟http://127.0.0.1:8888/就可以看到頁面輸出hello world!。

下面我們在本地寫一個頁面,通過jsonp訪問我們建立的node伺服器:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
    </head>
    <body>
        <div id="output"></div>
        
        <script type="text/javascript">
            // 建立script標籤
            function importScript(src){
                var el = document.createElement(`script`);
                el.src = src;
                el.async = true;
                el.defer = true;
                document.body.appendChild(el);
            }
            
            // 響應的方法
            function jsonpcallback(rs) {
                console.log(JSON.stringify(rs));
                document.getElementById("output").innerHTML = JSON.stringify(rs);
            }
            
            // 發起get請求
            importScript(`http://127.0.0.1:8888?userid=xiaoqingnian&callback=jsonpcallback`);
        </script>
    </body>
</html>

我們當然需要將上述node伺服器中的程式碼稍作修改:

var http = require(`http`); // 提供web服務
var url = require(`url`);    // 解析GET請求
  
var data = {
    `name`: `zhaomenghuan`, 
    `age`: `22`
};
  
http.createServer(function(req, res){  
    // 將url字串轉換成Url物件
    var params = url.parse(req.url, true);  
    console.log(params);
    // 查詢引數
    if(params.query){
        // 根據附件條件查詢
        if(params.query.userid === `xiaoqingnian`){
            // 判斷是否為jsonp方式請求,若是則使用jsonp方式,否則為普通web方式
            if (params.query.callback) {  
                var resurlt =  params.query.callback + `(` + JSON.stringify(data) + `)`;
                res.end(resurlt);  
            } else {  
                res.end(JSON.stringify(data));
            }
        } 
    }      
}).listen(8888);

我們在命令列可以看到:

Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: `?userid=xiaoqingnian&callback=jsonpcallback`,
  query: { userid: `xiaoqingnian`, callback: `jsonpcallback` },
  pathname: `/`,
  path: `/?userid=xiaoqingnian&callback=jsonpcallback`,
  href: `/?userid=xiaoqingnian&callback=jsonpcallback` }

經過伺服器端jsonp處理,然後返回一個函式:

jsonpcallback({"name":"zhaomenghuan","age":"22"})

而我們在頁面中定義了一個jsonpcallback()的方法,所以當我們在請求頁面動態生成script呼叫伺服器地址,這樣相當於在頁面執行了下我們定義的函式。jsonp的實現原理主要是script標籤src可以跨域執行程式碼,類似於你引用js庫,然後呼叫這個js庫裡面的方法;這是這裡我們可以認為反過來了,你是在本地定義函式,呼叫的邏輯通過伺服器返回的一個函式執行了,所以jsonp並沒有什麼神奇的,和XMLHttpRequest、ajax半毛錢關係都沒有,而且JSONP需要伺服器端支援,始終是無狀態連線,不能獲悉連線狀態和錯誤事件,而且只能走GET的形式。

node 處理 post 請求例項

當然這裡我們可以直接在後臺設定響應頭進行跨域(CORS),如:

var http = require("http");    // 提供web服務
var query = require("querystring");    // 解析POST請求

http.createServer(function(req,res){
      // 報頭新增Access-Control-Allow-Origin標籤,值為特定的URL或"*"(表示允許所有域訪問當前域)
      res.setHeader("Access-Control-Allow-Origin","*");
      
      var postdata = ``;
      // 一旦監聽器被新增,可讀流會觸發 `data` 事件
    req.addListener("data",function(chunk){
        postdata += chunk;
    })
    // `end` 事件表明已經得到了完整的 body
    req.addListener("end",function(){
        console.log(postdata);     // `appid=xiaoqingnian`
        // 將接收到引數串轉換位為json物件
        var params = query.parse(postdata);
        if(params.userid == `xiaoqingnian`){
            res.end(`{"name":"zhaomenghuan","age":"22"}`);
        }
    })
    
}).listen(8080);

我們通過流的形式接收前端post傳遞的引數,通過監聽data和end事件,後面在講解event模組的時候再深入探究。

CORS預設只支援GET/POST這兩種http請求型別,如果要開啟PUT/DELETE之類的方式,需要在服務端在新增一個”Access-Control-Allow-Methods”報頭標籤:

res.setHeader(
      "Access-Control-Allow-Methods",
      "PUT, GET, POST, DELETE, HEAD, PATCH"
);

前端訪問程式碼如下:

var xhr = new XMLHttpRequest();
xhr.onload = function () {
    console.log(this.responseText);
};
xhr.onreadystatechange = function() {
    console.log(this.readyState);
};

xhr.open("post", "http://127.0.0.1:8080", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("userid=xiaoqingnian");

url 模組API詳解

url.parse——解析url字串

上述程式碼中比較關鍵的是我們通過url.parse方法將url字串轉成Url物件,用法如下:

url.parse(urlStr, [parseQueryString], [slashesDenoteHost])

接收引數:

  • urlStr:url字串
  • parseQueryString:引數為true時,query會被解析為JSON格式,否則為普通字串格式,預設為false;如:

    • 引數為true:query: { userid: `xiaoqingnian`, callback: `jsonpcallback` }
    • 引數為false:query: `userid=xiaoqingnian&callback=jsonpcallback`
  • slashesDenoteHost:預設為false,當url是 ‘http://’ 或 ‘ftp://’ 等標誌的協議字首打頭的,或直接以地址打頭,如 ‘127.0.0.1’ 或 ‘localhost’ 時候是沒有區別的;當且僅當以2個斜槓打頭的時候,比如 ‘//127.0.0.1’ 才有區別。這時候,如果其值為true,則第一個單個 ‘/’ 之前的部分被解析為 ‘host’ 和 ‘hostname’,如 ” host : ‘127.0.0.1’ “,如果為false,包括2個反斜槓在內的所有字串被解析為pathname。如:
> url.parse(`//www.foo/bar`,true,true)
Url {
  protocol: null,
  slashes: true,
  auth: null,
  host: `www.foo`,
  port: null,
  hostname: `www.foo`,
  hash: null,
  search: ``,
  query: {},
  pathname: `/bar`,
  path: `/bar`,
  href: `//www.foo/bar` }
> url.parse(`//www.foo/bar`,true,false)
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: ``,
  query: {},
  pathname: `//www.foo/bar`,
  path: `//www.foo/bar`,
  href: `//www.foo/bar` }

這裡的URL物件和瀏覽器中的location物件類似,location中如果我們需要使用類似的方法,我們需要自己構造。

url.format——格式化URL物件

我們可以通過url.format方法將一個解析後的URL物件格式化成url字串,用法為:

url.format(urlObj)

例子:

url.format({
  protocol: `http:`,
  slashes: true,
  auth: `user:pass`,
  host: `host.com:8080`,
  port: `8080`,
  hostname: `host.com`,
  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` 
})

結果為:
`http://user:pass@host.com:8080/p/a/t/h?query=string#hash`

url.resolve——拼接url字串

我們可以通過url.resolve為URL或 href 插入 或 替換原有的標籤,接收引數:
from源地址,to需要新增或替換的標籤。

url.resolve(from, to)

例子為:

url.resolve(`/one/two/three`, `four`)
=> `/one/two/four`
url.resolve(`http://example.com/`, `/one`)    
=> `http://example.com/one`
url.resolve(`http://example.com/one`, `/two`) 
=> `http://example.com/two`

Query String 模組Query String

querystring.escape——字串編碼

querystring.escape(`appkey=123&version=1.0.0+`)
// `appkey%3D123%26version%3D1.0.0%2B`

querystring.unescape——字串解碼

querystring.unescape(`appkey%3D123%26version%3D1.0.0%2B`)
// `appkey=123&version=1.0.0+`

querystring.stringify(querystring.encode)——序列化物件

querystring.stringify(obj[, sep][, eq][, options])
querystring.encode(obj[, sep][, eq][, options])

接收引數

  • obj: 欲轉換的物件
  • sep:設定分隔符,預設為 ‘&`
  • eq:設定賦值符,預設為 ‘=`
querystring.stringify({foo: `bar`, baz: [`qux`, `quux`], corge: ``})
// `foo=bar&baz=qux&baz=quux&corge=`

querystring.stringify({foo: `bar`, baz: [`qux`, `quux`], corge: ``},`,`,`:`)
// `foo:bar,baz:qux,baz:quux,corge:`

querystring.parse(querystring.decode)——解析query字串

querystring.parse(str[, sep][, eq][, options])
querystring.decode(str[, sep][, eq][, options])

接收引數

  • str:欲轉換的字串
  • sep:設定分隔符,預設為 ‘&`
  • eq:設定賦值符,預設為 ‘=`
  • [options] maxKeys 可接受字串的最大長度,預設為1000
querystring.parse(`foo=bar&baz=qux&baz=quux&corge=`)
// { foo: `bar`, baz: [ `qux`, `quux` ], corge: `` }

querystring.parse(`foo:bar,baz:qux,baz:quux,corge:`,`,`,`:`)
{ foo: `bar`, baz: [ `qux`, `quux` ], corge: `` }

node http模組發起請求

平時喜歡看部落格,畢竟買書要錢而且有時候沒有耐心讀完整本書,所以很喜歡逛一些網站,但是很多時候把所有的站逛一下又沒有那麼多時間,哈哈,所以就準備把常去的網站的文章爬出來做一個文章列表,一來省去收集的時間,二來藉此熟悉熟悉node相關的東西。這裡我們首先看一個爬蟲的小例子,下面以SF為例加以說明(希望不要被封號)。

http.request與http.get的區別

http.request(options, callback)

options可以是一個物件或一個字串。如果options是一個字串, 它將自動使用url.parse()解析。http.request() 返回一個 http.ClientRequest類的例項。ClientRequest例項是一個可寫流物件。如果需要用POST請求上傳一個檔案的話,就將其寫入到ClientRequest物件。使用http.request()方法時都必須總是呼叫req.end()以表明這個請求已經完成,即使響應body裡沒有任何資料。如果在請求期間發生錯誤(DNS解析、TCP級別的錯誤或實際HTTP解析錯誤),在返回的請求物件會觸發一個`error`事件。

Options配置說明:

  • host:請求傳送到的伺服器的域名或IP地址。預設為`localhost`。
  • hostname:用於支援url.parse()。hostname比host更好一些
  • port:遠端伺服器的埠。預設值為80。
  • localAddress:用於繫結網路連線的本地介面。
  • socketPath:Unix域套接字(使用host:port或socketPath)
  • method:指定HTTP請求方法的字串。預設為`GET`。
  • path:請求路徑。預設為`/`。如果有查詢字串,則需要包含。例如`/index.html?page=12`。請求路徑包含非法字元時丟擲異常。目前,只否決空格,不過在未來可能改變。
  • headers:包含請求頭的物件。
  • auth:用於計算認證頭的基本認證,即`user:password`
  • agent:控制Agent的行為。當使用了一個Agent的時候,請求將預設為Connection: keep-alive。可能的值為:

    • undefined(預設):在這個主機和埠上使用[全域性Agent][]。
    • Agent物件:在Agent中顯式使用passed。
    • false:在對Agent進行資源池的時候,選擇停用連線,預設請求為:Connection: close。
  • keepAlive:{Boolean} 保持資源池周圍的套接字在未來被用於其它請求。預設值為false
  • keepAliveMsecs:{Integer} 當使用HTTP KeepAlive的時候,通過正在保持活動的套接字傳送TCP KeepAlive包的頻繁程度。預設值為1000。僅當keepAlive被設定為true時才相關。

http.get(options, callback)

因為大部分的請求是沒有報文體的GET請求,所以Node提供了這種便捷的方法。該方法與http.request()的唯一區別是它設定的是GET方法並自動呼叫req.end()。

爬蟲例項

這裡我們使用es6的新特性寫:

const https = require(`https`);
https.get(`https://segmentfault.com/blogs`, (res) => {
    console.log(`statusCode: `, res.statusCode);
      console.log(`headers: `, res.headers);
      var data = ``;
      res.on(`data`, (chunk) => {
        data += chunk;
      });
      res.on(`end`, () => {
          console.log(data);
      })
}).on(`error`, (e) => {
      console.error(e);
});

這樣一小段程式碼我們就可以拿到segmentfault的部落格頁面的原始碼,需要說明的是因為這裡請求的網站是https協議,所以我們需要引入https模組,用法同http一致。下面需要做的是解析html程式碼,下面我們需要做的就是解析原始碼,這裡我們可以引入cheerio,一個node版的類jQuery模組,npm地址:https://www.npmjs.com/package…

首先第一步安裝:

npm install cheerio

然後就是將html程式碼load進來,如下:

var cheerio = require(`cheerio`),
var $ = cheerio.load(html);

最後我們就是分析dom結構咯,通過類似於jQuery的方法獲取DOM元素的內容,然後就將資料重新組裝成json結構的資料。這裡就是分析原始碼然後,這裡我就不詳細分析了,直接上程式碼:

function htmlparser(html){
    var baseUrl = `https://segmentfault.com`;
    
    var $ = cheerio.load(html);
    var bloglist = $(`.stream-list__item`);
    
    var data = [];
    
    bloglist.each(function(item){
        var page = $(this);
        var summary = page.find(`.summary`);
        var blogrank = page.find(`.blog-rank`);
        
        var title = summary.find(`.title a`).text();
        var href = baseUrl + summary.find(`.title a`).attr(`href`);
        var author = summary.find(`.author li a`).first().text().trim();
        var origin = summary.find(`.author li a`).last().text().trim();
        var time = summary.find(`.author li span`)[0].nextSibling.data.trim();
        var excerpt = summary.find(`p.excerpt`).text().trim();
        var votes = blogrank.find(`.votes`).text().trim();
        var views = blogrank.find(`.views`).text().trim();
        
        data.push({
            title: title,
            href: href,
            author: author,
            origin: origin,
            time: time,
            votes: votes,
            views: views,
            excerpt: excerpt
        })
    })
    
    return data;
}

結果如下:

[{ title: `轉換流`,
    href: `https://segmentfault.com/a/1190000007036273`,
    author: `SwiftGG翻譯組`,
    origin: `SwiftGG翻譯組`,
    time: `1 小時前`,
    votes: `0推薦`,
    views: `14瀏覽`,
    excerpt: `作者:Erica Sadun,原文連結,原文日期:2016-08-29譯者:Darren;校對:shank
s;定稿:千葉知風 我在很多地方都表達了我對流的喜愛。我在 Swift Cookbook 中介紹了一些。現
在,我將通過 Pearson 的內容更新計劃...` },
......
]

這裡我們只是抓取了文章列表的一頁,如果需要抓取多頁,只需要將內容再次封裝一下,傳入一個地址引數?page=2,如:https://segmentfault.com/blog…
另外我們也沒有將詳情頁進一步爬蟲,畢竟文章的目的只是學習,同時方便自己檢視列表,這裡保留原始地址。

溫馨提示:大家不要都拿sf做測試哦,不然玩壞了就不好。

模擬登陸

哈哈,寫到這裡已經很晚了,用node試了試模擬登陸SF,結果404,暫時沒有什麼思路,等有時間再試試專門開篇講解咯。這裡推薦一篇之前看到的文章:記一次用 NodeJs 實現模擬登入的思路

參考

文章程式碼原始碼下載:https://github.com/zhaomenghu…

相關文章