Показаны сообщения с ярлыком SEO. Показать все сообщения
Показаны сообщения с ярлыком SEO. Показать все сообщения

четверг, 30 декабря 2010 г.

JavaScript combining с помощью ScriptManager контрола

Почти любое современное web-приложение использует различные javascript-библиотеки для манипулирования данными на клиентской стороне. Будь то пользовательский файл, призванный помочь в управлении DOM-моделью на клиенте, будь то сторонняя библиотека (jQuery, Dojo, PrototypeJS и др.), будь то встроенные механизмы VisualStudio при генерации исходного html кода. Каждая библиотека – это отдельный файл на сервере, для загрузки которого браузер использует отдельный запрос. Если же на Вашей странице есть ссылки на несколько .js файлов, то загрузка по отдельности каждого файла крайне неэффективно скажется в целом на производительности web-приложения, особенно если построение DOM-модели вынуждено прервано до момента окончательной подгрузки файлов. Для пользователя данная страница будет выглядеть как частично загруженная и в подвисшем состоянии. Уж совсем некрасиво.

Допустим в нашем ASP.NET приложении мы имеем 3 различных javascript файла: jscript1.js, jscript2.js, jscript3.js, в которых нам необходимы 3 функции:

function Execute1() {
alert("Execute 1");
}

function Execute2() {
alert("Execute 2");
}

function Execute3() {
alert("Execute 3");
}


В ASP.NET для загрузки сторонних скриптов можно использовать привычный нам ScriptManager контрол.


<asp:ScriptManager runat="server">
<Scripts>
<asp:ScriptReference Path="~/Scripts/JScript1.js" />
<asp:ScriptReference Path="~/Scripts/JScript2.js" />
<asp:ScriptReference Path="~/Scripts/JScript3.js" />
</Scripts>
</asp:ScriptManager>


Хорошо. Загрузим данную страницу и проанализируем ответ сервера на запрос к нашей странице с помощью Developer Tools в IE.


image


Как видно, при запросе к серверу для получения практически ничего не делающей .aspx страницы в ответ мы получаем 7! файлов: сама .aspx страница (отмечена зеленым), 3 сгенерированные студией при компиляции проекта библиотеки и 3 пользовательских .js файла.


Если же проверить степень оптимизации нашей страницы для поисковиков с помощью утилиты YSlow (разширение для FireFox), то увидим что даже пустая страница  без контента не на 100% оптимизирована. Основная проблема – много HTTP-запросов для построения контента.


image




Мощность ScriptManager контрола в том, что множество сложных задач он позволяет делать в несколько строчек кода. Если раньше для объединения множества .js файлов необходимо было писать свои custom решения либо использовать сторонние библиотеки, такие как MbCompression, использовать сторонние модули, такие как ScriptCompressionModule и т.д., то сейчас достаточно внести необходимые для combine-инга скрипты в блок <CompositeScript> контрола:


<asp:ScriptManager runat="server">
<CompositeScript>
<Scripts>
<asp:ScriptReference Path="~/Scripts/JScript1.js" />
<asp:ScriptReference Path="~/Scripts/JScript2.js" />
<asp:ScriptReference Path="~/Scripts/JScript3.js" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>


Проверим ответы сервера и качество оптимизации


image


image


Как видно количество закачанных файлов уменьшилось до 5 (3 файла были объеденены в один) и общая оценка оптимизации сменилась с B на A. А общий коэфициент оптимизации вырос с 91 до 95. Тем самым, с помощью буквально нескольких строчек мы решили довольно сложную задачу, возникающую уже в стадии SEO-оптимизации проекта – объединение множества скриптов в один.

пятница, 27 августа 2010 г.

Создание GridView-контрола в WebForms и MVC с SEO-friendly пейжингом

SEO-friendly пейжинг в ASP.NET webForms

В предыдущем посте я описывал разные способы создания аналога контрола <asp:GridView /> средствами ASP.NET MVC. В итоге полученные результаты хоть и были более трудоемкими в создании кода, но обладали некоторыми свойствами, недоступными привычному WebForms контролу, такими как, например: загрузка данных через AJAX-запрос, более удобное назначение стилей элементов таблицы, контроль над генерируемым кодом.

Конечно, рассмотрев несколько удачных примеров замены серверного контрола в среде WebForms на аналогичные (и даже более богатые возможностями) контролы в среде MVC, мы с ухмылкой посматриваем на старый проверенный элемент разметки страницы <asp:GridView runat=”server”… />, подбирая в уме замену более подходящим вариантом. Но не стоит спешить выделять блок страницы и жать кнопку <del> – смогут ли решить следующую задачу найденные нами jQuery-контролы, с которой старый добрый GridView опять справляется на 5 баллов.

Итак задача. Создать таблицу с пейжингом для отображения новостей сайта. Добавить возможность перехода по пейжингу поисковым роботам, чтобы предоставить доступ к старым записям для более полной индексации ресурса.

Вроде бы ничего нового. Но… Что значит - Добавить возможность перехода по пейжингу поисковым роботам?

Посещая ресурс, поисковый робот загружает только тот HTML-код, который был возвращен на запрос к сайту. Если мы используем jQuery-плагин, который загружает содержимое таблицы после загрузки основной страницы дополнительным AJAX-запросом через JavaScript-функцию, то поисковый робот получит HTML-код с пустой таблицей.

image

Если же мы используем плагин, который осуществляет переход по страницам таблицы посредством вызова JavaScript-функции – поисковый робот опять-таки далее загрузки основной таблицы не пойдет.

Почему? Ответ простой – поисковые роботы не выполняют .js-код при загрузке содержимого страницы.

Но тут же и на время стопорится работа и при использовании привычного WebForms контрола <asp:GridView  />, так как для перехода по страницам пейжинга он использует postback-механизм (нажатие кнопки для осуществления перехода на следующую страницу), что нарушает еще одно правило поведения робота на странице: поисковый робот на нежимает на кнопки.

image

Попробуйте загрузить любую страницу интернета с помощью Lynx-браузера и увидите ее так, как видит ее поисковый бот. Вот пример страницы Торговой площадки Ydobno, отображенной с помощью lynx-браузера.

image

Как видим – никаких стилей, никаких скриптов. Чистый текст. Отдльно от всего текста браузер выделяет список ссылок, по которым поисковой робот в дальнейшем может переходить.

image

Тем самым, только открытая ссылка на следующую страницу даст возможнсть поисковому боту ходить постранично. Все что требует запуска .js-функции либо нажатию непостредственно кнопки будет проигнорировано. Нам необходимо иметь элементы пейжинга, непосредственно ссылающиеся на страницы. Примерно следующего вида:

image

Хорошо, проблема ясна. Теперь можно искать ее решение. Для начала конечно же эксперементируем с нашим серверным контролом. Как упоминалось в самом начале об этом контроле – что нам снег, что нам зной, что нам пейжинг сотворить. Но, конечно, не без дополнительных манипуляций.

В паре с серверным контролом используем еще один контрол - <asp:DataPager runat=”server” />, который как раз и позволяет нам генерировать пейжинг в одном из следующих форматов: числовое отображение количества страниц (1-10, 11-20 и т.д.), переход вперед-назад, custom template пейжинг. При этом мы можем объединять варианты, получая числовые ссылки и ссылки вперед-назад по краям.

Итак, добавляем на страницу следующий контрол:

<asp:DataPager ID="DataPager1" runat="server" QueryStringField="page" PagedControlID="gvEmployee">
<Fields>
<asp:NextPreviousPagerField FirstPageText="|&lt;&lt;" ShowFirstPageButton="False" ShowNextPageButton="False" ShowPreviousPageButton="True" PreviousPageText="&lt;&lt;" />
<asp:NumericPagerField ButtonCount="10" CurrentPageLabelCssClass="dataPager-current" />
<asp:NextPreviousPagerField LastPageText="&gt;&gt;|" ShowLastPageButton="False" ShowPreviousPageButton="False" ShowNextPageButton="True" NextPageText="&gt;&gt;" />
</Fields>
</asp:DataPager>



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


image


Серверный контрол <asp:GridView …/> не имплементирует интерфейс IPageableItemContainer. Для решения данной проблемы мы создаем новый класс DataPagerGridView, унаследованный от GridView, имплементирующий необходимый интерфейс:


/// <summary>
/// DataPagerGridView is a custom control that implements GrieView and IPageableItemContainer
/// </summary>
public class DataPagerGridView : GridView, IPageableItemContainer
{
public DataPagerGridView()
: base()
{
PagerSettings.Visible = false;
}

/// <summary>
/// TotalRowCountAvailable event key
/// </summary>
private static readonly object EventTotalRowCountAvailable = new object();

/// <summary>
/// Call base control's CreateChildControls method and determine the number of rows in the source
/// then fire off the event with the derived data and then we return the original result.
/// </summary>
/// <param name="dataSource"></param>
/// <param name="dataBinding"></param>
/// <returns></returns>
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int rows = base.CreateChildControls(dataSource, dataBinding);

// if the paging feature is enabled, determine the total number of rows in the datasource
if (this.AllowPaging)
{
// if we are databinding, use the number of rows that were created, otherwise cast the datasource to an Collection and use that as the count
int totalRowCount = dataBinding ? rows : ((ICollection)dataSource).Count;

// raise the row count available event
IPageableItemContainer pageableItemContainer = this as IPageableItemContainer;
this.OnTotalRowCountAvailable(new PageEventArgs(pageableItemContainer.StartRowIndex, pageableItemContainer.MaximumRows, totalRowCount));

// make sure the top and bottom pager rows are not visible
if (this.TopPagerRow != null)
this.TopPagerRow.Visible = false;

if (this.BottomPagerRow != null)
this.BottomPagerRow.Visible = false;
}
return rows;
}

/// <summary>
/// Set the control with appropriate parameters and bind to right chunk of data.
/// </summary>
/// <param name="startRowIndex"></param>
/// <param name="maximumRows"></param>
/// <param name="databind"></param>
void IPageableItemContainer.SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
int newPageIndex = (startRowIndex / maximumRows);
this.PageSize = maximumRows;

if (this.PageIndex != newPageIndex)
{
bool isCanceled = false;
if (databind)
{
// create the event arguments and raise the event
GridViewPageEventArgs args = new GridViewPageEventArgs(newPageIndex);
this.OnPageIndexChanging(args);

isCanceled = args.Cancel;
newPageIndex = args.NewPageIndex;
}

// if the event wasn't cancelled change the paging values
if (!isCanceled)
{
this.PageIndex = newPageIndex;

if (databind)
this.OnPageIndexChanged(EventArgs.Empty);
}
if (databind)
this.RequiresDataBinding = true;
}
}

/// <summary>
/// IPageableItemContainer's StartRowIndex = PageSize * PageIndex properties
/// </summary>
int IPageableItemContainer.StartRowIndex
{
get { return this.PageSize * this.PageIndex; }
}

/// <summary>
/// IPageableItemContainer's MaximumRows = PageSize property
/// </summary>
int IPageableItemContainer.MaximumRows
{
get { return this.PageSize; }
}

/// <summary>
///
/// </summary>
event EventHandler<PageEventArgs> IPageableItemContainer.TotalRowCountAvailable
{
add { base.Events.AddHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
remove { base.Events.RemoveHandler(DataPagerGridView.EventTotalRowCountAvailable, value); }
}

/// <summary>
///
/// </summary>
/// <param name="e"></param>
protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
{
EventHandler<PageEventArgs> handler = (EventHandler<PageEventArgs>)base.Events[DataPagerGridView.EventTotalRowCountAvailable];
if (handler != null)
{
handler(this, e);
}
}
}


Готово. Теперь можно добавлять custom-контрол на страницу, связывать с источником данных и с пейжинг-контролом:


<%@ Register TagPrefix="pager" Namespace="WebApplication2" Assembly="WebApplication2" %>
...
<pager:DataPagerGridView ID="gvEmployee" runat="server" DataSourceID="dsEmployee" AllowPaging="true" />
<asp:DataPager ID="DataPager1" runat="server" QueryStringField="page" PagedControlID="gvEmployee">
<Fields>
<asp:NextPreviousPagerField FirstPageText="|&lt;&lt;" ShowFirstPageButton="False" ShowNextPageButton="False" ShowPreviousPageButton="True" PreviousPageText="&lt;&lt;" />
<asp:NumericPagerField ButtonCount="10" CurrentPageLabelCssClass="dataPager-current" />
<asp:NextPreviousPagerField LastPageText="&gt;&gt;|" ShowLastPageButton="False" ShowPreviousPageButton="False" ShowNextPageButton="True" NextPageText="&gt;&gt;" />
</Fields>
</asp:DataPager>


Судя по декларации DataPager-контрола имеем ссылку на предыдущую страницу в начале и на следующую страницу в конце пейжинга, а также имеет ссылки на 10 текущих страниц (поскольку у нас мало данных, то показано всего 2 страницы).


Вуаля! Получаем следующий результат:


image


Отлично! Полученный результат вполне удовлетворяет нашим пожеланием относительно возможности максимально глубокого сканирования поисковым ботом содержимого сайта. И, опять таки, решение построение на базе мощного серверного контрола <asp:GridView /> при помощи <asp:DataPager />. Теперь, загружая данную страницу, поисковый бот получит и список ссылок на другие страницы пейжинга, что позволит ему последовательно посетить и просканировать все элементы данной таблицы.


 


SEO-friendly пейжинг в ASP.NET MVC


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


Поскольку конечным результатом нам необходимо отобразить запрашиваемый срез данных и показать пейжинг для перехода к другим страницам контрола, то для более удобной манипуляции данными на сервере было бы хорошо использовать объект, позволюящий нам хранить лишь нужный нам в данный момент набор данных, но при этом сохраняя информацию об общем количестве объектов, как далеко мы находимся от начала набора и т.д. В процессе приготовления к данному материалу я наткнулся на реализацию данного класса на codeplex - PagedList<T> (либо же можно найти похожую реализацию на этом блоге). Для работы с данными в веб-ориентированных проектах данный класс будет крайне пригодным, так как позволяет превратить любой объект, наследуемый от IEnumerable, в объект PagedList, благодаря которому можно получить много полезной информации о наборе данных: первая ли это страница из всего набора, последняя ли это страница, общее количество данных, количество данных в отображаемой наборе и т.д.


Для отображения данных на страницу создадим отдельный HtmlHelper-метод – GridView.


public static class GridViewExtensions
{
public static void GridView<T>(
this HtmlHelper html,
PagedList<T> data,
Action<PagedList<T>> headerTemplate,
Action<T, string> itemTemplate,
string cssClass,
string cssAlternatingClass,
Action<PagedList<T>> footerTemplate,
Action<PagedList<T>> paging)
{
headerTemplate(data);

for (int i = 0; i < data.Count; i++)
{
var item = data[i];
itemTemplate(item, (i % 2 == 0 ? cssClass : cssAlternatingClass));
}

footerTemplate(data);
paging(data);
}
}


Чтобы жестко не прописывать логику генерации кода контрола, а сделать его универсальным для отображения разных табличных данных – прибегаем к использованию Action-методов (чтобы не загружать наш контрол делегатными функциями), логику работы которых мы будем индивидуально прописывать в шаблоне View. Конечно это заставит нас писать больше кода для каждого View, в котором мы будем использовать данный контрол, поэтому для полного комфорта было бы хорошо ввести два контрола – один с широкими возможностями для изменения внешнего вида, второй – построение единого шаблона отображения, но и требующего написания меньше кода. Остановимся пока на первом варианте.


Создаем новый strongly-typed view и вводим следующий тип (он не будет присутствовать в выпадающем списке) -PagedList<EmployeeModel>


image



Добавляем следующий код на страницу:


<% Html.GridView<MvcApplication1.Models.EmployeeModel>(
this.ViewData.Model,
data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },
(item, css) =>
{ %>
<tr class="<%=css%>">
<td><%=item.EmployeeID%></td>
<td><%=item.FirstName%></td>
<td><%=item.LastName%></td>
</tr>
<% },
"item",
"item-alternating",
data =>
{ %>
</table>
<% },
data =>
{ %>
<%= Html.Pager(Model, "/Employee/GetList")%>
<%}); %>



Необходимо разобрать более детально этот код для лучшего его понимания.

Для генерации табличного контрола мы создали статический html-helper метод GridView, в который в нужной последовательности необходимо передать данные, согласно декларации метода. Хотелось бы сильнее обратить внимание на следующую запись:


data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },





согласно декларации метода в классе третьим параметором мы должны были передать Action-метод

...
Action<PagedList<T>> headerTemplate
...



Как Action-метод может быть заменен такой конструкцией? Сначала необходимо разобраться что из себя представляет класс Action и как серверный код превращается в html-код.


Начнем со второго вопроса. Не задумывались ла Вы, почему компилятор при компиляции страницы указывает лишь об ошибках внутри серверных блоков (<% %>, <%= %>), но пропускает любое упоминание об ошибке в записи Html-тега например? Все очень просто: для компилятора все что внутри серверных блоков – это инструкции для выполнения, все что за пределами – это строки текста, которые просто надо вывести на страницу. Представим что мы имеем следующую декларацию на странице:


<ul>
<% for(int i=1; i<=3; i++) { %>
<li><%= i %></li>
<% } %>
</ul>

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


Response.Write("<ul>");
for(int i=1; i<=3; i++)
{
Response.Write("<li>");
Response.Write(i);
Response.Write("</li>");
}
Response.Write("</ul>");


Именно поэтому компилятор не показывает сообщение об ошибке записи тега, как как для него – это обычный блок текста, который необходимо вывести в output-поток согласно своей позиции.


Что же такое Action-методы? Объекты данного класса используются для замены делегатов-функций определенного синтаксиса – принимают один параметр и на выходе не возвращают ничего (void). К тому же Action-методы позволяют задавать декларации с использованием анонимных функций и лямбда-выражений. Следующий код демонстрирует простой пример использования Action-метода для замены делегатов.


public static void Main()
{
Action<string> messageTarget;
messageTarget = s => ShowWindowsMessage(s);

messageTarget("Hello, World!");
}

private static void ShowWindowsMessage(string message)
{
MessageBox.Show(message);
}


В нашем же примере мы используем тоже анонимные функции и лямбда-выражение для декларации Action-метода.

В итоге, наш код


data =>
{ %>
<table class="grid" cellpadding="0" cellspacing="0">
<% },


можно превратить в следующую аналогичную запись:





delegate(PagedList<T> data) { Response.Write("<table class='grid' cellpadding='0' cellspacing='0'>"); },


Хорошо,  детально разобрали декларацию нового HtmlHelper-метода GridView. Но, как видно, для отображения пейжинга на странице используем еще один HtmlHelper-метод – Pager.


Я не буду вдаваться в детали его реализации – более детально можно почитать на этом ресурсе. Одно лишь замечу, что в качестве источника данных для построения пейжинга данный контрол использует объект реализованного выше класса PagedList.


После компиляции мы получил следующий результат:


image


Если просмотреть html-код, то видим, что весь контент присутствует и ссылки на месте. Это значит, что при загрузке данной страницы поисковым роботом, он получит всю информацию для индексации.


Отлично, поставленная задача опять выполнена, хотя и потребовалось много дополнительной ручной работы – создать дополнительные контролы и дополнительные классы для оперирования данными. Но при этом опять с лучшим результатом, чем для серверного контрола <asp:GridView runat=”server” />, а именно получили ссылку без параметров строки (article.aspx?Id=2, как в примере с WebForms), а полноценную ссылку. В этом случае поисковики больше доверяют ссылке без параметров строки.


Для упрощения работы с созданием табличных контролов можно посоветовать использовать контролы сторонних разработчиков. Для примера компания Telerik предлагает удобный Grid-контрол для MVC.


image


Примеры его использования можно посмотреть здесь.