Open SmallStoneSK opened 5 years ago
继上周第一次开发Chrome插件github-star-trend之后,我就一直寻思有什么现实问题可以用插件来解决呢?正当我在浏览器中搜索寻找灵感时,打开的众多tab选项卡令我灵光一闪。
github-star-trend
咦,为什么不做一个插件用来管理tab呢?每次同时打开过多的tab选项卡时,被挤压的标题总是让我分不清哪个是哪个,查看起来十分不便。于是乎,经过一个周末下午的折腾,我倒腾出这么个东西(gif图可能有点大,请耐心等待...):
国际惯例,正式进入主题之前让我们来先了解点预备知识。默默打开Chrome插件的官方文档,直奔我们的Tabs。可以看到它为我们提供了很多方法,而且竟然还有executeScript,这个可以说权限非常大了,不过跟我们这次的需求没啥关系。。。
Tabs
executeScript
由于我们的需求是管理tab选项卡,所以首先肯定得获取所有的tab信息。扫了一遍Methods,最相关的就是方法query:
Methods
query
Gets all tabs that have the specified properties, or all tabs if no properties are specified.
正如官方介绍,该方法可以根据指定条件返回相应的tabs;且当不指定属性时,可以获得所有的tabs。这恰好满足我们的需求,按照API指示,我在callback中尝试打印出了拿到的tabs对象:
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恰好在两个不同的窗口内,所以正好符合预期。
windowId
另外id,index, highlighted,favIconUrl,title等字段信息在后文中也起到非常重要的作用,相关的释义都可以在这里查看。
id
index
highlighted
favIconUrl
title
在构思Chrome插件UI时,为了突出当前窗口中的当前tab,我们就必须从上述数据中找出这个tab。由于每个窗口中都有一个tab是highlighted的,所以我们无法直接确定哪个tab是当前窗口的。不过,我们可以这样:
chrome.tabs.query( {active: true, currentWindow: true}, tabs => console.log(tabs[0]) );
根据文档,通过指定active和currentWindow这两个属性为true,我们就能顺利拿到当前窗口的当前tab。然后再根据tab的windowId和highlighted进行匹配,我们就能从tabs数组中定位出哪个才是真正的当前tab了。
active
currentWindow
根据上面所述,我们已经可以拿到所有的tabs信息以及确定出哪个tab是当前窗口的当前tab,所以我们可以根据这些数据构建出一个列表。而接下来要做的就是,当用户点击其中某一项时,浏览器就能切换到所对应的tab选项卡。带着这个需求,再次翻阅文档找到了highlight:
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可以实现我们的需求。
remove
Closes one or more tabs.
chrome.tabs.remove(tabId);
tabId即tab数据中的id属性,因此关闭选项卡的功能实现起来也没有问题。
不同于插件github-star-trend,这次复杂度更高,涉及到更多的交互操作。为此,我们引入react,antd和webpack,不过整体开发起来还是比较容易的,更多的可能还是在于Chrome插件提供的API熟练度。
react
antd
webpack
{ "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
browser_action
default_popup
该文件是我们的核心文件之一,主要负责tabs数据的获取和处理等维护工作。
根据API文档所示,获取tabs数据是一个异步操作,我们在其回调函数中才能拿到。这也意味着我们的应用一开始应该是处于一个LOADING的状态,拿到数据之后成为OK状态,另外再考虑到异常情况(例如无数据或出错),我们可以将其定义为EXCEPTION状态。
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来控制异步操作。
didMount
Promise.all
由于获取tabs数据这一操作是异步的,不同电脑,不同状态,不同tab数量时该操作的耗时都可能不同,所以为了更好的用户体验,我们可以在一开始用antd的Spin组件来充当占位符。需要注意的是,如果获取tabs数据非常快,Loading动画会有一闪而过的感觉,并不十分友好。因此我们用个300ms的promise搭配Promise.all使用,可以保证至少300ms的Loading动画。
Spin组件
接下来就是拿到tabs数据之后的convert工作。
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中的设计,我们可以先搭起代码的骨架:
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、搜索和删除等操作),具体代码考虑到篇幅就不贴了,感兴趣的可以进这里查看。
renderOK
整个插件的制作过程,到这儿就已经完了。如果你有更好的idea或设计,可以提PR哦~通过这次学习,熟悉了对Tabs的操作,同时对Chrome插件的制作流程也算是有了更深的感悟。
1. 前言
继上周第一次开发Chrome插件
github-star-trend
之后,我就一直寻思有什么现实问题可以用插件来解决呢?正当我在浏览器中搜索寻找灵感时,打开的众多tab选项卡令我灵光一闪。咦,为什么不做一个插件用来管理tab呢?每次同时打开过多的tab选项卡时,被挤压的标题总是让我分不清哪个是哪个,查看起来十分不便。于是乎,经过一个周末下午的折腾,我倒腾出这么个东西(gif图可能有点大,请耐心等待...):
2. 准备工作
国际惯例,正式进入主题之前让我们来先了解点预备知识。默默打开Chrome插件的官方文档,直奔我们的
Tabs
。可以看到它为我们提供了很多方法,而且竟然还有executeScript
,这个可以说权限非常大了,不过跟我们这次的需求没啥关系。。。2.1 query
由于我们的需求是管理tab选项卡,所以首先肯定得获取所有的tab信息。扫了一遍
Methods
,最相关的就是方法query
:正如官方介绍,该方法可以根据指定条件返回相应的tabs;且当不指定属性时,可以获得所有的tabs。这恰好满足我们的需求,按照API指示,我在callback中尝试打印出了拿到的tabs对象:
仔细观察不难发现,两个tab的
windowId
不同。这是由于我在本地同时打开了两个Chrome窗口,而这两个tab恰好在两个不同的窗口内,所以正好符合预期。另外
id
,index
,highlighted
,favIconUrl
,title
等字段信息在后文中也起到非常重要的作用,相关的释义都可以在这里查看。在构思Chrome插件UI时,为了突出当前窗口中的当前tab,我们就必须从上述数据中找出这个tab。由于每个窗口中都有一个tab是
highlighted
的,所以我们无法直接确定哪个tab是当前窗口的。不过,我们可以这样:根据文档,通过指定
active
和currentWindow
这两个属性为true,我们就能顺利拿到当前窗口的当前tab。然后再根据tab的windowId
和highlighted
进行匹配,我们就能从tabs数组中定位出哪个才是真正的当前tab了。2.2 highlight
根据上面所述,我们已经可以拿到所有的tabs信息以及确定出哪个tab是当前窗口的当前tab,所以我们可以根据这些数据构建出一个列表。而接下来要做的就是,当用户点击其中某一项时,浏览器就能切换到所对应的tab选项卡。带着这个需求,再次翻阅文档找到了
highlight
:根据该API的指示,它需要的是
windowId
和tab的index
,而这些信息都在每个tab实体中可以拿到。不过这里有一个坑需要注意:那就是如果在当前窗口切换到另一个窗口的tab时,虽然另一个窗口的tab得以切换,但是Chrome窗口仍聚焦于当前窗口。所以需要用以下的方法,令另外的那个窗口得到聚焦:2.3 remove
为了增强插件的实用性,我们可以在tabs列表中加入删除指定tab选项卡的功能。而在翻阅文档之后,可以确定
remove
可以实现我们的需求。tabId即tab数据中的
id
属性,因此关闭选项卡的功能实现起来也没有问题。3. 开工
不同于插件
github-star-trend
,这次复杂度更高,涉及到更多的交互操作。为此,我们引入react
,antd
和webpack
,不过整体开发起来还是比较容易的,更多的可能还是在于Chrome插件提供的API熟练度。3.1 manifest.json
permissions
字段中申请tabs
权限。content_security_policy
使其忽略(如果是prod模式打的包,就不需要设置)。browser_action
属性,而其default_popup
字段正是我们接下来要开发的页面。3.2 App.js
该文件是我们的核心文件之一,主要负责tabs数据的获取和处理等维护工作。
根据API文档所示,获取tabs数据是一个异步操作,我们在其回调函数中才能拿到。这也意味着我们的应用一开始应该是处于一个
LOADING
的状态,拿到数据之后成为OK
状态,另外再考虑到异常情况(例如无数据或出错),我们可以将其定义为EXCEPTION
状态。思路很简单,就是在
didMount
的时候获取tabs数据,不过我们在这里用到Promise.all
来控制异步操作。由于获取tabs数据这一操作是异步的,不同电脑,不同状态,不同tab数量时该操作的耗时都可能不同,所以为了更好的用户体验,我们可以在一开始用antd的
Spin组件
来充当占位符。需要注意的是,如果获取tabs数据非常快,Loading动画会有一闪而过的感觉,并不十分友好。因此我们用个300ms的promise搭配Promise.all
使用,可以保证至少300ms的Loading动画。接下来就是拿到tabs数据之后的
convert
工作。Chrome提供的API获取到的数据是一个扁平的数组,不同窗口内的tab也被混在同一个数组内。我们更希望能按窗口进行分组,这样在浏览和查找时对用户更直观,操作更方便,用户体验更好。所以我们需要对tabsData进行一次转换:
3.3 TabList.js
根据
App.js
中的设计,我们可以先搭起代码的骨架:接下来就是
renderOK
的实现,由于没有固定的设计稿,我们可以尽情发挥自己的想象。这里借助antd
粗略地实现了一版交互(加入了切换tab、搜索和删除等操作),具体代码考虑到篇幅就不贴了,感兴趣的可以进这里查看。4. 完结
整个插件的制作过程,到这儿就已经完了。如果你有更好的idea或设计,可以提PR哦~通过这次学习,熟悉了对Tabs的操作,同时对Chrome插件的制作流程也算是有了更深的感悟。
5. 参考