Chrome Plug-in Development-tab Tab Manager

1. Preface

After developing Chrome plug-in for the first time last weekgithub-star-trendSince then, I have been wondering what practical problems can be solved by plug-ins? Just as I was searching the browser for inspiration, the numerous tab tabs that I opened gave me a flash of light.

Why not make a plug-in to manage tab? Every time too many tab tabs are opened at the same time, the squeezed title always makes me unable to distinguish which is which, which is very inconvenient to view. As a result, after a weekend afternoon’s ordeal, I made this thing available (gif may be a little big, please wait patiently …):

preview

2. Preparations

According to convention, let’s get some preliminary knowledge before entering the topic formally. Open Chrome Plug-in in SilenceOfficial documents, straight to usTabs. It can be seen that it provides us with many methods, and even there areexecuteScript, this can be said that the authority is very large, but it has nothing to do with our needs this time. . .

2.1 query

Since our requirement is to manage tab, we must first obtain all tab information. Sweep it againMethods, the most relevant is the methodquery

Gets all tabs that have the specified properties, or all tabs if no properties are specified.

As officially introduced, this method can return the corresponding tabs; according to the specified conditions; And when no attribute is specified, all tabs can be obtained. This just meets our needs. According to API instructions, I tried to print out the tabs object I got in callback:

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

If you look closely, it is not difficult to find that the two tab’swindowIdDifferent. This is because I opened two Chrome windows locally at the same time, and these two TABs are just in two different windows, so they are exactly in line with the expectation.

In additionid,index,highlighted,favIconUrl,titleSuch field information also plays a very important role in the following text, and relevant definitions can be found inHereCheck.

When designing the Chrome plug-in UI, in order to highlight the current tab in the current window, we must find this tab from the above data. Since there is a tab in each windowhighlighted, so we cannot directly determine which tab is in the current window. However, we can do this:

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

According to the document, by specifyingactiveAndcurrentWindowIf these two properties are true, we can get the current tab of the current window smoothly. And then according to tab’swindowIdAndhighlightedBy matching, we can locate which tab is the real current tab from the tabs array.

2.2 highlight

According to the above, we can already get all tabs information and determine which tab is the current tab of the current window, so we can build a list based on the data. The next thing to do is, when the user clicks on one of the items, the browser can switch to the corresponding tab tab. With this requirement, I went through the documents again and found it.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});

According to the instructions of the API, what it needs iswindowIdAnd tab’sindexAnd this information is available in every tab entity. However, there is one pit to note: if the tab of the other window is switched when the current window is switched to the tab of the other window, the Chrome window is still focused on the current window. Therefore, the following methods are needed to focus the other window:

chrome.windows.update(windowId, {focused: true});

2.3 remove

In order to enhance the practicability of the plug-in, we can add the function of deleting the specified tab in the tabs list. After browsing the documents, it can be determined thatremoveWe can meet our needs.

Closes one or more tabs.

chrome.tabs.remove(tabId);

TabId is theidProperty, there is no problem in implementing the function of closing tabs.

3. Commencement

Unlike plug-insgithub-star-trendThis time it is more complicated and involves more interactions. To this end, we introducereact,antdAndwebpackHowever, the overall development is still relatively easy, and more likely still lies in the proficiency of API provided by Chrome plug-in.

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. Since the plug-in developed this time is related to tabs, we need to use thepermissionsApplication in fieldPermission.
  2. Since eval is used for webpack packaging in dev mode, Chrome browser will report an error due to security policy, so it needs to be set upcontent_security_policyMake it ignore (if it is a prod mode packet, it does not need to be set).
  3. This plug-in interaction is to click the button to pop up a floating layer, so it needs to be set upbrowser_actionProperty, whichdefault_popupThe fields are exactly the pages we will develop next.

3.2 App.js

This document is one of our core documents and is mainly responsible for the acquisition and processing of tabs data and other maintenance work.

According to the API document, obtaining tabs data is an asynchronous operation, which we can only obtain in its callback function. This also means that our application should be in aLOADINGThe state of, after get the data becomeOKState, in addition to considering the abnormal situation (such as no data or error), we can define it asEXCEPTIONStatus.

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() {}
}

The idea is very simple, is indidMountWhen we get tabs data, but we use it here.Promise.allTo control asynchronous operation.

Because the operation of acquiring tabs data is asynchronous, the time consumption of the operation may be different for different computers, different states and different tab numbers, so in order to better user experience, we can use antd’s at the beginning.Spin componentTo act as placeholders. It should be noted that if tabs data is acquired very quickly, the Loading animation will have a flash feeling and is not very friendly. So we use a 300ms promise to matchPromise.allUse, can guarantee at least 300ms Loading animation.

The next step is to get the tabs dataconvertWork.

The data obtained by the API provided by Chrome is a flat array, and the TABs in different windows are also mixed in the same array. We prefer to group by window, which makes browsing and searching more intuitive to users, easier to operate and better user experience. So we need to do a conversion of 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

According toApp.jsIn the design of, we can first put up the skeleton of the code:

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

The next step isrenderOKBecause there is no fixed design draft, we can fully exert our imagination. Here with the help ofantdRoughly implemented a version of interaction (adding tab switching, search and delete operations, etc.), the specific code is not posted considering the length, interested can enterHereCheck.

4. Closure

The whole plug-in production process has been finished here. If you have a better idea or design, you can mentionPROh ~ through this study, I am familiar with the operation of Tabs, and at the same time I have a further understanding of the production process of Chrome plug-in.

5. Reference

All code in this article is managed inHere, think it’s okstarFor a moment, you can alsoAttentionMy Blog.