8 July 2011

Разработка приложения живого поиска по Twitter с помощью Knockout, jQuery и ASP.NET MVC 3

JavaScript
Translation
Original author: Anders Fjeldstad
Достаточно не тривиально разработать хорошо спроектированный front-end веб-приложения с уровнем отклика, производительностью и фичами, которые ожидают пользователи сегодня. Легко потеряться в кипящей смеси jQuery обработчиков событий, HTML разметки и AJAX вызовов и даже относительно простой GUI быстро может стать кошмаром для сопровождения.

Один из способов добавления некоторой структуры и порядка на клиентской стороне – использование фреймворка вроде Knockout. Knockout – это свободная Javascript библиотека с открытым исходным кодом, которая помогает реализовать Model-View-View Model (MVVM) паттерн на клиенте. Она хорошо документирована и официальный веб сайт великолепная точка старта с кучей реальных примеров, которые не только демонстрируют использование встроенных фич библиотеки, но и показывают, как расширить её свои собственными фичами.

В этом посте мы рассмотрим, как Knockout может быть использован совместно с плагином отображения, каким-нибудь jQuery и ASP.NET MVC 3 backend (или модель, как вам нравится) для построения простого, но эффективного приложения для живого поиска в Twitter.


Цель


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


Шаг 1: Определение модели


Twitter представляет поисковый API, который достаточно прост для использования и который может возвращать ответ на поисковые запросы в XML (Atom) или JSON формате. Мы могли бы позволить нашему клиенту напрямую общаться с Twitter API, но это лишило бы нас возможности улучшить приложение через добавление кеширования, какой-нибудь заглушки на случай падения Twitter и т.д.; поэтому хорошей идей было бы создать вместо этого нашу собственную модель, и для этого мы собираемся использовать очень простое ASP.NET MVC 3 приложение.

Во-первых, создайте стандартное пустое веб-приложение (с Razor в качестве движка отображения) и добавьте контроллер – давайте назовём его TwitterController. Затем переименуйте “Index” действие, которое автоматически создалось вместе с новым контроллером, в “Search”. Контроллер будет выглядеть как-то так:
public class TwitterController : Controller
{
[HttpGet]
public ActionResult Search()
{
return View();
}
}


* This source code was highlighted with Source Code Highlighter.

Далее, мы собираемся создать метод действия, который выполняет актуальный поиск в Twitter. Для этой задачи, мы будем использовать Atom версию поискового API. URL запроса соответствует следующему формату search.twitter.com/search.atom?q=[URL-encoded search parameters]. Т.к. Atom – это XML, мы можем использовать LINQ to XML для того, чтобы распарсить и преобразовать множество результатов (Twitter возвращает массу данных, которые нам будут не нужны, так что мы их сократим до минимума).

Но у нас нет желания посылать XML на клиент – лучше JSON. К счастью, легко конвертировать большинство объектов в JSON, и в данном случае простейший путь – это использовать встроенный Json метод, который возвращает JsonResult. Всё сводится к:
[HttpPost]
public ActionResult Search(string query)
{
var atomResult = XDocument.Load(string.Format(
"http://search.twitter.com/search.atom?q={0}",
HttpUtility.UrlEncode(query)));
XNamespace ns = "http://www.w3.org/2005/Atom";
var searchResult =
from tweet in atomResult.Descendants(ns + "entry")
let image = tweet.Elements(ns + "link")
.Where(e => e.Attributes()
.Any(a => a.Name == "rel" &&
a.Value == "image"))
.First().Attribute("href").Value
let url = tweet.Elements(ns + "link")
.Where(e => e.Attributes()
.Any(a => a.Name == "rel" &&
a.Value == "alternate"))
.First().Attribute("href").Value
select new
{
id = tweet.Element(ns + "id").Value,
author = tweet.Element(ns + "author").Element(ns + "name").Value,
imageUrl = image,
tweetUrl = url,
tweetText = tweet.Element(ns + "title").Value
};
return Json(searchResult);
}


* This source code was highlighted with Source Code Highlighter.

(Код выше включает процедуру парсинга для выделения аватара в профиле автора и ссылки на каждый отдельный твит.)

На этом всё – мы построили нашу модель!

Шаг 2: Реализация модели вида


До того, как мы начнём разрабатывать GUI, нам будет нужно добавить некоторые ссылки в наше решение. Либо через NuGet, либо вручную загрузив jQuery, Knockout и jQuery.tmpl. Затем добавьте “Search.cshtml” вид в Views/Twitter (не используйте “master page” или layout – легче получить представление о сути в небольшой задаче, похожую на нашу, если мы не будем размазывать решение по множеству отдельных файлов). Добавьте упомянутые Javascript ссылки. Когда вы это сделаете, у вас будет что-то похожее на это:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Twitter Live Search</title>
<script src="@Url.Content("~/Scripts/jquery-1.6.2.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.tmpl.min.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout-1.2.1.js")"
type="text/javascript"></script>
</head>
<body>
<!-- TODO -->
</body>
</html>


* This source code was highlighted with Source Code Highlighter.

Настало время построить нашу модель вида. Модель вида включает логику приложения на клиентской стороне или, если вам нравится, она определяет что приложение может делать и какие свойства имеет, без непосредственной привязки к любому элементу разметки или DOM элементам.

Для этой задачи модель вида чрезвычайно проста: содержит 2 свойства, используемые для хранения текущего поискового запроса и найденных твитов, и соответственно метод для выполнения поиска по модели. Это может выглядеть так:
<script type="text/javascript">
function SearchViewModel() {
this.query = ko.observable('');
this.tweets = ko.observableArray();
}

SearchViewModel.prototype.search = function () {
var q = this.query();
if (q.length > 0) {
$.ajax({
type: 'POST',
url: '/Twitter/Search',
data: { query: q },
dataType: 'json',
success: function (data) {
// TODO
}.bind(this)
});
} else {
// TODO
}
};
</script>


* This source code was highlighted with Source Code Highlighter.

Трудно сказать, что здесь есть что-то значимое; свойства запроса и твитов изначально пусты, поисковый метод использует jQuery для того, чтобы отправить AJAX запрос к нашей модели на сервере и вернуть результаты в виде JSON объекта (массива твитов).

Как нам следует реализовать “успешный” обработчик в методе поиска? Мы можем очистить массив твитов и добавить новые результаты поиска. Но помните, что мы хотим постоянно получать “обновления” для наших поисковых результатов, в идеале мы хотели бы просто добавлять новые твиты (и удалять старые), не заменяя коллекцию целиком.

Это великолепная возможность попробовать Mapping плагин для Knockout. После того, как мы его загрузили и добавили ссылку на скрипт, мы можем реализовать успешный обработчик AJAX запроса:
success: function (data) {
ko.mapping.updateFromJS(this.tweets, data);
}.bind(this)


* This source code was highlighted with Source Code Highlighter.

И затем задать “key definition” для коллекции твитов (который используется отображением для того, чтобы определить какие элементы следует добавить, обновить или удалить), мы просто немного перепишем наш конструктор:
function SearchViewModel() {
this.query = ko.observable('');
this.tweets = ko.mapping.fromJS(
[],
{
key: function (tweet) { return ko.utils.unwrapObservable(tweet.id) }
});
}


* This source code was highlighted with Source Code Highlighter.

Заметьте, что мы определяем ключ для коллекции твитов как значение ID, которое Twitter также удобно предоставляет. Если вам нужно больше информации о Mapping плагине, посетите официальный сайт Knockout.

Следующее: реализация фичи авто-обновления. Каждый раз, когда есть поисковый запрос, соответствующие твиты должны обновляться, скажем, каждые 3 секунды для отражения изменений в модели. Один из способов достичь этого – завести успешный calback для поискового AJAX запроса вызывающего рекурсивно, но задержкой, метод поиска. Полная модель вида в этом случае выглядит так:
<script type="text/javascript">
function SearchViewModel() {
this.query = ko.observable('');
this.tweets = ko.mapping.fromJS(
[],
{
key: function (tweet) { return ko.utils.unwrapObservable(tweet.id) }
});
this.autoRefreshId = null;
}

SearchViewModel.prototype.search = function () {
var timeoutId = this.autoRefreshId;
if (timeoutId) {
window.clearTimeout(timeoutId);
this.autoRefreshId = null;
}
var q = this.query();
if (q.length > 0) {
$.ajax({
type: 'POST',
url: '/Twitter/Search',
data: { query: q },
dataType: 'json',
success: function (data) {
ko.mapping.updateFromJS(this.tweets, data);
this.autoRefreshId = window.setTimeout(
this.search.bind(this), 3000);
}.bind(this)
});
} else {
ko.mapping.updateFromJS(this.tweets, []);
}
};
</script>


* This source code was highlighted with Source Code Highlighter.

(Я добавил пару строк для отключения авто-обновления, когда строка ввода поискового запроса пуста; плюс проверка, которая прерывает любой работающий поиск, когда пользователь начинает новый. Также, поисковые результаты обнуляются, если выполнить поиск по пустому запросу.)

Шаг 3: Дизайн вида


Для того чтобы сохранить вещи простыми и очевидными, вид будет состоять только из строки ввода поискового запроса и списка твитов. Мы хотим достигнуть чего-то в этом духе:
<form>
<input type="search" placeholder="Input #hashtag, @@username or something else" autofocus />
<input type="submit" value="Search" />
</form>

<ul id="tweets">
<li>
<a href="[url-of-tweet]" title="[author-name]">
<img src="[url-of-author-image]" alt="[author-name]" />
</a>
<h6>[author-name]</h6>
<p>[tweet-text]</p>
</li>
<!-- More tweets... -->
</ul>


* This source code was highlighted with Source Code Highlighter.

Давайте также добавим немного декларативных Knockout привязок данных, которые свяжут вид и модель вида. Если мы немного модифицируем поисковую форму
<form data-bind="submit: search">
<input type="search" data-bind="value: query, valueUpdate: 'afterkeydown'" placeholder="Input #hashtag, @@username or something else" autofocus />
<input type="submit" value="Search" />
</form>


* This source code was highlighted with Source Code Highlighter.


…Knockout будет обновлять свойство запроса модели вида автоматически, когда пользователь изменит текст в поле поискового запроса, и поиск будет выполнен, как только пользователь нажмёт клавишу Enter.

Что насчёт поисковых результатов? Превосходный сценарий для использования Knockout привязки шаблонов! Во-первых, создайте шаблон для каждого твита:
<script id="tweetTemplate" type="text/html">
<li data-bind="attr: { id: id }">
<a data-bind="attr: { href: tweetUrl, title: author }">
<img data-bind="attr: { src: imageUrl, alt: author }" />
</a>
<h6 data-bind="text: author"></h6>
<p data-bind="text: tweetText"></p>
</li>
</script>


* This source code was highlighted with Source Code Highlighter.

Затем data-bind свойство HTML списка твитов в модели вида, задавая ваш шаблон как способ для отображения каждого из них. Относительно легко:
<ul id="tweets"
data-bind="template: {
name: 'tweetTemplate',
foreach: tweets
}, visible: tweets().length > 0">
</ul>


* This source code was highlighted with Source Code Highlighter.

(Заметьте, что список не будет видимым, если модель вида не содержит твитов)

Бонус: Добавьте анимацию, которая используется, когда новые твиты добавляются в список, используя afterAdd опцию привязки шаблона:
<ul id="tweets"
data-bind="template: {
name: 'tweetTemplate',
foreach: tweets,
afterAdd: function (element) {
$(element).hide().fadeIn('fast');
}
}, visible: tweets().length > 0">
</ul>


* This source code was highlighted with Source Code Highlighter.

К этому моменту осталось доделать только одну вещь – логику начальной загрузки. Когда DOM загрузился, нам нужно создать экземпляр нашей модели вида и попросить Knockout присоединить наши привязки к ней. Одна строка кода в jQuery обработчике события “document ready”:
<script type="text/javascript">
$(function () {
ko.applyBindings(new SearchViewModel());
});
</script>


* This source code was highlighted with Source Code Highlighter.

И сделано! Проверено на моей Windows 7 машине в Chrome 12, Internet Explorer 9 и Firefox 5. Здесь законченное решение (где я добавил немного CSS для того чтобы это выглядело менее ужасно): TwitterLiveSearchLab.zip
Tags:knockoutjsjqueryasp.net mvctwittermvvm
Hubs: JavaScript
+20
6.5k 53
Comments 17