Meteor. Разрабатываем TODO List

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

Еще хочу предупредить, что урок получился достаточно объемным, но кода в нем было написано в разы меньше, чем текста. Просто хочу поделиться опытом как можно использовать метеор не при создании простенького примера, и заострить внимание на различных моментах, которые я посчитал важными. Поэтому в уроке будет использоваться множество сторонних пакетов, облегчающих процесс разработки.

И еще одно предупреждение: в данном уроке будут использоваться следующие технологии для непосредственного написания примера:

jade — html препроцессор; less — css препроцессор; coffeescript — язык программирования, компилируемый в javascript. Видео, демонстрирующее приложение, полученное в ходе урока[embedded content]

И кому все еще интересно, добро пожаловать под кат.

Сам метеор базируется на nodejs и mongodb, так же у метеора нет поддержки Windows, и если вы собрались пощупать метеор вам необходимо обзавестись операционной системой Linux или MacOS.Первым делом устанавливаем nodejs и mongodb.

Следующим шагом необходимо установить метеор. Он не лежит в npm репозитории, поэтому не нужно торопиться и командовать npm install -g meteor, в данном случае лишь загрузиться его старая версия, для правильной установки необходимо выполнить в консоли:

$ curl https://install.meteor.com/ | sh

После установки метеора можно сходу командовать $ meteor create 'todo-list' todo-list: created.

To run your new app: cd todo-list meteor $ cd todo-list $ meteor [[[[[ ~/dev/meteor-getting-started/todo-list ]]]]]

=> Started proxy. => Started MongoDB. => Started your app.

=> App running at: http://localhost:3000/ Данный вывод означает, что все прошло хорошо, и наш хелловорлд можно проверить в браузере.

helloworld

Теперь, после проверки работоспособности нового проекта, файлы, находящиеся в корне проекта, можно удалить — нам они не особо интересны. Также можно заметить, что была создана директория .meteor, там хранится различная служебная информация о проекте и даже автоматически сгенерированный .gitignore. Кстати для ручного управления пакетами можно изменять файл packages, но консольные утилиты тоже достаточно удобны.

Если у вас такой же результат как и у меня, значит минимальное окружение для разработки метеор проекта готово, если же что-то пошло не так — проверьте корректность установки nodejs, mongodb и meteor, например, у меня на компьютере сейчас следующая конфигурация:

$ node -v v0.10.33 $ mongod --version db version v2.4.12 $ meteor --version Meteor 1.0 Теперь можно закончить с формальностями и приступим к разработке нашего туду листа. Для удобства рекомендую открыть новую вкладку консоли, так как перезапускать наше метеор приложение больше не потребуется, но будем использовать консольный интерфейс фреймворка для установки пакетов.

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

$ meteor add Как я писал выше, приложение будем разрабатывать на less, jade и coffeescript, а значит самое время установить их. Все пакеты, используемые в уроке, и кучу других можно найти на сайте Atmosphere. Собственно названия пакетов:

less, coffeescript — это официальные пакеты, поэтому не содержат имя автора; mquandalle: jade —, а вот это не официальный пакет, поэтому название состоит из двух составляющих, но выполнен он хорошо, и никаких проблем при его использовании у меня не возникало. В пакеты less и coffeescript встроена поддержка sourcemap, поэтому и процесс дебага в браузере будет простым, sourcemap поддерживается самим метеором: он предоставляет необходимый api для подключения данного функционала, поэтому нам не придется, что-то специально настраивать.По ходу разработки мы добавим еще несколько популярных пакетов, и я постараюсь описать назначения каждого. Кстати jquery и underscore уже включены в метеор, как и множество других пакетов, полный список можно посмотреть в файле ./.meteor/versions, в созданном проекте.

Теперь, по-моему мнению, самое время разобраться с тем как метеор подключает файлы в проект, и какие способы регуляции этого существуют. Здесь нам не потребуется писать конфигурационные файлы для grant или gulp, что бы скомпилировать шаблоны, стили и скрипты, метеор уже позаботился об этом. Для скафолдинга существует проект для Yeoman, но мне приятнее все создавать в ручную. В предыдущем проекте я использовал примерно следующую структуру папок: todo-list/ — корень проекта ├── client — тут будут чисто клиентские файлы │ ├── components — компоненты приложения будут состоять из шаблона │ │ и скрипта, реализующего его логику │ ├── config — файлы конфигурации │ ├── layouts — базовые шаблоны, не имеющие никакой логики │ ├── lib — различные скрипты, которые могут понадобится на │ │ клиенте │ ├── routes — клиентский роутинг │ └── styles — стили ├── collections — здесь будем хранить модели ├── lib — скрипты, которые могут понадобится везде ├── public — статика: картинки, robots.txt и все такое ├── server — файлы для серверной части приложения │ ├── methods — тут будут серверные методы, типа реста, │ │ только удобнее │ ├── publications — расшаривание данных из коллекций │ ├── routes — серверный роутинг, собственно можно будет │ │ контролировать http запросы │ └── startup — инициализация сервера Возможно что-то нам не пригодится, но в любом случае в метеоре нет никаких ограничений на именование папок и файлов, так что можете придумать, любую, удобную для вас структуру. Главное следует помнить о некоторых нюансах:

все файлы из папки public в корне проекта будут доступны пользователям по ссылки из браузера и не будут автоматически подключаться к проекту; все файлы из папки server в корне, доступны только серверной части приложения; все файлы из папки client в корне, доступны только клиентской части приложения; все остальное, что находится в корне, доступно в любой среде; файлы в проект подключаются автоматически, по следующим правилам: загрузка начинается с поддиректорий, и первой всегда обрабатывается директория с именем lib, далее все* папки и файлы загружаются в алфавитном порядке; файлы начинающиеся с main. загружаются последними. Например рассмотрим, как будет загружаться наш проект в браузере: первым делом будут загружены файлы из директории lib в корне проекта, далее будет обрабатываться папка client, в ней также первым делом загрузятся файлы из lib, а потом в алфавитном порядке: components → config →… → styles. И уже после файлы из папки collections. Файлы из папок public и server, не будут загружены в браузер, но, например, скрипты, хранящиеся в папке public, можно подключить через тег script, как это мы привыкли делать в других проектах, однако разработчики фреймворка не рекомендуют подобный подход.Также для регуляции среды выполнения в общих файлах можно воспользоваться следующими конструкциями:

if Meteor.isClient # Код, выполняемый только в браузере if Meteor.isServer # Код, выполняемый только на сервере И для регулирования времени выполнения скриптов мы можем использовать метод Meteor.startup (), в браузере это аналог функции $ из библиотеки jQuery, а на сервере, код в данной функции выполнится сразу же после загрузки всех скриптов в порядке загрузки этих файлов. Подробнее об этих переменных и методах.

Для верстки я буду использовать Bootstrap, да знаю, что он всем приелся, но верстальщик из меня никакой, а с бутстрапом я более менее знаком.Для этого устанавливаем пакет mizzao: bootstrap-3 — он самый популярный среди прочих, и думаю при его использовании у нас не должно возникнуть проблем.

Далее создаем в папке client/layouts файл head.jade. Это будет единственный файл в нашем приложении не имеющий формат шаблона, короче просто создадим шапку страницы, а позже разберем что такое шаблоны.

//- client/layouts/head.jade head meta (charset='utf-8') meta (name='viewport', content='width=device-width, initial-scale=1') meta (name='description', content='') meta (name='author', content='')

title Meteor. TODO List. Можно открыть браузер и убедиться, что после добавления файла страница имеет указанный заголовок.

Прежде чем начнем верстать предлагаю провести базовую настройку клиентского роутинга, а чуть позже мы разберем этот момент подробнее. Для роутинга можно воспользоваться популярным решением, имеющим весь необходимый нам функционал. Установим пакет iron: router (репозиторий).

После установки в директории client/config создаем файл router.coffee, со следующим содержанием:

# client/config/router.coffee Router.configure layoutTemplate: «application» Очевидно, что здесь мы задаем базовый шаблон для нашего приложения, он будет называться application. Поэтому в папке layouts создаем файл application.jade. В этом файле мы опишем шаблон, некоторую сущность, которая на этапе сборки приложения превратится в код на javascript. Кстати в метеоре используется их собственный усатый шаблонизатор spacebars и библиотека blaze.

Если коротко, то процесс работы шаблонов выглядит следующим образом (на сколько я понял из документации). Шаблоны spacebars компилируются в объект библиотеки blaze, которая в последствии будет работать непосредственно с DOM. В описании к проекту есть сравнение с другими популярными библиотеками:

по сравнению с обычными текстовыми шаблонизаторами блейз работает с домом, он не будет заново рендерить весь шаблон, что бы поменять один аттрибут, и у него нет проблем с вложенными шаблонами; по сравнению с шаблонами Ember, блейз рендерит только изменения, нет нужды в явных дата-байдингах и в описаниях зависимостей между шаблонами; по сравнению с шаблонами angular и polymer, блейз имеет понятный и простой синтаксис, меньший порог входа и вообще не позиционируется как технология будущего, а просто работает; по сравнению с React имеет простой синтаксис описания шаблонов и простое управление данными. Это я практически перевел параграф из официального описания библиотеки, так что прошу не кидаться в меня камнями, если с чем-то не согласны. Сам я сталкивался с этими технологиями (кроме ember) и в принципе согласен с авторами библиотеки, из минусов в блейзе хочу заметить завязку на метеоре.Но мы в своем проекте не используем явно ни blaze, ни spacebars. Для jade шаблонов процесс компиляции имеет такую последовательность: jade → spacebars → blaze.

Все шаблоны в метеор описываются в теге template, где должен быть аттрибут с именем шаблона. Помните, мы в настройках роутера указали layoutTemplate: «application», вот application, как раз и является именем шаблона.

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

//- client/layouts/application.jade template (name='application') nav.navbar.navbar-default.navbar-fixed-top .container .navbar-header button.navbar-toggle.collapsed ( type='button', data-toggle='collapse', data-target='#navbar', aria-expanded='false', aria-controls='navbar' ) span.sr-only Toggle navigation span.icon-bar span.icon-bar span.icon-bar a.navbar-brand (href='#') TODO List #navbar.collapse.navbar-collapse ul.nav.navbar-nav

.container +yield

.footer .container p.text-muted TODO List, 2014. Здесь нужно понимать, что это не совсем привычный нам jade, с его миксинами, джаваскриптом и инклудами. Jade должен скомпилироваться в шаблон spacebars, и это накладывает некоторые особенности. От jade, можно сказать мы заберем только синтаксис, остальное нам просто не нужно. В данном шаблоне используется конструкция +yield, это конструкция означает, что вместо нее будет отрендерен шаблон yield, это особенность iron: router, он автоматически подставит нужный шаблон в зависимости от пути, чуть позже мы займемся роутерами, а сейчас можно внести косметические изменения в верстку и посмотреть на результат.

// client/styles/main.less html { position: relative; min-height: 100%; }

body { margin-bottom: 60 px;

& > .container{ padding: 60 px 15 px 0; } }

.footer { position: absolute; bottom: 0; width: 100%; height: 60 px; background-color: #f5f5f5;

.container .text-muted { margin: 20 px 0; } } При изменениях стилей, кстати, не требуется обновлять страницу в браузере, достаточно сохранить файл, и они сразу же применятся, вот такой удобный инструмент из коробки есть для верстальщиков в метеоре.

my_helloworld

В самом метеоре нет стандартного механизма роутинга, я предлагаю использовать пакет iron: router, он хорошо документирован, активно поддерживается, обладает богатым функционалом и также является самым популярным решениям для роутинга в контексте метеора.Еще эту библиотеку можно использовать для серверного роутинга. Например, мне, на реальном проекте, это понадобилось для авторизации пользователей, так как основной проект сделан на Ruby on Rails, а пользователям нет нужды думать, что это два различных приложения и проходить в них авторизацию дважды. Вообще для серверного роутинга и создания REST api для метеора есть несколько популярных подходов.

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

Для начала зададим ссылки на наши страницы

//- client/layouts/application.jade //- … #navbar.collapse.navbar-collapse ul.nav.navbar-nav li a (href='/') Home li a (href='/about') About Создаем контроллеры в папке клиентских роутеров, пока это будут просто заглушки

# client/routes/home.coffee Router.route '/', name: 'home' class @HomeController extends RouteController

action: → console.log 'Home Controller' super ()

# client/routes/about.coffee Router.route '/about', name: 'about' class @AboutController extends RouteController

action: → console.log 'About Controller' super () В функцию Router.route нужно передать два параметра, первый это путь, причем путь может быть паттерном (например: /: user/orders/: id/info), все параметры из паттерна будут доступны в объекте контроллера, через свойство params. Вторым параметром передается объект с опциями. Чтобы вынести всю логику отдельно от простого описания пути и имени, можно создать контроллеры, в нашем случае это простые заглушки, здесь в свойствах мы не указываем явно имена контроллеров, потому что по умолчанию iron: router пытается найти контроллер с именем Controller, и конечно, наши контроллеры должны быть доступны глобально, в кофескрипте мы это делаем, привязывая переменную к текущему контексту, в обычном js, достаточно просто объявить переменную не через var.

К слову, в метеоре не используется, например amd для загрузки кода, файлы просто загружаются в определенной последовательности. Поэтому все взаимодействие между модулями, описанными в разных файлах, осуществляется через глобальные переменные. Что, как по мне, достаточно удобно, а при использовании кофе, случайно объявить глобальную переменную достаточно сложно, и она сразу будет заметна.

iron: router также попытается автоматически отрендерить шаблон, с именем роута (но шаблоны можно указать и явно), создадим их //- client/components/home/home.jade template (name='home') h1 Home

//- client/components/about/about.jade template (name='about') h1 About Можно открыть браузер и убедиться, что наш роутинг работает, покликав на ссылки в шапке. Причем работает без обновления страницы.

base_routing

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

Здесь можно посмотреть, что получилось в итоге, а здесь посмотреть код проекта в текущем состоянии. Многие технические задания, приходящие к нам в компанию, первой задачей описывают систему пользователей. Так как это довольно распространенная задача, считаю необходимым и в нашем уроке рассмотреть способы аутентификации пользователей, тем более метеор для этого предоставляет стандартные средства.Мы не будем сильно углубляться в механизмы, а просто используем готовые решения, которые позволят нам создавать пользователей через логин/пароль или сервисы google и github. Я привык в рельсах настраивать связку devise и omniauth парой генераторов и несколькими строчками в конфиге. Так вот метеор мало того, что предоставляет это из коробки, так еще и настройка сервисов происходит максимально просто.

Установим следующие пакеты:

accounts-base — базовый пакет для пользователей приложения на метеоре; accounts-password, accounts-github, accounts-google — добавим поддержку для аутентификации через логин/пароль и сервисы github и google; ian: accounts-ui-bootstrap-3 — пакет для упрощения интеграции аккаунтов в приложение на бутстрапе. Пакет ian: accounts-ui-bootstrap-3 нам позволит одной строчкой добавить форму аутентификации/регистрации в приложение, а также предоставит интерфейс к настройке сторонних сервисов. Сам проект, там есть небольшая документация и скриншоты того как выглядят интеграция формы и настройка сервисов.Модифицируем нашу шапку

//- client/layouts/application.jade //- … #navbar.collapse.navbar-collapse ul.nav.navbar-nav li a (href='/') Home li a (href='/about') About ul.nav.navbar-nav.navbar-right //- шаблон кнопки авторизации пользователя //- идет в пакете ian: accounts-ui-bootstrap-3 +loginButtons И получим следующий результат

base_auth_form

После конфигурации можем убедиться, что токены авторизации сохранились в базе данных. $ meteor mongo MongoDB shell version: 2.4.9 connecting to: 127.0.0.1:3001/meteor meteor: PRIMARY> show collections meteor_accounts_loginServiceConfiguration meteor_oauth_pendingCredentials system.indexes users meteor: PRIMARY> db.meteor_accounts_loginServiceConfiguration.find () { «service» : «github», «clientId» :»», «secret» :»», »_id» : «AjKrfCXAioLs7aBTN» } { «service» : «google», «clientId» :»», «secret» :»», »_id» : «HaERjHLYmAAhehskY» } Сконфигурируем нашу систему пользователей, так как я хочу настроить верификацию адреса электронной почты, необходимо настроить smtp, кстати для отправки email используется пакет email. Он не входит в стандартный набор метеора, поэтому его необходимо установить вручную, если вам нужна работа с почтой.

# server/config/smtp/coffee smtp = username: «meteor-todo-list@yandex.ru» password: «meteor-todo-list1234» server: «smtp.yandex.ru» port:»587»

# Экранируем символы _(smtp).each (value, key) → smtp[key] = encodeURIComponent (value)

# Шаблон url доступа к smtp url = «smtp://#{smtp.username}:#{smtp.password}@#{smtp.server}:#{smtp.port}»

# Задаем переменную окружения, метеор будет использовать данные из нее process.env.MAIL_URL = url И сконфигурируем аккаунты, что бы метеор запрашивал подтверждение адреса электронной почты.

# server/config/accounts.coffee emailTemplates = from: 'TODO List ' siteName: 'Meteor. TODO List.'

# заменяем стандартные настройки для почты _.deepExtend Accounts.emailTemplates, emailTemplates

# включаем верификацию Accounts.config sendVerificationEmail: true

# добавляем кастомную логику при регистрации пользователей Accounts.onCreateUser (options = {}, user) → u = UsersCollection._transform (user) options.profile ||= {} # сохраняем хеш адреса, чтобы можно было получит аватар для пользователя # у которого не указан публичный адрес почты options.profile.emailHash = Gravatar.hash (u.getEmail () || ») # запоминаем сервис, через который пользователь зарегистрировался options.service = _(user.services).keys ()[0] if user.services # сохраняем дополнительные параметры и возвращаем объект, # который запишется в бд _.extend user, options В нашем приложении не будет возможности подключать несколько сервисов к одному аккаунту, так как это требует тонкой настройки. Возможно скоро в метеоре проработают данный момент, но пока существует готовое, более менее нормальное, решение mondora: connect-with, но оно еще сырое. Можно попытаться самим мержить аккаунты, в этом нет ничего сложного, и в сети есть множество примеров и других решений: раз, два, три.

Также по аккаунтам есть подробная документация, мы всего лишь поставили пакеты и видим магию, но под капотом это происходит не многим сложнее.

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

Следующим шагом мы займемся страницей пользователя, но прежде чем преступить необходимо рассмотреть как реализованы некоторые вещи в метеоре. При создании проекта, автоматически были добавлены два пакета autopublish и insecure, так вот сейчас самое время от них избавиться, так как они предоставляет пользователю безграничный доступ ко всем коллекциям в бд, и их можно использовать только для прототипирования. Удаляются пакеты командой $ meteor remove Коллекции Коллекции в метеоре можно сравнить с коллекциями в монге, собственно они с ними же и работают, и у них также есть методы find, insert, update, upsert (агрегацию можно организовать на сервере при помощи пакета zvictor: mongodb-server-aggregation). Одна из коллекций у нас уже создана и доступ к ней можно получить через Meteor.users, например, попробуйте выполнить в консоли браузера Meteor.users.findOne (). Здесь важно отметить, что все данные коллекций кешируются в браузере, и если выполнить миллион раз в цикле на клиенте Meteor.users.find (options).fetch (), то ничего кроме браузера вы не нагрузите. Это достигается при помощи библиотеки minimongo, которая достаточно умная, что бы делать выборку в зависимости от переданных параметров на клиенте.С голыми данными не очень приятно работать, хотелось бы добавить бизнес-логику в объекты коллекции, это можно сделать при помощи функции _transform у коллекции, в которую передаются объекты после получения их с сервера и там их уже можно обработать, однако чтобы не вникать в эти тонкости, можно воспользоваться пакетом dburles: collection-helpers, который к коллекции добавляет метод helpers, куда можно передать объект, от которого будут наследоваться все данные.

Установим пакет, и напишем методы для обновления данных о пользователе. Также при создании пользователя мы добавили вычисляемое поле с хешем аватара пользователя в сервисе Gravatar — добавим метод который сможет возвращать ссылку на изображение с некоторыми параметрами. Еще добавим методы для проверки сервиса регистрации пользователя и методы возвращающие различную публичную информацию.

# collections/users.coffee Users = Meteor.users

# статические методы и свойства _.extend Users, # список полей доступных для редактирования allowFieldsForUpdate: ['profile', 'username']

# Добавляем методы и свойства в модель Users.helpers # метод обновления пользователя, можно вызывать прямо на клиенте update: (data) → Users.update @_id, data

# метод для обновления, который будет только устанавливать данные # сразу позаботимся о запрещенных полях set: (data) → d = {} f = _(Users.allowFieldsForUpdate) for key, value of data when f.include (key) d[key] = value @update $set: d

# метод мержить текущие данные с переданными, # чтобы потом их можно было использовать для обновления # и нечего не потерять merge: (data) → current = @get () @set _.deepExtend (current, data)

# получение только данных модели, все методы и свойства, # указанные здесь находятся в прототипе get: → r = {} r[key] = @[key] for key in _(@).keys () r

# список все адресов почты getEmails: → p = [@profile?.email] s = _(@services).map (value, key) → value?.email e = _(@emails).map (value, key) → value?.address _.compact p.concat (e, s)

# основной адрес getEmail: → @getEmails ()[0]

# публичная информация getUsername: → @username || @_id getName: → @profile?.name || «Anonymous» getPublicEmail: → @profile?.email

urlData: → id: @getUsername ()

# вычисляем ссылку на граватар, на основе адреса почты # или хеша автоматически вычисленного при регистрации getAvatar: (size) → size = Number (size) || 200 options = s: size d: 'identicon' r: 'g' hash = »00000000000000000000000000000000» if email = @getPublicEmail () hash = Gravatar.hash (email) else hash = @profile?.emailHash || hash Gravatar.imageUrl hash, options

# проверка сервиса используемого при регистрации isFromGithub: → @service == 'github' isFromGoogle: → @service == 'google' isFromPassword: → @service == 'password'

# текущий пользователь может редактировать # некоторые данные о себе isEditable: → @_id == Meteor.userId ()

# Экспорт коллекции @UsersCollection = Users Вроде разобрались, что такое коллекции в метеоре, следует упомянуть, что нежелательно хранить состояния в модели, так как все данные в коллекции реактивны, если изменится запись в бд, то сохраненный где-то в памяти, объект модели потеряет свою актуальность, и последующая работа с ним может привести нас к использованию устаревших данных, позже на примерах рассмотрим, как можно работать с моделями.

Публикации Я создал три записи пользователя в бд $ meteor mongo meteor: PRIMARY> db.users.count () 3 А когда пытаюсь получить данные в браузере, то не нашел ни одной записи без аутентификации и одну (собственную) в противном случае.

fail_publish

В данном приложении не будем скрывать пользователей от всех, просто скроем приватную информацию, вроде токенов аутентификации.

Так как мы удалили пакет autopublish, теперь процессом публикации данных необходимо заняться вручную, это позволит контролировать нам данные передаваемые пользователю.

Опубликуем коллекцию пользователей.

# server/publications/users.coffee Meteor.publish 'users', (limit = 20) → UsersCollection.find {}, fields: service: 1 username: 1 profile: 1 limit: limit Данный код будет предоставлять доступ к пользователям всем желающим, необходимо только подписаться, я сразу подумал о том чтобы предоставить пользователям возможность постраничной загрузки данных, в случае если не указать лимит выдачи, все записи о пользователях сразу будут выгружены, при подписки на данную публикацию, что не очень хорошо по понятным причинам, тоже самое происходить при использовании autopublish, только автоматически и со всеми коллекциями.

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

# server/publications/profile.coffee Meteor.publish 'profile', → # проверям авторизован ли пользователь, # запрашивающий подписку if @userId # подписываем на его запись в бд UsersCollection.find { _id: @userId }, fields: service: 1 username: 1 profile: 1 emails: 1 else # просто говорим, что все готово @ready () return Второй параметр передаваемый в метод Meteor.publish, это функция, которая должна вернуть курсор коллекции. Эта функция может принимать любое количество аргументов и она выполняется в контексте объекта, в котором доступны некоторые методы, позволяющие оповещать пользователя о различных изменениях в данных и предоставляющих доступ к некоторым свойствам подключения. Например, в публикации профиля мы используем метод ready, в случае когда пользователь не авторизован, это означает, что данные в публикации готовы, и на стороне клиента вызовется коллбек при подписке, но никаких данных он не получит. Подробнее о публикациях.

Подписки Я уже неоднократно заметил, что для получения данных и для отслеживания изменений в них, сперва необходимо подписаться на публикации, вообще все что происходит с данными в метеор приложении можно легко отслеживать и контролировать, а если вы просто создаете прототип, где это для вас не ключевые моменты, всегда можно воспользоваться пакетом autopublish.Мы для подписок будем использовать iron: router, и он будет контролировать весь необходимый процесс, так как для ручного управления за этим процессом придется следить за многим, а данная библиотека решает все проблемы. Некоторые данные желательно выдавать постранично, поэтому прежде чем создать контроллер для пользователей, мы немного абстрагируемся и создадим класс, обладающий функционалом для управления страницами и который будет наследоваться от контроллера библиотеки iron: router.

# client/lib/0.pageable_route_controller.coffee varName = (inst, name = null) → name = name && »_#{name}» || » »#{inst.constructor.name}#{name}_limit»

class @PagableRouteController extends RouteController

pageable: true # будем проверять, что это за контроллер perPage: 20 # количество данных на одной странице

# количество загружаемых данных limit: (name = null) → Session.get (varName (@, name)) || @perPage

# следующая страница incLimit: (name = null, inc = null) → inc ||= @perPage Session.set varName (@, name), (@limit (name) + inc)

# сборс количества resetLimit: (name = null) → Session.set varName (@, name), null

# все ли данные были загруженны? loaded: (name = null) → true Давайте еще создадим шаблон в виде кнопки, при клике на которую будет вызываться метод incLimit, для текущего контроллера, конечно если он поддерживает данный функционал. Можно бы было сделать и бесконечный скроллинг, но так проще.

//- client/components/next_page_button/next_page_button.jade template (name='nextPageButton') unless loaded a.btn.btn-primary.btn-lg.NextPageButton (href = '#') | More # client/components/next_page_button/next_page_button.coffee Template.nextPageButton.helpers loaded: → ctrl = Router.current () if ctrl.pageable ctrl.loaded (@name) else true

Template.nextPageButton.events 'click .NextPageButton': (event) → ctrl = Router.current () if ctrl.pageable ctrl.incLimit (@name, @perPage) Здесь мы для компонента уже задаем некоторую логику. Как можно заметить все шаблоны складываются в глобальное пространство имен Template. Обратится к шаблону мы можем через Template.. Для описания методов используемых в шаблоне нужно использовать метод helpers, куда передается объект с методами. В данном примере мы описываем лишь один метод loaded, который проверяет, что из себя представляет текущий контроллер и отдает результат, показывающий все ли данные были загружены. В самом шаблоне мы дергаем этот метод в конструкции unless loaded, также в шаблоне можно забирать данные из текущего контекста. Хелперы шаблона можно сравнить с прототипом объекта, при использовании их в шаблоне, но внутри самой функции есть ограничения, так как каждый хелпер вызывается примерно так .apply (context, arguments), то есть у нас нет возможности обратится ко всем хелперам шаблона, внутри функции, что в общем-то иногда может мешать.

Для обработки событий шаблона, нужно их описать в методе events, куда передается объект, с ключами следующего формата . В обработчик передается jQuery событие и шаблон, в котором было вызвано событие, так как мы можем обрабатывать дочерние события в родительском шаблоне, это иногда может оказаться полезным.

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

# client/routes/users.coffee Router.route '/users', name: 'users' class @UsersController extends PagableRouteController

# количество пользователей на одной странице perPage: 20

# подписываемся на коллекцию пользователей, с заданными лимитом, # чтобы не получать лишние данные # # подписка происходит через данный метод, чтобы iron: router # не рендерил шаблон загрузки страницы, каждый раз при обновлении # подписки subscriptions: → @subscribe 'users', @limit ()

# возвращаем всех пользователей из локальной коллекции data: → users: UsersCollection.find ()

# все ли пользователи загружены? loaded: → @limit () > UsersCollection.find ().count ()

# сбрасываем каждый раз лимит, при загрузки страницы onRun: → @resetLimit () @next () В методе subscriptions происходит подписка к публикации users. Есть еще практически аналогичный метод waitOn, только в нем роутер будет ожидать пока все данные выгрузятся, а после отрендерит страницу, до этого момента он будет отображать шаблон, который можно задать через свойство loadingTemplate. Данные, возвращаемые методом data, будут привязаны к шаблону, и мы сможем их использовать через текущий контекст. UsersCollection.find () возвращает курсор, а не сами данные, но блейз будет все превращения делать за нас, как-будто мы работаем уже с готовыми данными. Так как мы подписываемся на ограниченное количество данных, вызов UsersCollection.find ().fetch () вернет нам лишь данные, загруженные на клиент, то есть если мы, например, установим лимит 1, то и find будет работать только с загруженной выборкой (одной записью), а не всеми данными в коллекции из базы. К примеру здесь мы переопределяем метод loaded, думаю суть его ясна, но следует помнить, что count будет возвращать нам количество локальных записей, а значит будет равен limit, пока все данные не будут выгружены, поэтому и условие строго больше.

В iron: router есть несколько хуков, например, нам было бы не плохо каждый раз при открытии страницы с пользователями сбрасывать лимит загруженных. Иначе если мы ранее выгрузили большой объем данных, то страница может долго рендериться. Поэтому для сброса лимита данных удобно использовать хук onRun. Выполняется он один раз, при загрузке страницы. Есть только момент, что при горячей замене кода, которую выполняет метеор, после сохранения файлов с кодом, этот хук выполнятся не будет, так что вручную обновляйте страницу при дебаге контроллера, использующего этот хук (с другими такой проблемы нет). Еще про хуки и подписки.

Реактивные переменные и функции Вот мы подписались на публикацию, но все равно может быть не понятно почему клики по кнопке из шаблона nextPageButton, будет приводить нас к загрузке новой порции данных, а все благодаря манипуляциям с объектом Session в PagableRouteController. Данные в этом объекте являются реактивными, и iron: router автоматически будет отслеживать в них изменения. Можете в консоли браузера попробовать набрать Tracker.autorun (function () { console.log ('autorun test', Session.get ('var')); }) И попробовать изменить значение с помощью вызова Session.set ('var', 'value'), результат не заставит себя ждать.

reactive_var

Именно благодаря подобному механизму, iron: router понимает когда нужно обновить подписку, таким же образом данные в шаблонах обновляются автоматически. Подробнее про реактивные переменные хорошо описано в официальной документации, и помимо переменных в Session есть возможность создать реактивные объекты, с методами set и get для управления значениями, которые тоже будут отслеживаться трекером и шаблонами. А трекер, это что-то вроде слушателя, вы можете создать функцию, которая не будет содержать реактивных переменных, но тоже будет отслеживаться трекером, для этого нужно воспользоваться Tracker.Dependency. Вообще у данной библиотеки есть и другие возможности, но на практике мне их применять не приходилось, возможно и зря.

Еще один небольшой пример, который можно выполнить в консоли браузера:

var depend = new Tracker.Dependency (); var reactFunc = function () { depend.depend (); return 42; } Tracker.autorun (function () { console.log (reactFunc ()); }); // 42 depend.changed () // 42 depend.changed () // 42 Еще немного о подписках Я рассказал как можно использовать подписки на примере iron: router, но это не единственный механизм. Главное следует помнить, что использовать подписки нужно осторожно, иначе вы рискуете выгрузить большой объем данных и автоматически отслеживать в них изменения, там где это не нужно. iron: router предоставляет нам очень простой способ управления подписками, он сам отключит все не нужные, подключит нужные, обновит текущие в случае необходимости, как, например, это происходит при загрузке следующей страницы у нас.Давайте заверстаем список пользователей и убедимся, что все это работает на практ

© Habrahabr.ru