📜 ⬆️ ⬇️

Chrome extension over the weekend

image

Problem

As usual, late at night, getting on the bus, I took out the phone, and while I was typing “habr ...” he was cut off. I thought out loud: “But I couldn’t say before?”, I was a little sorry that the phones rarely squeak while they are discharged. And then…

Then my friend and I decided to approach the issue like a man. He wrote a program for android, and I expanded Chrome. About the latter and will be discussed.
')
Task

So, the idea: an android application monitors the battery status and periodically notifies the server about the level of charge. Moreover, it does this somehow rationally so that the charge does not suffer from this. The chrome extension puts its icon in a specially designated place, the icon shows the charge of the android battery and draws attention in every way if it is almost completely discharged. And so that everything does not seem too simple, one had to realize the idea in one weekend. Otherwise, the value / effort balance fell out of the free app.

Thus, the choice of approach to the task turned out to be even more important than the speed of ten-finger printing.

For the cause.

Decision

Expand Chrome was not so difficult, but I had to do everything as quickly as possible. Weekend alone, but a lot of hotelok. To speed up development, I wanted not to bathe with event handling and updating HTML and knockout here came up better than anyone. And since all the logic for the Chrome extension is written in javascript, typescript helps to avoid many rakes. These two of the casket immediately went into circulation. With the technology ahead, now the most important thing. A lot of payload was not expected, but a vegetable garden can be made of spaghetti here too. The easiest and most reliable option was seen in the MVC pattern. With a knockout, he didn’t bite a bit, that is MVVM itself, but it was clear that the management would be conducted from the background page (more on that later) on which there would be no knockout. The choice is made. Forward - to code. Time is already an hour less.

Execution

I started by creating a new project in Visual Studio 2013, for simplicity I chose an ASP.NET Empty application. I saw that fathers use a more appropriate template here - HTML Application with TypeScript - I did not have it, so I had to sweat with setting typescript compile-on-save

Bleed the project with a minimum of libraries and their typescript declarations. Buddy Boris Yankov helped a lot with a chic set of typescript declarations , although some had to be finished myself along the way.

Further MVC components:

M: models made two, one for the whole list, one for a separate android device (PopupViewModel and DeviceStatusViewModel). In essence, they are ViewModel, but hereinafter simply Model
V: view 2 break more hassle than good, just popup.html created
C: well, he called himself that - Controller

I also did a DeviceStatus - this is a structure that is sent between the extension and the server, as well as between the Controller and the Model.

The super-important file is manifest.json , which Chrome needs to recognize the extension.

A couple more manipulations, and this is how the picture came out:

image

Now it was necessary to fill all this with meaning, that is, with classes. Chrome limits the capabilities of its extensions and diligently puts the rake everywhere, so the controller I went to the background page right away, because it is he who talks to the server, and this can only be done from the background. About this corresponding entry in manifest.json:

"background": { "scripts": ["lib/jquery-1.7.2.min.js", "src/DeviceStatus.js", "src/Controller.js"] } 


After a couple of cones, it became clear that the files should be listed in the order of dependence on each other (jQuery is needed here for simplicity ajax requests and a couple more trifles).

The client part, as expected, was spartan. It appears only when you click on the icon and it does not have many tasks: add a device and set a charge threshold.

Like that:
image

First of all, you need to let Chrome know through manifest.json what to open and when:

  "browser_action": { "default_icon": "images/icon.png", "default_popup": "views/popup.html" }, 


The presentation code (popup.html) is extremely simple - some HTML and attributes to bind to the knockout model. The boring details lowered, they can be, and so look at the living example. One thing is important here - just because Chrome does not work with knockout, it needs to be given authority through manifest.json:

  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 


this unsafe-eval is needed for knockout. Details here .

image

Meat has already grown enough to connect to Chrome and watch what happens - it helps a lot to forget about dinner and keep working. Chrome has a great button for this — the Load Unpacked Extension; I sent it to the root of the VS project. After a couple of adjustments, manifest.json managed to get a button in the right place and a test image.

Inspiration is spurred, then - the model. It is also very simple - all the properties from the DeviceStatus class are wrapped in observable and a couple of event handlers are adding a device, deleting a device and selecting the active one. Along the way, we decided that we would support several devices, and which one the icon at the top corresponds to - let us decide for the user (hence the division into PopupViewModel and DeviceStatusViewModel).

Now it looked like this:
image

It's time to teach my main UI to get data. Another Chrome rake was planted in the form of the inability to directly call the methods of the background page (I have a Controller sitting there). All communication passes through the corresponding API Chrome and only asynchronously.

Calls look like this:

  chrome.extension.sendMessage({ method: "GetAllDevices" }, (allDevices: DeviceStatus[]) => { if (!allDevices || allDevices.length == 0) { console.info("Received empty device list"); return; } ... }); 


And on the background page, a small bike scatters these messages according to the Controller methods:

 var server = new Controller(); chrome.extension.onMessage.addListener( function (request: any, sender: any, sendResponse: (result: any) => void ) { return server[request.method].call(server, request.data, sendResponse); }); 


Calls did not take much:


The responsibilities of the controller are slightly wider:


Naturally, the first time it did not work and had to debug the code. And simultaneously on the background page and in the client part. With the client part, everything is simple - right click on the extension button and “Inspect popup”, here you can see both the debugger and the DOM, and what is very important - typescript gave me a bunch of * .map files, and Chrome picked them up itself, and in the Chrome console I debugged typescript, not javascript. I really liked it, except for one thing - typescript prudently creates the _this variable and writes a reference to this into it. This allows you to work without loss within objects, but the debugger did not know this and often gave out all sorts of nonsense when I tried to watch the values ​​of variables. After a few cones, I realized that during debugging, all this should be changed to _this, in order to see the true meaning, then everything fell into place.

Now the background page. At first I used console.log, but very soon it began to be missed, and a very useful link came up here - on the extensions page, as it turned out (and why not an hour earlier?) There is this line:
Inspect views: background page ,
on it the background debugger opened.

Keeping and reading the state turned out to be quite simple, Chrome provided the API, and even claims that it will be synchronized with the rest of the data between different Chroms, if configured. Synchronization did not check. And it looks like this:

  private ReadState(callback: () => void): void { this.devices = []; chrome.storage.sync.get(["aid", "ds"], (storedValues: any) => { if (storedValues.aid != null && storedValues.aid != "") this.deviceId = storedValues.aid; var devicesJson = storedValues.ds; this.devices = JSON.parse(devicesJson); callback(); }); } private WriteState(): void { chrome.storage.sync.set({ "aid": this.deviceId, "ds": JSON.stringify(this.devices) }); } 


Request status from the server - a completely normal ajax:
  private RequestStatus(data: any, successCallback: (status: DeviceStatus) => void , errorCallback: (error: string) => void ): void { $.ajax({ url: "https://localhost/cbs/" + data.deviceId, type: "GET", success: successCallback, error: (xhr, error) => { console.error(error); errorCallback(error); } }); } 


One of the most sophisticated rakes on the way met while debugging the model binding to the view. Knockout didn’t want to register an event handler; instead, it called it on the spot. Another Chrome extension, knockout context debugger, helped detect the problem.

And with the notifications I had to sweat. Chrome provides an API for notifications, but with one significant limitation - it only hangs on the screen for 5 seconds, after which almost nothing is left of it, only a small bell in the system tray. And this is not configurable. After several unsuccessful attempts, the problem was resolved through webkit notifications.

  var n = webkitNotifications.createNotification(opt.iconUrl, opt.title, opt.message); n.onclose = () => { ... }; n.show(); 


And then Chrome began to remind the battery something like this:
image

With the icon above, too, everything is simple, Chrome again provided the API, and with the help of simple manipulations with the icon, hint and text everything worked like a clock:
  if (updateIcon) { chrome.browserAction.setIcon({ path: path }); chrome.browserAction.setTitle({ title: title }); chrome.browserAction.setBadgeText({ text: "!" }); } 


After that, it remained only to marry the code format from the android application with the extension. Chose a 12-character code to avoid repetitions.

By midnight on Sunday the first version was ready.

Then there were 3 more small updates with buns and insecticides, so it would not be quite honest to say that only the weekend and nothing more was spent, but the main task was completed on time.

Update:
if anyone is interested in writing a client under iOS, let me know

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


All Articles