We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
继上周第一次开发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插件的制作流程也算是有了更深的感悟。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
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. 参考
The text was updated successfully, but these errors were encountered: