【請抓緊時間上車】實現一個12306的chrom外掛

守護四葉草發表於2019-03-03

原始碼地址: github.com/zjians/1230…

起因(吐槽):

剛過完年不久,相信大家還能回想到春運時被搶票支配的恐懼。但是本教程並不是教大家如何刷票的。半個月前我幫一個朋友買一張從宿州到上海的火車票,結果。。效果類似下圖WTF:

【請抓緊時間上車】實現一個12306的chrom外掛

於是想著買箇中間站的票能上車也行,到車上再補票就好了,於是對著車次點了一下,將中間站的名稱ctrl+c,ctrl+v一個一個查詢有沒有餘票。一頓操作猛如虎,但是憑著程式設計師的自尊,emm這樣不行,操作太傻了。於是週末便擼了這款外掛:

【請抓緊時間上車】實現一個12306的chrom外掛

【請抓緊時間上車】實現一個12306的chrom外掛

嗯嗯,優雅,程式設計師的自尊又回來了


經過(開發過程):

首先開啟chrome開發文件 crxdoc-zh.appspot.com/extensions/…

本來以為要刷很多文件,結果看了20分鐘,嗯,感謝自己是個前端,會了。

首先基礎工作就是定義一個manifest.json (清單檔案),用於定義外掛相關的配置。先貼一份本外掛的配置檔案(各項作用看註釋),詳細解釋請看官方文件

{     "manifest_version": 2,    "name": "12306",    "version": "1.0",    "description": "查詢當前車次各站點的餘票",    "author": "https://github.com/zjians/12306",    "page_action": {        "default_icon": {            "16": "assets/icons/icon16.png",            "48": "assets/icons/icon48.png",           "128": "assets/icons/icon128.png"        },        "default_title": "12306上車票"    },     "content_scripts": [{         "matches": ["https://*.12306.cn/*"],         "js": ["js/jquery.min.js","js/main.js"],         "css": ["assets/styles/main.css"],         "run_at": "document_start"    }],    "web_accessible_resources": ["assets/images/*" ]  }複製程式碼

如果你不想看文件,那麼我整理了一份比較全的manifest解釋,幾乎覆蓋了常用的所有設定,可用於快速查詢:

{
 "manifest_version": 2,
 /*
   指定您的應用包要求的清單檔案格式的版本。從 Chrome 18 開始,開發人員應該指定 2
 */

 "name":"我的應用名稱",
 "version":"我的應用版本",

 "default_locale":"zh", // 預設語言
 /*
   對於含有 _locales 目錄的應用來說這一屬性是必需的,指定_locales中的子目錄,包含該應用預設字串。
   在沒有 _locales 目錄的應用中該屬性不能存在
 */

 "description":"關於應用的描述",

 "icons":{ /*可定義一個或多個, 應用或主題背景的圖示*/
   "16":"icon16.png",
   "48":"icon48.png",
   "128": "icon128.png"
 },

 /*
  下面的browser_action或page_action選擇某一個使用
  如果外掛只對特定頁面有效,則使用page_action(頁面按鈕),(比如搶票外掛,只對12306網站有效)
  如果外掛對所有頁面都有效,則使用browser_action瀏覽器按鈕),(比如截圖外掛,對所有頁面都有效)
 */
 "browser_action": { // 如果有 browser_action, 即在chrome toolbar 的右邊新增了一個 icon,
   "default_icon": "test.jpg",
   "default_title": "Google Mail",      // tooltip, 游標停留在 icon 上時顯示
   "default_popup": "popup.html"  // 如果有 popup 的頁面, 則使用者點選圖示就會渲染此 HTML 頁面
 },

 "page_action":{ // 如果 page_action 並不應用在當前頁面, icon會顯示灰色
   "default_icon": {
     "19": "images/icon19.png",
     "38": "images/icon38.png"
   },
   "default_title": "Google Mail",
   "default_popup": "popup.html"
 },

 //可選
 "author":"開發者",
 "automation":"",


 "background":{
   "scripts":["background.js"],
   "page": "background.html",
   "persistent":false
 },
 /*
  後臺網頁
  1.應用通常需要有一個長時間執行的指令碼來管理一些任務或狀態,而後臺網頁就是為這一目的而設立。
  通常情況下,後臺頁面不需要任何 HTML 標記,這種情況下後臺頁面可以單獨使用 JavaScript檔案實現。
  後臺頁面將由應用系統生成,包含 scripts 屬性中列出的每一個檔案。
  2.page:如果您需要在您的後臺頁面中指定 HTML,您可以改用 page 屬性:
  3.persistent:應用和應用通常需要長時間執行的指令碼來管理某些任務或狀態,這就是事件頁面的作用。
  事件頁面只在需要時載入,當事件頁面不活動時就會解除安裝,以便釋放記憶體和其他系統資源。
  如何得到事件頁面 就是設定一個"persistent"鍵,如果沒有設定,你將得到一個普通的後臺頁面。
 */


 "content_scripts": [{
   "matches": ["https://*.domain.com/*"], // 匹配的地址網頁
   "exclude_matches":[],
   "js": ["jquery.js","yourScript.js"], // 內容指令碼
   "css": ["yourStyles.css"], // 在頁面上新增的css樣式
   "run_at":"document_idle",
   "all_frames": true //該匹配下面的所有視窗
 },{ // 可以針對不同的規則插入不同的內容
   "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
   "js": ["js/show-image-content-size.js"]
 }],
 /*
   內容指令碼: 其實就是向你想要的網頁中插入一個指令碼程式碼,執行你想要做的事情
            內容指令碼是在網頁的上下文中執行的 JavaScript 檔案,
            它們可以通過標準的文件物件模型(DOM)來獲取瀏覽器訪問的網頁詳情,或者作出更改。
   
   1.run_at 可選。
   控制 js 中的 JavaScript 檔案何時插入,
   可以為 "document_start""document_end""document_idle",預設為 "document_idle"。 

      1.1如果是 "document_start",這些檔案將在 css 中指定的檔案之後,但是在所有其他 DOM 構造或指令碼執行之前插入。 

      1.2.如果是 "document_end",檔案將在 DOM 完成之後立即插入,但是在載入子資源(如影像與框架)之前插入。 

      1.3.如果是 "document_idle",瀏覽器將在 "document_end" 和剛發生 window.onload 事件這兩個時刻之間選擇合適的時候插入,
      具體的插入時間取決於文件的複雜程度以及載入文件所花的時間,並且瀏覽器會盡可能地為加快頁面載入速度而優化。 
    
   2.all_frames 可選。
   控制內容指令碼執行在匹配頁面的所有框架中還是僅在頂層框架中。 預設為 false,意味著僅在頂層框架中執行
 */

"web_accessible_resources": [ // 普通頁面能夠直接訪問的外掛資源列表,如果不設定是無法直接訪問的
   "images/*.png",
   "style/double-rainbow.css",
   "script/double-rainbow.js",
   "script/main.js",
   "templates/*"
 ],

 "update_url": "你的外掛在chrome商店的地址", // 如果你使用 Chrome 開發者資訊中心釋出的擴充套件程式,可忽略這一項
 // 如果你想從自己的伺服器上更新外掛,則需要指定update_url,指向你的伺服器地址。

"homepage_url": "https://www.xxx.com", // 外掛的主頁

"permissions":[
   "tabs", // 如果擴充套件使用chrome.tabs或chrome.windows模組,則新增此項
   "bookmarks", // 使用chrome.bookmarks模組來建立、組織和管理書籤
   "http://www.blogger.com/",    
   "http://*.google.com/",    
   "unlimitedStorage", // 提供了一個無限的HTML5配額來儲存客戶端資料,如資料庫和本地儲存檔案。沒有這個許可權,擴充套件僅限於5 MB的本地儲存
   "history" // 歷史記錄的使用許可權  chrome.history 
   "notifications",// 提示
   "cookies",// 如果擴充套件程式使用chrome.cookies模組,則新增此項
 ],
 /*  
   擴充套件或app將使用的一組許可權。每個許可權是一列已知字串列表中的一個,
   如geolocatioin或者一個匹配模式,來指定可以訪問的一個或者多個主機。
   許可權可以幫助限定危險,如果你的擴充套件或者app被攻擊。
   有些許可權在安裝之前,會告知使用者
*/

key:'',
/**開發時為擴充套件指定的唯一標識值。
  注意:通常您並不需要直接使用這個值,而是在您的程式碼中使用相對路徑或者chrome.extension.getURL()得到的絕對路徑。
  這個值並不是開發時顯式指定的,而是Chrome在安裝.crx時輔助生成的。(開發時可以通過上傳擴充套件或者手工打包生成crx檔案)。 
  安裝完crx,在Chrome的使用者資料目錄下的Default/Extensions/<extensionId>/<versionString>/manifest.json檔案中,您可以看到這個擴充套件的key。
**/

"commands": {
     // commands API 用來新增快捷鍵
     // 需要在 background page 上新增監聽器繫結 handler
   "toggle-feature-foo": {
     "suggested_key": {
       "default": "Ctrl+Shift+Y",
       "mac": "Command+Shift+Y"
     },
     "description": "Toggle feature foo",
     "global": true
       // 當 chrome 沒有 focus 時也可以生效的快捷鍵
       // 僅限 Ctrl+Shift+[0..9]
   },
   "_execute_browser_action": {
     "suggested_key": {
       "windows": "Ctrl+Shift+Y",
       "mac": "Command+Shift+Y",
       "chromeos": "Ctrl+Shift+U",
       "linux": "Ctrl+Shift+J"
     }
   },
   "_execute_page_action": {
     "suggested_key": {
       "default": "Ctrl+Shift+E",
       "windows": "Alt+Shift+P",
       "mac": "Alt+Shift+P"
     }
   },
   ...
 },
 "content_capabilities": ...,
 "optional_permissions": ["tabs"], // 其他需要的 permission, 在使用 chrome.permissions API 時用到, 並非安裝外掛時需要

 "short_name": "Short Name", // 外掛名字簡寫

"storage": {//  使用 storage.managed api 的話, 需要一個 schema 檔案指定儲存欄位型別等, 類似定義資料庫表的 column
   "managed_schema": "schema.json"
 },
......
}複製程式碼

嗯,配置完以後,就可以在頁面中插入自己的指令碼了,於是就可以為所欲為了。

這裡說下開發中遇到的三個問題:

1. 獲取站點的縮寫碼,比如【北京北】的站點碼為VAP,因為請求資料的時候傳過去的引數,使用的不是站點中文名稱,而是站點碼。

於是我在網站中發現了這麼一個變數:station_names,如下圖所示:

【請抓緊時間上車】實現一個12306的chrom外掛

     很顯然解析這個變數就可以獲得站點對應的站點碼了,但是chrome外掛和原始網頁是兩個相互分開的執行環境,也就是說我在外掛的指令碼中無法獲取頁面指令碼中的變數。但是外掛是可以獲取頁面的dom內容的,於是把station_names掛到dom上,然後在外掛中獲取dom上的屬性。這樣便通過dom獲取到了頁面指令碼中的變數值,程式碼如下:

const script = document.createElement('script');script.type = 'text/javascript';script.innerHTML = "document.body.setAttribute('data-station-name', station_names);";document.head.appendChild(script);document.head.removeChild(script);const station_names = document.body.getAttribute('data-station-name');複製程式碼

2. 解析12306返回的資料

你可能會問,解析資料不是很簡單的嗎?我也是這麼認為,但是直到我看到了他的返回:

【請抓緊時間上車】實現一個12306的chrom外掛

自己解析肯定是不現實了,那麼就找找網站的指令碼是如何解析這個資料的吧,於是我找到了這個函式,就是他了:

【請抓緊時間上車】實現一個12306的chrom外掛

經過上面函式的處理,得到了我想要的結果物件:【請抓緊時間上車】實現一個12306的chrom外掛

完美!

3. 本來以為可以開心的玩耍了,但是第二天一試,居然請求不到資料了。。

檢視請求地址才發現,原來查詢地址是每天都變的,好在請求失敗以後,會返回可用的地址,於是在外掛執行時,檢測一下目前可用的請求地址:

let queryUrl = 'leftTicket/queryZ' // 請求地址$.ajax({    type: 'GET',    url: `https://kyfw.12306.cn/otn/${queryUrl}?leftTicketDTO.train_date=2019-02-20&leftTicketDTO.from_station=VNP&leftTicketDTO.to_station=NKH&purpose_codes=ADULT`,    error: function (res) { // 如果失敗了,會返回可用的地址      queryUrl = res.responseJSON.c_url    }  })複製程式碼

ok!完成。

結束語:

1.如果覺得有用,請反手給個star鼓勵一下

2.請上車後補票 ?

感謝觀看

                                                【請抓緊時間上車】實現一個12306的chrom外掛


相關文章