В один прекрасный день, за пару часов до конца работы, мне приходит задача: «Нужно написать приложение для браузера, которое должно по клику пользователя отправлять данные со страницы на сайт клиента. Что за приложение и какой браузер — полностью на ваш выбор...».
Немного поразмыслив я пришел к варианту google chrome extension:
Когда мысли немного улеглись, первое что я сделал — это ввел в поиске харба "расширение Google Chrome". Увидев обширный вариант статей по данной теме, я со спокойной душой ушел домой полностью уверенный в том, что завтра с утра прочитав их, к концу рабочего дня дело будет 'в шляпе' (как же я тогда ошибался). Прочитав парочку их них я имел общее представление о том как это работает, но этого оказалось мало для воплащения моих идей. Что ж, приступим…
Открываем google chrome, вводим chrome://extensions, ставим галочку Developer mode, нажимаем кнопку Load unpacked extension, выбрав папку нажимаем Ok.
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/7eb/a85/4ad/7eba854ad31f65f16dde9ddff4c83401.png)
В начале былослово манифест. Ниже можете увидеть содержимое этого файла (manifest.json — это обязательное название файла манифеста)
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).
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/c15/cdb/b92/c15cdbb928397f55279e7ae168e4416b.png)
Я выбрал browser_action, как мне необходимо работать не с одним сайтом, а с несколькими. И да, нанесение красивых символов на иконку.
Вот что Google говорит по этому поводу:
И так, что будет делать наше приложение; скажу сразу, приложение, которое будет далее описано, — это малая часть того что было сделано для клиента. Когда менеджер заходит на сайт hantim.ru для просмотра информации о контракте/вакансии, приложение парсит html код страницы и находит информацию (вакансия, город и тд). При клике на иконку расширения — отображается форма логина, куда менеджер вводит свои данные, а потом может добавить выбранную вакансию/контракт в свой профиль на корпоративном сайте.
Теперь о том как все это работает. Google предоставляет нам такую картину:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/02e/24e/409/02e24e409cadc89e4158f613a0951c12.png)
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 — загружается только один раз за все время работы браузера, а имеено при его запуске». Здесь вы можете увидеть что мы загружаем 2 js файла (lib.js — набор функций, bg.js — 'голова' приложения).
Первым делом мы дожидаемся window.onload, потом посылаем запрос на katran.by (получим json данные, с какого сайта и каким RegExp'пом мы дастанем необходимые данные), потом вешаем handler'ы на вкладки браузера (для этого мы и указали в манифесте permissions ~ tabs).
onActivated — происходит тогда, когда пользователь перешел на новую вкладку (по клику или по alt+tab).
onUpdated — происходит тогда, когда страница полностью (загрузился не только DOM, а и все картинки) загрузилась во вкладке.
onClicked — происходит тогда, когда пользователь кликает на иконке приложения. Небольшое замечание, если во время клика default_popup установлено, то обработчик onClicked — не запустится. default_popup это html страница которая будет отображаеться после нажатия на иконку расширения. default_popup можно выставить в манифесте, а так же с помощью chrome.browserAction.setPopup({popup: «find.html»}); или chrome.pageAction.setPopup({popup: «find.html»});
Этатемная магия конструкция, нужна для приема данных, посланных от script_in_content.js с помощью port.
Обработкой данных занимается factory(obj)
Что же происходит когда пользователь загружает вкладку, а происходит следующее:
Есть 2 способа передать данные от background.html к script_in_content.js:
И так, мы послали данные в script_in_content.js, значит настало время рассмотреть его код.
Первое что бросается в глаза — это прием данных от background.html, как можете заметить он такой же как и в bg.js:
Как помните, ранее в bg.js мы запустили
Если совпадение домена было найдено, то запускаем парсер страницы
Если скрипт что-то нашел на странице, то все что он нашел складывает в массивчик, добавляет к нему текущий url страницы и отправляет назад в bg.js, мол: «Смотри что я нашел...». В ответ на это, bg.js анализирует входные данные, и если RegExp'ы нашли что-то — пишет поверх иконки колличество совпадений (1, 2 и тд.)
Это вроде все основные моменты рыботы связки bg.js и script_in_content.js.
Теперь поговорим о popup. Когда пользователь кликает по иконке приложения — отображается форма login.html.
Менеджер вводит свои данные от корпоративного сайта, нажимает Login и тут происходит следующее:
Задача login.js состоит в том, что бы повесить
а делается это с помощью следующей конструкции (как увидите, мы пожем на прямую вызывать методы объекта bg.js):
а данные о пользователе сохраняются в переменной. Смена popup происходит следующим образом:
Небольшое замечание, если пользователь открыл popup login.html поставил курсор в поле 'Ваш E-mail:' и нажимает TAB (первый раз) в надежде перейти к паролю, то его ожидает разачарование, фокус не сменится. Данный баг все еще актуален.
Так, осталось совсем чуть-чуть.
После того как мы успешно авторизовались, мы поменя popup на find.html.
Как только find.html загрузился, в работу вступает find.js. Его задача спросить bg.js: 'Что там у тебя есть на текущую страницу' — и отобразить то, что отдал bg.js.
Так выглядит готовое решение.
![](http://habrastorage.org/r/w1560/storage3/6c7/fde/8ff/6c7fde8ff8bf7260a7054863a99013aa.png)
C кнопкой 'Добавить' я думаю сами разберетесь, как она работает. На последок хочу сказать как все это дело отлаживается.
background.html — что бы посмотреть работу скриптов bg.js и lib.js нужно кликнуть на линку background.html на странице chrome://extensions.
![](http://habrastorage.org/r/w1560/storage3/abf/69d/f0d/abf69df0dee0f58b89d4e7185a19782b.png)
script_in_content.js — он выполняется в контексте страницы, поэтому можете смело инспектировать страницу и смотреть консоль с выводом ошибок в нее.
login.html и find.html — что бы вывести их Developer Tools, нужно кликнуть на иконке приложения и правым кликом мышки выбрать инспекцию страницы.
![](http://habrastorage.org/r/w1560/storage3/dfc/9b8/05d/dfc9b805d1ca2049d426aed5205f8c0b.png)
PS. Весь JavaScript должен находится в js файлах, если вы его вставите в html — chrome будет ругаться.
Также пару ссылок:
на документацию: manifest.json, Chrome's API
на github.com: исходный код
PSS. Со слов x256 поправил введение.
Немного поразмыслив я пришел к варианту google chrome extension:
- Crome использует Blink движок (до апреля 2013 года использовался WebKit), Blink является форком WebKit (а это Safari), так же не забываем новую Opera'у (хотя я все еще использую старую с bookmarks'ами). Таким образом, написав расширения для chrome, мы с минимальными переделками (а то и без них) сможем его портировать на еще 2 браузера
- Нет опыта работы с API Google Chrome
- Google все-таки компания добра :)
Когда мысли немного улеглись, первое что я сделал — это ввел в поиске харба "расширение Google Chrome". Увидев обширный вариант статей по данной теме, я со спокойной душой ушел домой полностью уверенный в том, что завтра с утра прочитав их, к концу рабочего дня дело будет 'в шляпе' (как же я тогда ошибался). Прочитав парочку их них я имел общее представление о том как это работает, но этого оказалось мало для воплащения моих идей. Что ж, приступим…
Открываем google chrome, вводим chrome://extensions, ставим галочку Developer mode, нажимаем кнопку Load unpacked extension, выбрав папку нажимаем Ok.
![](https://habrastorage.org/getpro/habr/post_images/7eb/a85/4ad/7eba854ad31f65f16dde9ddff4c83401.png)
В начале было
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).
![](https://habrastorage.org/getpro/habr/post_images/c15/cdb/b92/c15cdbb928397f55279e7ae168e4416b.png)
Я выбрал 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 предоставляет нам такую картину:
![](https://habrastorage.org/getpro/habr/post_images/02e/24e/409/02e24e409cadc89e4158f613a0951c12.png)
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);
});
Эта
Обработкой данных занимается 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.jschrome.browserAction.setPopup({popup: "find.html"});
— устанавливаем popup страницу, если пользователь авторизовался ранее
Есть 2 способа передать данные от background.html к script_in_content.js:
- сhrome.tabs.executeScript(integer tabId, InjectDetails details, function callback) — одно но, таким спосабом можно передавать данные только в виде строки (не объект, не массив)
- с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);
}
Так выглядит готовое решение.
![](http://habrastorage.org/storage3/6c7/fde/8ff/6c7fde8ff8bf7260a7054863a99013aa.png)
C кнопкой 'Добавить' я думаю сами разберетесь, как она работает. На последок хочу сказать как все это дело отлаживается.
background.html — что бы посмотреть работу скриптов bg.js и lib.js нужно кликнуть на линку background.html на странице chrome://extensions.
![](http://habrastorage.org/storage3/abf/69d/f0d/abf69df0dee0f58b89d4e7185a19782b.png)
script_in_content.js — он выполняется в контексте страницы, поэтому можете смело инспектировать страницу и смотреть консоль с выводом ошибок в нее.
login.html и find.html — что бы вывести их Developer Tools, нужно кликнуть на иконке приложения и правым кликом мышки выбрать инспекцию страницы.
![](http://habrastorage.org/storage3/dfc/9b8/05d/dfc9b805d1ca2049d426aed5205f8c0b.png)
PS. Весь JavaScript должен находится в js файлах, если вы его вставите в html — chrome будет ругаться.
Также пару ссылок:
на документацию: manifest.json, Chrome's API
на github.com: исходный код
PSS. Со слов x256 поправил введение.