И так что мы хотим:
— Навигация по сайту с использованием history api
— Получения данных с сервера в виде json объекта с последующим рендером на клиенте
— При прямом переходе рендер должен происходить на сервере
— Что бы все было легко и просто
С кругом потребностей определились, теперь определимся с технологиями:
— На сервере будет трудиться expressjs под nodejs
— В качестве шаблонитизатора jade
— Для клиента History.js
Сервер
Для тех кто никогда не работал с nodejs для начала стоит ее установить. Как это сделать быстро под Ubuntu можно посмотреть тут. Создадим себе папку для проекта и перейдем в нее. Далее установим необходимые модули:
npm i express jade
И создадим две директории:
— view — тут будут лежать шаблоны
— public — тут будет статичный контент
Далее напишем сервер и остановимся лишь на основных моментах.
Первое чем я хотел себе облегчить жизнь, это не задумываться о том как пришел к нам запрос по ajax или нет. Для этого мы перехватим стандартный res.render
app.all('*', function replaceRender(req, res, next) { var render = res.render, view = req.path.length > 1 ? req.path.substr(1).split('/'): []; res.render = function(v, o) { var data; res.render = render; //тут мы должны учесть что первым аргументом может придти //имя шаблона if ('string' === typeof v) { if (/^\/.+/.test(v)) { view = v.substr(1).split('/'); } else { view = view.concat(v.split('/')); } data = o; } else { data = v; } //в res.locals располагаются дополнительные данные для рендринга //Например такие как заголовок страницs (res.locals.title) data = merge(data || {}, res.locals); if (req.xhr) { //Если это аякс то отправляем json res.json({ data: data, view: view.join('.') }); } else { //Если это не аякс, то сохраняем текущее //состояние (понадобиться для инициализации history api) data.state = JSON.stringify({ data: data, view: view.join('.') }); //И добавляем префикс к шаблону. Далее я расскажу для чего он нужен. view[view.length - 1] = '_' + view[view.length - 1]; //Собственно сам рендер res.render(view.join('/'), data); } }; next(); });
res.render перегрузили, теперь мы можем спокойно вызывать в наших контроллерах res.render(data) или res.render(‘view name’, data), и сервер сам либо отрендрит либо вернет json на клиента в зависимости от типа запроса.
Посмотрим на код еще раз, а я попробую объяснить зачем нужен префикс ‘_’ к шаблонам в случае «рендринга на сервере».
Проблема заключается в следующем. В jade отсутствуют layout’ы, в место них используются блоки, блоки могут расширять, заменять или дополнять друг друга (все это хорошо описано в документации).
Рассмотрим пример.
Предположим у нас есть вот такая структура отображений:
!!! 5 html head title Page title body #content block content
index.jade
extends layout block content hello world
Если мы сейчас отрендрим index.jade то он отрендриться вместе с layout.jade. Это не доставляет проблем до тех пор пока мы не хотим экспортировать index.jade на клиента и рендрить его там, но уже без layout.jade. Поэтому я решил добавить еще один шаблон, который бы позволял это делать легко и просто.
!!! 5 html head title Page title body #content block content
_index.jade
extends layout block content include index
index.jade
hello world
Теперь если мы хотим отрендрить блок с layout’ом, то мы рендрим файл _index.jade, если нам не нужен layout, то рендрим index.jade. Мне показался такой способ наиболее простым и понятным. Если придерживаться правила что только шаблоны с префиксом "_" расширяют layout.jade то можно безболезненно экспортировать все остальное на клиента. (Несомненно есть и другие способы сделать такое, можете рассказать о них в комментариях, будет интересно узнать)
Следующий момент на котором я остановлюсь, это экспорт шаблонов на клиента. Для этого напишем функцию которая будет на вход получать путь к шаблону относительно viewdir, а на выход будет возвращать скомпилированную функцию приведенную к строке.
function loadTemplate(viewpath) { var fpath = app.get('views') + viewpath, str = fs.readFileSync(fpath, 'utf8'); viewOptions.filename = fpath; viewOptions.client = true; return jade.compile(str, viewOptions).toString(); }
Теперь напишем контроллер который будет собирать javascript файл с шаблонами.
app.get('/templates', function(req, res) { var str = 'var views = { ' + '"index": (function(){ return ' + loadTemplate('/index.jade') + ' }()),' + '"users.index": (function(){ return ' + loadTemplate('/users/index.jade') + ' }()),' + '"users.profile": (function(){ return ' + loadTemplate('/users/profile.jade') + ' }()),' + '"errors.error": (function(){ return ' + loadTemplate('/errors/error.jade') + ' }()),' + '"errors.notfound": (function(){ return ' + loadTemplate('/errors/notfound.jade') + ' }())' + '};' res.set({ 'Content-type': 'text/javascript' }).send(str); });
Теперь когда клиент запросит /template, в ответ он получит такой объект:
var view = { 'имя шаблона': <функция> };
И на клиенте что бы отрендрить нужный шаблон, достаточно будет вызвать view[‘имя шаблона’](data);
Закончим рассматривать серверную часть, т.к. все остальное особо к делу не относится и на прямую не связано с нашей задачей. Тем более код можно посмотреть тут.
Клиент
Так как мы экспортируем на клиента уже скомпилированные шаблоны, нам нет нужды подключать сам шаблонитизатор, достаточно подключить его runtime и не забываем подгружать наши шаблоны, подключив их как обычный javascript файл.
Следующая библиотека из списка это History.js, название которой говорит само за себя. Я выбрал версию только для html5 браузеров, это все современные браузеры, хотя библиотека может работать в старых браузерах через url hash.
Осталось совсем немного клиентского кода.
Первое напишем функцию render(). Она достаточно простая и выполняет рендер заданного шаблона в блок content.
var render = (function () { return function (view, data) { $('#content').html(views[view](data)); } }());
Теперь код инициализирующий работу с History.js
$(function () { var initState; if (History.enabled) { $('a').live('click', function () { var el = $(this), href = el.attr('href'); $.get(href, function(result) { History.pushState(result, result.data.title, href); }, 'json'); return false; }); History.Adapter.bind(window,'statechange', function() { var state = History.getState(), obj = state.data; render(obj.view, obj.data); }); //init initState = $('body').data('init'); History.replaceState(initState, initState.data.title, location.pathname + location.search); } });
Код достаточно простой. Первое что мы делаем, это смотрим поддерживает ли браузер history api. Если нет, то ничего не меняем и клиент работает по старинке.
А если поддерживает, мы перехватываем все клики по a, посылаем аякс запрос на сервер.
Не забываем навесить обработчик события «statechange», в этот момент нам нужно перерисовывать наш content блок, и добавить инициализацию начального состояния, я решил хранить его в теге body, атрибут data-init, сюда пишутся начальные значения при рендере на сервере.
Строчка data.state = JSON.stringify({ data: data, view: view.join(‘.’) }); в функции replaceRender
Вот собственно и все.
Рабочий пример тут (Если умрет, значит хаброэффект его накрыл :))
Код можно посмотреть тут
ссылка на оригинал статьи http://habrahabr.ru/post/161943/
Добавить комментарий