繼上週第一次開發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.github
正如官方介紹,該方法能夠根據指定條件返回相應的tabs;且當不指定屬性時,能夠得到全部的tabs。這剛好知足咱們的需求,按照API指示,我在callback中嘗試打印出了拿到的tabs對象:web
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": "test title1",
"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剛好在兩個不一樣的窗口內,因此正好符合預期。chrome
另外id
,index
, highlighted
,favIconUrl
,title
等字段信息在後文中也起到很是重要的做用,相關的釋義均可以在這裏查看。json
在構思Chrome插件UI時,爲了突出當前窗口中的當前tab,咱們就必須從上述數據中找出這個tab。因爲每一個窗口中都有一個tab是highlighted
的,因此咱們沒法直接肯定哪一個tab是當前窗口的。不過,咱們能夠這樣:windows
chrome.tabs.query(
{active: true, currentWindow: true},
tabs => console.log(tabs[0])
);
複製代碼
根據文檔,經過指定active
和currentWindow
這兩個屬性爲true,咱們就能順利拿到當前窗口的當前tab。而後再根據tab的windowId
和highlighted
進行匹配,咱們就能從tabs數組中定位出哪一個纔是真正的當前tab了。數組
根據上面所述,咱們已經能夠拿到全部的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插件的製做流程也算是有了更進一步的感悟。