midway的使用教學

2022-06-10 06:05:44

一、寫在前面

先說下本文的背景,這是一道筆者遇到的Node後端面試題,遂記錄下,通過本文的閱讀,你將對樓下知識點有所瞭解:

  • midway專案的建立與使用
  • typescript在Node專案中的應用
  • 如何基於Node自身API封裝請求
  • cheerio在專案中的應用
  • 正規表示式在專案中的應用
  • 單元測試

二、midway專案的建立和使用

第一步: 輸入命令**npm init midway**初始化midway專案

第二步:選擇**koa-v3 - A web application boilerplate with midway v3(koa)**,按下回車

➜  www npm init midway
npx: installed 1 in 4.755s
? Hello, traveller.
  Which template do you like? …

 ⊙ v3
▸ koa-v3 - A web application boilerplate with midway v3(koa)
  egg-v3 - A web application boilerplate with midway v3(egg)
  faas-v3 - A serverless application boilerplate with midway v3(faas)
  component-v3 - A midway component boilerplate for v3

 ⊙ v2
  web - A web application boilerplate with midway and Egg.js
  koa - A web application boilerplate with midway and koa

第三步:輸入你要建立的專案名稱,例如**「midway-project」****, ****What name would you like to use for the new project? ‣ midway-project**

第四步:跟著提示走就好了,分別執行**cd midway-project****npm run dev**, 這個時候如果你沒有特別設定的話,開啟**http://localhost:7001**就可以看到效果了

➜  www npm init midway
npx: installed 1 in 4.755s
✔ Hello, traveller.
  Which template do you like? · koa-v3 - A web application boilerplate with midway v3(koa)
✔ What name would you like to use for the new project? · midway-project
Successfully created project midway-project
Get started with the following commands:

$ cd midway-project
$ npm run dev


Thanks for using Midway

Document ❤ Star: https://github.com/midwayjs/midway



   ╭────────────────────────────────────────────────────────────────╮
   │                                                                │
   │      New major version of npm available! 6.14.15 → 8.12.1      │
   │   Changelog: https://github.com/npm/cli/releases/tag/v8.12.1   │
   │               Run npm install -g npm to update!                │
   │                                                                │
   ╰────────────────────────────────────────────────────────────────╯

➜  www

具體的官網已經寫的很詳細了,不再贅述,參見:

三、如何抓取百度首頁的內容

3.1、基於node自身API封裝請求

在node.js的https模組有相關的get請求方法可以獲取頁面元素,具體的如下請參見:,我把它封裝了一下

import { get } from 'https';

async function getPage(url = 'https://www.baidu.com/'): Promise<string> {
  let data = '';
  return new Promise((resolve, reject) => {
    get(url, res => {
      res.on('data', chunk => {
        data += chunk;
      });

      res.on('error', err => reject(err));

      res.on('end', () => {
        resolve(data);
      });
    });
  });
}

額,你要測試這個方法,在node環境的話,其實也很簡單的,這樣寫

(async () => {
  const ret = await getPage();
  console.log('ret:', ret);
})();

四、如何獲取對應標籤元素的屬性

題目是,從獲取的HTML原始碼文字裡,解析出id=lg的div標籤裡面的img標籤,並返回此img標籤上的src屬性值

4.1、cheerio一把梭

如果你沒趕上JQuery時代,那麼其實你可以學下cheerio這個庫,它有這個JQuery類似的API ------為伺服器特別客製化的,快速、靈活、實施的jQuery核心實現.具體的參見:,github地址是:

在瞭解了樓上的知識點以後呢,那其實就很簡單了,調調API出結果。下文程式碼塊的意思是,獲取id為lg的div標籤,獲取它的子標籤的img標籤,然後呼叫了ES6中陣列的高階函數map,這是一個冪等函數,會返回與輸入相同的資料結構的資料,最後呼叫get獲取一下並字串一下。

 @Get('/useCheerio')
  async useCheerio(): Promise<IPackResp<IHomeData>> {
    const ret = await getPage();
    const $ = load(ret);
    const imgSrc = $('div[id=lg]')
      .children('img')
      .map(function () {
        return $(this).attr('src');
      })
      .get()
      .join(',');

    return packResp({ func: 'useCheerio', imgSrc });
  }

4.2、正則一把梭

看到一大坨字串,嗯,正則也是應該要想到的答案。筆者正則不太好,這裡寫不出一步到位的正則,先寫出匹配id為lg的div的正則,然後進一步匹配對應的img標籤的src屬性,是的,一步不行,那咱就走兩步,最終結果和走一步是一樣的。

 @Get('/useRegExp')
  async useRegExp(): Promise<IPackResp<IHomeData>> {
    const ret = await getPage();
    // 匹配id為lg的div正則
    const reDivLg = /(?<=<div.*?id="lg".*?>)(.*?)(?=<\/div>)/gi;
    // 匹配img標籤的src屬性
    const reSrc = /<img.*?src="(.*?)".*?\/?>/i;
    const imgSrc = ret.match(reDivLg)[0].match(reSrc)[1];

    return packResp({ func: 'useRegExp', imgSrc });
  }

五、單元測試

這裡要實現兩個測試點是,1、如果介面請求時間超過1秒鐘,則Assert斷言失敗, 2、如果介面返回值不等於"https://www.baidu.com/img/bd_logo1.png",則Assert斷言失敗
midway整合了jest的單元測試, 官網已經寫的很詳細了,具體的參見:

關於1秒鐘這事,我們可以計算下請求的時間戳,具體的如下:

const startTime = Date.now();
// make request
const result: any = await createHttpRequest(app).get('/useRegExp');
const cost = Date.now() - startTime;

最後再斷言下就好了 expect(cost).toBeLessThanOrEqual(1000);

最終的程式碼如下:

  it.only('should GET /useRegExp', async () => {
    const startTime = Date.now();
    // make request
    const result: any = await createHttpRequest(app).get('/useRegExp');
    const cost = Date.now() - startTime;

    // 2. 如果介面請求時間超過1秒鐘,則Assert斷言失敗
    const {
      data: { imgSrc },
    } = result.body as IPackResp<IHomeData>;

    expect(imgSrc).not.toBe('https://www.baidu.com/img/bd_logo1.png');
    notDeepStrictEqual(imgSrc, 'https://www.baidu.com/img/bd_logo1.png');
    expect(cost).toBeLessThanOrEqual(1000);
    expect(imgSrc).toBe('//www.baidu.com/img/flexible/logo/pc/index.png');
    deepStrictEqual(imgSrc, '//www.baidu.com/img/flexible/logo/pc/index.png');
  });

  it.only('should GET /useCheerio', async () => {
    const startTime = Date.now();
    // make request
    const result: any = await createHttpRequest(app).get('/useCheerio');
    const cost = Date.now() - startTime;

    const {
      data: { imgSrc },
    } = result.body as IPackResp<IHomeData>;

    expect(imgSrc).not.toBe('https://www.baidu.com/img/bd_logo1.png');
    notDeepStrictEqual(imgSrc, 'https://www.baidu.com/img/bd_logo1.png');
    expect(cost).toBeLessThanOrEqual(1000);
    expect(imgSrc).toBe('//www.baidu.com/img/flexible/logo/pc/index.png');
    deepStrictEqual(imgSrc, '//www.baidu.com/img/flexible/logo/pc/index.png');
  });

六、寫在後面

這裡,如果你眼睛夠細,你會發現一個很有意思的現象,你從瀏覽器開啟百度首頁,然後控制檯輸出樓上的需求是這樣的

const lg = document.getElementById('lg');
undefined
lg.childNodes.forEach((node) => { if(node.nodeName.toLowerCase() === 'img') { console.log(node.src) } })
2VM618:1 https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/logo_white-d0c9fe2af5.png
VM618:1 https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png
undefined

然而,通過Node自帶的https庫,你會發現//www.baidu.com/img/flexible/logo/pc/index.png這個
咦,震驚.jpg. 發生了什麼?莫不是度度做了什麼處理?
於是乎,我用wget測試了下wget -O baidu.html [https://www.baidu.com](https://www.baidu.com), 發現正常發請求是這樣的

➜  tmp wget -O baidu.html https://www.baidu.com
--2022-06-10 00:36:17--  https://www.baidu.com/
Resolving www.baidu.com (www.baidu.com)... 182.61.200.6, 182.61.200.7
Connecting to www.baidu.com (www.baidu.com)|182.61.200.6|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2443 (2.4K) [text/html]
Saving to: ‘baidu.html’

baidu.html                                                      100%[=====================================================================================================================================================>]   2.39K  --.-KB/s    in 0s

2022-06-10 00:36:18 (48.3 MB/s) - ‘baidu.html’ saved [2443/2443]

➜  tmp cat baidu.html
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=https://www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視訊</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登入</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登入</a>');
                </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關於百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必讀</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意見反饋</a>&nbsp;京ICP證030173號&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
➜  tmp

但是當我給上模擬瀏覽器的請求後wget --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16" [https://www.baidu.com](https://www.baidu.com)

➜  tmp wget --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16"  https://www.baidu.com
--2022-06-10 00:38:53--  https://www.baidu.com/
Resolving www.baidu.com (www.baidu.com)... 182.61.200.7, 182.61.200.6
Connecting to www.baidu.com (www.baidu.com)|182.61.200.7|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: ‘index.html’

index.html                                                          [ <=>                                                                                                                                                  ] 350.76K  --.-KB/s    in 0.01s

2022-06-10 00:38:53 (35.1 MB/s) - ‘index.html’ saved [359175]

➜  tmp

這個是跟瀏覽器的行為一直的,輸出的結果是三個img標籤。

關於Node.js的https庫對這塊的處理我沒有去深究了,我就是通過樓上的例子猜了下,應該是它那邊伺服器做了對使用者端的相關判定,然後返回相應html文字,所以這裡想辦法給node.js設定一個樓上的user-agent我猜是可以得到跟PC一樣的結果的,這個作業就交給讀者了,歡迎在下方留言討論!

專案地址: https://github.com/ataola/play-baidu-midway-crawler
線上存取: http://106.12.158.11:8090/