原始碼地址: github.com/zjians/1230…
起因(吐槽):
剛過完年不久,相信大家還能回想到春運時被搶票支配的恐懼。但是本教程並不是教大家如何刷票的。半個月前我幫一個朋友買一張從宿州到上海的火車票,結果。。效果類似下圖WTF:
於是想著買箇中間站的票能上車也行,到車上再補票就好了,於是對著車次點了一下,將中間站的名稱ctrl+c,ctrl+v一個一個查詢有沒有餘票。一頓操作猛如虎,但是憑著程式設計師的自尊,emm這樣不行,操作太傻了。於是週末便擼了這款外掛:
嗯嗯,優雅,程式設計師的自尊又回來了
經過(開發過程):
首先開啟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,如下圖所示:
很顯然解析這個變數就可以獲得站點對應的站點碼了,但是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返回的資料
你可能會問,解析資料不是很簡單的嗎?我也是這麼認為,但是直到我看到了他的返回:
自己解析肯定是不現實了,那麼就找找網站的指令碼是如何解析這個資料的吧,於是我找到了這個函式,就是他了:
經過上面函式的處理,得到了我想要的結果物件:
完美!
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.請上車後補票 ?
感謝觀看