繼上週第一次開發Chrome插件github-star-trend
以後,我就一直尋思有什麼現實問題能夠用插件來解決呢?正當我在瀏覽器中搜索尋找靈感時,打開的衆多tab選項卡令我靈光一閃。html
咦,爲何不作一個插件用來管理tab呢?每次同時打開過多的tab選項卡時,被擠壓的標題老是讓我分不清哪一個是哪一個,查看起來十分不便。因而乎,通過一個週末下午的折騰,我倒騰出這麼個東西(gif圖可能有點大,請耐心等待...):react
按照慣例,正式進入主題以前讓咱們來先了解點預備知識。默默打開Chrome插件的官方文檔,直奔咱們的Tabs
。能夠看到它爲咱們提供了不少方法,並且居然還有executeScript
,這個能夠說權限很是大了,不過跟咱們此次的需求沒啥關係。。。webpack
因爲咱們的需求是管理tab選項卡,因此首先確定得獲取全部的tab信息。掃了一遍Methods
,最相關的就是方法query
:git
Gets all tabs that have the specified properties, or all tabs if no properties are specified.
正如官方介紹,該方法能夠根據指定條件返回相應的tabs;且當不指定屬性時,能夠得到全部的tabs。這剛好知足咱們的需求,按照API指示,我在callback中嘗試打印出了拿到的tabs對象:github
chrome.tabs.query({}, tabs => console.log(tabs));
[ { "active": true, "audible": false, "autoDiscardable": true, "discarded": false, "favIconUrl": "https://static.clewm.net/static/images/favicon.ico", "height": 916, "highlighted": true, "id": 25, "incognito": false, "index": 0, "mutedInfo": {"muted":false}, "pinned": true, "selected": true, "status": "complete", "title": "草料文本二維碼生成器", "url": "https://cli.im/text?bb032d49e2b5fec215701da8be6326bb", "width": 1629, "windowId": 23 }, ... { "active": true, "audible": false, "autoDiscardable": true, "discarded": false, "favIconUrl": "https://www.google.com/images/icons/product/chrome-32.png", "height": 948, "highlighted": true, "id": 417, "incognito": false, "index": 0, "mutedInfo": {"muted": false}, "pinned": false, "selected": true, "status": "complete", "title": "chrome.tabs - Google Chrome", "url": "https://developers.chrome.com/extensions/tabs#method-query", "width": 1629, "windowId": 812 } ]
仔細觀察不難發現,兩個tab的windowId
不一樣。這是因爲我在本地同時打開了兩個Chrome窗口,而這兩個tab剛好在兩個不一樣的窗口內,因此正好符合預期。web
另外id
,index
, highlighted
,favIconUrl
,title
等字段信息在後文中也起到很是重要的做用,相關的釋義均可以在這裏查看。chrome
在構思Chrome插件UI時,爲了突出當前窗口中的當前tab,咱們就必須從上述數據中找出這個tab。因爲每一個窗口中都有一個tab是highlighted
的,因此咱們沒法直接肯定哪一個tab是當前窗口的。不過,咱們能夠這樣:json
chrome.tabs.query( {active: true, currentWindow: true}, tabs => console.log(tabs[0]) );
根據文檔,經過指定active
和currentWindow
這兩個屬性爲true,咱們就能順利拿到當前窗口的當前tab。而後再根據tab的windowId
和highlighted
進行匹配,咱們就能從tabs數組中定位出哪一個纔是真正的當前tab了。windows
根據上面所述,咱們已經能夠拿到全部的tabs信息以及肯定出哪一個tab是當前窗口的當前tab,因此咱們能夠根據這些數據構建出一個列表。而接下來要作的就是,當用戶點擊其中某一項時,瀏覽器就能切換到所對應的tab選項卡。帶着這個需求,再次翻閱文檔找到了highlight
:數組
Highlights the given tabs and focuses on the first of group. Will appear to do nothing if the specified tab is currently active.
chrome.tabs.highlight({windowId, tabs});
根據該API的指示,它須要的是windowId
和tab的index
,而這些信息都在每一個tab實體中能夠拿到。不過這裏有一個坑須要注意:那就是若是在當前窗口切換到另外一個窗口的tab時,雖然另外一個窗口的tab得以切換,可是Chrome窗口仍聚焦於當前窗口。因此須要用如下的方法,令另外的那個窗口獲得聚焦:
chrome.windows.update(windowId, {focused: true});
爲了加強插件的實用性,咱們能夠在tabs列表中加入刪除指定tab選項卡的功能。而在翻閱文檔以後,能夠肯定remove
能夠實現咱們的需求。
Closes one or more tabs.
chrome.tabs.remove(tabId);
tabId即tab數據中的id
屬性,所以關閉選項卡的功能實現起來也沒有問題。
不一樣於插件github-star-trend
,此次複雜度更高,涉及到更多的交互操做。爲此,咱們引入react
,antd
和webpack
,不過總體開發起來仍是比較容易的,更多的可能仍是在於Chrome插件提供的API熟練度。
{ "permissions": [ "tabs" ], "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "browser_action": { "default_icon": { "16": "./icons/logo_16.png", "32": "./icons/logo_32.png", "48": "./icons/logo_48.png" }, "default_title": "Tab Killer", "default_popup": "./popup.html" } }
permissions
字段中申請tabs
權限。content_security_policy
使其忽略(若是是prod模式打的包,就不須要設置)。browser_action
屬性,而其default_popup
字段正是咱們接下來要開發的頁面。該文件是咱們的核心文件之一,主要負責tabs數據的獲取和處理等維護工做。
根據API文檔所示,獲取tabs數據是一個異步操做,咱們在其回調函數中才能拿到。這也意味着咱們的應用一開始應該是處於一個LOADING
的狀態,拿到數據以後成爲OK
狀態,另外再考慮到異常狀況(例如無數據或出錯),咱們能夠將其定義爲EXCEPTION
狀態。
class App extends React.PureComponent { state = { tabsData: [], status: STATUS.LOADING } componentDidMount() { this.getTabsData(); } getTabsData() { Promise.all([ this.getAllTabs(), this.getCurrentTab(), Helper.waitFor(300), ]).then(([allTabs, currentTab]) => { const tabsData = Helper.convertTabsData(allTabs, currentTab); if(tabsData.length > 0) { this.setState({tabsData, status: STATUS.OK}); } else { this.setState({tabsData: [], status: STATUS.EXCEPTION}); } }).catch(err => { this.setState({tabsData: [], status: STATUS.EXCEPTION}); console.log('get tabs data failed, the error is:', err.message); }); } getAllTabs = () => new Promise(resolve => chrome.tabs.query({}, tabs => resolve(tabs))) getCurrentTab = () => new Promise(resolve => chrome.tabs.query({active: true, currentWindow: true}, tabs => resolve(tabs[0]))) render() { const {status, tabsData} = this.state; return ( <div className="app-container"> <TabsList data={tabsData} status={status}/> </div> ); } } const Helper = { waitFor(timeout) { return new Promise(resolve => { setTimeout(resolve, timeout); }); }, convertTabsData() {} }
思路很簡單,就是在didMount
的時候獲取tabs數據,不過咱們在這裏用到Promise.all
來控制異步操做。
因爲獲取tabs數據這一操做是異步的,不一樣電腦,不一樣狀態,不一樣tab數量時該操做的耗時均可能不一樣,因此爲了更好的用戶體驗,咱們能夠在一開始用antd的Spin組件
來充當佔位符。須要注意的是,若是獲取tabs數據很是快,Loading動畫會有一閃而過的感受,並不十分友好。所以咱們用個300ms的promise搭配Promise.all
使用,能夠保證至少300ms的Loading動畫。
接下來就是拿到tabs數據以後的convert
工做。
Chrome提供的API獲取到的數據是一個扁平的數組,不一樣窗口內的tab也被混在同一個數組內。咱們更但願能按窗口進行分組,這樣在瀏覽和查找時對用戶更直觀,操做更方便,用戶體驗更好。因此咱們須要對tabsData進行一次轉換:
convertTabsData(allTabs = [], currentTab = {}) { // 過濾非法數據 if(!(allTabs.length > 0 && currentTab.windowId !== undefined)) { return []; } // 按windowId進行分組歸類 const hash = Object.create(null); for(const tab of allTabs) { if(!hash[tab.windowId]) { hash[tab.windowId] = []; } hash[tab.windowId].push(tab); } // 將obj轉成array const data = []; Object.keys(hash).forEach(key => data.push({ tabs: hash[key], windowId: Number(key), isCurWindow: Number(key) === currentTab.windowId })); // 進行排序,將當前窗口的順序往上提,保證更好的體驗 data.sort((winA, winB) => { if(winA.isCurWindow) { return -1; } else if(winB.isCurWindow) { return 1; } else { return 0; } }); return data; }
根據App.js
中的設計,咱們能夠先搭起代碼的骨架:
export class TabsList extends React.PureComponent { renderLoading() { return ( <div className={'loading-container'}> <Spin size="large"/> </div> ); } renderOK() { // TODO... } renderException() { return ( <div className={'no-result-container'}> <Empty description={'沒有數據哎~'}/> </div> ); } render() { const {status} = this.props; switch(status) { case STATUS.LOADING: return this.renderLoading(); case STATUS.OK: return this.renderOK(); case STATUS.EXCEPTION: default: return this.renderException(); } } }
接下來就是renderOK
的實現,因爲沒有固定的設計稿,咱們能夠盡情發揮本身的想象。這裏藉助antd
粗略地實現了一版交互(加入了切換tab、搜索和刪除等操做),具體代碼考慮到篇幅就不貼了,感興趣的能夠進這裏查看。
整個插件的製做過程,到這兒就已經完了。若是你有更好的idea或設計,能夠提PR哦~經過此次學習,熟悉了對Tabs的操做,同時對Chrome插件的製做流程也算是有了更進一步的感悟。