Skip to content
New issue

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插件开发 - tab选项卡管理器 #11

Open
SmallStoneSK opened this issue Jun 23, 2019 · 0 comments
Open

chrome插件开发 - tab选项卡管理器 #11

SmallStoneSK opened this issue Jun 23, 2019 · 0 comments
Labels
Chrome extension Chrome插件

Comments

@SmallStoneSK
Copy link
Owner

SmallStoneSK commented Jun 23, 2019

1. 前言

继上周第一次开发Chrome插件github-star-trend之后,我就一直寻思有什么现实问题可以用插件来解决呢?正当我在浏览器中搜索寻找灵感时,打开的众多tab选项卡令我灵光一闪。

咦,为什么不做一个插件用来管理tab呢?每次同时打开过多的tab选项卡时,被挤压的标题总是让我分不清哪个是哪个,查看起来十分不便。于是乎,经过一个周末下午的折腾,我倒腾出这么个东西(gif图可能有点大,请耐心等待...):

preview

2. 准备工作

国际惯例,正式进入主题之前让我们来先了解点预备知识。默默打开Chrome插件的官方文档,直奔我们的Tabs。可以看到它为我们提供了很多方法,而且竟然还有executeScript,这个可以说权限非常大了,不过跟我们这次的需求没啥关系。。。

2.1 query

由于我们的需求是管理tab选项卡,所以首先肯定得获取所有的tab信息。扫了一遍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恰好在两个不同的窗口内,所以正好符合预期。

另外idindex, highlightedfavIconUrltitle等字段信息在后文中也起到非常重要的作用,相关的释义都可以在这里查看。

在构思Chrome插件UI时,为了突出当前窗口中的当前tab,我们就必须从上述数据中找出这个tab。由于每个窗口中都有一个tab是highlighted的,所以我们无法直接确定哪个tab是当前窗口的。不过,我们可以这样:

chrome.tabs.query(
  {active: true, currentWindow: true},
  tabs => console.log(tabs[0])
);

根据文档,通过指定activecurrentWindow这两个属性为true,我们就能顺利拿到当前窗口的当前tab。然后再根据tab的windowIdhighlighted进行匹配,我们就能从tabs数组中定位出哪个才是真正的当前tab了。

2.2 highlight

根据上面所述,我们已经可以拿到所有的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});

2.3 remove

为了增强插件的实用性,我们可以在tabs列表中加入删除指定tab选项卡的功能。而在翻阅文档之后,可以确定remove可以实现我们的需求。

Closes one or more tabs.

chrome.tabs.remove(tabId);

tabId即tab数据中的id属性,因此关闭选项卡的功能实现起来也没有问题。

3. 开工

不同于插件github-star-trend,这次复杂度更高,涉及到更多的交互操作。为此,我们引入reactantdwebpack,不过整体开发起来还是比较容易的,更多的可能还是在于Chrome插件提供的API熟练度。

3.1 manifest.json

{
  "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"
  }
}
  1. 由于这次开发的插件跟tabs相关,所以我们需要在permissions字段中申请tabs权限。
  2. 由于webpack在dev模式下打包会用到eval,Chrome浏览器出于安全策略会报错,因此需要设置content_security_policy使其忽略(如果是prod模式打的包,就不需要设置)。
  3. 本次插件的交互是点击按钮弹出一个浮层,所以需要设置browser_action属性,而其default_popup字段正是我们接下来要开发的页面。

3.2 App.js

该文件是我们的核心文件之一,主要负责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进行一次转换:

data convert

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;
}

3.3 TabList.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、搜索和删除等操作),具体代码考虑到篇幅就不贴了,感兴趣的可以进这里查看。

4. 完结

整个插件的制作过程,到这儿就已经完了。如果你有更好的idea或设计,可以提PR哦~通过这次学习,熟悉了对Tabs的操作,同时对Chrome插件的制作流程也算是有了更深的感悟。

5. 参考

@SmallStoneSK SmallStoneSK added the Chrome extension Chrome插件 label Jun 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Chrome extension Chrome插件
Projects
None yet
Development

No branches or pull requests

1 participant