继上周第一次开发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插件的制做流程也算是有了更进一步的感悟。