Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ovcharik/meteor-getting-started
Урок для хабры. Разработка первого метеор приложения.
https://github.com/ovcharik/meteor-getting-started
coffeescript jade less meteor tutorial
Last synced: 3 months ago
JSON representation
Урок для хабры. Разработка первого метеор приложения.
- Host: GitHub
- URL: https://github.com/ovcharik/meteor-getting-started
- Owner: ovcharik
- Created: 2014-10-31T23:55:14.000Z (about 10 years ago)
- Default Branch: master
- Last Pushed: 2014-11-11T18:46:10.000Z (about 10 years ago)
- Last Synced: 2024-09-28T08:42:35.020Z (4 months ago)
- Topics: coffeescript, jade, less, meteor, tutorial
- Language: CoffeeScript
- Size: 1.56 MB
- Stars: 34
- Watchers: 5
- Forks: 12
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Meteor. Разрабатываем TODO List.
В данном уроке я не хочу обсуждать, почему метеор убийца веба, тем более я так не считаю, но определенную симпатию к этому фреймворку имею. Поэтому хочу показать с чего можно начать при разработке приложения на нем, какие есть пакеты и вообще, что такое этот метеор.
Сразу хочу сказать, что у меня нет большого опыта в разработке веб приложений. Я занимаюсь этим всего около двух лет, а с метеором знаком вообще лишь пару месяцев.
Еще хочу предупредить, что урок получился достаточно объемным, но кода в нем было написано в разы меньше, чем текста. Просто хочу поделиться опытом как можно использовать метеор не при создании простенького примера, и заострить внимание на различных моментах, которые я посчитал важными. Поэтому в уроке будет использоваться множество сторонних пакетов, облегчающих процесс разработки.
И еще одно предупреждение: в данном уроке будут использоваться
следующие технологии для непосредственного написания примера:* [jade](http://jade-lang.com/) - html препроцессор;
* [less](http://lesscss.org/) - css препроцессор;
* [coffeescript](http://coffeescript.org/) - язык программирования, компилируемый в javascript.Видео, демонстрирующее приложение, полученное в ходе урока
[![result](http://img.youtube.com/vi/ATxB_CzCv8U/0.jpg)](http://www.youtube.com/watch?v=ATxB_CzCv8U)
И кому все еще интересно, добро пожаловать под кат.
+-------------+
| Продолжение |
+-------------+## Установка Meteor
Сам метеор базируется на `nodejs` и `mongodb`, так же у метеора нет поддержки Windows, и если вы собрались пощупать метеор вам необходимо обзавестись операционной системой Linux или MacOS.
Первым делом устанавливаем [nodejs](http://nodejs.org/download/) и [mongodb](http://www.mongodb.org/downloads).
Следующим шагом необходимо установить метеор. Он не лежит в 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](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/helloworld.png)
Теперь, после проверки работоспособности нового проекта, файлы, находящиеся в корне проекта, можно удалить - нам они не особо интересны. Также можно заметить, что была создана директория `.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](https://atmospherejs.com/). Собственно названия пакетов:
* `less`, `coffeescript` - это официальные пакеты, поэтому не содержат имя автора;
* `mquandalle:jade` - а вот это не официальный пакет, поэтому название состоит из двух составляющих, но выполнен он хорошо, и никаких проблем при его использовании у меня не возникало.В пакеты `less` и `coffeescript` встроена поддержка `sourcemap`, поэтому и процесс дебага в браузере будет простым, `sourcemap` поддерживается самим метеором: он предоставляет необходимый api для подключения данного функционала, поэтому нам не придется, что-то специально настраивать.
По ходу разработки мы добавим еще несколько популярных пакетов, и я постараюсь описать назначения каждого. Кстати `jquery` и `underscore` уже включены в метеор, как и множество других пакетов, полный список можно посмотреть в файле `./.meteor/versions`, в созданном проекте.
## Структура приложения
Теперь, по-моему мнению, самое время разобраться с тем как метеор подключает файлы в проект, и какие способы регуляции этого существуют. Здесь нам не потребуется писать конфигурационные файлы для `grant` или `gulp`, что бы скомпилировать шаблоны, стили и скрипты, метеор уже позаботился об этом. Для скафолдинга существует [проект](https://github.com/Pent/generator-meteor) для 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`, как это мы привыкли делать в других проектах, однако разработчики фреймворка не рекомендуют подобный подход.
Также для регуляции среды выполнения в общих файлах можно воспользоваться следующими конструкциями:
```coffeescript
if Meteor.isClient
# Код, выполняемый только в браузере
if Meteor.isServer
# Код, выполняемый только на сервере
```И для регулирования времени выполнения скриптов мы можем использовать метод `Meteor.startup()`, в браузере это аналог функции `$` из библиотеки `jQuery`, а на сервере, код в данной функции выполнится сразу же после загрузки всех скриптов в порядке загрузки этих файлов. [Подробнее об этих переменных и методах](https://docs.meteor.com/#/full/core).
## Базовый шаблон приложения
Для верстки я буду использовать Bootstrap, да знаю, что он всем приелся, но верстальщик из меня никакой, а с бутстрапом я более менее знаком.
Для этого устанавливаем пакет `mizzao:bootstrap-3` - он самый популярный среди прочих, и думаю при его использовании у нас не должно возникнуть проблем.
Далее создаем в папке `client/layouts` файл `head.jade`. Это будет единственный файл в нашем приложении не имеющий формат шаблона, короче просто создадим шапку страницы, а позже разберем что такое шаблоны.
```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` ([репозиторий](https://github.com/EventedMind/iron-router)).
После установки в директории `client/config` создаем файл `router.coffee`, со следующим содержанием:
```coffeescript
# client/config/router.coffee
Router.configure
layoutTemplate: "application"
```Очевидно, что здесь мы задаем базовый шаблон для нашего приложения, он будет называться `application`. Поэтому в папке `layouts` создаем файл `application.jade`. В этом файле мы опишем шаблон, некоторую сущность, которая на этапе сборки приложения превратится в код на `javascript`. Кстати в метеоре используется их собственный усатый шаблонизатор `spacebars` и библиотека `blaze`.
Если коротко, то процесс работы шаблонов выглядит следующим образом (на сколько я понял из документации). Шаблоны `spacebars` компилируются в объект библиотеки `blaze`, которая в последствии будет работать непосредственно с `DOM`. В описании к проекту есть сравнение с другими популярными библиотеками:
* по сравнению с обычными текстовыми шаблонизаторами блейз работает с домом, он не будет заново рендерить весь шаблон, что бы поменять один аттрибут, и у него нет проблем с вложенными шаблонами;
* по сравнению с шаблонами `Ember`, блейз рендерит только изменения, нет нужды в явных дата-байдингах и в описаниях зависимостей между шаблонами;
* по сравнению с шаблонами `angular` и `polymer`, блейз имеет понятный и простой синтаксис, меньший порог входа и вообще не позиционируется как технология будущего, а просто работает;
* по сравнению с `React` имеет простой синтаксис описания шаблонов и простое управление данными.Это я практически перевел параграф из официального [описания](https://atmospherejs.com/meteor/blaze) библиотеки, так что прошу не кидаться в меня камнями, если с чем-то не согласны. Сам я сталкивался с этими технологиями (кроме `ember`) и в принципе согласен с авторами библиотеки, из минусов в блейзе хочу заметить завязку на метеоре.
Но мы в своем проекте не используем явно ни `blaze`, ни `spacebars`. Для `jade` шаблонов процесс компиляции имеет такую последовательность: `jade` -> `spacebars` -> `blaze`.
Все шаблоны в метеор описываются в теге `template`, где должен быть аттрибут с именем шаблона. Помните, мы в настройках роутера указали `layoutTemplate: "application"`, вот `application`, как раз и является именем шаблона.
Вроде разобрались что такое шаблоны в метеоре, самое время сверстать каркас страницы, он будет состоять из шапки и подвала.
```jade
//- 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`, он автоматически подставит нужный шаблон в зависимости от пути, чуть позже мы займемся роутерами, а сейчас можно внести косметические изменения в верстку и посмотреть на результат.
```less
// client/styles/main.less
html {
position: relative;
min-height: 100%;
}body {
margin-bottom: 60px;& > .container{
padding: 60px 15px 0;
}
}.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
background-color: #f5f5f5;.container .text-muted {
margin: 20px 0;
}
}
```При изменениях стилей, кстати, не требуется обновлять страницу в браузере, достаточно сохранить файл, и они сразу же применятся, вот такой удобный инструмент из коробки есть для верстальщиков в метеоре.
![my_helloworld](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/my_helloworld.png)
## Роутинг
В самом метеоре нет стандартного механизма роутинга, я предлагаю использовать пакет `iron:router`, он [хорошо документирован](https://github.com/EventedMind/iron-router/blob/devel/Guide.md), активно поддерживается, обладает богатым функционалом и также является самым популярным решениям для роутинга в контексте метеора.
Еще эту библиотеку можно использовать для серверного роутинга. Например, мне, на реальном проекте, это понадобилось для авторизации пользователей, так как основной проект сделан на `Ruby on Rails`, а пользователям нет нужды думать, что это два различных приложения и проходить в них авторизацию дважды. Вообще для серверного роутинга и создания REST api для метеора есть несколько популярных [подходов](http://www.meteorpedia.com/read/REST_API).
Создадим базовые роутеры, чтобы на примере посмотреть как работает данная библиотека и каким функционалом обладает, а позже будем навешивать на них основной функционал.
Для начала зададим ссылки на наши страницы
```jade
//- client/layouts/application.jade
//- ...
#navbar.collapse.navbar-collapse
ul.nav.navbar-nav
li
a(href='/') Home
li
a(href='/about') About
```Создаем контроллеры в папке клиентских роутеров, пока это будут просто заглушки
```coffeescript
# client/routes/home.coffee
Router.route '/', name: 'home'
class @HomeController extends RouteControlleraction: ->
console.log 'Home Controller'
super()# client/routes/about.coffee
Router.route '/about', name: 'about'
class @AboutController extends RouteControlleraction: ->
console.log 'About Controller'
super()
```В функцию `Router.route` нужно передать два параметра, первый это путь, причем путь может быть паттерном (например: `/:user/orders/:id/info`), все параметры из паттерна будут доступны в объекте контроллера, через свойство `params`. Вторым параметром передается объект с опциями. Чтобы вынести всю логику отдельно от простого описания пути и имени, можно создать контроллеры, в нашем случае это простые заглушки, здесь в свойствах мы не указываем явно имена контроллеров, потому что по умолчанию `iron:router` пытается найти контроллер с именем `Controller`, и конечно, наши контроллеры должны быть доступны глобально, в кофескрипте мы это делаем, привязывая переменную к текущему контексту, в обычном js, достаточно просто объявить переменную не через `var`.
> К слову, в метеоре не используется, например `amd` для загрузки кода, файлы просто загружаются в определенной последовательности. Поэтому все взаимодействие между модулями, описанными в разных файлах, осуществляется через глобальные переменные. Что, как по мне, достаточно удобно, а при использовании кофе, случайно объявить глобальную переменную достаточно сложно, и она сразу будет заметна.
`iron:router` также попытается автоматически отрендерить шаблон, с именем роута (но шаблоны можно указать и явно), создадим их
```jade
//- client/components/home/home.jade
template(name='home')
h1 Home//- client/components/about/about.jade
template(name='about')
h1 About
```Можно открыть браузер и убедиться, что наш роутинг работает, покликав на ссылки в шапке. Причем работает без обновления страницы.
![base_routing](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/base_routing.png)
> По ходу разработки данного урока я попытаюсь все изменения в коде вносить в репозиторий, в соответствии с последовательностью изложения, что бы вы могли проследить весь процесс, так как в посте некоторые вещи могут быть пропущены. [Репозитарий](https://github.com/ovcharik/meteor-getting-started/commits/master).
[Здесь](http://hgsm-base-client-routing.meteor.com) можно посмотреть, что получилось в итоге, а [здесь](https://github.com/ovcharik/meteor-getting-started/tree/4035f092dc65cee6c93bfb308e890ad7fe17ba27/todo-list) посмотреть код проекта в текущем состоянии.
## Пользователи и аутентификация
Многие технические задания, приходящие к нам в компанию, первой задачей описывают систему пользователей. Так как это довольно распространенная задача, считаю необходимым и в нашем уроке рассмотреть способы аутентификации пользователей, тем более метеор для этого предоставляет стандартные средства.
Мы не будем сильно углубляться в механизмы, а просто используем готовые решения, которые позволят нам создавать пользователей через логин/пароль или сервисы `google` и `github`. Я привык в рельсах настраивать связку `devise` и `omniauth` парой генераторов и несколькими строчками в конфиге. Так вот метеор мало того, что предоставляет это из коробки, так еще и настройка сервисов происходит максимально просто.
Установим следующие пакеты:
* `accounts-base` - базовый пакет для пользователей приложения на метеоре;
* `accounts-password`, `accounts-github`, `accounts-google` - добавим поддержку для аутентификации через логин/пароль и сервисы `github` и `google`;
* `ian:accounts-ui-bootstrap-3` - пакет для упрощения интеграции аккаунтов в приложение на бутстрапе.Пакет `ian:accounts-ui-bootstrap-3` нам позволит одной строчкой добавить форму аутентификации/регистрации в приложение, а также предоставит интерфейс к настройке сторонних сервисов. [Сам проект](https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3/), там есть небольшая документация и скриншоты того как выглядят интеграция формы и настройка сервисов.
Модифицируем нашу шапку
```jade
//- 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](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/base_auth_form.png)
* [Результат](http://hgsm-base-user-auth.meteor.com/)
* [Репозиторий](https://github.com/ovcharik/meteor-getting-started/tree/78c28cce3af54989ca8c89c453e37c578e8f1d52/todo-list)После конфигурации можем убедиться, что токены авторизации сохранились в базе данных.
$ 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`. Он не входит в стандартный набор метеора, поэтому его необходимо установить вручную, если вам нужна работа с почтой.
```coffeescript
# server/config/smtp/coffee
smtp =
username: "[email protected]"
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
```И сконфигурируем аккаунты, что бы метеор запрашивал подтверждение адреса электронной почты.
```coffeescript
# 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`, но оно еще сырое. Можно попытаться самим мержить аккаунты, в этом нет ничего сложного, и в сети есть множество примеров и других решений: [раз](https://atmospherejs.com/mondora/connect-with), [два](http://www.meteorpedia.com/read/Merging_OAuth_accounts), [три](http://stackoverflow.com/questions/18358007/using-meteor-accounts-package-to-link-multiple-services).
Также по аккаунтам есть подробная [документация](https://docs.meteor.com/#/full/accounts_api), мы всего лишь поставили пакеты и видим магию, но под капотом это происходит не многим сложнее.
> Не стоит меня сильно пинать, за то что так поверхностно рассмотрел систему аккаунтов, просто хотел показать, что в ней нет ничего сложного. На подробное рассмотрение потребуется отдельный пост. А мы в уроке создали необходимый базовый функционал и можем продолжить идти к конечному результату.
Следующим шагом мы займемся страницей пользователя, но прежде чем преступить необходимо рассмотреть как реализованы некоторые вещи в метеоре.
## Коллекции, публикации и подписки.
При создании проекта, автоматически были добавлены два пакета `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](http://ru.gravatar.com/) - добавим метод который сможет возвращать ссылку на изображение с некоторыми параметрами. Еще добавим методы для проверки сервиса регистрации пользователя и методы возвращающие различную публичную информацию.
```coffeescript
# 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?.emailurlData: ->
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](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/fail_publish.png)
В данном приложении не будем скрывать пользователей от всех, просто скроем приватную информацию, вроде токенов аутентификации.
Так как мы удалили пакет `autopublish`, теперь процессом публикации данных необходимо заняться вручную, это позволит контролировать нам данные передаваемые пользователю.
Опубликуем коллекцию пользователей.
```coffeescript
# server/publications/users.coffee
Meteor.publish 'users', (limit = 20) ->
UsersCollection.find {},
fields:
service: 1
username: 1
profile: 1
limit: limit
```Данный код будет предоставлять доступ к пользователям всем желающим, необходимо только подписаться, я сразу подумал о том чтобы предоставить пользователям возможность постраничной загрузки данных, в случае если не указать лимит выдачи, все записи о пользователях сразу будут выгружены, при подписки на данную публикацию, что не очень хорошо по понятным причинам, тоже самое происходить при использовании `autopublish`, только автоматически и со всеми коллекциями.
Также мы ограничили видимость выгружаемых данных, ничего кроме информации из поля `profile` и имени пользователя, подписчики не смогут увидеть. Но например мы хотим предоставить доступ к адресам электронной почты для текущего пользователя, создадим еще одну публикацию.
```coffeescript
# 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`, в случае когда пользователь не авторизован, это означает, что данные в публикации готовы, и на стороне клиента вызовется коллбек при подписке, но никаких данных он не получит. [Подробнее](https://docs.meteor.com/#/full/meteor_publish) о публикациях.
### Подписки
Я уже неоднократно заметил, что для получения данных и для отслеживания изменений в них, сперва необходимо подписаться на публикации, вообще все что происходит с данными в метеор приложении можно легко отслеживать и контролировать, а если вы просто создаете прототип, где это для вас не ключевые моменты, всегда можно воспользоваться пакетом `autopublish`.
Мы для подписок будем использовать `iron:router`, и он будет контролировать весь необходимый процесс, так как для ручного управления за этим процессом придется следить за многим, а данная библиотека решает все проблемы. Некоторые данные желательно выдавать постранично, поэтому прежде чем создать контроллер для пользователей, мы немного абстрагируемся и создадим класс, обладающий функционалом для управления страницами и который будет наследоваться от контроллера библиотеки `iron:router`.
```coffeescript
# 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`, для текущего контроллера, конечно если он поддерживает данный функционал. Можно бы было сделать и бесконечный скроллинг но так проще.
```jade
//- client/components/next_page_button/next_page_button.jade
template(name='nextPageButton')
unless loaded
a.btn.btn-primary.btn-lg.NextPageButton(href = '#')
| More
``````coffeescript
# client/components/next_page_button/next_page_button.coffee
Template.nextPageButton.helpers
loaded: ->
ctrl = Router.current()
if ctrl.pageable
ctrl.loaded(@name)
else
trueTemplate.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`.
```coffeescript
# 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`. Выполняется он один раз, при загрузке страницы. Есть только момент, что при горячей замене кода, которую выполняет метеор, после сохранения файлов с кодом, этот хук выполнятся не будет, так что вручную обновляйте страницу при дебаге контроллера, использующего этот хук (с другими такой проблемы нет). Еще про [хуки](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#hooks) и [подписки](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#subscriptions).
### Реактивные переменные и функции
Вот мы подписались на публикацию, но все равно может быть не понятно почему клики по кнопке из шаблона `nextPageButton`, будет приводить нас к загрузке новой порции данных, а все благодаря манипуляциям с объектом `Session` в `PagableRouteController`. Данные в этом объекте являются реактивными, и `iron:router` автоматически будет отслеживать в них изменения. Можете в консоли браузера попробовать набрать
```javascript
Tracker.autorun( function() {
console.log( 'autorun test', Session.get('var') );
} )
```И попробовать изменить значение с помощью вызова `Session.set('var', 'value')`, результат не заставит себя ждать.
![reactive_var](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/reactive_var.png)
Именно благодаря подобному механизму, `iron:router` понимает когда нужно обновить подписку, таким же образом данные в шаблонах обновляются автоматически. Подробнее про реактивные переменные хорошо описано в официальной [документации](https://docs.meteor.com/#/full/session), и помимо переменных в `Session` есть возможность создать [реактивные объекты](https://docs.meteor.com/#/full/reactivevar_pkg), с методами `set` и `get` для управления значениями, которые тоже будут отслеживаться трекером и шаблонами. А [трекер](https://docs.meteor.com/#/full/tracker), это что-то вроде слушателя, вы можете создать функцию, которая не будет содержать реактивных переменных, но тоже будет отслеживаться трекером, для этого нужно воспользоваться [Tracker.Dependency](https://docs.meteor.com/#/full/tracker_dependency). Вообще у данной библиотеки есть и другие возможности, но на практике мне их применять не приходилось, возможно и зря.
Еще один небольшой пример, который можно выполнить в консоли браузера:
```javascript
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`, но это [не единственный механизм](https://docs.meteor.com/#/full/meteor_subscribe). Главное следует помнить, что использовать подписки нужно осторожно, иначе вы рискуете выгрузить большой объем данных и автоматически отслеживать в них изменения, там где это не нужно. `iron:router` предоставляет нам очень простой способ управления подписками, он сам отключит все не нужные, подключит нужные, обновит текущие в случае необходимости, как, например, это происходит при загрузке следующей страницы у нас.
Давайте заверстаем список пользователей и убедимся, что все это работает на практике.
```jade
//- client/components/users.jade
template( name='users' )
h1 Users
.row
//- это данные, которые передает роутер шаблону
+each users
.col-xs-6.col-md-4.col-lg-3
//- рендерим карточку пользователя
//- в блоке each контекст меняется, поэтому мы
//- можем не передавать в шаблон никаких параметров
+userCard
//- кнопка загрузки следующей страницы
+nextPageButton//- client/components/user_avatar/user_avatar.jade
//- унифицируем шаблон аватара, возможно понадобится добавить логику
template(name='userAvatar')
img(src="{{user.getAvatar size}}", alt=user.getUsername, class="{{class}}")//- client/components/user_card.jade
//- в этом шаблоне используются данные пользователя
//- а также функции описанные в модели ранее
template(name='userCard')
.panel.panel-default
.panel-body
.pull-left
+userAvatar user=this size=80.user-card-info-block
ul.fa-ul
//- сервис и имя пользователя
li
if isFromGithub
i.fa.fa-li.fa-github
else if isFromGoogle
i.fa.fa-li.fa-google
else
i.fa.fa-li
b= getName
//- идентификатор либо логин
li
i.fa.fa-li @
//- ссылка на пользователя
a(href="{{ pathFor route='users_show' data=urlData }}")= getUsername
//- адрес почты, если указан
if getPublicEmail
li
i.fa.fa-li.fa-envelope
= getPublicEmail
```![users_page](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/users_page.png)
В результате у нас работает пагинация, и так как все данные реактивны, новые пользователи системы добавятся на страницу автоматически, без каких-либо перезагрузок, потому что мы подписались на коллекцию, а значит любые изменения данных в базе на сервере, сразу же будут отображены на страницу пользователя. Можете попробовать в новой вкладке зарегистрировать нового пользователя, либо поменять значения прямо в базе при помощи утилиты `mongo` - изменения отобразятся на странице, и вам для этого не придется ничего делать.
И чтобы убедиться в том, что данный подход работает оптимально, можно посмотреть логи браузера. Я установил количество пользователей на страницу равное одному. Протокол DDP достаточно простой и легко читаемый, поэтому не буду вдаваться в подробности. В логах можно просто увидеть, что все ненужные подписки были отписаны, а пользователи загрузились всего три раза, по одному на каждое обновление подписки.
![users_log](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/users_log.png)
## Страница пользователя и еще немного о шаблонах
Давайте создадим страницу пользователя, где будет возможность изменять некоторые данные и завершим на этом работу с пользователями, чтобы можно было перейти к созданию собственных коллекций.
Для этого первым делом для авторизованного пользователя вместо домашней страницы будем показывать страницу текущего пользователя, модифицируем немного контроллер.
```coffeescript
# client/routers/home.coffee
Router.route '/', name: 'home'
class @HomeController extends PagableRouteController# авторизован ли пользователь?
isUserPresent: ->
!!Meteor.userId()# подписываемся на профайл если пользователь авторизован
# на сайте
waitOn: ->
if @isUserPresent()
@subscribe 'profile'# возвращаем данные о текущем пользователе, если такой имеется
data: ->
if @isUserPresent()
{ user: UsersCollection.findOne Meteor.userId() }# рендерим шаблон профайла если пользователь авторизован
# и домашнюю страницу в противном случае
action: ->
if @isUserPresent()
@render 'profile'
else
@render 'home'
```И также создадим контроллер, в котором можно будет просматривать профиль любого пользователя.
```coffeescript
# client/routers/user_show.coffee
Router.route '/users/:id', name: 'users_show'
class @UsersShowController extends PagableRouteController# используем уже готовый шаблон
template: 'profile'# подписываемся на нужного пользователя
waitOn: ->
@subscribe 'user', @params.id# ищем нужного пользователя
data: ->
user: UsersCollection.findOneUser(@params.id)
```Для удобства поиска пользователей либо по идентификатору, либо по логину я создал дополнительные методы в коллекции: один возвращает курсор, второй данные.
```coffeescript
# collections/users.coffee
# ...
_.extend Users,
# ...
findUser: (id, options) ->
Users.find { $or: [ { _id: id }, { username: id } ] }, optionsfindOneUser: (id, options) ->
Users.findOne { $or: [ { _id: id }, { username: id } ] }, options
```Данные для страницы пользователя получить пытаемся, а они не опубликованы, исправляем это.
```coffeescript
# server/publications/user.coffee
Meteor.publish 'user', (id) ->
UsersCollection.findUser id,
fields:
service: 1
username: 1
profile: 1
limit: 1
```Почти все готово, создадим шаблон и посмотрим на результат. При создании шаблона я решил создать компонент, который, в зависимости от прав доступа, будет давать возможность редактировать поле модели.
```jade
//- client/components/editable_field/editable_field.jade
//- вот тут солянка из вызовов хелперов
//- и обращений к данным контекста, кстати если имя хелпера
//- и свойства в текущем контексте совпадают
//- то предпочтение отдается хелперу
//- обратиться явно к контексту можно через this.
template(name='editableField')
.form-group.EditableFiled
if data.isEditable
div(class=inputGroupClass)
if hasIcon
.input-group-addon
if icon
i.fa.fa-fw(class='fa-{{icon}}')
else
i.fa.fa-fw=iconSymbol
input.Field.form-control(placeholder=placeholder, value=value, name=name)
else
if defaultValue
span.form-control-static
if hasIcon
if icon
i.fa.fa-fw(class='fa-{{icon}}')
else
i.fa.fa-fw=iconSymbol
= defaultValue
```Для интерполяции переменных в строках, в шаблонах, можно использовать усатые конструкции: `class='fa-{{icon}}'`, `icon` - это переменная.
```coffeescript
# client/components/editable_field/editable_field.coffee
Template.editableField.helpers
value: ->
ObjAndPath.valueFromPath @data, @pathname: ->
ObjAndPath.nameFromPath @scope, @pathhasIcon: ->
@icon || @iconSymbolinputGroupClass: ->
(@icon || @iconSymbol) && 'input-group' || ''Template.editableField.events
# кидаем событие выше, при изменении данных в инпуте
'change .Field': (event, template) ->
data = $(event.target).serializeJSON()
$(template.firstNode).trigger 'changed', [data]
``````jade
//- client/components/profile/profile.jade
template(name='profile')
//- смена контекста, и блок внутри не будет отрендерен,
//- если такого свойства нет
+with user
.profile-left-side
.panel.panel-default
.panel-body
.container-fluid
.row.row-bottom
//- аватар пользователя, параметром передаем конструкции
//- вида <ключ>=<значение>, которые сложатся в один объект
//- и станут контекстом шаблона userAvatar
+userAvatar user=this size=200 class='profile-left-side-avatar'
.row
//- редактируемые поля для текущего пользователя
+editableField fieldUsername
+editableField fieldName
+editableField fieldEmail.profile-right-side
h1 Boards
``````coffeescript
# client/components/profile/profile.coffee
Template.profile.helpers
fieldUsername: ->
data: @
defaultValue: @getUsername()
placeholder: 'Username'
scope: 'user'
path: 'username'
iconSymbol: '@'fieldName: ->
data: @
defaultValue: @getName()
placeholder: 'Name'
scope: 'user'
path: 'profile.name'
icon: 'user'fieldEmail: ->
data: @
defaultValue: @getPublicEmail()
placeholder: 'Public email'
scope: 'user'
path: 'profile.email'
icon: 'envelope'Template.profile.events
# отлавливаем изменения в редактируемых полях
# и обновляем пользователя
'changed .EditableFiled': (event, template, data) ->
user = template.data?.user
return unless user
data = data.user
user.merge data
```Как мне кажется, верстка метеоровских шаблонов в `jade`, достаточно семантична, не нужно задумываться о многих вещах и читать кучу документации - все и так достаточно очевидно. Но если у вас возникли проблемы с пониманием кода выше, советую полистать документацию к пакету [mquandalle:jade](https://atmospherejs.com/mquandalle/jade) и [spacebars](https://atmospherejs.com/meteor/spacebars). Просто у меня при знакомстве с версткой шаблонов в метеоре проблем не возникало, считаю, что они их, в самом деле, сделали очень удобными.
В общем все готово, открывайте форму аутентификации в шапке, входите в систему, и вместо заголовка "Home" на странице сразу же отобразится ваш профайл, без всяких перезагрузок.
![profile](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/profile.png)
* [Результат](http://hgsm-base-users.meteor.com/)
* [Репозитарий](https://github.com/ovcharik/meteor-getting-started/tree/2110ed5168155893fbaf27a29df0675070765d81/todo-list)Если вам что-то не понятно до текущего момента, то советую ознакомится с текущим состоянием проекта в [репозитарии](https://github.com/ovcharik/meteor-getting-started/tree/b1067c219e591de6d6eb387d10a107cfff180e69/todo-list), я старался комментировать в файлах все происходящее, также возможно стоит еще раз полистать написанное выше, может не совсем последовательно, но я старался уделить внимание всем ключевым моментам, и конечно же можно склонировать проект, на данном этапе и пощупать его руками. Дальше я собираюсь затронуть еще несколько тем, связанных с серверным кодом: как создавать свои собственные коллекции, как можно защищать данные в коллекциях от нежелательного редактирования, расскажу немного про использование RPC и использование библиотек `npm` на сервере.
## Еще про коллекции и подписки
Прежде чем приступим к созданию своих коллекций предлагаю создать механизм, который будет автоматически вычислять некоторые поля при вставки/изменении данных в бд. Для этого добавим пакет [aldeed:collection2](https://github.com/aldeed/meteor-collection2), в который входит [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema). Эти пакеты позволят нам легко валидировать данные, добавлять индексы к коллекции и прочее.
Добавим к пакету `aldeed:simple-schema` немного новых возможностей.
```coffeescript
# lib/simple_schema.coffee
_.extend SimpleSchema,# Данный метод будет из нескольких переданных объектов
# собирать одну схему и возвращать ее
build: (objects...) ->
result = {}
for obj in objects
_.extend result, obj
return new SimpleSchema result# Если добавить к схеме данный объект,
# то у модели появится два поля которые будут автоматически
# вычисляться
timestamp:
createdAt:
type: Date
denyUpdate: true
autoValue: ->
if @isInsert
return new Date
if @isUpsert
return { $setOnInsert: new Date }
@unset()updatedAt:
type: Date
autoValue: ->
new Date
```И создадим новую коллекцию
```coffeescript
# collections/boards.coffee
# схема данных
boardsSchema = SimpleSchema.build SimpleSchema.timestamp,
'name':
type: String
index: true'description':
type: String
optional: true # не обязательное поле# автоматически генерируем автора доски
'owner':
type: String
autoValue: (doc) ->
if @isInsert
return @userId
if @isUpsert
return { $setOnInsert: @userId }
@unset()# список пользователей доски
'users':
type: [String]
defaultValue: []'users.$':
type: String
regEx: SimpleSchema.RegEx.Id# регистрируем коллекцию и добавляем схему
Boards = new Mongo.Collection 'boards'
Boards.attachSchema boardsSchema# защита данных
Boards.allow
# создавать доски может любой авторизованный пользователь
insert: (userId, doc) ->
userId && true
# обновлять данные может только создатель доски
update: (userId, doc) ->
userId && userId == doc.owner# статические методы
_.extend Boards,
findByUser: (userId = Meteor.userId(), options) ->
Boards.find
$or: [
{ users: userId }
{ owner: userId }
]
, optionscreate: (data, cb) ->
Boards.insert data, cb# методы объектов
Boards.helpers
update: (data, cb) ->
Boards.update @_id, data, cbaddUser: (user, cb) ->
user = user._id if _.isObject(user)
@update
$addToSet:
users: user
, cbremoveUser: (user, cb) ->
user = user._id if _.isObject(user)
@update
$pop:
users: user
, cbupdateName: (name, cb) ->
@update { $set: {name: name} }, cbupdateDescription: (desc, cb) ->
@update { $set: {description: desc} }, cb# joins
getOwner: ->
UsersCollection.findOne @ownergetUsers: (options) ->
UsersCollection.find
$or: [
{ _id: @owner }
{ _id: { $in: @users } }
]
, optionsurlData: ->
id: @_id# экспорт
@BoardsCollection = Boards
```Первым делом при создании коллекции мы определили схему, это позволит нам валидировать данные и автоматически вычислять некоторые поля. Подробнее о валидации можно почитать на странице пакета [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema), там достаточно богатый функционал, а при установки дополнительного пакета `aldeed:autoform`, от тоже автора, можно генерировать формы, которые сразу же будут оповещать об ошибках, при создании записи.
Новую коллекцию в бд мы создаем вызовом `Boards = new Mongo.Collection 'boards'`, если ее нет, либо подключаемся к существующей. В принципе это весь необходимый функционал для создания новых коллекций, там есть [еще пара](https://docs.meteor.com/#/full/mongo_collection) опций, которые можно указать при создании.
С помощью метода `allow` у коллекции мы можем контролировать доступ к изменению данных в коллекции. В текущем примере мы запрещаем создавать новые записи в коллекции для всех неавторизованных пользователей, и разрешаем изменять данные только для создателя доски. Эти проверки будут осуществляться на сервере и можно не переживать, что какой-нибудь кулцхакер поменяет эту логику на клиенте. Также в вашем распоряжении есть практически аналогичный метод `deny`, думаю суть его ясна. Подробнее про [allow](https://docs.meteor.com/#/full/allow) и [deny](https://docs.meteor.com/#/full/deny).
При выводе карточки доски я хочу сразу отображать данные о создателе доски. Но если мы подпишемся только на доски, то эти данные поступать на клиент не будут. Однако публикации в метеоре дают возможность подписки на любые данные, даже автоматически вычисляемые, типа счетчиков коллекций и прочего.
```coffeescript
# server/publications/boards.coffee
Meteor.publish 'boards', (userId, limit = 20) ->
findOptions =
limit: limit
sort: { createdAt: -1 }if userId
# доски конкретного пользователя
cursor = BoardsCollection.findByUser userId, findOptions
else
# все доски
cursor = BoardsCollection.find {}, findOptionsinited = false
userFindOptions =
fields:
service: 1
username: 1
profile: 1# колбек для добавления создателя доски к подписке
addUser = (id, fields) =>
if inited
userId = fields.owner
@added 'users', userId, UsersCollection.findOne(userId, userFindOptions)# отслеживаем изменения в коллекции,
# что бы добавлять пользователей к подписке
handle = cursor.observeChanges
added: addUser
changed: addUserinited = true
# при инициализации сразу же добавляем пользователей,
# при помощи одного запроса в бд
userIds = cursor.map (b) -> b.owner
UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) =>
@added 'users', u._id, u# перестаем слушать курсор коллекции, при остановке подписки
@onStop ->
handle.stop()return cursor
```Так как монга не умеет делать запросы через несколько коллекций и выдавать уже обработанные данные, как это происходит в реляционных бд, нам придется доставать данные о создателей досок при помощи еще одного запроса, да и так удобнее работать в рамках моделей данных.
Первым делом в зависимости от запроса мы достаем из базы нужные доски, после этого нам необходимо еще одним запросом достать пользователей. Методы `added`, `changed` и `removed` в контексте публикации могут управлять данными передаваемыми на клиент. Если мы в публикации возвращаем курсор коллекции, то эти методы будут вызываться автоматически в зависимости от состояния коллекции, поэтому мы и возвращаем курсор, но дополнительно в самой публикации подписываемся на изменения данных в коллекции досок, и высылаем на клиент данные о пользователях по мере необходимости.
С помощью логов соединения по веб-сокетам либо при помощи [данной](https://github.com/arunoda/meteor-ddp-analyzer) утилиты, можно убедиться, что подобный подход будет работать оптимально. И тут важно понимать, что в нашем случае изменения в коллекции пользователей не будут синхронизироваться с клиентом, но так и задумывалось. Кстати для простого "джоина" можно просто возвращать массив курсоров в результате подписки.
Для отображения досок пользователей, я добавил новые подписки в роутеры и заверстал необходимые шаблоны, но все эти моменты мы уже рассмотрели выше, если вам интересны все изменения, то их можно увидеть [здесь](https://github.com/ovcharik/meteor-getting-started/commit/548ae3c4a6b8a752fa07c69e21ac3da929e1a70e). А в итоге мы должны получить следующее, правда доски придется создавать через консоль, что бы проверить работоспособность.
![boards](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/boards.png)
> Для создания реактивных публикаций также можно использовать пакет [mrt:reactive-publish](https://github.com/Diggsey/meteor-reactive-publish).
## Дорабатываем сервер
Давайте для досок добавим возможность задавать фоновое изображение, для этого нам необходимо настроить сервер, чтобы он смог принимать файлы, обрабатывать их, сохранять и отдавать при запросе.
### NPM
Для обработки изображений я привык использовать [ImageMagick](http://www.imagemagick.org/), и для нода есть соответствующие пакеты, которые предоставляют интерфейс к данной библиотеке. Чтобы дать метеору возможность использовать `npm` пакеты нужно добавить `meteorhacks:npm`, после этого все необходимые пакеты можно описать в файле `packages.json`. Например мне нужен только пакет [gm](https://github.com/aheckmann/gm) и мой `packages.json` будет выглядеть следующим образом:
```json
{
"gm": "1.17.0"
}
```Все пакеты `npm` подключенные через `meteorhacks:npm` буду оборачиваться в один метеоровский пакет, поэтому при сборке приложения через команду `meteor build` не возникнет никаких проблем и все зависимости автоматически разрешаться.
Подключать `npm` пакеты на сервере нужно через команду `Meteor.npmRequire()`, работает она также как и функция `require` в ноде.
### RPC и синхронные вызовы асинхронных функций
Для загрузки и обработки изображения создадим серверный метод, который можно будет вызывать с клиента.
```coffeescript
# server/lib/meteor.coffee
Meteor.getUploadFilePath = (filename) ->
"#{process.env.PWD}/.uploads/#{filename}"# server/methods/upload_board_image.coffee
# подключаем библиотеку для обработки изображения
gm = Meteor.npmRequire 'gm'# ресайз и сохранение изображения
resizeAndWriteAsync = (buffer, path, w, h, cb) ->
gm(buffer)
.options({imageMagick: true})
.resize(w, "#{h}^", ">")
.gravity('Center')
.crop(w, h, 0, 0)
.noProfile()
.write(path, cb)# делаем обработку изображения синхронной
resizeAndWrite = Meteor.wrapAsync resizeAndWriteAsync# регистрируем метод для загрузки изображения к доске
Meteor.methods
uploadBoardImage: (boardId, data) ->
board = BoardsCollection.findOne(boardId)
if board.owner != @userId
throw new Meteor.Error('notAuthorized', 'Not authorized')data = new Buffer data, 'binary'
name = Meteor.uuid() # уникальное имя для изображения
path = Meteor.getUploadFilePath nameresizeAndWrite data, "#{path}.jpg", 1920, 1080
resizeAndWrite data, "#{path}_thumb.jpg", 600, 400# сохраняем данные к доске
BoardsCollection.update { _id: boardId },
$set:
background:
url: "/uploads/#{name}.jpg"
thumb: "/uploads/#{name}_thumb.jpg"
return
```В методе `uploadBoardImage` мы принимаем идентификатор доски, к которой добавляется изображение и строку с бинарными данными этого изображения.
Если в методе будет брошено исключение, то оно передастся пользователю на клиент, первым параметром коллбека. А данные возвращенные методом придут на клиент вторым параметром коллбека.
Чтобы можно было использовать исключения и возвраты функций при асинхронном стиле программирования, в серверной части метеора есть метод оборачивающий асинхронные функции в синхронные, через библиотеку [fibers](https://github.com/laverdet/node-fibers). Если в кратце, благодаря этой библиотеке, вызовы обернутых функций не будут занимать очередь выполнения, так что на сервере можно писать синхронный код и не беспокоится о неправильной последовательности выполнения кода. Методом `Meteor.wrapAsync()` оборачиваются функции, которые последним параметром принимают коллбек. В этом коллбеке первым параметром должна идти ошибка, а вторым результат, такой формат параметров у всех стандартных библиотек в ноде. Если придет ошибка, то обернутая функция бросит исключение с этой ошибкой, иначе из функции вернется второй параметр переданный в коллбек.
### Роутинг
> Я понимаю, что для выдачи статики с сервера лучше использовать готовые и обкатанные решения по многим причинам, но тут я собираюсь отдавать статику нодом.
В метеоре для серверного роутинга есть стандартный пакет [webapp](https://docs.meteor.com/#/full/webapp), но у нас уже установлено гораздо более удобное решение в виде `iron:router`. Аналогично, как и на клиенте, создадим серверный маршрут.
```coffeescript
# server/routes/uploads.coffee
fs = Meteor.npmRequire 'fs'Router.route '/uploads/:file',
where: 'server'
action: ->
try
filepath = Meteor.getUploadFilePath(@params.file)
file = fs.readFileSync(filepath)
@response.writeHead 200, { 'Content-Type': 'image/jpg' }
@response.end file, 'binary'
catch e
@response.writeHead 404, { 'Content-Type': 'text/plain' }
@response.end '404. Not found.'
```Здесь главное роуту передать свойство `where: 'server'`, иначе он не будет работать. В действии мы пытаемся считать с диска указанный файл, так как в этой директории будут только изображения одного формата, я максимально упростил данный метод.
Объекты `request` и `response` доступные в контексте роута, это объекты классов из стандартной библиотеки нода [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) и [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse) соответственно.
> В `iron:router` есть еще [интерфейс](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#server-routing) для создания REST API.
### Использование RPC
Для использования давайте создадим форму добавления новой доски.
> Тут я еще создал автокомплит для добавления пользователей к доске, там также используется RPC, подробнее с реализацией можно ознакомится в [репозитарии](https://github.com/ovcharik/meteor-getting-started/tree/master/todo-list/client/components/users_autocomplete).
```jade
//- client/components/new_board_form/new_board_form.jade
template(name='newBoardForm')
//- панель с динамическими стилями
.panel.panel-default.new-board-panel(style='{{panelStyle}}')
.panel-body
h1 New board
form(action='#')
.form-group
input.form-control(type='text',placeholder='Board name',name='board[name]')
.form-group
textarea.form-control(placeholder='Description',name='board[description]')
.form-group
//- прячем инпут с файлом, но оставляем метку на этот инпут, для красоты
label.btn.btn-default(for='newBoardImage') Board image
.hide
input#newBoardImage(type='file', accept='image/*')
button.btn.btn-primary(type='submit') Submit
``````coffeescript
# client/components/new_board_form/new_board_form.coffee
# переменные для текущего пользовательского изображения
currentImage = null
currentImageUrl = null
currentImageDepend = new Tracker.Dependency# сброс пользовательского изображения
resetImage = ->
currentImage = null
currentImageUrl = null
currentImageDepend.changed()# загрузка изображения на сервер
uploadImage = (boardId) ->
if currentImage
reader = new FileReader
reader.onload = (e) ->
# вызов серверного метода
Meteor.call 'uploadBoardImage', boardId, e.target.result, (error) ->
if error
alertify.error error.message
else
alertify.success 'Image uploaded'
reader.readAsBinaryString currentImage# хелперы шаблона формы
Template.newBoardForm.helpers
# задаем фоновое изображение для формы,
# функция будет вызываться автоматически, так как имеет зависимость
panelStyle: ->
currentImageDepend.depend()
currentImageUrl && "background-image: url(#{currentImageUrl})" || ''# данный колбек срабатывает каждый раз, когда форма рендерится на страницу
Template.newBoardForm.rendered = ->
resetImage()# события формы
Template.newBoardForm.events
# при отправки формы, мы создаем новую запись
# если все прошло хорошо, загружаем изображение,
# и сбрасываем форму
'submit form': (event, template) ->
event.preventDefault()
form = event.target
data = $(form).serializeJSON()
BoardsCollection.create data.board, (error, id) ->
if error
alertify.error error.message
else
form.reset()
alertify.success 'Board created'
resetUsers()
uploadImage(id)
resetImage()# при выборе изображения меняем фон формы
# и запоминаем текущий выбор
'change #newBoardImage': (event, template) ->
files = event.target.files
image = files[0]
unless image and image.type.match('image.*')
resetImage()
returncurrentImage = image
reader = new FileReader
reader.onload = (e) =>
currentImageUrl = e.target.result
currentImageDepend.changed()reader.readAsDataURL(image)
```Тут для загрузки и обработки изображения мы выполняем удаленный метод через `Meteor.call`. Как видно, вызов удаленный процедуры на клиенте, мало чем отличается от обычного вызова функции, а все данные, переданные аргументами, будут загружены на сервер по веб-сокету. Для чтения пользовательских файлов я воспользовался [File API](http://www.w3.org/TR/file-upload/) из спецификации HTML5.
> Пример с загрузкой изображений возможно не самый удачный, но хорошо демонстрирует возможности серверной части метеора. Если вы пишете для продакшена, то можно воспользоваться готовым решением [CollectionFS](https://github.com/CollectionFS/Meteor-CollectionFS).
![new_board](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/new_board.png)
* [Результат](http://hgsm-boards.meteor.com/), загрузка картинок там не заработала, толи дело в ImageMagick, толи в невозможности создать свою директорию для хранения
* [Репозитарий](https://github.com/ovcharik/meteor-getting-started/tree/8bc4dec60f3605e57f0a4330634980923a9925e5/todo-list)В принципе это все, что я хотел осветить в данном уроке, но для завершенности, доделаю функционал карточек в досках, там не будет использоваться что-то, чего нет в данном уроке. С изменениями можно как обычно ознакомится в [репозитарии](https://github.com/ovcharik/meteor-getting-started/commit/2c65c13e1c4c8b8402b74f9fc3c67146a966d227).
![tasks](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/tasks.png)
* [Результат](http://hgsm-result.meteor.com/)
* [Репозитарий](https://github.com/ovcharik/meteor-getting-started/tree/master/todo-list)## Ссылки
* [Meteor](https://www.meteor.com/)
- [Документация](http://docs.meteor.com/);
- [Туториал](https://www.meteor.com/install) от разработчиков фреймворка;
* [Atmosphere](https://atmospherejs.com/) - пакеты для метеора
- [mquandalle:jade](https://atmospherejs.com/mquandalle/jade) - пакет добавляющий возможность использовать jade разметку для шаблонов метеора
- [iron:router](https://atmospherejs.com/iron/router) - клиентский и серверный роутинг;
- [mizzao:bootstrap-3](https://atmospherejs.com/mizzao/bootstrap-3) - Twitter Bootstrap для метеора;
- [ian:accounts-ui-bootstrap-3](https://atmospherejs.com/ian/accounts-ui-bootstrap-3) - шаблоны на основе Twitter Bootstrap для форм входа и регистрации;
- [dburles:collection-helpers](https://atmospherejs.com/dburles/collection-helpers) - с помощью этого пакета можно добавить логику к объектам модели;
- [jparker:gravatar](https://atmospherejs.com/jparker/gravatar) - генерация ссылок на изображения в сервисе [Gravatar](http://ru.gravatar.com/);
- [aldeed:collection2](https://atmospherejs.com/aldeed/collection2) - дополнительные возможности к метеоровским коллекциям;
- [sergeyt:typeahead](https://atmospherejs.com/sergeyt/typeahead) - автокомплит для бутсрапа;
- [meteorhacks:npm](https://atmospherejs.com/meteorhacks/npm) - простая интеграция `npm` пакетов в метеор приложение;
- [CollectionFS](https://github.com/CollectionFS/Meteor-CollectionFS) - управление файлами;
* Полезные ресурсы:
- [MeteorHacks](https://meteorhacks.com/) - сборник туториалов;
- [EventedMind](https://www.eventedmind.com/) - видео уроки;
- [Meteorpedia](http://www.meteorpedia.com/read/Main_Page) - неофициальная Wiki;
- [Еще множество ресурсов](https://github.com/ericdouglas/Meteor-Learning);
* [Репозтарий](https://github.com/ovcharik/meteor-getting-started) проекта, разрабатываемого в этом уроке