Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

Thursday, September 29, 2011

Javascript State Machine v2

Совершенно случайно набрел на библиотеку под названием State Machine.

Это библиотека, реализующая событийную модель основанную на состояниях.

При реализации сложной логики событий эта библиотека сильно упрощает жизнь разработчика.

Для чего она и что умеет. Я думаю начну с примера, который хорошо показывает ее возможности.

var fsm = StateMachine.create({

    events: [
      { name: 'start', from: 'none',   to: 'green'  },
      { name: 'warn',  from: 'green',  to: 'yellow' },
      { name: 'panic', from: 'green',  to: 'red'    },
      { name: 'panic', from: 'yellow', to: 'red'    },
      { name: 'calm',  from: 'red',    to: 'yellow' },
      { name: 'clear', from: 'red',    to: 'green'  },
      { name: 'clear', from: 'yellow', to: 'green'  },
    ],

    callbacks: {
      onbeforestart: function(event, from, to) { log("STARTING UP"); },
      onstart:       function(event, from, to) { log("READY");       },

      onbeforewarn:  function(event, from, to) { log("START   EVENT: warn!",  true);  },
      onbeforepanic: function(event, from, to) { log("START   EVENT: panic!", true);  },
      onbeforecalm:  function(event, from, to) { log("START   EVENT: calm!",  true);  },
      onbeforeclear: function(event, from, to) { log("START   EVENT: clear!", true);  },

      onwarn:        function(event, from, to) { log("FINISH  EVENT: warn!");         },
      onpanic:       function(event, from, to) { log("FINISH  EVENT: panic!");        },
      oncalm:        function(event, from, to) { log("FINISH  EVENT: calm!");         },
      onclear:       function(event, from, to) { log("FINISH  EVENT: clear!");        },

      onleavegreen:  function(event, from, to) { log("LEAVE   STATE: green");  },
      onleaveyellow: function(event, from, to) { log("LEAVE   STATE: yellow"); },
      onleavered:    function(event, from, to) { log("LEAVE   STATE: red");    async(to); return false; },

      ongreen:       function(event, from, to) { log("ENTER   STATE: green");  },
      onyellow:      function(event, from, to) { log("ENTER   STATE: yellow"); },
      onred:         function(event, from, to) { log("ENTER   STATE: red");    },

      onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); }
    }
});

var async = function(to) {
    pending(to, 3);
    setTimeout(function() {
      pending(to, 2);
      setTimeout(function() {
        pending(to, 1);
        setTimeout(function() {
          fsm.transition(); // trigger deferred state transition
        }, 1000);
      }, 1000);
    }, 1000);
};

var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); };

fsm.start();

У нас есть состояние какого-то процесса, например ядерного синтеза, и датчики, сигнализирующие о текущем состоянии системы. Зеленый – все ОК. Желтый – предупреждение. Красный – тревога.

При переходе из одного состояния в другое система должна реагировать определенным образом на это. Например смена состояния с зеленого на желтый сопровождается генерацией событий onleavegreen, onyellow, onbeforewarn, onwarn, onchangestate, где мы соответствующим образом можем отреагировать на смену состояний процесса.

Также, в качестве примера возможностей библиотеки, функция async симулирует этапный переход из одного состояния процесса в другой. Метод transition является аналогом метода resolve у объекта $.Deferred() из jQuery. Он сообщяет StateMachine о готовности перехода из одного состояния в другое.

Как это все выглядит в динамике можно посмотреть тут.

Saturday, September 24, 2011

Cоздание прототипа социальной сети на ExtJS. Первые и не последние проблемы с ExtJS 4

Постоянно меняющиеся требования и сжатые сроки подтолкнули нас к использованию ExtJS 4 для создания прототипа.

Проблемы в ExtJS, с которыми мы столкнулись при разработке, едва ли превысили опыт, который мы получили при препарировании ExtJS.

В интернете много статей про ExtJS 4 уровня basic. Например очень порадовала эта статья http://css.dzone.com/articles/how-use-extjs-4-jquery. А вот серьезных “не кассовых” статей по решению (или созданию) каких-то проблем с помощью ExtJS не много.

Предлагаю вашему внимаю первую статью в рамках ExtJS patch lab… Ну и немного о самой соц. сети… совсем немного.

Для ленты постов решено было использовать Ext.panel.Grid с Ext.data.Store. Можно сказать это те фундаментальные вещи, ради которых и выбрали использование ExtJS.

Почему именно grid, а не dataView (в последствии конечное пришлось отказаться от грида в пользу вьюшки). У грида понравился визуальный режим сортировки, фильтрации данных, “бесконечный” скролл. Радость от предоставленных возможностей просто переполняла.

Ext.data.Store

Данные приходят от сервера с помощью API вызова, далее в сыром виде передаются в метод loadData класса Store. Использовать встроеный в Store прокси не стали из-за наличия у нас API как у Foursqure.

var myStore = Ext.create('Ext.data.Store', {
    model: 'Post',
filterOnLoad: true,
idProperty: 'id',
filters: new Ext.util.Filter({
filterFn: function (item) {
return item.get("status") > 0;
}
})
});

API.Events.get().done(function(posts){
myStore.loadData(posts);
});

Модель Post из себя представляет наследника класса Ext.data.Model.

Ext.define('Post', {
    extend: 'Ext.data.Model',
    idProperty: 'id',
constructor: function (raw) { return this.callParent([this.prepare(raw), raw[this.idProperty]]); },
prepare: function (raw) {


} });

В модели Post происходит подготовка сырых данных через метод prepare. В конструктор родительского класса данные приходят уже в обработанном виде.

Что происходит в методе prepare:
Например приведение даты поста из строки к объекту Date

this.data = new Date(this.data);
…получение строкового значения наименования поста из typeId через хэш таблицу соответствий
this.typeName = Dict.post.types[this.typeId];
И так далее...

Store хранит внутри себя данные в виде инстанцированных моделей, в методе loadData происходит проверка, если данные еще сырые, то передаем их в конструктор модели, и записываем ее у себя, и так с каждым элементом массива.

Приятная полезность #1

Таким образом loadData принимает как массив сырых объектов, так и уже инстанцированных моделей.

Особенность #1

При передачи в loadData единичного объекта произойдет ошибка, метод ожидает только массива, пусть даже из одного элемента.

Особенность #2

Объекты в javascript передаются по ссылке, поэтому переданные сырые данные в loadData станут массивом моделей.

API.Events.get().done(function(posts){
//posts is array of objects
myStore.loadData(posts);
//posts is array of Models
});

Если вам нужны исходные данные после загрузки в неизменном виде, используйте метод Ext.Array.clone(), но этот метод не поддерживает глубокого клонирования, как jQuery.extend.

Проблема #1

У метода loadData есть второй параметр append (append True to add the records to the existing records in the store, false to remove the old ones first). Т.е. от сервера приходят новые посты, мы их должны добавить к уже имеющимся. Для этого выставляем второй параметр true.

В нашем случае хранилище использует фильтрацию по загрузке (filterOnLoad = true)

Как работает фильтрация:

Store хранит все записи в двух коллекциях, основной – data, и в скрытой – snapshot. Ключ “filterOnLoad: true” в Store заставляет хранилище при загрузке данных любым способом проверять их на фильтрах и отбрасывать отфильтрованные в свое внутреннее хранилище snapshot.

Использование фильтров в Store и метод loadData несовместимы.

Листинг из файла ext-all-debug.js (Метод loadRecords используется loadData… и не только)

loadRecords: function(records, options) {

if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records);

}
Вот здесь и кроется проблема, options.addRecords в нашем случае true, удаление snapshot не происходит, данные попадают только в основную коллекцию.
API.Posts.get().done(function(posts){    
myStore.loadData(posts); //posts.length 8, filtered 2 entries
});

API.Posts.get().done(function(posts){
myStore.loadData(posts, true); //posts.length 10, filtered all entries
});

В этоге при первой загрузке прошли только 2 записи, а во втором все.

me.data содержит 2 + 10 записей, а me.snapshot только 8, т.к. данные из второго вызова не попали в скрытое хранилище.

Для того, чтобы пересортировать нашу коллекцию, нам нужно очистить старые фильтры и применить новые, нельзя просто “перечитать” фильтры (что очень и очень странно).

Метод mystore.clearFilter() убивает 10 последних записей, в итоге в хранилище у нас 8 записей.

Решение

loadRecords: function(records, options) {

if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records);
//add records in snapshot too.
if (options.addRecords && me.snapshot) {
me.snapshot.addAll(records)
}


}

Проблема #2

Ключ idProperty не используется по назначению.

Поле idProperty это аналог первичного ключа в БД. В нашем случае данные имеют уникальный ключ ID, по которому свежие посты должны добавляться в хранилище. В случае изменения какого-то поста, например добавления к нему комментария мы должны обновить эту запись в хранилище. Метод loadData в режиме append с выставленным ключом idProperty = "id" должен решить эту проблему. Но на самом деле этот ключ не используется для этого, и мы не можем сделать объединение данных автоматом.

Покопавшись в исходникам я обнаружил что такой функционал присутствует, но попросту не используется. Метод me.data.addAll(records) может принимать не только массивы, но и объекты ключ-значение. Нужно лишь слегка модифицировать тот же метод loadRecords чтобы использовать эту возможность.

Решение

loadRecords: function(records, options) {
… if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records); if (this.idProperty) { var indexRecord = {}; for (i = 0; i < length; i++) { indexRecord[records[i].data[this.idProperty]] = records[i]; } me.data.addAll(indexRecord); if (options.addRecords && me.snapshot) { me.snapshot.addAll(indexRecord); } records = []; for (key in indexRecord) { records.push(indexRecord[key]); } length = records.length; } else { me.data.addAll(records); if (options.addRecords && me.snapshot) { me.snapshot.addAll(records); } }
… }

После этим изменений в качестве ключа мы имеем значения id, и можем добавлять новые данные, они не будут дублироваться…

P.S. Не забываем, что лезть в исходники со своими правками плохо, используйте Ext.override. Таким образом вы избавляетесь от проблемы при обновлении версии фреймворка, вы просто пишите свой патч файл, и при исправлении в новой версии старой проблемы – вы удаляете свое решение из патча.

После этим изменений в качестве ключа мы имеем значения id, и можем добавлять новые данные, они не будут дублироваться…

P.S. Не забываем, что лезть в исходники со своими правками плохо, используйте Ext.override. Таким образом вы избавляетесь от проблемы при обновлении версии фреймворка, вы просто пишите свой патч файл, и при исправлении в новой версии старой проблемы – вы удаляете свое решение из патча.

На горизонте рассмотрение проблемы синхронизации Store с его представлением, и что делать если представлений несколько.

Wednesday, September 21, 2011

Новая версия плагина Observable 1.4

sticker

Скачать плагин можно здесь https://github.com/freeart/observable

Что нового/интересного?

Для начала краткая историческая справка, плагин создавался для упрощения привязки функций-обработчиков к представлению.

Первая версия плагина работа весьма просто.

Есть такая dom-структура

<div role="document" class="container">

<span class="run" data-fn="run" data-event="click">Run it!</span>
<span class="stop" data-fn="stop" data-event="click">Stop!</span>

</div>
… и бизнес-объект
var testWorker = { 
run: function(e){
//..run code
},
stop: function(e){
//..stop code
}
}

Плагин Observable ищет в ветке [role="document"] все декларации событий в виде data-параметров. data-fn = “имя функции-обработчика в указанном объекте testWorker” и data-event = “имя события – в данном примере click” – обязательные параметры. Также существуют необязательные параметры data-pref, data-scope, data-selector, data-type, но о них позже.

Это примерно эквивалентно

$('[role="document"]').delegate(".run", "click", function(e){
    //..run code
});
$('[role="document"]').delegate(".stop", "click", function(e){
//..stop code
});

Зачем же нужен этот плагин, если и так все работает замечательно!?

  • Заставляет использовать бизнес-объекты с публичными методами, которые хорошо организуют код.
    Например методы run/stop объекта testWorker создают более понятный код, чем в случае с анонимными обработчиками.
  • Отделяет логику от представления: дизайнер рисует, верстальщик верстает, программист привязывает обработчики к UI и описывает логику приложения.
    Посмотрите в сторону Objective-C, Flash, WPF.
  • Для привязки функций-обработчиков не используются атрибуты id или class – дизайн никак не пересекается с логикой приложения.
    Например имя класса .run вестальщик может изменить на что-то более ему понятное/удобное = .button, а программист получает нерабочий код. А декларация в виде data-атрибутов никому не мешает.
  • Упрощает код, на небольшом количестве обработчиков этого конечное не понять, а вот когда количество обработчиков превышает 20-30 шт. - код превращается в неуправляемую массу.
  • Не анонимные методы в качестве обработчиков заставляют писать заведомо правильный и масштабируемый код. Бизнес-объект testWorker имеет метод run, и может быть вызван не только по нажатие на кнопку “run it!”, но и по другому условию, например при загрузке страницы.
    $.ready(function(){
        testWorker.run()
    });
  • Функции-обработчики приходится “группировать” – получая в итоге привязку одной бизнес-единицы к представлению.
    $('[role="document"]').observable(testWorker);
    С точки зрения семантики эта строчка означает привязку бизнес-объекта testWorker к представлению document.

Начиная с версии Observable 1.3 стали поддерживаться конструкции маппинга функций-обработчиков вторым параметром. Он используется в случае когда у вас dom-структура еще не загружена.

$('[role="document"]').observable(testWorker, {
"data-pref": "run,stop",
"data-fn-run": "run",
"data-event-run": "click",
"data-fn-stop": "run",
"data-event-stop": "click"


});

Вот и дошли до этого атрибута data-pref. Т.к. data-параметры не поддерживают массивы, то для объединения data-fn с data-event используется такой синтаксис:

"data-pref": "run", 
"data-fn-run": "run",
"data-event-run": "click"

Синтаксис полностью совместимый с dom версией, но несколько неуклюж и громоздок.

Поэтому версия Observable 1.4 получила поддержку сокращенного синтаксиса:

$('[role="document"]').observable(testWorker, [
{
fn: "run",
event: "click"
},
{
fn: "stop",
event: "click"
}
]);

В случае если вы по каким-то соображениям не хотите в верстке использовать data-параметры, вы можете использовать поле selector, и по старинке привязать обработчик по селектору.

$('[role="document"]').observable(testWorker, [
{
fn: "run", event: "click",
selector: ".run" //эквивалентен $("[role="document"]").delegate(".run", "click", testWorker.run) }, {
fn: "stop", event: "click",
selector: ".stop" //эквивалентен $("[role="document"]").delegate(".stop", "click", testWorker.stop) } ]);

Остались два параметра data-scope и data-type. По умолчанию привязка обработчиков происходит через delegate, при желании вы можете указать bind. Но нужно понимать что bind можно привязать только если этот элемент присутствует в dom-дереве на момент.

Все функции-обработчики выполняются как обычные обработчики dom элементов в контексте dom-элемента, т.е. this == элемент, который сгенерировал событие, в наших примерах это span dom элементы .run и .stop . Если вы хотите использовать свой контекст, например того бизнес объекта, который вы передаете в метод observable, т.е. testWorker, в этом случае указываем data-scope = "this", или любой другой в рамках testWorker, т.к. ссылку на имя контекста scope ищется от нашего ROOT объекта testWorker.

$('[role="document"]').observable(testWorker, [
{    
fn: "run", event: "click",
scope: "this"
}, {
fn: "stop", event: "click",
scope: "this" } ]);

Методы run и stop в этом случае вызывается таким образом:

fn.call(testWorker, event, thisElement, testWorker);

Что почти эквивалентно такому приему:

$('[role="document"]').gelegate("click", ".run", function(e){
    //..run code
}.bind(testWorker));
var testWorker = {
run: function(e, element, obj){
//..run code
// для справки: this === element, obj === testWorker
console.log(this);
},
stop: function(e, element, obj){
//..stop code
console.log(this);
}
}
$('[role="document"]').observable(testWorker);

Sunday, July 17, 2011

Ленивая инициализация событий на jQuery

Rich приложения, подобные google+, gmail, facebook имеют множество динамически создаваемых элементов управления. Следить за привязкой событий при каждом переходе между интерфейсами может превратиться в ад. Выделять менеджер собитий в отдельный модуль тоже не целесообразно – придется поддерживать список событий в актуальности и со временем этот менеджер стан просто помойкой событий.

<div data-fn="test">Test click</div>

Предлагаю простой вариант ленивой призявки событий к шаблону/интерфейсу одной строчкой:

$.observable({
    test: function(){
        alert($(this).text()); //Test click
    }
});

data аттрибут используется для описания привязываемого события. data-fn – имя метода в объекте events, data-event – тип события, data-type – вариант привязки события, через bind или delegate.

var MainInterface = function() {
    this.dom = $('<div></div>');

    this.dom.append(
        '<div data-fn="say" data-event="click" data-type="delegate" data-word="hello">say hello</div>'
    );
    this.dom.append(
        '<div data-fn="say" data-event="click" data-type="delegate" data-word="world">say world</div>
    ');

    this.dom.observable(this);

    this.say = function () {
        alert($(this).data('word'));
    }
}

var mainInterface = new MainInterface();
mainInterface.dom.appendTo($(document.body));

Какие плюсы мы получаем: Программист пишет чистый код отдельно от верстки, не заботясь об именах классов элементов. Верстка и логика не пересекаются, меняя как угодно дизайн, названия классов, название id элементов мы даем волю дизайнерам и не выясняем почему очередная кнопка отвалилась после изменения дизайна. В качестве шаблонизаторов можно использовать любой движок, любите haml? пожалуйста - добавте data аттрибуты и все события привяжутся автоматом после рендера.

Скачать плагин можно здесь https://github.com/freeart/observable

Sunday, June 12, 2011

Javascript. Модульное проектирование RIA проектов.

“Работая над проблемой,
я никогда не думаю о красоте.
Я думаю только о решении проблемы.
Но если полученное решение некрасиво,
я знаю, что оно неверно.”
Richard Buckminster Fuller

Проблемы при разработке больших проектов

ninja2

Отделение котлет данных от представления, проектирование, велосипеды… главное без фанатизма. Какая проблема часто встречается при разработке RIA приложений. Команда сильных программистов «влюбляется» в новый проект, и с мыслями «сейчас мы сделаем нечто — главное все сделать самим, чтобы ни один чужой баг не закрался» начинают отказываться от готовых фреймворков, библиотек, решений.
Правда существует одно логичное объяснение этой тенденции, боязнь завязаться на чужой продукт, бывает такое, что в ходе разработки выясняется — такой интерфейс будет очень сложно разработать на доступном инструментарии, и рождаются костыли, правки чужих фреймворков и тд. Хотел бы предложить решение этой проблемы. Создание менеджера интерфейсов (модулей). Один модуль может использовать ExtJS, другой dhtmlx, а может и вообще чистый Javascript

Модульность как лекарство от многих болезней

Модульность – это принцип разделения сложной системы на отдельные части – модули. Какую выгоду мы получаем – упрощаем разработку, тестирование и поддержание системы, сводим число связей между различными частями системы к минимуму.

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

Не чужое – а открытое

Какие плюсы использования готовых решений? Проще найти нового человека на проект, когда вы используете известный сторонний продукт. В ситуации когда вы остались единственный из создателей фреймворка и решаетесь уйти, то скорее всего новая команда перепишет все с нуля. Плюс правка багов и добавление новых фич пока вы работаете над бизнес логикой. Хотите мощное оружие в разделении данных от представления – используйте ExtJS например, зачем писать свой фреймворк!? Нужно динамически подгружать зависимости – посмотрите в сторону YepNope. Работу с dom предоставьте jQuery или любому другому известному фреймворку. Сконцентрируйтесь на результате. Я понимаю что большинство программистов любит писать все с нуля – но это не от того что он хочет учится, развиваться… это от лени… да-да, именно от лени. Изучение чужого кода, чтение документации куда сложнее чем разработка всего “с нуля”. Когда пишешь все “с нуля” многому не научишься, люди учатся в основном от других, исследуя чужую работу перенимаешь опыт (это как съесть печень своего противника), а в совокупности со своим опытом придумываешь что-то новое… более совершенное. Создавая стабильные приложения за короткие сроки вы получите больше признания чем закапываясь в правках очередного своего мега-фреймворка после выхода нового браузера IE6, IE7, IE8, IE9.
Если у вас цель написать свой фреймворк, вместо разработки ERP на которую вам дали срок 2 месяца – то начните с ОС… или с архитектуры процессора… а почему нет!?

Ядро – всему голова

Ядро приложения – это менеджер частей системы. Топ менеджер комнании, который выполняет функции супервизора. Он контролирует доступ к ресурсам и общение между объектами компании. Но никак не контроль загрузки товара на склад, выдача провианта и тд. Этим пусть занимаются утилиты и библиотеки, которым будут делегироваться задания.

В обязанности ядра входят:

  • Делегация загрузки модулей и необходимых зависимостей отделу библиотеке (в нашем случае я выбрал YepNope).
  • Делегация создания канвы для интерфейса модуля.
  • Передача информации между модулями.
  • Оповещение о каких либо событиях системы (закрытие приложения, активация нового модуля, изменения размера окна, потеря коннекта с сервером и тд.).
  • Выполнение запросов модулей (например загрузка другого модуля)

Полный код проекта смотрите на GitHub

Упрощенный вариант ядра:

/*=== core.js ===*/
!function () {

    var listOfLibraries = {};
    
    $(window).resize(function() {
        $.each(listOfLibraries, function(){
            this.body.onResize($(window).width(), $(window).height());
        });
    });

    $("script[id=core]").bind('request', function (data) {
        switch (data.header) {
            case "attach":
                var library = {
                    name: data.body.name,
                    body: new data.body.module()
                };
                listOfLibraries[library.name] = library;
                $('<div id="'+library.name+'"></div>').appendTo('body');
                var moduleContext = $('#'+library.name);
                library.body.main(moduleContext);
                break;
            }
    });
}();

После своей загрузки, ядро слушает канал сообщений request, при подключении нового модуля он добавляет его в коллекцию listOfLibraries. В этой коллекции хранятся все загруженные модули.

Даешь независимость модулям!

Пример модуля:

/*=== HelloWorld.js ===*/
!function () { 

    var Lib = function () {
        var context;
        this.main = function (moduleContext) { 
            context = moduleContext;
            context.html('Hello World');
        }
        this.onResize = function(width, height){
            context.width(width);
            context.height(height);
        }
    }

    $("script[id=core]").trigger({  
        type: "request",                
        header: "attach",
        body: {
            module: Lib, 
            name: "HelloWorld"
        }
    });
} ();

Как видно из примера ядро общается с внешними компонентами по средствам событий, вернее одного события “request” – пула сообщений.
Тело сообщения состоит из заголовка (header) и тела (body). В данном случае модуль сообщает ядру что он хочет зарегистрироваться в системе под именен “HelloWorld” и сообщает ссылку на тело модуля.

Каждый модуль получает от ядра контекст с которым ему нужно работать. Общение с ядром, другими модулями или элементами страницы должно осуществляться через сообщения.

Зачем же нужно тогда ядро, если мы могли легко в модуле создать контекст и работать с ним!?

Отвечаю: для реализации многооконного интерфейса, для подгрузки модулей по требованию, для возможности загрузки зависимостей модулей, для создания единой системы общения между модулями, для просто поддержания кода… и тд.

В итоге мы получили легковесное модульное решение, при котором даже при полном изменении логики ядра – код модулей не меняется.

Все изменения касаются лишь добавления новых событий. Для унификации интерфейса для общения ядра с модулями лучше выделить абстрактный класс с базовой реализацией рутины и все Lib классы модулей наследовать от него.

window.Module = function () { 
    this.context;  
}
window.Module.prototype = {
    main: function (context) {
        this.context = context;
    },
    onResize: function (width, height) {

    }
}

При подключении модуля к ядру нужно лишь убедиться, что подключаемый модуль наследует функционал абстрактного класса Module, в случае успеха подключать его к системе.

Полный код проекта смотрите на GitHub

Friday, November 5, 2010

Рецепт быстрого приготовления расширений под популярные браузеры.

0147 (1)

Возникла задача написание простенького расширения под все (по возможности) популярные браузеры. Деятельность расширения заключается во внедрении javascript`а в тело документа.

Доктор прописал инъекции javascript`а

Казалось бы все просто, выполняем в строке браузера код:

javascript:var s = document.createElement('script');
s.type='text/javascript';
document.body.appendChild(s);
s.src='http://our_site/script.js';
void(0);
Но что делать если ваш скрипт должен обрабатывать все страницы чужого сайта. Не заставлять же пользователя постоянно click`ать по закладке с кодом инъекции после каждого перехода на новую страницу. Наивные поиски простого решения не увенчались успехом:
  • Это был перехват всех ссылок на страничке и добавление своего кода “javascript:”, разумеется код выполнялся в рамках открытой страницы и потом переходил на новую “чистую” страницу через (location.href).
  • Вариант с setTimeout был вообще в порядке бреда… выполение отваливалось после начала загрузки новой страницы.
Написание расширений под множество браузеров меня пугала больше всего, но что делать, начал вспоминать наиболее популярные:
  • IE 7+ (на 6-ку решил просто забить, много затрат ради небольшой аудитории пользователей – уж простите меня)
  • Firefox 1.5+
  • Chrome 4+
  • Opera 9+ (в итоге оказалось extension`ы можно писать только под Opera 11)
  • Safari 3+

“С Firefox и Chrome проблем не должно возникнуть” – подумал я.

Google Chrome


За час был написал extension для Chrome – масса документации на официальном сайте, масса примеров. В общем рай для разработчика.

Примеры и Руководство

фрагмент из background.html

function onRequest(tabId, changeInfo, tab) {
if (changeInfo.status == 'complete'){
chrome.tabs.executeScript(tabId, { code:"код без javascript:" });
};
};
chrome.tabs.onUpdated.addListener(onRequest);

Все получилось просто и со вкусом. Напрямую вставить в тело документа <script> не получилось, гугл ругался на чужой домен в адресе скрипта, разбираться было не когда, и executeScript сильно упростил реализацию.

Расширение собирается средствами Google Chrome:

image

”Как все здорово пошло” – подумал я. “Такими темпами я завтра для всех браузеров напишу”…

Mozilla Firefox


Хорошо обстоят дела с документацией, с примерами правда похуже чем у Chrome, но есть даже online builder addon`ов (делает пустышку zip, который необходимо “собрать” в xpi добавлением своих исходников).

Правда вначале смутил very basic extension на главной который не работал. Который через пару дней убрали со страницы слава богу.

Документация, Addon Builder

Синтаксис тоже вполне логичный и стандартный, фрагмент из chrome/content/js/main.js

var test = { 
onload: function(aEvent){
var doc = aEvent.originalTarget;
if (doc instanceof HTMLDocument && doc.location.href != "about:blank" && !doc.defaultView.frameElement) {
var s = doc.createElement('script');
s.type = 'text/javascript';
doc.body.appendChild(s);
s.src = 'http://our_site/script.js';
}
}
}
window.addEventListener("load", function(){
var appcontent = document.getElementById("appcontent"); if (appcontent) {
appcontent.addEventListener("DOMContentLoaded", test.onload, true);
}
}, false);

Safari


Upd: Спасибо Tails за информацию.

Документация и примеры
Подробная пошаговая инструкция

Главное нужно получить Сертефикат разработчика Safari, для этого нужно получить AppleID и вступить в группу разработчиков на apple.com

фрагмент из inject.js

var s = document.createElement('script'); 
s.type = 'text/javascript'; 
document.body.appendChild(s); 
s.src = 'script.js';

Microsoft Internet Explorer


Вот тут конечное я дня 2 убил на изучение спецификации по COM объектам и поиска рабочих примеров addon`ов. В итоге нашел на одном китайском сайте рабочий пример с привязкой к событиям браузера.

Привязываемся к событию DISPID_DOCUMENTCOMPLETE, и в случае возникновения выполняем код:

VARIANT vFlags = { 0 }; 
VariantInit(&vFlags);
vFlags.vt = VT_I4;
vFlags.intVal |= navNoReadFromCache;
BSTR testUrl = SysAllocString(L"javascript: код инъекции");
mWebBrowser2->Navigate(testUrl, &vFlags, NULL, NULL, NULL);

Но радость была бы не полной без такого замечательного бага. У IE проблема с обновлением по F5 и событием DocumentComplete.

Оно просто не вызывается при обновлении странички через F5

Для решения проблемы с F5, вешаем hook и по нажатию выполняем код:

CComBSTR url; 
mWebBrowser2->get_LocationURL(&url);
VARIANT v;
v.vt=VT_I4;
v.lVal= (navNoHistory);
mWebBrowser2->Navigate(url, &v, NULL, NULL, NULL);

Который вызовет DISPID_DOCUMENTCOMPLETE “искусственным” путем.

Opera


Тут оказалось все очень просто и красиво, правда есть баги в работе addon`ов, но Opera 11 все таки еще alpha.

Статьи с примерами

фрагмент из includes/base.js

window.addEventListener('load', function(event) { 
var currentDocument = (window.document) ? window.document : false;
opera.extension.onmessage = function(event) {
if (currentDocument) {
var s = currentDocument.createElement('script');
s.type='text/javascript';
currentDocument.body.appendChild(s);
s.src='http://our_site/script.js';
}
};
}, false);

фрагмент из background.js

window.addEventListener("load", function() {
function myEvent() {
var tab = opera.extension.tabs.getFocused();
if (tab) {
tab.postMessage('go');
}
}
opera.extension.onconnect = myEvent;
}, false);

Из обнаруженных мною проблем:

  • Из javascript`а делаем back history и событие onconnect не срабатывает.
  • Из функции в onmessage нельзя обратиться к window.document, может оно конечное так и задумано, но не логично.

На мой взгляд ребята из opera хорошо постарались над api.

Под Opera младше 11 версии возможна установка UserJS через свой installer (админских прав не нужно), установка происходит через файл opera.ini который расположен в профиле пользователя, раздел [User Prefs] > «User Javascript File»

Примеры скриптов И еще

Фрагмент из userjs.js

addEventListener('load', function(e){ 
	if (!document.body) { 
		return; 
	} 
	var s = document.createElement('script'); 
	s.type = 'text/javascript'; 
	document.body.appendChild(s); 
	s.src = 'userjs.js'; 
}, false);

Wednesday, September 1, 2010

jQuery плагин визуального стека сообщений

Стояла задача динамического визуального отображения реакции на действия пользователя со стороны системы.
Другими словами: изменилось какое-то состояние объекта - вывести сообщения об удачном завершении сохранения изменения в базе/системе.

Untitled-1

Пример использования, который показывает функционал плагина:

var msgObj = $.notify.add("Начало сохранения", 3); 
//Выводим сообщение о начале действий (продолжительностью 3 сек) и сохраняем ссылку на сообщение в msgObj
$.get('/test.php', function (result) {
if (result.ok) { //если все выполнилось удачно
$.notify.remove(msgObj, 0.5);
//удаляем предыдущее сообщение через 0,5 сек, вдруг все сохранилось быстрее чем мы думали
$.notify.add("Сохранение выполнено успешно", 1);
//и добавляем новое сообщение в стек
} else { //если произошла ошибка
$.notify.remove(msgObj, 0.5);
//удаляем предыдущее сообщение через 0,5 сек, вдруг все сломалось быстрее чем мы ожидали
$.notify.add("Ошибка сохранения", "error", 3);
//выводим сообщение "необычного" оформления на 3 сек
}
});

Скачать jquery.notify.js