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

от автора

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


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

ссылка на оригинал статьи http://habrahabr.ru/post/169909/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *