В этой статье я расскажу как создать с нуля виджет для сайта (на примере виджета опросов). Т.к. основной темой статьи все-таки является создание виджета, то создание самого опроса будет рассмотрено поверхностно.
Шаг 1. Создание таблиц в БД
В качестве базы данных будем использовать MySQL.
Создадим несколько таблиц:
polls — таблица с опросами
CREATE TABLE `polls`
(
`poll_id` INT(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
`question` TEXT
) ENGINE = MYISAM AUTO_INCREMENT = 1 DEFAULT CHARACTER SET UTF8 COLLATE UTF8_GENERAL_CI;
answers — таблица с ответами для опросов
CREATE TABLE `answers`
(
`answer_id` INT(11) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
`answer` TEXT,
`num` INT(11) NOT NULL DEFAULT '0', // порядковый номер ответа
`poll_id_fk` INT NOT NULL DEFAULT '0',
FOREIGN KEY (`poll_id_fk`) REFERENCES polls(`poll_id`)
) ENGINE = MYISAM AUTO_INCREMENT = 1 DEFAULT CHARACTER SET UTF8 COLLATE UTF8_GENERAL_CI;
Шаг 2. Создание страницы с опросом
Будем отталкиваться от предпосылки, что наш виджет должен быть произвольной ширины (в рамках разумного), чтобы его можно было встраивать в любые сайты, например, в центре текста по ширине статьи или в боковую часть сайта (обычно узкую). Поэтому при создании страницы с опросом старайтесь проектировать «резиновый» дизайн.
В данном примере ограничимся самым простым выводом опроса.
<?php
// получаем через параметр номер опроса
$poll_id = (int) (!isset($_GET["id"]) ? -1 : 0 + $_GET["id"]);
// если что-то не то, выходим
if ($poll_id <= 0)
exit;
$sql = mysql_query("SELECT * FROM polls WHERE poll_id = $poll_id");
// если ничего не выбралось, выходим
if (!($sql && ($row = mysql_fetch_array($sql))))
exit;
// сохраним вопрос в переменную для дальнейшего использования
$question = $row['question'];
// IE блокирует чужие cookie (через iframe), добавляйте эту строчку, если будете их писать
header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
<?php
// если передан параметр цвет фона, то задействуем его
if (isset($_GET['bg_color'])) {
$bg_color = $_GET['bg_color'];
echo "html,body { background: #$bg_color; }";
}
?>
</style>
</head>
<body onresize="resize_canvas()">
<?php
// выводим опрос с ответами
echo "<h1>$question</h1>";
$sql = mysql_query("SELECT * FROM answers WHERE poll_id_fk = $poll_id ORDER BY num ASC");
if ($sql) {
while ($row = mysql_fetch_array($sql)) {
$answer = $row['answer'];
echo "<p>$answer</p>";
}
}
?>
<script type="text/javascript">
// этот скрипт сообщает родительскому окну высоту своего содержимого
// с помощью механизма кросс-доменного обмена
var parent_url = decodeURIComponent(document.location.hash.replace(/^#/, ''));
function send(msg) {
XD.postMessage(msg, parent_url, parent);
}
function resize_canvas() {
send($('body').height());
}
</script>
</body>
</html>
Т.к. пример у нас обучающий, то мы не делаем процедуру голосования, вывод результатов и т.д.
В тексте скрипта, как может показаться, много всего сходу непонятного, но все вопросы снимутся по прочтению следующих шагов. Отметим здесь только обработчик onresize, который посылает при изменении размера страницы высоту в родительское окно.
Шаг 3. Создание скрипта для встраивания виджета на сайт
Сегодня стандартом де-факто для большинства виджетов является механизм встраивания через iframe.
Для этого есть ряд причин:
1. Владельцы сайтов, которые решили встроить ваш виджет не хотят, чтобы чужой код получал доступ к его содержимому (это как минимум небезопасно).
2. Встроить ваш виджет через iframe очень легко и не требует дополнительных навыков и знаний.
3. Из-за возможных ошибок в вашем коде может слететь верстка сайта, встраивающего ваш виджет.
4. Если вы храните в cookies какую-то информацию, например, идентификатор проголосовавшего, то при встраивании виджета через iframe данные будут храниться в привязке к URL сайта виджета (а не сайта, в который он встроен), что может быть полезно, например, если тот же самый опрос висит и на другом сайте. Человек уже проголосовавший один раз будет и на другом сайте видеть свой ответ, а не голосовать каждый раз заново.
и т.д.
Создадим функцию, которая будет внедрять на странице сайта наш виджет. Обратите внимание, в код зашиты два идентификатора (для простоты чтения, их можно вынести как параметры). Это widget_container — контейнер, внутри которого будет создан фрейм, и сам фрейм widget_iframe.
function createWidget(config) {
var Util = {
extendObject: function(a, b) {
for(prop in b){
a[prop] = b[prop];
}
return a;
},
proto: 'https:' == document.location.protocol ? 'https://' : 'http://'
}
var options = Util.extendObject({
id: 0,
domain: "example.com",
bg_color: "FFFFFF"
}, config);
options.widget_url = [Util.proto, options.domain, "/?", "id=", options.id, "&bg_color=", options.bg_color].join("");
options.widget_url += "#" + encodeURIComponent(document.location.href);
Widget = {
created: false,
widgetElement: null,
show: function() {
if (this.created)
return;
this.widgetElement = document.createElement('div');
this.widgetElement.setAttribute('id', 'widget_container');
this.widgetElement.innerHTML = '
<iframe id="widget_iframe" src="' + options.widget_url + '" scrolling="no" width="100%" height="0" frameborder="0"></iframe>';
document.body.insertBefore(this.widgetElement, document.body.nextSibling);
this.widgetElement.style.display = 'block';
this.created = true;
}
}
XD.receiveMessage(function(message) {
if (message.data > 0 && document.getElementById("widget_iframe"))
{
document.getElementById("widget_iframe").height = message.data;
}
}, Util.proto + options.domain);
Widget.show();
}
createWidget(widgetOptions);
Внутри функции описывается несколько объектов:
Util — помогает работать с параметрами.
Widget — сам объект нашего встраиваемого виджета.
XD — объект кросс-доменного обмена сообщениями (см. шаг 4, п. 2).
Как видно из кода, сначала создается элемент DIV, потом внутри него создается IFRAME, в который загружается страница с заданными в скрипте параметрами (по умолчанию это example.com/?id=0&bg_color=FFFFFF). Также устанавливается обработчик событий (получение сообщений от фрейма), который меняет высоту элемента, в данном случае растягивает виджет по высоте его содержимого.
Шаг 4. Решение проблем отображения
При встраивании виджета через iframe возникают определенные трудности.
1. Т.к. по сути в виджете загружен отдельный html-документ, то хочется как-то в него передавать свои настройки. Актуальным вопросом здесь являются стили, чтобы содержимое виджета вписывалось в дизайн сайта-потребителя. Можно реализовать передачу параметров через глобальную переменную, в нашем примере — это widgetOptions.
var widgetOptions = {
id: 1, // id опроса
bg_color: 'FF0000' // цвет фона
};
После того, как переменная объявлена, загружаем наш скрипт, создающий виджет:
(function() {
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = "http://example.com/widget.js"; // путь скрипта из шага 3
document.getElementsByTagName('head')[0].appendChild(script);
})();
2. Есть проблема растягивания iframe по высоте его содержимого, чтобы не было никаких полос прокруток, а все смотрелось гармонично.
Для этого воспользуемся небольшим готовым скриптом:
/*
* a backwards compatable implementation of postMessage
* by Josh Fraser (joshfraser.com)
* released under the Apache 2.0 license.
*
* this code was adapted from Ben Alman's jQuery postMessage code found at:
* http://benalman.com/projects/jquery-postmessage-plugin/
*
* other inspiration was taken from Luke Shepard's code for Facebook Connect:
* http://github.com/facebook/connect-js/blob/master/src/core/xd.js
*
* the goal of this project was to make a backwards compatable version of postMessage
* without having any dependency on jQuery or the FB Connect libraries
*
* my goal was to keep this as terse as possible since my own purpose was to use this
* as part of a distributed widget where filesize could be sensative.
*
*/
// everything is wrapped in the XD function to reduce namespace collisions
var XD = function(){
var interval_id,
last_hash,
cache_bust = 1,
attached_callback,
window = this;
return {
postMessage : function(message, target_url, target) {
if (!target_url) {
return;
}
target = target || parent; // default to parent
if (window['postMessage']) {
// the browser supports window.postMessage, so call it with a targetOrigin
// set appropriately, based on the target_url parameter.
target['postMessage'](message, target_url.replace( /([^:]+://[^/]+).*/, '$1'));
} else if (target_url) {
// the browser does not support window.postMessage, so set the location
// of the target to target_url#message. A bit ugly, but it works! A cache
// bust parameter is added to ensure that repeat messages trigger the callback.
target.location = target_url.replace(/#.*$/, '') + '#' + (+new Date) + (cache_bust++) + '&' + message;
}
},
receiveMessage : function(callback, source_origin) {
// browser supports window.postMessage
if (window['postMessage']) {
// bind the callback to the actual event associated with window.postMessage
if (callback) {
attached_callback = function(e) {
if ((typeof source_origin === 'string' && e.origin !== source_origin)
|| (Object.prototype.toString.call(source_origin) === "[object Function]" && source_origin(e.origin) === !1)) {
return !1;
}
callback(e);
};
}
if (window['addEventListener']) {
window[callback ? 'addEventListener' : 'removeEventListener']('message', attached_callback, !1);
} else {
window[callback ? 'attachEvent' : 'detachEvent']('onmessage', attached_callback);
}
} else {
// a polling loop is started & callback is called whenever the location.hash changes
interval_id && clearInterval(interval_id);
interval_id = null;
if (callback) {
interval_id = setInterval(function(){
var hash = document.location.hash,
re = /^#?d+&/;
if (hash !== last_hash && re.test(hash)) {
last_hash = hash;
callback({data: hash.replace(re, '')});
}
}, 100);
}
}
}
};
}();
Из фрейма с нашим виджетом будем посылать его высоту (при инициализации и при изменении размера окна) в родительское окно. Скрипт на странице с виджетом будет получать это значение и растягивать окно с виджетом по высоте его содержимого.
<body onresize="resize_canvas()">
<script type="text/javascript">
var parent_url = decodeURIComponent(document.location.hash.replace(/^#/, ''));
function send(msg) {
XD.postMessage(msg, parent_url, parent);
}
function resize_canvas() {
send($('body').height());
}
</script>
</body>
Шаг 5. Оптимизация и релиз
После того, как наш виджет сделан, хорошим тоном считается минимизировать javascript код, чтобы не было сильного увеличения времени загрузки сайта-потребителя, а также для уменьшение трафика самого сайта виджета.
Подробнее об этом можно почитать в статье на Хабре Оптимизация Javascript с помощью Google Closure Compiler.
Если у вас полезный виджет, которым пользуются многие сайты, рекомендую поддержать https протокол, чтобы пользователям, выбравшим защищенное соединение, не отображалось сообщение о содержании небезопасного контента.
В скрипте для вставки вашего виджета строка пути теперь будет выглядеть следующим образом:
script.src = (document.location.protocol == "https:" ? "https:" : "http:") + "//example.com/widget.js";
P.S.
Исходников данного примера нет, поэтому не могу их выложить. Если кто-то на основе этой статьи соберет готовый проект и захочет им поделиться — wellcome!
Автор: StanSemenoff