Node.js 小打小鬧之無頭瀏覽器

  • 2019 年 11 月 6 日
  • 筆記

入坑篇

前線客服傳來消息 — 「用戶回饋一打開我們的 App,就直接閃退了」,剛聽到這個消息,我很吃驚,上一期發的新版本 QA 都有驗證過。難道是因為功能許可權的問題導致的,趕緊跟客服確認具體情況。原來是客戶前幾天都能正常使用 App,今天一打開就莫名閃退了。剛了解清楚具體情況,一下子閃退的消息,就如滔滔江水一涌而來,隨後也就開始了 iOS 證書過期填坑之旅。

我們公司的產品有幾十個客戶,但並不是每一家客戶都有回饋,而只是其中的幾家。回饋閃退的幾家客戶中,都是同時使用 Android 和 iOS 兩個平台,但回饋閃退問題的都是使用 iOS 平台的用戶,Android 平台並沒有出現閃退問題。

梳理完思路後,我們就想到了是不是回饋閃退的客戶使用的 App 證書或描述文件出問題了,因此立馬登陸蘋果開發者後台,登陸後發現果然是幾個客戶使用的證書,今天就過期了。那是不是證書過期導致閃退的呢?原生開發人員,馬上更新一下證書,打了個包進行驗證。果然,用新的證書打出來的包,就能正常使用,不會出現閃退了。網上找了相關的資料,也很多小夥伴遇到同樣的問題 —— 「企業版證書過期,App 出現閃退」。問題是已經定位了,但客戶那邊怎麼解決呢?客戶一打開我們的 App 就立馬閃退了,沒有辦法進行強制更新。此後,在網上繞了一大圈,看了很多文章,發現我們最終的方案,只能重新打包讓用戶重裝。

蒼天啊!大地啊!為什麼蘋果企業證書即將過期,沒有發郵件通知,這真是一個大坑!!!事情竟然已經發生,只能咽下苦水,乖乖地接受外部的 「轟炸」 了。接下來我們立即針對閃退的客戶重新打包,然後讓公司客服與客戶溝通,說明情況…

這個問題以後要如何避免?難道要安排專人,每天定時檢查證書的有效性?最初的這個想法,其實我是拒絕的。這種臟活累活,肯定要請我們吃飯的傢伙 —— ?(Computer)來幫我們處理咯。前陣子剛好偶遇Google出品的一個神器 —— GoogleChrome/puppeteer (Headless Chrome Node API),接下來我們就先來介紹這款神器。

跳坑篇

puppeteer 簡介

puppeteer 是一個 Node.js 的庫,支援調用 Chrome 的 API 來操縱 Web,相比較 Selenium 或是 PhantomJS,它最大的特點就是它的操作 DOM 可以完全在記憶體中進行模擬既在 V8 引擎中處理而不打開瀏覽器,而且關鍵是這個是 Chrome 團隊在維護,會擁有更好的兼容性和前景。

puppeteer 的神技:

  • 對網頁進行截圖保存為圖片或 pdf。
  • 抓取單頁應用(SPA)執行並渲染(解決傳統 HTTP 爬蟲抓取單頁應用難以處理非同步請求的問題)。
  • 做表單的自動提交、UI 的自動化測試、模擬鍵盤輸入等。
  • 用瀏覽器自帶的一些調試工具和性能分析工具幫助我們分析問題。
  • 在最新的無頭瀏覽器環境里做測試、使用最新瀏覽器特性。
  • 寫爬蟲,做你想做的事情。

是不是感覺 puppeteer 棒棒噠。其實還有其它一些無頭瀏覽器,比如:

  • phantomjs:Scriptable Headless WebKit 【Star – 25877】
  • slimerjs:A scriptable browser like PhantomJS, based on Firefox 【Star – 2738】
  • Splash:Lightweight, scriptable browser as a service with an HTTP API 【1974】
  • trifleJS:Headless automation for Internet Explorer 【Star – 764】

(友情提示:Star 統計時間 —— 2018-08-15)

簡單介紹完 puppeteer,接下來我們就來稍微介紹一下思路。其實實現思路很簡單,只需要使用 puppeteer 模擬登錄?開發者網站,進入證書管理的頁面,獲取所有證書的有效期,然後設置計算出即將過期的天數。

最終的流程如下:

  • 訪問開發者官網
  • 進入證書管理頁面,獲取指定類型證書(All、Pending、Development 或 Production)。
  • 取得證書列表,以當天的時間點為每個證書計算即將過期的天數。
  • 基於處理完的數據,進行預警通知(郵件、簡訊或微信)。

puppeteer 實戰

基於 puppeteer API 的版本為:0.11.0

const puppeteer = require('puppeteer');    (async() => {      const browser = await puppeteer.launch({          headless: false // 開發調試階段,設置為false      });      const page = await browser.newPage();      page.setViewport({          width: 1376,          height: 768,      });      page.on('response', async(response) => {          if (response && response.status == 200) {              // 判斷是否載入完概覽視圖,然後再次進入證書頁面              if (response.url.indexOf('tpl.overview-view.html') != -1) {                  await getCertsInfo();              }              // 判斷是否為生產環境證書列表請求              if (response.url.indexOf('status=4&certificateStatus=0&type=distribution')                  != -1) {                  const res = await response.json();                  if (res && res.certRequests) {                      console.dir(calcCertsDays(res.certRequests));                      await browser.close();                  }              }          }      });        // 跳轉到蘋果官網並等待頁面資源載入完成      await page.goto('https://developer.apple.com/cn/', {          waitUntil: 'load'      });        // 跳轉到登錄頁面      await page.click('.ac-gn-account > a');      await page.waitForSelector('#accountname', {          timeout: 50000      });      await login();        // 執行登錄操作      async function login() {          await page.focus('#accountname');          await page.type('開發者帳號', { // 此處替換為真實帳號              delay: 100          });          await page.focus('#accountpassword');          await page.type('開發者帳號密碼', { // 此處替換為真實密碼              delay: 100          });          await page.click('#submitButton2');      }        // 獲取證書資訊(等待模板載入完成後,才進入證書管理頁面)      async function getCertsInfo() {          const CERT_ITEM_SELECTOR = '#main section.getting-started > a:nth-child(2)';          await page.waitForSelector(CERT_ITEM_SELECTOR);          await page.click(CERT_ITEM_SELECTOR);          const PROD_CERT_SELECTOR = 'li.subitem > a[href*="certificate/distribution"]';          await page.waitForSelector(PROD_CERT_SELECTOR);          await page.click(PROD_CERT_SELECTOR);          await page.waitForSelector(PROD_CERT_SELECTOR);      }        // 計算每個證書的天數      function calcCertsDays(certs) {          if (Array.isArray(certs)) {              const today = new Date();              return certs.map(cert => {                  return {                      name: cert.name,                      type: cert.typeString,// Apple Push Services || iOS Distribution                      expirationDay: dateDiff(today, new Date(cert.expirationDate))                  };              });          }      }        // 計算兩個日期的間隔天數      function dateDiff(today, expirationDate) {          return parseInt((Math.abs(expirationDate.getTime() - today.getTime()))            / 1000 / 60 / 60 / 24);      }  })();

總結

通過 puppeteer 這款神器提供強大的 API,我們只是實現了基本功能,後面還有一些功能需要優化和開發,比如異常處理、帳號資訊靈活配置和預警機制等。個人感覺 puppeteer 在以後的工作中,還會有很多用武之地,比如此前本人使用 puppeteer 實現了簡單的業務功能測試。

此外在填坑過程中,偶遇了另一款神器 —— fastlane (The easiest way to automate building and releasing your iOS and Android apps) ,感覺真是相見恨晚啊(前陣子部門剛花大力氣,實現App自動打包)。有興趣的小夥伴,可以了解一下 puppeteerfastlane 這兩款神器。