История одного Google Chrome расширения

в 10:19, , рубрики: chrome extension, Google Chrome, javascript, Веб-разработка, расширения chrome, метки: , ,

В один прекрасный день, за пару часов до конца работы, мне приходит задача: «Нужно написать приложение для браузера, которое должно по клику пользователя отправлять данные со страницы на сайт клиента. Что за приложение и какой браузер — полностью на ваш выбор...».

Немного поразмыслив я пришел к варианту google chrome extension:

  • Crome использует Chromium движок, который является форком WebKit (а это Safari), так же не забываем Blink (а это уже новая (хотя я все еще использую старую с bookmarks'ами) Opera). Таким образом, написав расширения для chrome, мы с минимальными переделками (а то и без них) сможем его портировать на еще 2 браузера
  • Нет опыта работы с API Google Chrome
  • Google все-таки компания добра :)

Когда мысли немного улеглись, первое что я сделал — это ввел в поиске харба "расширение Google Chrome". Увидев обширный вариант статей по данной теме, я со спокойной душой ушел домой полностью уверенный в том, что завтра с утра прочитав их, к концу рабочего дня дело будет 'в шляпе' (как же я тогда ошибался). Прочитав парочку их них я имел общее представление о том как это работает, но этого оказалось мало для воплащения моих идей. Что ж, приступим…

Открываем google chrome, вводим chrome://extensions, ставим галочку Developer mode, нажимаем кнопку Load unpacked extension, выбрав папку нажимаем Ok.

История одного Google Chrome расширения

В начале было слово манифест. Ниже можете увидеть содержимое этого файла (manifest.json — это обязательное название файла манифеста)

manifest.json

{
    "manifest_version": 2,
    "name": "My application",   // тут надеюсь все понятно
    "version": "0.9",

    "icons": {
        "16": "./16x16.png",
        "32": "./32x32.png",
        "48": "./48x48.png",
        "128": "./128x128.png"
    },

    "permissions": [
        "tabs",
        "http://*/*",
        "https://*/*"
    ],

    "background" : {
        "page": "background.html"
    },

    "content_scripts":[{
        "matches": [
            "http://*/*",
            "https://*/*"
        ],
        "js": [
            "script_in_content.js"
        ]
    }],

    "browser_action": {
        "default_title": "Application",
        "default_icon" : "./16x16.png"
        //  "default_popup": "login.html"        // это имя html-страницы расширения, которая будет всплывать при нажатии на иконку, можно с помощью JS устанавливать различные html страницы
    }
}

manifest_version — на данный момент значение 2 обязательное.
version — версия вашего расширения, может содержать только цифры и `.` (те. '2.1.12', '0.59' и тд)
icons — это список всех иконок которые будут отображатся в браузере в различных местах (16 — в адресной строке, 48 — в списке всех расширений и тд.)
permissions — здесь перечислен массив с разрешениями, мне нужно было только tabs.http и https нужен для ajax обмена с любыми сайтами, а также для того что-бы script_in_content.js мог обмениватся данными с фоновой страницей — background.html.
background — это имя фоновой страницы. Фоновая страницы — важный элемент, хотя для некоторых приложений он и не обязателен. Зачем она нужна немного по-позже.
content_scripts — здесь говорится что файл script_in_content.js, будет автоматически загружатся для страницы открытой во вкладке. Страница должны быть открыта с сайтов http://*/* те, всех сайтов с http, но не https, хотя можно было бы указать и их.
browser_action — существует 2 варанта отображения иконки расширения: browser_action и page_action

page_action говорит о том что расширение индивидуально для каждой вкладки, то есть значок будет выводиться в адресной строке. Этот значек можно спрятать/отобразить с помощью JS в зависимости от обстоятельств.

browser_action наоборот не считаются индивидуальными и отображаются не в адресной строке, а в панеле для расширений. Даную иконку никак нельзя скрыть на JS (но можно заблокировать), она отображается постоянно. У browser_action есть одно преимущество по сравнению с page_action, поверх иконки browser_action можно написать пару красивых символов (у меня влазит только 4).

История одного Google Chrome расширения

Я выбрал browser_action, как мне необходимо работать не с одним сайтом, а с несколькими. И да, нанесение красивых символов на иконку.
Вот что Google говорит по этому поводу:

Do use page actions for features that make sense for only a few pages.
Don't use page actions for features that make sense for most pages. Use browser actions instead.

И так, что будет делать наше приложение; скажу сразу, приложение, которое будет далее описано, — это малая часть того что было сделано для клиента. Когда менеджер заходит на сайт hantim.ru для просмотра информации о контракте/вакансии, приложение парсит html код страницы и находит информацию (вакансия, город и тд). При клике на иконку расширения — отображается форма логина, куда менеджер вводит свои данные, а потом может добавить выбранную вакансию/контракт в свой профиль на корпоративном сайте.

Теперь о том как все это работает. Google предоставляет нам такую картину:
История одного Google Chrome расширения

1) Inspected window — это то что мы открыли во вкладке, content scripts — это наш script_in_content.js, он имеет полный доступ к DOM страницы.
2) Background page — это сердце приложения, в нашем случае — это background.html.
3) DevTools page — это то, что будет отображатся при клике на иконку расширения (login.html или find.html в нашем случае).

Единственное что меня смущает в данной картинке так это связь DevTools page и Inspected window. Я не нашел решения, что бы передать данные из одной области в другую. Но если выставить Background page как посредника, и через него передавать данный, то все заработает.

И так, настало время кода. Начнем с невидимой стороны.

background.html

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script type="text/javascript" src="lib.js"></script>
        <script type="text/javascript" src="bg.js"></script>
    </head>
    <body></body>
</html>

Надеюсь с этим вопросов не должно возникнуть. Одно замечание: «background.html — загружается только один раз за все время работы браузера, а имеено при его запуске». Здесь вы можете увидеть что мы загружаем 2 js файла (lib.js — набор функций, bg.js — 'голова' приложения).

bg.js

/**
 * OnLoad function
 * 
 * @return void
 */
window.onload = function(){

    // tmp storage
    window.bg = new bgObj();

    // some variables  !!! important
    window.bg.api_site_host = 'http://katran.by';

    // get all graber hosts:   !!!once!!!
    new Ajax({
        url: window.bg.api_site_host+'/regexp.php',
        response: 'json',
        async: false,
        onComplete: function(data){
            if(data && data.status && (data.status === 'ok'))
                window.bg.grabber_hosts = data.data;
        }
    }).send();

    // set handler to tabs
    chrome.tabs.onActivated.addListener(function(info) {
        window.bg.onActivated(info);
    });

    // set handler to tabs:  need for seng objects
    chrome.extension.onConnect.addListener(function(port){
        port.onMessage.addListener(factory);
    });

    // set handler to extention on icon click
    chrome.browserAction.onClicked.addListener(function(tab) {
        window.bg.onClicked(tab);
    });

    // set handler to tabs
    chrome.tabs.onUpdated.addListener(function(id, info, tab) {
        // if tab load
        if (info && info.status && (info.status.toLowerCase() === 'complete')){
            // if user open empty tab or ftp protocol and etc.
            if(!id || !tab || !tab.url || (tab.url.indexOf('http:') == -1))
                return 0;

            // save tab info if need
            window.bg.push(tab);

            // connect with new tab, and save object
            var port = chrome.tabs.connect(id);
            window.bg.tabs[id].port_info = port;

            // run function in popup.html
            chrome.tabs.executeScript(id, {code:"initialization()"});

            // send id, hosts and others information into popup.js
            window.bg.tabs[id].port_info.postMessage({method:'setTabId', data:id});
            window.bg.tabs[id].port_info.postMessage({method:'setHosts', data:window.bg.grabber_hosts});
            window.bg.tabs[id].port_info.postMessage({method:'run'});

            // if user is logged into application set find.html popup
            if(window.bg.user.id)
                chrome.browserAction.setPopup({popup: "find.html"});
        };
    });

    window.bg.onAppReady();
};


/**
 * Functino will be called when popup.js send some data by port interface
 * 
 * @return void
 */
function factory(obj){
    if(obj && obj.method){
        if(obj.data)
            window.bg[obj.method](obj.data);
        else
            window.bg[obj.method]();
    }
}


/**
 * Popup object
 *
 * @version 2013-10-11
 * @return  Object
 */
window.bgObj = function(){
};


/**
 * Pablic methods
 */
window.bgObj.prototype = {

    /**
     * some internal params
     */
    tabs: {},
    user: {},
    popup_dom: {},
    active_tab: {},
    grabber_hosts: {},
    done_urls: [],

    /**
     * init() function
     */
    onAppReady: function()
    {
        // if user not logged into application set login.html popup
        chrome.browserAction.setPopup({popup: "login.html"});
    },

    /**
     * Function add tab into $tabs object, if need
     */
    push: function(tab)
    {
        if(tab.id && (tab.id != 0)){
            if(!this.tabs[tab.id])
                this.tabs[tab.id] = {tab_obj:tab};
        }
    },

    /**
     * Function will be called from popup.js
     */
    mustParsed: function(data)
    {
        if(this.tabs[data.tab_id]){
            var id = data.tab_id;
            this.tabs[id].must_parsed = data.find;

            // run parser in popup.js, if need
            if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true))
                this.tabs[id].port_info.postMessage({method:'parsePage'});
        }
    },

    /**
     * Function will be called from popup.js
     */
    matchesCount: function(data)
    {
        if(data.tab_id && this.tabs[data.tab_id]){
            var id = data.tab_id;
            this.tabs[id].matches = data.matches;
            this.tabs[id].matches_count = this.tabs[id].matches.length+'';

            if(this.tabs[id].matches_count && this.tabs[id].matches_count != '0'){
                chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count});
                return 0;
            }
        }

        // show default text
        chrome.browserAction.setBadgeText({text:''});
    },

    /**
     * Function will be called when user change active tab
     */
    onActivated: function(info)
    {
        // set active tab
        this.active_tab = info;

        var data = {};
        data.matches  = [];

        if(info.tabId){
            data.tab_id  = info.tabId;
            if(!this.tabs[data.tab_id])
                this.tabs[data.tab_id] = {};
            if(!this.tabs[data.tab_id].matches)
                this.tabs[data.tab_id].matches = [];

            data.matches = this.tabs[data.tab_id].matches;
        }

        // set actual count of matches for current tab
        this.matchesCount(data);

        // if user is logged into application set find.html popup
        if(this.user.id)
            chrome.browserAction.setPopup({popup: "find.html"});
    },

    /**
     * Function will be called when user click on extension icon
     */
    onClicked: function(tab)
    {
        alert('Произошла ошибка. Обратитесь к разработчикам данного приложения.');
        return 0;
    },

    /**
     * Function will be called from login.js
     */
    loginUser: function(user_data)
    {
        var self = this;
        var json_data = false;

        // get all graber hosts:   !!!once!!!
        new Ajax({
            url: window.bg.api_site_host+'/login.php?user='+encodeURIComponent(JSON.stringify(user_data)),
            method: 'post',
            response: 'json',
            async: false,
            onComplete: function(data){
                if(data && data.status){
                    // if login - ok
                    if(data.status === 'ok')
                        self.user = data.data;

                    json_data = data;
                }
            }
        }).send();

        // return value for login.js
        return json_data;
    },

    /**
     * Function will be called from login.js and others places
     */
    setPopup: function(popup_file)
    {
        chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file});
    },

    /**
     * Function will be called from find.js and others places
     */
    getMatches: function()
    {
        // init if need
        if(!this.tabs[this.active_tab.tabId])
            this.tabs[this.active_tab.tabId] = {};
        if(!this.tabs[this.active_tab.tabId].matches)
            this.tabs[this.active_tab.tabId].matches = [];

        // if user alredy send this url - remove
        for(var i = 0, cnt = this.tabs[this.active_tab.tabId].matches.length; i < cnt; i++){
            for(var j = 0, len = this.done_urls.length; j < len; j++){
                if(this.tabs[this.active_tab.tabId].matches[i].url === this.done_urls[j]){
                    this.tabs[this.active_tab.tabId].matches[i].url = '';
                    break;
                }
            }
        }

        return this.tabs[this.active_tab.tabId].matches;
    },

    /**
     * Function will be called from find.js and others places
     */
    addUrlToGrabber: function(url)
    {
        // if $url == ''  -  already used
        if(json_data.status && (json_data.status === 'ok')){
            var matches = this.tabs[this.active_tab.tabId].matches;
            for(var i = 0, cnt = matches.length; i < cnt; i++){
                if(matches[i].url && (matches[i].url === url))
                    matches[i].url = '';
                    this.done_urls.push(url);
            }
        }

        // return value for login.js
        return json_data;
    },


    /**
     * Empty method
     */
    empty: function()
    {
    }
}

Первым делом мы дожидаемся window.onload, потом посылаем запрос на katran.by (получим json данные, с какого сайта и каким RegExp'пом мы дастанем необходимые данные), потом вешаем handler'ы на вкладки браузера (для этого мы и указали в манифесте permissions ~ tabs).

    chrome.tabs.onActivated.addListener(function(info) {
        window.bg.onActivated(info);
    });

onActivated — происходит тогда, когда пользователь перешел на новую вкладку (по клику или по alt+tab).

    chrome.tabs.onUpdated.addListener(function(id, info, tab) {
        .....
    });

onUpdated — происходит тогда, когда страница полностью (загрузился не только DOM, а и все картинки) загрузилась во вкладке.

    chrome.browserAction.onClicked.addListener(function(tab) {
        window.bg.onClicked(tab);
    });

onClicked — происходит тогда, когда пользователь кликает на иконке приложения. Небольшое замечание, если во время клика default_popup установлено, то обработчик onClicked — не запустится. default_popup это html страница которая будет отображаеться после нажатия на иконку расширения. default_popup можно выставить в манифесте, а так же с помощью chrome.browserAction.setPopup({popup: «find.html»}); или chrome.pageAction.setPopup({popup: «find.html»});

    chrome.extension.onConnect.addListener(function(port){
        port.onMessage.addListener(factory);
    });

Эта темная магия конструкция, нужна для приема данных, посланных от script_in_content.js с помощью port.
Обработкой данных занимается factory(obj)

function factory(obj){
    if(obj && obj.method){
        if(obj.data)
            window.bg[obj.method](obj.data);
        else
            window.bg[obj.method]();
    }
}

Что же происходит когда пользователь загружает вкладку, а происходит следующее:

  • Вызывается handler onUpdated
  • if (info && info.status && (info.status.toLowerCase() === 'complete')) если все загружено — продолжаем разбор полетов.
  • if(!id || !tab || !tab.url || (tab.url.indexOf('http:') == -1)) если пользователь открыл не web-сайт (проверку на https — забыл, только сейчас заметил :) ), а к примеру вкладку настройки или ftp и тд., то ничего не делаем
  • window.bg.push(tab); — созраняем информацию об текущей вкладке
  • chrome.tabs.executeScript(id, {code:"initialization()"}); — сейчас мы приказываем script_in_content.js выполнить функцию initialization()
  • window.bg.tabs[id].port_info.postMessage({method:'setTabId', data:id}) — мы посылаем данные в script_in_content.js
  • chrome.browserAction.setPopup({popup: "find.html"}); — устанавливаем popup страницу, если пользователь авторизовался ранее

Есть 2 способа передать данные от background.html к script_in_content.js:

  1. сhrome.tabs.executeScript(integer tabId, InjectDetails details, function callback) — одно но, таким спосабом можно передавать данные только в виде строки (не объект, не массив)
  2. сhrome.tabs.sendMessage(integer tabId, any message, function responseCallback) — так можно передавать что угодно, правдо потребуются дополнитеные настройки

И так, мы послали данные в script_in_content.js, значит настало время рассмотреть его код.

script_in_content.js

// set handler to tabs:  need for seng objects to backgroung.js
chrome.extension.onConnect.addListener(function(port){
    port.onMessage.addListener(factory);
});


/**
 * Function remove spaces in begin and end of string
 *
 * @version 2012-11-05
 * @param   string  str
 * @return  string
 */
function trim(str)
{
    return String(str).replace(/^s+|s+$/g, '');
}


/**
 * Functino will be called from background.js
 * 
 * @return void
 */
function initialization(){
    window.popup = new popupObj();
}


/**
 * Functino will be called when background.js send some data by port interface
 * 
 * @return void
 */
function factory(obj){
    if(obj && obj.method){
        if(obj.data)
            window.popup[obj.method](obj.data);
        else
            window.popup[obj.method]();
    }
}


/**
 * Popup object
 *
 * @version 2013-10-11
 * @return  Object
 */
window.popupObj = function(){
};


/**
 * Pablic methods
 */
window.popupObj.prototype = {

    /**
     * some internal params
     */
    available_hosts: [],
    total_host: null,
    matches: [],
    tab_id: null,
    port: null,
    cars: [],

    /**
     * Function will be called from bg.js
     */
    setHosts: function(hosts)
    {
        this.available_hosts = hosts;
    },

    /**
     * Function will be called from bg.js
     */
    setTabId: function(id)
    {
        this.tab_id = id;
    },

    /**
     * Function check total host
     */
    run: function()
    {
        // get total host
        if(document.location.host && (document.location.host != ''))
            this.total_host = document.location.host;
        else if(document.location.hostname && (document.location.hostname != ''))
            this.total_host = document.location.hostname;

        if(!this.total_host || (this.total_host === ''))
            return 0;

        var find = false;
        // if total host in array $available_hosts - parse page for finde cars
        for (host in this.available_hosts) {
            if(this.total_host.indexOf(host) != -1){
                this.total_host = host;
                find = true;
                break;
            }
        };

        // create connection to backgroung.html and send request
        this.port = chrome.extension.connect();
        this.port.postMessage({method:'mustParsed', data:{tab_id:this.tab_id, find:find}});
    },

    /**
     * Function will be called from bg.js
     * Parse page
     */
    parsePage: function()
    {
        // reset variable before parse
        this.matches = [];

        if(!this.available_hosts[this.total_host])
            return 0;

        var html = window.document.body.innerHTML;
        var reg_exp = this.available_hosts[this.total_host];
        var matches = {};
        var match = [];
        var find = false;
        for(var i = 0, len = reg_exp.length; i < len; i++) {
            var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag);
            match = exp.exec(html);

            if(match && match.length && reg_exp[i].index){
                matches[reg_exp[i].field] = trim(match[reg_exp[i].index]);
                find = true;
            }
            else if(match && match.length){
                matches[reg_exp[i].field] = match;
                find = true;
            }
        }

        // this url will be send to site
        if(find === true){
            matches.url = document.location.href;
            this.matches.push(matches);
        }

        // send count of matches
        this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}});
    }
}

Первое что бросается в глаза — это прием данных от background.html, как можете заметить он такой же как и в bg.js:

chrome.extension.onConnect.addListener(function(port){
    port.onMessage.addListener(factory);
});

Как помните, ранее в bg.js мы запустили initialization(), setTabId(), setHosts() и run(). Найбольший интерес представляет window.popup.run(). Там проверяется доменное имя сервера открытой страницы, и если это имя совпадает со списком сайтов которые нам интересны (данные с которых необходимо передать на корпоративный ресурс) — find = true; и отправляем запрос window.bg.mustParsed(obj) в bg.js.

    /**
     * Function will be called from script_in_content.js
     */
    mustParsed: function(data)
    {
        if(this.tabs[data.tab_id]){
            var id = data.tab_id;
            this.tabs[id].must_parsed = data.find;

            // run parser in popup.js, if need
            if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true))
                this.tabs[id].port_info.postMessage({method:'parsePage'});
        }
    }

Если совпадение домена было найдено, то запускаем парсер страницы parsePage() в script_in_content.js.

    /**
     * Function will be called from bg.js
     * Parse page
     */
    parsePage: function()
    {
        // reset variable before parse
        this.matches = [];

        if(!this.available_hosts[this.total_host])
            return 0;

        var html = window.document.body.innerHTML;
        var reg_exp = this.available_hosts[this.total_host];
        var matches = {};
        var match = [];
        var find = false;
        for(var i = 0, len = reg_exp.length; i < len; i++) {
            var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag);
            match = exp.exec(html);

            if(match && match.length && reg_exp[i].index){
                matches[reg_exp[i].field] = trim(match[reg_exp[i].index]);
                find = true;
            }
            else if(match && match.length){
                matches[reg_exp[i].field] = match;
                find = true;
            }
        }

        // this url will be send to site
        if(find === true){
            matches.url = document.location.href;
            this.matches.push(matches);
        }

        // send count of matches
        this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}});
    }

Если скрипт что-то нашел на странице, то все что он нашел складывает в массивчик, добавляет к нему текущий url страницы и отправляет назад в bg.js, мол: «Смотри что я нашел...». В ответ на это, bg.js анализирует входные данные, и если RegExp'ы нашли что-то — пишет поверх иконки колличество совпадений (1, 2 и тд.) chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count});.

Это вроде все основные моменты рыботы связки bg.js и script_in_content.js.
Теперь поговорим о popup. Когда пользователь кликает по иконке приложения — отображается форма login.html.
Менеджер вводит свои данные от корпоративного сайта, нажимает Login и тут происходит следующее:

login.html

<!DOCTYPE html>
<html>
    <head>
        <meta   http-equiv="content-type" content="text/html; charset=UTF-8">
        <script type="text/javascript"    src="login.js"></script>
        <link   type="text/css"           rel="stylesheet" href="login.css">
        <title>Grabber popup</title>
    </head>
    <body>
        <div class="body">
            <div class="emptyLogin">
                <div id="error_message"> </div>
                <form name="login_form" action="" method="get" id="popup_login_form">
                    <table>
                        <tbody>
                            <tr>
                                <td align="right">Ваш E-mail:</td>
                                <td><input type="text" name="login" value="" tabindex="1"></td>
                            </tr>
                            <tr>
                                <td align="right">Пароль:</td>
                                <td><input type="password" name="pass" value="" tabindex="2"></td>
                            </tr>
                            <tr>
                                <td colspan="2" align="center"><input type="submit" value="Login" class="button"></td>
                            </tr>
                        </tbody>
                    </table>
                </form>
            </div>
            <div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div>
        </div>
    </body>
</html>

login.js

/**
 * OnLoad function
 * 
 * @return void
 */
window.onload = function(){

    // set some events handlers
    document.getElementById('popup_login_form').onsubmit = function(obj){
        // fade popup
        document.getElementById('loader').style.display = 'block';
        document.getElementById('error_message').innerHTML = ' ';

        if(obj.target.elements && obj.target.elements.length && (obj.target.elements.length === 3)){
            var data = {};
            data.login = obj.target.elements[0].value;
            data.pass  = obj.target.elements[1].value;

            setTimeout(function(){
                var bg_wnd = chrome.extension.getBackgroundPage();
                var result = bg_wnd.bg.loginUser(data);

                if(result && result.status && (result.status === 'error'))
                    document.getElementById('error_message').innerHTML = result.mess;
                else{
                    // set new popup html code and close popup window
                    bg_wnd.bg.setPopup('find.html');
                    window.close();
                }

                // hide fade on popup
                document.getElementById('loader').style.display = 'none';
            }, 500);
        }
        return false;
    };
}

Задача login.js состоит в том, что бы повесить onsubmit на форму, и отправить логин/пароль в background.html (bg.js),
а делается это с помощью следующей конструкции (как увидите, мы пожем на прямую вызывать методы объекта bg.js):

                var bg_wnd = chrome.extension.getBackgroundPage();
                var result = bg_wnd.bg.loginUser(data);

bg_wnd.bg.loginUser(data) отправляет данные на сервер, если все хорошо, то popup login.html сменяет find.html,
а данные о пользователе сохраняются в переменной. Смена popup происходит следующим образом:

    /**
     * Function will be called from login.js and others places
     */
    setPopup: function(popup_file)
    {
        chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file});
    },

Небольшое замечание, если пользователь открыл popup login.html поставил курсор в поле 'Ваш E-mail:' и нажимает TAB (первый раз) в надежде перейти к паролю, то его ожидает разачарование, фокус не сменится. Данный баг все еще актуален.

Так, осталось совсем чуть-чуть.
После того как мы успешно авторизовались, мы поменя popup на find.html.

find.html

<!DOCTYPE html>
<html>
    <head>
        <meta   http-equiv="content-type" content="text/html; charset=UTF-8">
        <script type="text/javascript"    src="find.js"></script>
        <link   type="text/css"           rel="stylesheet" href="find.css">
        <title>Grabber</title>
    </head>
    <body>
        <div class="body">
            <div class="carsRows" id="popup_cars_rows">
                <h3 style="text-align: center; margin: 5px 0;">Найденные данный странице</h3>
                <form name="cars_form" action="" method="get" id="popup_cars_form">
                    <table id="popup_cars_table">
                        <thead>
                            <tr>
                                <th class="make">Вакансия</th>
                                <th class="info">Город</th>
                                <th class="addBtn"> </th>
                            </tr>
                        </thead>
                        <tbody>
                        </tbody>
                    </table>
                </form>
            </div>
            <div class="carsRows" id="popup_cars_rows_none" style="display: none;">
                <h3 style="text-align: center; margin: 5px 0;">Ничего не найдено на странице</h3>
            </div>
            <div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div>
        </div>
    </body>
</html>

find.js

/**
 * OnLoad function
 * 
 * @return void
 */
window.onload = function(){

    // set new popup html code and close popup window
    window.bg_wnd = chrome.extension.getBackgroundPage();
    var rows = window.bg_wnd.bg.getMatches();

    // function render popup
    renderPopup(rows);
}


/**
 * Function set cars into html
 *
 * @param  array  $rows
 * @return void
 */
function renderPopup(rows)
{
    if(rows.length === 0){
        document.getElementById('popup_cars_rows').style.display = 'none';
        document.getElementById('popup_cars_rows_none').style.display = 'block';
        return 0;
    }
    else{
        document.getElementById('popup_cars_rows').style.display = 'block';
        document.getElementById('popup_cars_rows_none').style.display = 'none';
    }

    for (var i = 0, cnt = rows.length; i < cnt; i++)
        renderRow(rows[i]);
}


/**
 * Function set cars into html
 *
 * @param  object $row
 * @return void
 */
function renderRow(row)
{
    var tbl = document.getElementById('popup_cars_table').children[1];

    // add divided row
    var td = tbl.insertRow(-1).insertCell(-1);
    td.setAttribute('colspan', '3');
    td.innerHTML = '<hr style="border: 1px solid #909090; width: 75%">';

    var tr = tbl.insertRow(-1);
    var td1 = tr.insertCell(-1);
    var td2 = tr.insertCell(-1);
    var td3 = tr.insertCell(-1);
    var vacancy = [];
    var city    = [];

    var hash = {
        vacancy: 'вакансия',
        city: 'город',
    }

    var table_row = [];
    for(key in row){
        if(hash[key]){
            if(key == 'vacancy')
                vacancy.push(row[key]);
            if(key == 'city')
                city.push(row[key]);
        }
    }

    td1.innerHTML = vacancy.join(' ');;
    td2.innerHTML = city.join(' ');
    td3.innerHTML = (row.url === '')?'<b><em>Добавлено</em></b>':'<input type="button" value="Дабавить" name="cars[]" class="button"><input type="hidden" value="'+row.url+'" name="url[]">';
    td3.children[0].addEventListener('click', function(){addToGrabber(event)}, false);
}


function addToGrabber(e)
{
    // hide fade on popup
    document.getElementById('loader').getElementsByTagName('img')[0].style.marginTop = (window.innerHeight/2-10)+'px';
    document.getElementById('loader').style.display = 'block';

    if(e && e.srcElement){
        var url = e.srcElement.parentNode.children[1].value;

        setTimeout(function(){
            var result = window.bg_wnd.bg.addUrlToGrabber(url);
            e.srcElement.parentNode.innerHTML = '<b><em>Добавлено</em></b>';

            // hide fade on popup
            document.getElementById('loader').style.display = 'none';
        }, 500);
    }
}

Как только find.html загрузился, в работу вступает find.js. Его задача спросить bg.js: 'Что там у тебя есть на текущую страницу' — и отобразить то, что отдал bg.js.

/**
 * OnLoad function
 * 
 * @return void
 */
window.onload = function(){

    // set new popup html code and close popup window
    window.bg_wnd = chrome.extension.getBackgroundPage();
    var rows = window.bg_wnd.bg.getMatches();

    // function render popup
    renderPopup(rows);
}

Так выглядит готовое решение.

История одного Google Chrome расширения

C кнопкой 'Добавить' я думаю сами разберетесь, как она работает. На последок хочу сказать как все это дело отлаживается.
background.html — что бы посмотреть работу скриптов bg.js и lib.js нужно кликнуть на линку background.html на странице chrome://extensions.
История одного Google Chrome расширения
script_in_content.js — он выполняется в контексте страницы, поэтому можете смело инспектировать страницу и смотреть консоль с выводом ошибок в нее.
login.html и find.html — что бы вывести их Developer Tools, нужно кликнуть на иконке приложения и правым кликом мышки выбрать инспекцию страницы.
История одного Google Chrome расширения

PS. Весь JavaScript должен находится в js файлах, если вы его вставите в html — chrome будет ругаться.
Также пару ссылок:
на документацию: manifest.json, Chrome's API
на github.com: исходный код

Автор: mamchyts

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js