Пошаговое создание виджета для сайта

в 15:56, , рубрики: javascript, php, Веб-разработка, виджеты, опросы, метки: , , ,

В этой статье я расскажу как создать с нуля виджет для сайта (на примере виджета опросов). Т.к. основной темой статьи все-таки является создание виджета, то создание самого опроса будет рассмотрено поверхностно.

Пошаговое создание виджета для сайта

Шаг 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

Источник

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


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