Для начала пару слов о проекте. Это промо страница для одного небольшого местного сайта знакомств. Была задача создать отдельную страницу с конкурсом на лучшее фото среди участниц. Всего 8 участниц. Голосовать может кто угодно, никакой регистрации или авторизации требовать не нужно. На странице будет обратный отсчет до конца конкурса.
Meteor оказался хорошим выбором для этого проекта. Или же проект оказался хорошим в качестве моей первой работы на Meteor, равнозначно. Главная особенность Meteor – это т.н. реактивность (Reactivity). Идея в том, что программист декларативно описывает логику, не задумываясь о протоколе коммуникации между клиентом и сервером. Обновление данных на клиенте происходит автоматически, как только данные изменились на сервере. Это значит никаких больше AJAX запросов в коде проекта.
В качестве базы данных используется MongoDB. Клиентская часть имеет доступ к данным базы так же, как и серверная. Даже интерфейс доступа такой же, для имитации запросов к базе на клиентской стороне используется Minimongo. Клиент через Minimongo оперирует JavaScript массивами, в отличии от сервера который делает прямые запросы к MongoDB базе.
file: model.js
// Общий для клиента и сервера код Members = new Meteor.Collection('members');
В примере выше объявляется коллекция «участники». Так как этот файл доступен и клиентской и серверной части проекта, доступ к переменной Members есть и на клиенте и на сервере. Это можно проверить просто открыв консоль в браузере и выполнив typeof Members или Members.find().fetch(). Отличие только в реализации, ведь на сервере методы Members будут оперировать с MongoDB напрямую, а на клиенте с JavaScript массивами через Minimongo обертку.
Эти коллекции управляются самим Meteor – он сам решает когда данные необходимо обновить на клиенте. Программист может ограничить объем данных, который будет представлен переменной Members на клиенте. Это будет подмножество данных с сервера. Делается это при помощи Meteor.publish() и Meteor.subscribe(). В данном случае все участники со всеми их данными должны быть доступны клиенту, поэтому никаких искусственных ограничений не накладывается.
file: server/server.js
Meteor.startup(function () { if (Members.find().count() === 0) { Members.insert({ name: 'Александра Богинич', title: 'Александру', url: 'http://mariels.ru/member/profile_alexandra_igorevna.html', photo: 'images/member/Александра Богинич.jpg', thumb: 'http://mariels.ru/$userfiles/thumb_450_1136_94.jpg', vote: 0 }); Members.insert({ name: 'Алена Мансурова', title: 'Алену', url: 'http://mariels.ru/member/profile_Alionushka.html', photo: 'images/member/Алена Мансурова.jpg', thumb: 'http://mariels.ru/$userfiles/thumb_444_1120_90.jpg', vote: 0 }); // и так далее... } });
В коде выше стандартный способ инициализации коллекции в Meteor. Так как код находится в файле server/server.js, то выполняется он только на сервере.
Данные есть, теперь их нужно вывести в браузере. В Meteor по умолчанию используется JavaScript шаблонизатор Handlebars. На самом деле, довольно кривой шаблонизатор и для выполнения простой задачи вроде «получить доступ к индексу массива в цикле foreach» приходится писать новый обработчик тега. Но, привыкнув, работать с ним можно.
file: client/view/members.html
<template name="members"> <div id="members"> {{#render_members members}} <span class="member span6"> <span class="info-cont"> <span class="shadow"></span> <a href="{{member.url}}" class="account"> <img src="{{member.thumb}}" width="" height="" class="avatar"/> <span>{{member.name}}</span> </a> </span> <img src="{{member.photo}}" class="image" /> <span class="rate-cont"> <span class="shadow"></span> <button class="btn {{#if voted}}btn-info{{else}}btn-warning{{/if}} pull-center btn-large" data-id="{{member._id}}" {{#if voted}}disabled{{/if}}> {{#if voted}} Проголосуйте снова завтра {{else}} Голосовать за <span>{{member.title}}</span> {{/if}} </button> </span> </span> {{/render_members}} </div> </template>
Тег render_members был создан только для того, чтобы делить вывод на строки (выводить через каждые две записи), а вообще это обычный foreach цикл. Переменная доступная шаблону только одна — массив members. В теле render_members доступны все поля каждого объекта из массива members. Если быть уж совсем точным, то members не массив, а курсор, но это не суть.
file: client/client.js
Template.members.members = function() { return Members.find({}, { sort: { vote: -1 }}); }
Members.find() возвращает курсор, тогда как Members.find().fetch() простой JavaScript массив. Используя курсор в качестве переменной шаблона members и обернув его в function() { } мы активируем реактивность Meteor на этой переменной шаблона. Это значит, что как только данные коллекции Members на сервере изменятся и обновления будут переданы на клиент, шаблон будет автоматически перерисован используя новые данные. И для этого не нужно никакого дополнительного кода на клиенте!
file: server/server.js
// Код только для сервера Votes = new Meteor.Collection('votes');
В коллекции Votes будут храниться все голоса и она может разрастись до нескольких тысяч записей. Мы не можем позволить такому огромному объему данных курсировать между сервером и клиентом по понятным причинам. К тому же, на клиенте нам совершенно ни к чему знать данные каждого голоса, такие как IP и дата. По этим причинам коллекция объявляется только в коде, выполняемом на сервере.
file: server/server.js
// Проверка валидности IP и даты последнего голосования var CanVote = Match.Where(function(ip) { check(ip, String); if (ip.length > 0) { var yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); // голосовать можно только раз в сутки return Votes.find({ ip: ip, date: { $gt: yesterday } }).count() == 0; } return false; }); // Методы сервера, доступные клиенту Meteor.methods({ // возвращает true если клиент может голосовать и false в обратном случае canVote: function() { return Match.test(headers.get('x-forwarded-for'), CanVote); }, // проголосовать за участницу vote: function(memberId) { check(memberId, String); check(headers.get('x-forwarded-for'), CanVote); var voteId = Votes.insert({ memberId: memberId, ip: headers.get('x-forwarded-for'), date: new Date() }); Members.update(memberId, { $set: { vote: Votes.find({ memberId: memberId }).count() } }); return voteId; }, // возвращает количество голосов за участницу getMemberVotes: function(memberId) { check(memberId, String); return Votes.find({memberId:memberId}).count(); }, // возвращает общее суммарное количество голосов getTotalVotes: function() { return Votes.find().count(); } });
При помощи Meteor.methods() объявляется интерфейс связи между клиентом и сервером в рамках проекта. Так как коллекция Votes не доступна на клиенте, здесь объявлены методы для получения нужных данных об этой коллекции, как то количество голосов за участницу и общее количество голосов.
В функции голосования добавляется новая запись в коллекцию Votes, а также обновляется количество голосов (votes) у соответствующей записи из коллекции Members. Это нужно чтобы использовать реактивность в выводе списка участников (сортируется по votes) и графика рейтингов.
// file: client/views/ratings.html
<template name="ratings"> <div id="ratings" class="well"> <h1 class="heading uppercase">Рейтинги</h1> <div class="chart"> {{#each_with_index members}} <div class="rating num{{index}}"> <img src="{{data.thumb}}" class="avatar"/> </div> {{/each_with_index}} </div> <div class="pull-center pull-center-1"> <div id="votes">{{votes}}</div> <div><strong>голосов</strong></div> </p> </div> </div> </template>
// file: client/client.js
Session.setDefault('totalVotes', 0); Meteor.startup(function() { // обновление значения totalVotes сессии Deps.autorun(function() { var total = 0; Members.find().forEach(function(m) { total += m.vote; }); Session.set('totalVotes', total); }); // обновление графика рейтингов топ-5 Deps.autorun(function() { var top = Members.findOne({}, { sort: { vote: -1 }}); // текущий лидер голосования // update ratings chart Members.find({}, { sort: { vote: -1 }, limit: 5 }).forEach(function(m, i) { var height = top ? Math.floor((m.vote / top.vote) * 190) + 100 : 100; $('.rating.num'+(i+1)).css('height', height); }); }); }); Template.ratings.members = function() { return Members.find({}, {limit: 5, sort: { vote: -1 }}); }; Template.ratings.votes = function() { return Session.get('totalVotes'); };
Session существует только на клиенте и она не персистентна, то есть сбрасывается при обновлении страницы. Объект Session так же как и курсор коллекций активирует реактивность, поэтому при изменении значения totalVotes в сессии будет перерисован шаблон ratings.
Deps.autorun() выполняется каждый раз, как реактивные данные в фукнции меняются. В данном случае это курсор Members.find(). Идея в том, что как только сервер обновит votes у какой-нибудь участницы, обновится и значение totalVotes сессии у всех клиентов, а это приведет к перериcовке блока рейтингов. Deps.autorun() используется для добавления коллбэка на изменения данных на клиенте. Есть способы подписаться на конкретные события коллекций типа added, changed, removed подробнее здесь.
Также здесь можно заметить использование jQuery. Его можно перемешивать с клиентским кодом Meteor почти без ограничений. Кстати, Meteor.startup(function {}) и jQuery(function() { }) идентичны.
file: client/views/index.html
<head> <!-- Много meta тегов в т.ч. для SEO и социалок --> <link href="stylesheets/project.css" media="screen" rel="stylesheet" type="text/css" /> <!-- ... другие стили CSS --> <script type="text/javascript" src="js/flipclock/flipclock.min.js"></script> <!-- другие скрипты JavaScript --> <title>Мисс Осень 2013</title> </head> <body> <div class="page-header-bg"></div> {{>header}} <div class="container-fluid"> <div class="container"> <div class="page-header"> <h1 class="header-gradient">Мисс Осень 2013</h1> </div> {{>page}} </div> </div> </body> <template name="page"> {{#if contestInProgress}} {{>partners}} {{>countdown}} {{>members}} {{>social}} {{>ratings}} {{>terms}} {{>partners}} {{>footer}} {{else}} {{>winner}} {{>partners}} {{>social}} {{>footer}} {{/if}} </template>
Meteor обрабатывает все JavaScript, HTML, CSS файлы найденные в проекте и объединяет по определенным правилам. Однако, файлы в папке public считаются статичными, доступны как есть и не обрабатываются Meteor. Стили можно было бы перенести под управление Meteor, но было решено использовать стандартный подход – включить ссылки на статичные файлы в заголовок HTML.
Некоторые сторонние библиотеки JavaScript тоже включаются как статичные файлы, хотя их можно было перенести в папку client и так же использовать из своего клиентского JavaScript кода. Дело в том, что не все библиотеки написаны так, что могут быть использованы в Meteor, в таких случаях всегда можно вернуться к стандартному включению в заголовок HTML. При различии способа включения сторонней библиотеки, использование в клиентском коде Meteor одинаково естественно.
file: client/client.js
contestEndDate = new Date('01/30/2014 12:00'); Session.set('inProgress', new Date() < contestEndDate); Template.header.contestInProgress = Template.page.contestInProgress = Template.footer.contestInProgress = function() { return Session.get('inProgress'); } Meteor.startup(function() { // обратный отсчет var targetDate = contestEndDate; var currentDate = new Date(); var offsetSeconds = (targetDate.getTime() - currentDate.getTime()) / 1000; offsetSeconds = Math.max(0, offsetSeconds); var clock = $('#countdown').FlipClock(offsetSeconds, { clockFace: 'DailyCounter', defaultClockFace: 'DailyCounter', countdown: true, callbacks: { stop: function() { Session.set('inProgress', false); } } }); });
В index.html можно увидеть еще одно применение реактивности. Переменная contestInProgress обозначает статус конкурса – в процессе или уже окончен. Вид страницы полностью меняется в зависимости от этого статуса. Статус устанавливается при инициализации страницы, а также меняется клиентом при возникновеннии события stop счетчика FlipClock.
Переменная contestInProgress есть в трех шаблонах и значение у нее одно и то же. Шаблоны независимы друг от друга и перерисовываются по отдельности.
Из кода видно, что из события инициируемого сторонней библиотекой FlipClock меняется значение сессии клиента Meteor. И это при том, что библиотека FlipClock загружается браузером клиента при загрузке страницы.
Вот тут открывается неочевидное преимущество Meteor. Раз так просто перерисовать страницу по завершении отсчета, так почему бы этого не сделать? Это всего лишь одна строчка кода, зато будет эффектно смотреться если кто-то в этот момент будет просматривать страницу.
Если бы проект разрабатывался на PHP+AJAX, эта была бы отдельная задача. Несложная, но учитывая что это событие случится только один раз за все существование проекта, возможно у программиста просто не дойдут руки сделать обновление статуса страницы. Да и зачем тратить на это время, если это увидят пару человек? Остальные, просто получат уже страницу с победителем. В этом и есть прелесть Meteor – программисту не нужно думать над протоколом общения и он может сконцентрироваться на тех мелочах, которые раньше бы откладывались в долгий ящик.
Традиционно, ощущения от использования Meteor в виде pros/cons:
ссылка на оригинал статьи http://habrahabr.ru/post/204262/
Добавить комментарий