How I got rid of a thousand tabs ...

... and was 3 years late. Ideally, it should be like this: the user launches the browser, and the browser shows what the user needs. But while this is not implemented, you have to use search engines. Ideally, it should be like this: the user opens a search engine, enters a search query, and it shows what the user needs. But while the ā€œI feel luckyā€ button does not work so well (although lately there has been a noticeable movement in this direction), sometimes you have to go to several addresses from the search results page.

The scenario of using search engines, apparently, was fixed historically (when the Internet was slow): getting to the search results page, I opened several tabs in the background, and while the rest were loading, it was already possible to read the first tab. In the case when I found the necessary information on one of the tabs, the rest had to be closed manually. If it didnā€™t close immediately, the tabs remained hanging, inflating the number of open tabs in the browser, which, as a rule, rarely closed after that.

In addition, if you click on the page with the links that open in a new window, several (logically) tabbed tabs are created. When you find the information you need, you canā€™t always remember which tabs are connected, you canā€™t close everything, which also leads to inflating the number of open tabs.

I always needed the ā€œFoundā€ button , which would clean up after me the consequences of the search (let's call it ā€œI was luckyā€ ). After plunging into the world of browser extensions, I thought it was something that could help in this case. So dimly began to appear a desire to write an extension that would solve my problems.

Iā€™ll tell you my story, I will lead the story in chronological order, the conclusions may be unexpected.

First step towards


The first thing I did was set up the infrastructure: webpack + babel . And right away I didnā€™t like that babel duplicated code for its helpers in each module. It was possible to configure it to use the object babelHelper, but then the code file babelHelperneeded to be connected in the webpack configuration . Keeping such a file in the project and pointing it in entrywas ugly, I made a plug-in for the webpack that did this automatically for me. After spending a lot of effort on the first step and writing some more code for the extension itself, I slowed down a bit.

Plugin

Foundation


Time passed, and only a plug-in for webpack was available, which did not solve my problems in any way. And every time I searched for something and did not close the tabs, there was a thought: ā€œIt would be nice to complete that extension ...ā€ The desire grew and grew, and now, one fine day, the quantity grew into quality.

Itā€™s time to tell what the main idea was: The
user gets to the search results page - SICKLE , we parse the search results, save the link addresses for ourselves, after the user clicked on one of the addresses, show him a notification with the other addresses and the ā€œFoundā€ button to close tabs.

When you go to the page there may be various options. The simplest: one request - one response from the server ( 200) The most difficult: one request - several server redirects ( 3xx ), after which client redirection (using <meta/>or javascript), the history API is also on top . And the combinations between them, as a rule, most sites fall into this category.

Simple transition case:

The case of a simple transition (answer 200)

Complex transition case:

Complex transition case (3xx + client redirects)

That is, saving the page address and checking only it is not always enough. Therefore, you need to create a logical Transition, where to write down all the addresses encountered on the path, and then check that the logical Transition contains the stored address. The task is clear, but not everything is so straightforward in execution.

In Chrome, there are two APIs related to navigation: webNavigation and webRequest - each with its own events. The first - connects the transitions and the browser UI, the latter - the underlying network requests. Therefore, if the change in address on the page occurred due to the history API, there will be no events for the latter, and if redirects occur during a network request, the former does not report it at all. Therefore, it is necessary to use both APIs, collecting a pinch from each event of each API, to form one logical Transition.

Some details
, webNavigation (wN) :

onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted

webRequest (wR):

onBeforeRequest -> [onBeforeRedirect -> onBeforeRequest]* -> onCompleted | onErrorOccurred

wR wN ( ), .. - wN.onBeforeNavigate wR.onBeforeRequest, - . .

, , .

Development


... Let us return to the moment when the quantity has grown into quality. A significant amount of time has passed from the beginning of development to this point: browsers began to support es6 modules , shadow DOM and other modern features. To build the project, I moved to Rollup , this time I did not have to write a plugin. After building the foundation - the ability to obtain information about any transition in any tab, it remains to implement the logic of parsing supported SICKLES and displaying notifications on related pages.

The first task is quite primitive: we know the address of the SICKLE, climb into the page content using the content script, get the data we are interested in, save it, wait for the user to go to one of the pages to show him a notification with the other pages.

For the second task, you need to implement the notification itself, what to show the user on the page. And here, too, content scripts can not do.

Initially, there was only one handler (aka a controller) that was responsible for the logic during user interaction with search engines. Then the idea came up, why not show notifications on related tabs when the user simply clicks on the links that open in new tabs. I had to redo the logic, making it more universal. Similar to middleware React / Redux, you can connect several Transition handlers, which in the future will allow you to implement the ability to disable / enable various handlers in the extension settings.

Privacy


Since the notification is the panel at the bottom of the screen, and it is added to the page layout, the script on the page can access this element as well as any other element on this page. That is, theoretically, the page could find out which search query you used, in which search engine and which other pages were offered to you, which is not very good.

A technology called shadow DOM comes to the rescue . It is not recommended to use it closed modeon the web when creating it shadowRoot, because it doesnā€™t make much sense (you still have to store the link to the element shadowRootsomewhere if you want to have access to it programmatically; you can also redefine the function attachShadowto createshadowRootin open mode, and then the scripts loaded after redefinition will already use the new version of the function).

In the case of expansion, this is not so. Content scripts and page scripts live in parallel worlds. Scripts from the page do not have access to the objects defined in the content scripts, while content scripts operate with the native implementation of the DOM functions of objects (an overridden function by a script from the page has no effect on the function with which the content script works). Combining these two conditions, we see that it is possible to create an element with a private shadowRootone by storing the link to it in a variable.

In this case, the script from the page will be able to access only the wrapper element, which will be empty for it. He will not be able to receive the text of the request or the proposed pages. Care must be taken not to give out a link to any element inside the notification or plain text in the generated events. Therefore, in the extension, the generated id is used in events, and the background script already understands what is required of it by this id. For the page, this id is quite meaningless.

Translation difficulties


Initially, the extension was developed only for Google Chrome , but since the WebExtensions API , somewhere in my head kept the ability to port to other browsers. And the presence of webextension-polyfill inspired confidence. But no matter how. The polyphil for this extension brought only the ability to use the chrome API with promises.

Firefox has become a disappointment of the year. The mismatch of the chrome API in Firefox ( Bug 1543647 , Bug 1595621 ) turned out to be critical for the extension to work, we can say it does not work in this browser (as expected).

Vivaldi was the closest, but also not without cost. EventwN.onCreatedNavigationTargetIt doesnā€™t occur when the user opens the link with the middle mouse button or through the Shift|Ctrl+ left mouse button, instead of the event wN.onCommitted transitionType == 'start_page', which is not in the chrome API , because of this, not in all cases the extension works correctly. Also in Vivaldi do not work hotkeys for extensions. What is a killer feature in this case in Chrome, allows you to quickly navigate through the tabs and close them, without the need to use a mouse for this.

Conclusion


During the writing of the code, the logic for displaying notifications changed several times, simplifying each time. As a result, it turned out that it was possible not to fence the garden with logical Transitions, but to catch the ā€œrelated transitionsā€ of the user (in the event wN.onCommittedthere is a flag transitionTypethat indicates what the transition was for, in many cases it is ā€œlinkā€, meaning that the user switched by reference), which would greatly simplify the code and work in many cases, but not in all.

Also, not being in the subject, I expected more compatibility in terms of webExtensions API. As always, itā€™s good to live in a world of modern browsers when you donā€™t need support for older versions. CSS animations are a wonderful thing: what you used to use the js library for is now done in a few lines on css. Custom elements do not work in extensions, but the shadow DOM works, allowing you to take advantage of all its features.

Expansion
chrome web store: Handy Search

Source: https://habr.com/ru/post/undefined/


All Articles