{"id":14967722,"url":"https://github.com/ovcharik/meteor-getting-started","last_synced_at":"2025-08-28T06:05:32.749Z","repository":{"id":22693194,"uuid":"26037046","full_name":"ovcharik/meteor-getting-started","owner":"ovcharik","description":"Урок для хабры. Разработка первого метеор приложения.","archived":false,"fork":false,"pushed_at":"2014-11-11T18:46:10.000Z","size":1641,"stargazers_count":34,"open_issues_count":0,"forks_count":12,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-03T06:02:17.006Z","etag":null,"topics":["coffeescript","jade","less","meteor","tutorial"],"latest_commit_sha":null,"homepage":null,"language":"CoffeeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ovcharik.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-10-31T23:55:14.000Z","updated_at":"2024-03-01T09:55:46.000Z","dependencies_parsed_at":"2022-08-17T16:31:08.346Z","dependency_job_id":null,"html_url":"https://github.com/ovcharik/meteor-getting-started","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ovcharik/meteor-getting-started","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ovcharik%2Fmeteor-getting-started","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ovcharik%2Fmeteor-getting-started/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ovcharik%2Fmeteor-getting-started/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ovcharik%2Fmeteor-getting-started/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ovcharik","download_url":"https://codeload.github.com/ovcharik/meteor-getting-started/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ovcharik%2Fmeteor-getting-started/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272451948,"owners_count":24937463,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-28T02:00:10.768Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coffeescript","jade","less","meteor","tutorial"],"created_at":"2024-09-24T13:38:30.951Z","updated_at":"2025-08-28T06:05:32.704Z","avatar_url":"https://github.com/ovcharik.png","language":"CoffeeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Meteor. Разрабатываем TODO List.\n\nВ данном уроке я не хочу обсуждать, почему метеор убийца веба, тем более я так не считаю, но определенную симпатию к этому фреймворку имею. Поэтому хочу показать с чего можно начать при разработке приложения на нем, какие есть пакеты и вообще, что такое этот метеор.\n\nСразу хочу сказать, что у меня нет большого опыта в разработке веб приложений. Я занимаюсь этим всего около двух лет, а с метеором знаком вообще лишь пару месяцев.\n\nЕще хочу предупредить, что урок получился достаточно объемным, но кода в нем было написано в разы меньше, чем текста. Просто хочу поделиться опытом как можно использовать метеор не при создании простенького примера, и заострить внимание на различных моментах, которые я посчитал важными. Поэтому в уроке будет использоваться множество сторонних пакетов, облегчающих процесс разработки.\n\nИ еще одно предупреждение: в данном уроке будут использоваться\nследующие технологии для непосредственного написания примера:\n\n* [jade](http://jade-lang.com/) - html препроцессор;\n* [less](http://lesscss.org/) - css препроцессор;\n* [coffeescript](http://coffeescript.org/) - язык программирования, компилируемый в javascript.\n\nВидео, демонстрирующее приложение, полученное в ходе урока\n\n[![result](http://img.youtube.com/vi/ATxB_CzCv8U/0.jpg)](http://www.youtube.com/watch?v=ATxB_CzCv8U)\n\nИ кому все еще интересно, добро пожаловать под кат.\n\n    +-------------+\n    | Продолжение |\n    +-------------+\n\n## Установка Meteor\n\nСам метеор базируется на `nodejs` и `mongodb`, так же у метеора нет поддержки Windows, и если вы собрались пощупать метеор вам необходимо обзавестись операционной системой Linux или MacOS.\n\nПервым делом устанавливаем [nodejs](http://nodejs.org/download/) и [mongodb](http://www.mongodb.org/downloads).\n\nСледующим шагом необходимо установить метеор. Он не лежит в npm репозитории, поэтому не нужно торопиться и командовать `npm install -g meteor`, в данном случае лишь загрузиться его старая версия, для правильно установки необходимо выполнить в консоли:\n\n    $ curl https://install.meteor.com/ | sh\n\n## Создание проекта\n\nПосле установки метеора можно сходу командовать\n\n    $ meteor create 'todo-list'\n    todo-list: created.\n\n    To run your new app:\n       cd todo-list\n       meteor\n    $ cd todo-list\n    $ meteor\n    [[[[[ ~/dev/meteor-getting-started/todo-list ]]]]]\n\n    =\u003e Started proxy.\n    =\u003e Started MongoDB.\n    =\u003e Started your app.\n\n    =\u003e App running at: http://localhost:3000/\n\nДанный вывод означает, что все прошло хорошо, и наш хелловорлд можно проверить в браузере.\n\n![helloworld](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/helloworld.png)\n\nТеперь, после проверки работоспособности нового проекта, файлы, находящиеся в корне проекта, можно удалить - нам они не особо интересны. Также можно заметить, что была создана директория `.meteor`, там хранится различная служебная информация о проекте и даже автоматически сгенерированный `.gitignore`. Кстати для ручного управления пакетами можно изменять файл `packages`, но консольные утилиты тоже достаточно удобны.\n\nЕсли у вас такой же результат как и у меня, значит минимальное окружение для разработки метеор проекта готово, если же что-то пошло не так - проверьте корректность установки `nodejs`, `mongodb` и `meteor`, например, у меня на компьютере сейчас следующая конфигурация:\n\n    $ node -v\n    v0.10.33\n    $ mongod --version\n    db version v2.4.12\n    $ meteor --version\n    Meteor 1.0\n\nТеперь можно закончить с формальностями и приступим к разработке нашего туду листа. Для удобства рекомендую открыть новую вкладку консоли, так как перезапускать наше метеор приложение больше не потребуется, но будем использовать консольный интерфейс фреймворка для установки пакетов.\n\n## Пакеты\n\nТут я опять же не хочу обсуждать почему в метеоре используется свой пакетный менеджер и почему они любят так велосипедить, к уроку это никакого отношения не имеет.\n\nУстановка пакетов производится командой\n\n    $ meteor add \u003cpackage-name\u003e\n\nКак я писал выше, приложение будем разрабатывать на `less`, `jade` и `coffeescript`, а значит самое время установить их. Все пакеты, используемые в уроке, и кучу других можно найти на сайте [Atmosphere](https://atmospherejs.com/). Собственно названия пакетов:\n\n* `less`, `coffeescript` - это официальные пакеты, поэтому не содержат имя автора;\n* `mquandalle:jade` - а вот это не официальный пакет, поэтому название состоит из двух составляющих, но выполнен он хорошо, и никаких проблем при его использовании у меня не возникало.\n\nВ пакеты `less` и `coffeescript` встроена поддержка `sourcemap`, поэтому и процесс дебага в браузере будет простым, `sourcemap` поддерживается самим метеором: он предоставляет необходимый api для подключения данного функционала, поэтому нам не придется, что-то специально настраивать.\n\nПо ходу разработки мы добавим еще несколько популярных пакетов, и я постараюсь описать назначения каждого. Кстати `jquery` и `underscore` уже включены в метеор, как и множество других пакетов, полный список можно посмотреть в файле `./.meteor/versions`, в созданном проекте.\n\n## Структура приложения\n\nТеперь, по-моему мнению, самое время разобраться с тем как метеор подключает файлы в проект, и какие способы регуляции этого существуют. Здесь нам не потребуется писать конфигурационные файлы для `grant` или `gulp`, что бы скомпилировать шаблоны, стили и скрипты, метеор уже позаботился об этом. Для скафолдинга существует [проект](https://github.com/Pent/generator-meteor) для Yeoman, но мне приятнее все создавать в ручную. В предыдущем проекте я использовал примерно следующую структуру папок:\n\n    todo-list/           - корень проекта\n    ├── client           - тут будут чисто клиентские файлы\n    │   ├── components   - компоненты приложения будут состоять из шаблона \n    │   │                  и скрипта, реализующего его логику\n    │   ├── config       - файлы конфигурации\n    │   ├── layouts      - базовые шаблоны, не имеющие никакой логики\n    │   ├── lib          - различные скрипты, которые могут понадобится на\n    │   │                  клиенте\n    │   ├── routes       - клиентский роутинг\n    │   └── styles       - стили\n    ├── collections      - здесь будем хранить модели\n    ├── lib              - скрипты, которые могут понадобится везде\n    ├── public           - статика: картинки, robots.txt и все такое\n    ├── server           - файлы для серверной части приложения\n    │   ├── methods      - тут будут серверные методы, типа реста,\n    │   │                  только удобнее\n    │   ├── publications - расшаривание данных из коллекций\n    │   ├── routes       - серверный роутинг, собственно можно будет\n    │   │                  контролировать http запросы\n    │   └── startup      - инициализация сервера\n\nВозможно что-то нам не пригодится, но в любом случае в метеоре нет никаких ограничений на именование папок и файлов, так что можете придумать, любую, удобную для вас структуру. Главное следует помнить о некоторых нюансах:\n\n* все файлы из папки `public` в корне проекта будут доступны пользователям по ссылки из браузера и не будут автоматически подключаться к проекту;\n* все файлы из папки `server` в корне, доступны только серверной части приложения;\n* все файлы из папки `client` в корне, доступны только клиентской части приложения;\n* все остальное, что находится в корне, доступно в любой среде;\n* файлы в проект подключаются автоматически, по следующим правилам:\n  - загрузка начинается с поддиректорий, и первой всегда обрабатывается директория с именем `lib`, далее все* папки и файлы загружаются в алфавитном порядке;\n  - файлы начинающиеся с `main.` загружаются последними.\n\nНапример рассмотрим, как будет загружаться наш проект в браузере: первым делом будут загружены файлы из директории `lib` в корне проекта, далее будет обрабатываться папка `client`, в ней также первым делом загрузятся файлы из `lib`, а потом в алфавитном порядке: `components` -\u003e `config` -\u003e ... -\u003e `styles`. И уже после файлы из папки `collections`. Файлы из папок `public` и `server`, не будут загружены в браузер, но, например, скрипты, хранящиеся в папке `public`, можно подключить через тег `script`, как это мы привыкли делать в других проектах, однако разработчики фреймворка не рекомендуют подобный подход.\n\nТакже для регуляции среды выполнения в общих файлах можно воспользоваться следующими конструкциями:\n\n```coffeescript\nif Meteor.isClient\n  # Код, выполняемый только в браузере\nif Meteor.isServer\n  # Код, выполняемый только на сервере\n```\n\nИ для регулирования времени выполнения скриптов мы можем использовать метод `Meteor.startup(\u003cfunc\u003e)`, в браузере это аналог функции `$` из библиотеки `jQuery`, а на сервере, код в данной функции выполнится сразу же после загрузки всех скриптов в порядке загрузки этих файлов. [Подробнее об этих переменных и методах](https://docs.meteor.com/#/full/core).\n\n## Базовый шаблон приложения\n\nДля верстки я буду использовать Bootstrap, да знаю, что он всем приелся, но верстальщик из меня никакой, а с бутстрапом я более менее знаком.\n\nДля этого устанавливаем пакет `mizzao:bootstrap-3` - он самый популярный среди прочих, и думаю при его использовании у нас не должно возникнуть проблем.\n\nДалее создаем в папке `client/layouts` файл `head.jade`. Это будет единственный файл в нашем приложении не имеющий формат шаблона, короче просто создадим шапку страницы, а позже разберем что такое шаблоны.\n\n```jade\n//- client/layouts/head.jade\nhead\n  meta(charset='utf-8')\n  meta(name='viewport', content='width=device-width, initial-scale=1')\n  meta(name='description', content='')\n  meta(name='author', content='')\n\n  title Meteor. TODO List.\n```\n\nМожно открыть браузер и убедиться, что после добавления файла страница имеет указанный заголовок.\n\nПрежде чем начнем верстать предлагаю провести базовую настройку клиентского роутинга, а чуть позже мы разберем этот момент подробнее. Для роутинга можно воспользоваться популярным решением, имеющим весь необходимый нам функционал. Установим пакет `iron:router` ([репозиторий](https://github.com/EventedMind/iron-router)).\n\nПосле установки в директории `client/config` создаем файл `router.coffee`, со следующим содержанием:\n\n```coffeescript\n# client/config/router.coffee\nRouter.configure\n  layoutTemplate: \"application\"\n```\n\nОчевидно, что здесь мы задаем базовый шаблон для нашего приложения, он будет называться `application`. Поэтому в папке `layouts` создаем файл `application.jade`. В этом файле мы опишем шаблон, некоторую сущность, которая на этапе сборки приложения превратится в код на `javascript`. Кстати в метеоре используется их собственный усатый шаблонизатор `spacebars` и библиотека `blaze`.\n\nЕсли коротко, то процесс работы шаблонов выглядит следующим образом (на сколько я понял из документации). Шаблоны `spacebars` компилируются в объект библиотеки `blaze`, которая в последствии будет работать непосредственно с `DOM`. В описании к проекту есть сравнение с другими популярными библиотеками:\n\n* по сравнению с обычными текстовыми шаблонизаторами блейз работает с домом, он не будет заново рендерить весь шаблон, что бы поменять один аттрибут, и у него нет проблем с вложенными шаблонами;\n* по сравнению с шаблонами `Ember`, блейз рендерит только изменения, нет нужды в явных дата-байдингах и в описаниях зависимостей между шаблонами;\n* по сравнению с шаблонами `angular` и `polymer`, блейз имеет понятный и простой синтаксис, меньший порог входа и вообще не позиционируется как технология будущего, а просто работает;\n* по сравнению с `React` имеет простой синтаксис описания шаблонов и простое управление данными.\n\nЭто я практически перевел параграф из официального [описания](https://atmospherejs.com/meteor/blaze) библиотеки, так что прошу не кидаться в меня камнями, если с чем-то не согласны. Сам я сталкивался с этими технологиями (кроме `ember`) и в принципе согласен с авторами библиотеки, из минусов в блейзе хочу заметить завязку на метеоре.\n\nНо мы в своем проекте не используем явно ни `blaze`, ни `spacebars`. Для `jade` шаблонов процесс компиляции имеет такую последовательность: `jade` -\u003e `spacebars` -\u003e `blaze`.\n\nВсе шаблоны в метеор описываются в теге `template`, где должен быть аттрибут с именем шаблона. Помните, мы в настройках роутера указали `layoutTemplate: \"application\"`, вот `application`, как раз и является именем шаблона.\n\nВроде разобрались что такое шаблоны в метеоре, самое время сверстать каркас страницы, он будет состоять из шапки и подвала.\n\n```jade\n//- client/layouts/application.jade\ntemplate(name='application')\n  nav.navbar.navbar-default.navbar-fixed-top\n    .container\n      .navbar-header\n        button.navbar-toggle.collapsed(\n          type='button',\n          data-toggle='collapse',\n          data-target='#navbar',\n          aria-expanded='false',\n          aria-controls='navbar'\n        )\n          span.sr-only Toggle navigation\n          span.icon-bar\n          span.icon-bar\n          span.icon-bar\n        a.navbar-brand(href='#') TODO List\n      #navbar.collapse.navbar-collapse\n        ul.nav.navbar-nav\n\n  .container\n    +yield\n\n  .footer\n    .container\n      p.text-muted TODO List, 2014.\n```\n\nЗдесь нужно понимать, что это не совсем привычный нам `jade`, с его миксинами, джаваскриптом и инклудами. `Jade` должен скомпилироваться в шаблон `spacebars`, и это накладывает некоторые особенности. От `jade`, можно сказать мы заберем только синтаксис, остальное нам просто не нужно. В данном шаблоне используется конструкция `+yield`, это конструкция означает, что вместо нее будет отрендерен шаблон `yield`, это особенность `iron:router`, он автоматически подставит нужный шаблон в зависимости от пути, чуть позже мы займемся роутерами, а сейчас можно внести косметические изменения в верстку и посмотреть на результат.\n\n```less\n// client/styles/main.less\nhtml {\n  position: relative;\n  min-height: 100%;\n}\n\nbody {\n  margin-bottom: 60px;\n\n  \u0026 \u003e .container{\n    padding: 60px 15px 0;\n  }\n}\n\n.footer {\n  position: absolute;\n  bottom: 0;\n  width: 100%;\n  height: 60px;\n  background-color: #f5f5f5;\n\n  .container .text-muted {\n    margin: 20px 0;\n  }\n}\n```\n\nПри изменениях стилей, кстати, не требуется обновлять страницу в браузере, достаточно сохранить файл, и они сразу же применятся, вот такой удобный инструмент из коробки есть для верстальщиков в метеоре.\n\n![my_helloworld](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/my_helloworld.png)\n\n## Роутинг\n\nВ самом метеоре нет стандартного механизма роутинга, я предлагаю использовать пакет `iron:router`, он [хорошо документирован](https://github.com/EventedMind/iron-router/blob/devel/Guide.md), активно поддерживается, обладает богатым функционалом и также является самым популярным решениям для роутинга в контексте метеора.\n\nЕще эту библиотеку можно использовать для серверного роутинга. Например, мне, на реальном проекте, это понадобилось для авторизации пользователей, так как основной проект сделан на `Ruby on Rails`, а пользователям нет нужды думать, что это два различных приложения и проходить в них авторизацию дважды. Вообще для серверного роутинга и создания REST api для метеора есть несколько популярных [подходов](http://www.meteorpedia.com/read/REST_API).\n\nСоздадим базовые роутеры, чтобы на примере посмотреть как работает данная библиотека и каким функционалом обладает, а позже будем навешивать на них основной функционал.\n\nДля начала зададим ссылки на наши страницы\n\n```jade\n//- client/layouts/application.jade\n//- ...\n#navbar.collapse.navbar-collapse\n  ul.nav.navbar-nav\n    li\n      a(href='/') Home\n    li\n      a(href='/about') About\n```\n\nСоздаем контроллеры в папке клиентских роутеров, пока это будут просто заглушки\n\n```coffeescript\n# client/routes/home.coffee\nRouter.route '/', name: 'home'\nclass @HomeController extends RouteController\n\n  action: -\u003e\n    console.log 'Home Controller'\n    super()\n\n# client/routes/about.coffee\nRouter.route '/about', name: 'about'\nclass @AboutController extends RouteController\n\n  action: -\u003e\n    console.log 'About Controller'\n    super()\n```\n\nВ функцию `Router.route` нужно передать два параметра, первый это путь, причем путь может быть паттерном (например: `/:user/orders/:id/info`), все параметры из паттерна будут доступны в объекте контроллера, через свойство `params`. Вторым параметром передается объект с опциями. Чтобы вынести всю логику отдельно от простого описания пути и имени, можно создать контроллеры, в нашем случае это простые заглушки, здесь в свойствах мы не указываем явно имена контроллеров, потому что по умолчанию `iron:router` пытается найти контроллер с именем `\u003cRouteName\u003eController`, и конечно, наши контроллеры должны быть доступны глобально, в кофескрипте мы это делаем, привязывая переменную к текущему контексту, в обычном js, достаточно просто объявить переменную не через `var`.\n\n\u003e К слову, в метеоре не используется, например `amd` для загрузки кода, файлы просто загружаются в определенной последовательности. Поэтому все взаимодействие между модулями, описанными в разных файлах, осуществляется через глобальные переменные. Что, как по мне, достаточно удобно, а при использовании кофе, случайно объявить глобальную переменную достаточно сложно, и она сразу будет заметна.\n\n`iron:router` также попытается автоматически отрендерить шаблон, с именем роута (но шаблоны можно указать и явно), создадим их\n\n```jade\n//- client/components/home/home.jade\ntemplate(name='home')\n  h1 Home\n\n//- client/components/about/about.jade\ntemplate(name='about')\n  h1 About\n```\n\nМожно открыть браузер и убедиться, что наш роутинг работает, покликав на ссылки в шапке. Причем работает без обновления страницы.\n\n![base_routing](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/base_routing.png)\n\n\u003e По ходу разработки данного урока я попытаюсь все изменения в коде вносить в репозиторий, в соответствии с последовательностью изложения, что бы вы могли проследить весь процесс, так как в посте некоторые вещи могут быть пропущены. [Репозитарий](https://github.com/ovcharik/meteor-getting-started/commits/master).\n\n[Здесь](http://hgsm-base-client-routing.meteor.com) можно посмотреть, что получилось в итоге, а [здесь](https://github.com/ovcharik/meteor-getting-started/tree/4035f092dc65cee6c93bfb308e890ad7fe17ba27/todo-list) посмотреть код проекта в текущем состоянии.\n\n## Пользователи и аутентификация\n\nМногие технические задания, приходящие к нам в компанию, первой задачей описывают систему пользователей. Так как это довольно распространенная задача, считаю необходимым и в нашем уроке рассмотреть способы аутентификации пользователей, тем более метеор для этого предоставляет стандартные средства.\n\nМы не будем сильно углубляться в механизмы, а просто используем готовые решения, которые позволят нам создавать пользователей через логин/пароль или сервисы `google` и `github`. Я привык в рельсах настраивать связку `devise` и `omniauth` парой генераторов и несколькими строчками в конфиге. Так вот метеор мало того, что предоставляет это из коробки, так еще и настройка сервисов происходит максимально просто.\n\nУстановим следующие пакеты:\n\n* `accounts-base` - базовый пакет для пользователей приложения на метеоре;\n* `accounts-password`, `accounts-github`, `accounts-google` - добавим поддержку для аутентификации через логин/пароль и сервисы `github` и `google`;\n* `ian:accounts-ui-bootstrap-3` - пакет для упрощения интеграции аккаунтов в приложение на бутстрапе.\n\nПакет `ian:accounts-ui-bootstrap-3` нам позволит одной строчкой добавить форму аутентификации/регистрации в приложение, а также предоставит интерфейс к настройке сторонних сервисов. [Сам проект](https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3/), там есть небольшая документация и скриншоты того как выглядят интеграция формы и настройка сервисов.\n\nМодифицируем нашу шапку\n\n```jade\n//- client/layouts/application.jade\n//- ...\n#navbar.collapse.navbar-collapse\n  ul.nav.navbar-nav\n    li\n      a(href='/') Home\n    li\n      a(href='/about') About\n  ul.nav.navbar-nav.navbar-right\n    //- шаблон кнопки авторизации пользователя\n    //- идет в пакете ian:accounts-ui-bootstrap-3\n    +loginButtons\n```\n\nИ получим следующий результат\n\n![base_auth_form](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/base_auth_form.png)\n\n* [Результат](http://hgsm-base-user-auth.meteor.com/)\n* [Репозиторий](https://github.com/ovcharik/meteor-getting-started/tree/78c28cce3af54989ca8c89c453e37c578e8f1d52/todo-list)\n\nПосле конфигурации можем убедиться, что токены авторизации сохранились в базе данных.\n\n    $ meteor mongo\n    MongoDB shell version: 2.4.9\n    connecting to: 127.0.0.1:3001/meteor\n    meteor:PRIMARY\u003e show collections\n    meteor_accounts_loginServiceConfiguration\n    meteor_oauth_pendingCredentials\n    system.indexes\n    users\n    meteor:PRIMARY\u003e db.meteor_accounts_loginServiceConfiguration.find()\n    {\n      \"service\" : \"github\",\n      \"clientId\" : \"\u003cid\u003e\",\n      \"secret\" : \"\u003csecret\u003e\",\n      \"_id\" : \"AjKrfCXAioLs7aBTN\"\n    }\n    {\n      \"service\" : \"google\",\n      \"clientId\" : \"\u003cid\u003e\",\n      \"secret\" : \"\u003csecret\u003e\",\n      \"_id\" : \"HaERjHLYmAAhehskY\"\n    }\n\nСконфигурируем нашу систему пользователей, так как я хочу настроить верификацию адреса электронной почты, необходимо настроить `smtp`, кстати для отправки email используется пакет `email`. Он не входит в стандартный набор метеора, поэтому его необходимо установить вручную, если вам нужна работа с почтой.\n\n```coffeescript\n# server/config/smtp/coffee\nsmtp =\n  username: \"meteor-todo-list@yandex.ru\"\n  password: \"meteor-todo-list1234\"\n  server:   \"smtp.yandex.ru\"\n  port:     \"587\"\n\n# Экранируем символы\n_(smtp).each (value, key) -\u003e smtp[key] = encodeURIComponent(value)\n\n# Шаблон url доступа к smtp\nurl = \"smtp://#{smtp.username}:#{smtp.password}@#{smtp.server}:#{smtp.port}\"\n\n# Задаем переменную окружения, метеор будет использовать данные из нее\nprocess.env.MAIL_URL = url\n```\n\nИ сконфигурируем аккаунты, что бы метеор запрашивал подтверждение адреса электронной почты.\n\n```coffeescript\n# server/config/accounts.coffee\nemailTemplates =\n  from: 'TODO List \u003cmeteor-todo-list@yandex.ru\u003e'\n  siteName: 'Meteor. TODO List.'\n\n# заменяем стандартные настройки для почты\n_.deepExtend Accounts.emailTemplates, emailTemplates\n\n# включаем верификацию\nAccounts.config\n  sendVerificationEmail: true\n\n# добавляем кастомную логику при регистрации пользователей\nAccounts.onCreateUser (options = {}, user) -\u003e\n  u = UsersCollection._transform(user)\n  options.profile ||= {}\n  # сохраняем хеш адреса, чтобы можно было получит аватар для пользователя\n  # у которого не указан публичный адрес почты\n  options.profile.emailHash = Gravatar.hash(u.getEmail() || \"\")\n  # запоминаем сервис, через который пользователь зарегистрировался\n  options.service = _(user.services).keys()[0] if user.services\n  # сохраняем дополнительные параметры и возвращаем объект,\n  # который запишется в бд\n  _.extend user, options\n```\n\nВ нашем приложении не будет возможности подключать несколько сервисов к одному аккаунту, так как это требует тонкой настройки. Возможно скоро в метеоре проработают данный момент, но пока существует готовое, более менее нормальное, решение `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).\n\nТакже по аккаунтам есть подробная [документация](https://docs.meteor.com/#/full/accounts_api), мы всего лишь поставили пакеты и видим магию, но под капотом это происходит не многим сложнее.\n\n\u003e Не стоит меня сильно пинать, за то что так поверхностно рассмотрел систему аккаунтов, просто хотел показать, что в ней нет ничего сложного. На подробное рассмотрение потребуется отдельный пост. А мы в уроке создали необходимый базовый функционал и можем продолжить идти к конечному результату.\n\nСледующим шагом мы займемся страницей пользователя, но прежде чем преступить необходимо рассмотреть как реализованы некоторые вещи в метеоре.\n\n## Коллекции, публикации и подписки.\n\nПри создании проекта, автоматически были добавлены два пакета `autopublish` и `insecure`, так вот сейчас самое время от них избавиться, так как они предоставляет пользователю безграничный доступ ко всем коллекциям в бд, и их можно использовать только для прототипирования. Удаляются пакеты командой\n\n    $ meteor remove \u003cpackage-name\u003e\n\n### Коллекции\n\nКоллекции в метеоре можно сравнить с коллекциями в монге, собственно они с ними же и работают, и у них также есть методы `find`, `insert`, `update`, `upsert` (агрегацию можно организовать на сервере при помощи пакета `zvictor:mongodb-server-aggregation`). Одна из коллекций у нас уже создана и доступ к ней можно получить через `Meteor.users`, например, попробуйте выполнить в консоли браузера `Meteor.users.findOne()`. Здесь важно отметить, что все данные коллекций кешируются в браузере, и если выполнить миллион раз в цикле на клиенте `Meteor.users.find(options).fetch()`, то ничего кроме браузера вы не нагрузите. Это достигается при помощи библиотеки `minimongo`, которая достаточно умная, что бы делать выборку в зависимости от переданных параметров на клиенте.\n\nС голыми данными не очень приятно работать, хотелось бы добавить бизнес-логику в объекты коллекции, это можно сделать при помощи функции `_transform` у коллекции, в которую передаются объекты после получения их с сервера и там их уже можно обработать, однако чтобы не вникать в эти тонкости, можно воспользоваться пакетом `dburles:collection-helpers`, который к коллекции добавляет метод `helpers`, куда можно передать объект, от которого будут наследоваться все данные.\n\nУстановим пакет, и напишем методы для обновления данных о пользователе. Также при создании пользователя мы добавили вычисляемое поле с хешем аватара пользователя в сервисе [Gravatar](http://ru.gravatar.com/) - добавим метод который сможет возвращать ссылку на изображение с некоторыми параметрами. Еще добавим методы для проверки сервиса регистрации пользователя и методы возвращающие различную публичную информацию.\n\n```coffeescript\n# collections/users.coffee\nUsers = Meteor.users\n\n# статические методы и свойства\n_.extend Users,\n  # список полей доступных для редактирования\n  allowFieldsForUpdate: ['profile', 'username']\n\n# Добавляем методы и свойства в модель\nUsers.helpers\n  # метод обновления пользователя, можно вызывать прямо на клиенте\n  update: (data) -\u003e\n    Users.update @_id, data\n\n  # метод для обновления, который будет только устанавливать данные\n  # сразу позаботимся о запрещенных полях\n  set: (data) -\u003e\n    d = {}\n    f = _(Users.allowFieldsForUpdate)\n    for key, value of data when f.include(key)\n      d[key] = value\n    @update $set: d\n\n  # метод мержить текущие данные с переданными,\n  # чтобы потом их можно было использовать для обновления\n  # и нечего не потерять\n  merge: (data) -\u003e\n    current = @get()\n    @set _.deepExtend(current, data)\n\n  # получение только данных модели, все методы и свойства,\n  # указанные здесь находятся в прототипе\n  get: -\u003e\n    r = {}\n    r[key] = @[key] for key in _(@).keys()\n    r\n\n  # список все адресов почты\n  getEmails: -\u003e\n    p = [@profile?.email]\n    s = _(@services).map (value, key) -\u003e value?.email\n    e = _(@emails).map (value, key) -\u003e value?.address\n    _.compact p.concat(e, s)\n\n  # основной адрес\n  getEmail: -\u003e\n    @getEmails()[0]\n\n  # публичная информация\n  getUsername    : -\u003e @username || @_id\n  getName        : -\u003e @profile?.name || \"Anonymous\"\n  getPublicEmail : -\u003e @profile?.email\n\n  urlData: -\u003e\n    id: @getUsername()\n\n  # вычисляем ссылку на граватар, на основе адреса почты\n  # или хеша автоматически вычисленного при регистрации\n  getAvatar: (size) -\u003e\n    size = Number(size) || 200\n    options =\n      s: size\n      d: 'identicon'\n      r: 'g'\n    hash = \"00000000000000000000000000000000\"\n    if email = @getPublicEmail()\n      hash = Gravatar.hash(email)\n    else\n      hash = @profile?.emailHash || hash\n    Gravatar.imageUrl hash, options\n\n  # проверка сервиса используемого при регистрации\n  isFromGithub:   -\u003e @service == 'github'\n  isFromGoogle:   -\u003e @service == 'google'\n  isFromPassword: -\u003e @service == 'password'\n\n  # текущий пользователь может редактировать\n  # некоторые данные о себе\n  isEditable: -\u003e @_id == Meteor.userId()\n\n# Экспорт коллекции\n@UsersCollection = Users\n```\n\nВроде разобрались, что такое коллекции в метеоре, следует упомянуть, что нежелательно хранить состояния в модели, так как все данные в коллекции реактивны, если изменится запись в бд, то сохраненный где-то в памяти, объект модели потеряет свою актуальность, и последующая работа с ним может привести нас к использованию устаревших данных, позже на примерах рассмотрим, как можно работать с моделями.\n\n### Публикации\n\nЯ создал три записи пользователя в бд\n\n    $ meteor mongo\n    meteor:PRIMARY\u003e db.users.count()\n    3\n\nА когда пытаюсь получить данные в браузере, то не нашел ни одной записи без аутентификации и одну (собственную) в противном случае.\n\n![fail_publish](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/fail_publish.png)\n\nВ данном приложении не будем скрывать пользователей от всех, просто скроем приватную информацию, вроде токенов аутентификации.\n\nТак как мы удалили пакет `autopublish`, теперь процессом публикации данных необходимо заняться вручную, это позволит контролировать нам данные передаваемые пользователю.\n\nОпубликуем коллекцию пользователей.\n\n```coffeescript\n# server/publications/users.coffee\nMeteor.publish 'users', (limit = 20) -\u003e\n  UsersCollection.find {},\n    fields:\n      service: 1\n      username: 1\n      profile: 1\n    limit: limit\n```\n\nДанный код будет предоставлять доступ к пользователям всем желающим, необходимо только подписаться, я сразу подумал о том чтобы предоставить пользователям возможность постраничной загрузки данных, в случае если не указать лимит выдачи, все записи о пользователях сразу будут выгружены, при подписки на данную публикацию, что не очень хорошо по понятным причинам, тоже самое происходить при использовании `autopublish`, только автоматически и со всеми коллекциями.\n\nТакже мы ограничили видимость выгружаемых данных, ничего кроме информации из поля `profile` и имени пользователя, подписчики не смогут увидеть. Но например мы хотим предоставить доступ к адресам электронной почты для текущего пользователя, создадим еще одну публикацию.\n\n```coffeescript\n# server/publications/profile.coffee\nMeteor.publish 'profile', -\u003e\n  # проверям авторизован ли пользователь,\n  # запрашивающий подписку\n  if @userId\n    # подписываем на его запись в бд\n    UsersCollection.find { _id: @userId },\n      fields:\n        service: 1\n        username: 1\n        profile: 1\n        emails: 1\n  else\n    # просто говорим, что все готово\n    @ready()\n    return\n```\n\nВторой параметр передаваемый в метод `Meteor.publish`, это функция, которая должна вернуть курсор коллекции. Эта функция может принимать любое количество аргументов и она выполняется в контексте объекта, в котором доступны некоторые методы, позволяющие оповещать пользователя о различных изменениях в данных и предоставляющих доступ к некоторым свойствам подключения. Например, в публикации профиля мы используем метод `ready`, в случае когда пользователь не авторизован, это означает, что данные в публикации готовы, и на стороне клиента вызовется коллбек при подписке, но никаких данных он не получит. [Подробнее](https://docs.meteor.com/#/full/meteor_publish) о публикациях.\n\n### Подписки\n\nЯ уже неоднократно заметил, что для получения данных и для отслеживания изменений в них, сперва необходимо подписаться на публикации, вообще все что происходит с данными в метеор приложении можно легко отслеживать и контролировать, а если вы просто создаете прототип, где это для вас не ключевые моменты, всегда можно воспользоваться пакетом `autopublish`.\n\nМы для подписок будем использовать `iron:router`, и он будет контролировать весь необходимый процесс, так как для ручного управления за этим процессом придется следить за многим, а данная библиотека решает все проблемы. Некоторые данные желательно выдавать постранично, поэтому прежде чем создать контроллер для пользователей, мы немного абстрагируемся и создадим класс, обладающий функционалом для управления страницами и который будет наследоваться от контроллера библиотеки `iron:router`.\n\n```coffeescript\n# client/lib/0.pageable_route_controller.coffee\nvarName = (inst, name = null) -\u003e\n  name = name \u0026\u0026 \"_#{name}\" || \"\"\n  \"#{inst.constructor.name}#{name}_limit\"\n\nclass @PagableRouteController extends RouteController\n\n  pageable: true # будем проверять, что это за контроллер\n  perPage: 20    # количество данных на одной странице\n\n  # количество загружаемых данных\n  limit: (name = null) -\u003e\n    Session.get(varName(@, name)) || @perPage\n\n  # следующая страница\n  incLimit: (name = null, inc = null) -\u003e\n    inc ||= @perPage\n    Session.set varName(@, name), (@limit(name) + inc)\n\n  # сборс количества\n  resetLimit: (name = null) -\u003e\n    Session.set varName(@, name), null\n\n  # все ли данные были загруженны?\n  loaded: (name = null) -\u003e\n    true\n```\n\nДавайте еще создадим шаблон в виде кнопки, при клике на которую будет вызываться метод `incLimit`, для текущего контроллера, конечно если он поддерживает данный функционал. Можно бы было сделать и бесконечный скроллинг но так проще.\n\n```jade\n//- client/components/next_page_button/next_page_button.jade\ntemplate(name='nextPageButton')\n  unless loaded\n    a.btn.btn-primary.btn-lg.NextPageButton(href = '#')\n      | More\n```\n\n```coffeescript\n# client/components/next_page_button/next_page_button.coffee\nTemplate.nextPageButton.helpers\n  loaded: -\u003e\n    ctrl = Router.current()\n    if ctrl.pageable\n      ctrl.loaded(@name)\n    else\n      true\n\nTemplate.nextPageButton.events\n  'click .NextPageButton': (event) -\u003e\n    ctrl = Router.current()\n    if ctrl.pageable\n      ctrl.incLimit(@name, @perPage)\n```\n\nЗдесь мы для компонента уже задаем некоторую логику. Как можно заметить все шаблоны складываются в глобальное пространство имен `Template`. Обратится к шаблону мы можем через `Template.\u003ctemplate-name\u003e`. Для описания методов используемых в шаблоне нужно использовать метод `helpers`, куда передается объект с методами. В данном примере мы описываем лишь один метод `loaded`, который проверяет, что из себя представляет текущий контроллер и отдает результат, показывающий все ли данные были загружены. В самом шаблоне мы дергаем этот метод в конструкции `unless loaded`, также в шаблоне можно забирать данные из текущего контекста. Хелперы шаблона можно сравнить с прототипом объекта, при использовании их в шаблоне, но внутри самой функции есть ограничения, так как каждый хелпер вызывается примерно так `\u003chelper-func\u003e.apply(context, arguments)`, то есть у нас нет возможности обратится ко всем хелперам шаблона, внутри функции, что в общем-то иногда может мешать.\n\nДля обработки событий шаблона, нужно их описать в методе `events`, куда передается объект, с ключами следующего формата `\u003cevent\u003e \u003cselector\u003e`. В обработчик передается `jQuery` событие и шаблон, в котором было вызвано событие, так как мы можем обрабатывать дочерние события в родительском шаблоне, это иногда может оказаться полезным.\n\nТеперь у нас все готово, чтобы создать страницу со списком всех пользователей и на примере посмотреть, как можно управлять подписками в `iron:router`.\n\n```coffeescript\n# client/routes/users.coffee\nRouter.route '/users', name: 'users'\nclass @UsersController extends PagableRouteController\n\n  # количество пользователей на одной странице\n  perPage: 20\n\n  # подписываемся на коллекцию пользователей, с заданными лимитом,\n  # чтобы не получать лишние данные\n  # \n  # подписка происходит через данный метод, чтобы iron:router\n  # не рендерил шаблон загрузки страницы, каждый раз при обновлении\n  # подписки\n  subscriptions: -\u003e\n    @subscribe 'users', @limit()\n\n  # возвращаем всех пользователей из локальной коллекции\n  data: -\u003e\n    users: UsersCollection.find()\n\n  # все ли пользователи загружены?\n  loaded: -\u003e\n    @limit() \u003e UsersCollection.find().count()\n\n  # сбрасываем каждый раз лимит, при загрузки страницы\n  onRun: -\u003e\n    @resetLimit()\n    @next()\n```\n\nВ методе `subscriptions` происходит подписка к публикации `users`. Есть еще практически аналогичный метод `waitOn`, только в нем роутер будет ожидать пока все данные выгрузятся, а после отрендерит страницу, до этого момента он будет отображать шаблон, который можно задать через свойство `loadingTemplate`. Данные, возвращаемые методом `data`, будут привязаны к шаблону, и мы сможем их использовать через текущий контекст. `UsersCollection.find()` возвращает курсор, а не сами данные, но блейз будет все превращения делать за нас, как-будто мы работаем уже с готовыми данными. Так как мы подписываемся на ограниченное количество данных, вызов `UsersCollection.find().fetch()` вернет нам лишь данные, загруженные на клиент, то есть если мы, например, установим лимит 1, то и `find` будет работать только с загруженной выборкой (одной записью), а не всеми данными в коллекции из базы. К примеру здесь мы переопределяем метод `loaded`, думаю суть его ясна, но следует помнить, что `count` будет возвращать нам количество локальных записей, а значит будет равен `limit`, пока все данные не будут выгружены, поэтому и условие строго больше.\n\nВ `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).\n\n### Реактивные переменные и функции\n\nВот мы подписались на публикацию, но все равно может быть не понятно почему клики по кнопке из шаблона `nextPageButton`, будет приводить нас к загрузке новой порции данных, а все благодаря манипуляциям с объектом `Session` в `PagableRouteController`. Данные в этом объекте являются реактивными, и `iron:router` автоматически будет отслеживать в них изменения. Можете в консоли браузера попробовать набрать\n\n```javascript\nTracker.autorun( function() {\n  console.log( 'autorun test', Session.get('var') );\n} )\n```\n\nИ попробовать изменить значение с помощью вызова `Session.set('var', 'value')`, результат не заставит себя ждать.\n\n![reactive_var](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/reactive_var.png)\n\nИменно благодаря подобному механизму, `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). Вообще у данной библиотеки есть и другие возможности, но на практике мне их применять не приходилось, возможно и зря.\n\nЕще один небольшой пример, который можно выполнить в консоли браузера:\n\n```javascript\nvar depend = new Tracker.Dependency();\nvar reactFunc = function() {\n  depend.depend(); return 42;\n}\nTracker.autorun(function() {\n  console.log( reactFunc() );\n});\n// 42\ndepend.changed()\n// 42\ndepend.changed()\n// 42\n```\n\n### Еще немного о подписках\n\nЯ рассказал как можно использовать подписки на примере `iron:router`, но это [не единственный механизм](https://docs.meteor.com/#/full/meteor_subscribe). Главное следует помнить, что использовать подписки нужно осторожно, иначе вы рискуете выгрузить большой объем данных и автоматически отслеживать в них изменения, там где это не нужно. `iron:router` предоставляет нам очень простой способ управления подписками, он сам отключит все не нужные, подключит нужные, обновит текущие в случае необходимости, как, например, это происходит при загрузке следующей страницы у нас.\n\nДавайте заверстаем список пользователей и убедимся, что все это работает на практике.\n\n```jade\n//- client/components/users.jade\ntemplate( name='users' )\n  h1 Users\n  .row\n    //- это данные, которые передает роутер шаблону\n    +each users\n      .col-xs-6.col-md-4.col-lg-3\n        //- рендерим карточку пользователя\n        //- в блоке each контекст меняется, поэтому мы\n        //- можем не передавать в шаблон никаких параметров\n        +userCard\n  //- кнопка загрузки следующей страницы\n  +nextPageButton\n\n//- client/components/user_avatar/user_avatar.jade\n//- унифицируем шаблон аватара, возможно понадобится добавить логику\ntemplate(name='userAvatar')\n  img(src=\"{{user.getAvatar size}}\", alt=user.getUsername, class=\"{{class}}\")\n\n//- client/components/user_card.jade\n//- в этом шаблоне используются данные пользователя\n//- а также функции описанные в модели ранее\ntemplate(name='userCard')\n  .panel.panel-default\n    .panel-body\n      .pull-left\n        +userAvatar user=this size=80\n\n      .user-card-info-block\n        ul.fa-ul\n          //- сервис и имя пользователя\n          li\n            if isFromGithub\n              i.fa.fa-li.fa-github\n            else if isFromGoogle\n              i.fa.fa-li.fa-google\n            else\n              i.fa.fa-li\n            b= getName\n          //- идентификатор либо логин\n          li\n            i.fa.fa-li @\n            //- ссылка на пользователя\n            a(href=\"{{ pathFor route='users_show' data=urlData }}\")= getUsername\n          //- адрес почты, если указан\n          if getPublicEmail\n            li\n              i.fa.fa-li.fa-envelope\n              = getPublicEmail\n```\n\n![users_page](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/users_page.png)\n\nВ результате у нас работает пагинация, и так как все данные реактивны, новые пользователи системы добавятся на страницу автоматически, без каких-либо перезагрузок, потому что мы подписались на коллекцию, а значит любые изменения данных в базе на сервере, сразу же будут отображены на страницу пользователя. Можете попробовать в новой вкладке зарегистрировать нового пользователя, либо поменять значения прямо в базе при помощи утилиты `mongo` - изменения отобразятся на странице, и вам для этого не придется ничего делать.\n\nИ чтобы убедиться в том, что данный подход работает оптимально, можно посмотреть логи браузера. Я установил количество пользователей на страницу равное одному. Протокол DDP достаточно простой и легко читаемый, поэтому не буду вдаваться в подробности. В логах можно просто увидеть, что все ненужные подписки были отписаны, а пользователи загрузились всего три раза, по одному на каждое обновление подписки.\n\n![users_log](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/users_log.png)\n\n## Страница пользователя и еще немного о шаблонах\n\nДавайте создадим страницу пользователя, где будет возможность изменять некоторые данные и завершим на этом работу с пользователями, чтобы можно было перейти к созданию собственных коллекций.\n\nДля этого первым делом для авторизованного пользователя вместо домашней страницы будем показывать страницу текущего пользователя, модифицируем немного контроллер.\n\n```coffeescript\n# client/routers/home.coffee\nRouter.route '/', name: 'home'\nclass @HomeController extends PagableRouteController\n\n  # авторизован ли пользователь?\n  isUserPresent: -\u003e\n    !!Meteor.userId()\n\n  # подписываемся на профайл если пользователь авторизован\n  # на сайте\n  waitOn: -\u003e\n    if @isUserPresent()\n      @subscribe 'profile'\n\n  # возвращаем данные о текущем пользователе, если такой имеется\n  data: -\u003e\n    if @isUserPresent()\n      { user: UsersCollection.findOne Meteor.userId() }\n\n  # рендерим шаблон профайла если пользователь авторизован\n  # и домашнюю страницу в противном случае\n  action: -\u003e\n    if @isUserPresent()\n      @render 'profile'\n    else\n      @render 'home'\n```\n\nИ также создадим контроллер, в котором можно будет просматривать профиль любого пользователя.\n\n```coffeescript\n# client/routers/user_show.coffee\nRouter.route '/users/:id', name: 'users_show'\nclass @UsersShowController extends PagableRouteController\n\n  # используем уже готовый шаблон\n  template: 'profile'\n\n  # подписываемся на нужного пользователя\n  waitOn: -\u003e\n    @subscribe 'user', @params.id\n\n  # ищем нужного пользователя\n  data: -\u003e\n    user: UsersCollection.findOneUser(@params.id)\n```\n\nДля удобства поиска пользователей либо по идентификатору, либо по логину я создал дополнительные методы в коллекции: один возвращает курсор, второй данные.\n\n```coffeescript\n# collections/users.coffee\n# ...\n_.extend Users,\n  # ...\n  findUser: (id, options) -\u003e\n    Users.find { $or: [ { _id: id }, { username: id } ] }, options\n\n  findOneUser: (id, options) -\u003e\n    Users.findOne { $or: [ { _id: id }, { username: id } ] }, options\n```\n\nДанные для страницы пользователя получить пытаемся, а они не опубликованы, исправляем это.\n\n```coffeescript\n# server/publications/user.coffee\nMeteor.publish 'user', (id) -\u003e\n  UsersCollection.findUser id,\n    fields:\n      service: 1\n      username: 1\n      profile: 1\n    limit: 1\n```\n\nПочти все готово, создадим шаблон и посмотрим на результат. При создании шаблона я решил создать компонент, который, в зависимости от прав доступа, будет давать возможность редактировать поле модели.\n\n```jade\n//- client/components/editable_field/editable_field.jade\n//- вот тут солянка из вызовов хелперов\n//- и обращений к данным контекста, кстати если имя хелпера\n//- и свойства в текущем контексте совпадают\n//- то предпочтение отдается хелперу\n//- обратиться явно к контексту можно через this.\u003ckey\u003e\ntemplate(name='editableField')\n  .form-group.EditableFiled\n    if data.isEditable\n      div(class=inputGroupClass)\n        if hasIcon\n          .input-group-addon\n            if icon\n              i.fa.fa-fw(class='fa-{{icon}}')\n            else\n              i.fa.fa-fw=iconSymbol\n        input.Field.form-control(placeholder=placeholder, value=value, name=name)\n    else\n      if defaultValue\n        span.form-control-static\n          if hasIcon\n            if icon\n              i.fa.fa-fw(class='fa-{{icon}}')\n            else\n              i.fa.fa-fw=iconSymbol\n          = defaultValue\n```\n\nДля интерполяции переменных в строках, в шаблонах, можно использовать усатые конструкции: `class='fa-{{icon}}'`, `icon` - это переменная.\n\n```coffeescript\n# client/components/editable_field/editable_field.coffee\nTemplate.editableField.helpers\n  value: -\u003e\n    ObjAndPath.valueFromPath @data, @path\n\n  name: -\u003e\n    ObjAndPath.nameFromPath @scope, @path\n\n  hasIcon: -\u003e\n    @icon || @iconSymbol\n\n  inputGroupClass: -\u003e\n    (@icon || @iconSymbol) \u0026\u0026 'input-group' || ''\n\nTemplate.editableField.events\n  # кидаем событие выше, при изменении данных в инпуте\n  'change .Field': (event, template) -\u003e\n    data  = $(event.target).serializeJSON()\n    $(template.firstNode).trigger 'changed', [data]\n```\n\n```jade\n//- client/components/profile/profile.jade\ntemplate(name='profile')\n  //- смена контекста, и блок внутри не будет отрендерен,\n  //- если такого свойства нет\n  +with user\n    .profile-left-side\n      .panel.panel-default\n        .panel-body\n          .container-fluid\n            .row.row-bottom\n              //- аватар пользователя, параметром передаем конструкции\n              //- вида \u003cключ\u003e=\u003cзначение\u003e, которые сложатся в один объект\n              //- и станут контекстом шаблона userAvatar\n              +userAvatar user=this size=200 class='profile-left-side-avatar'\n            .row\n              //- редактируемые поля для текущего пользователя\n              +editableField fieldUsername\n              +editableField fieldName\n              +editableField fieldEmail\n\n  .profile-right-side\n    h1 Boards\n```\n\n```coffeescript\n# client/components/profile/profile.coffee\nTemplate.profile.helpers\n  fieldUsername: -\u003e\n    data:         @\n    defaultValue: @getUsername()\n    placeholder: 'Username'\n    scope:       'user'\n    path:        'username'\n    iconSymbol:  '@'\n\n  fieldName: -\u003e\n    data:         @\n    defaultValue: @getName()\n    placeholder: 'Name'\n    scope:       'user'\n    path:        'profile.name'\n    icon:        'user'\n\n  fieldEmail: -\u003e\n    data:         @\n    defaultValue: @getPublicEmail()\n    placeholder: 'Public email'\n    scope:       'user'\n    path:        'profile.email'\n    icon:        'envelope'\n\nTemplate.profile.events\n  # отлавливаем изменения в редактируемых полях\n  # и обновляем пользователя\n  'changed .EditableFiled': (event, template, data) -\u003e\n    user = template.data?.user\n    return unless user\n    data = data.user\n    user.merge data\n```\n\nКак мне кажется, верстка метеоровских шаблонов в `jade`, достаточно семантична, не нужно задумываться о многих вещах и читать кучу документации - все и так достаточно очевидно. Но если у вас возникли проблемы с пониманием кода выше, советую полистать документацию к пакету [mquandalle:jade](https://atmospherejs.com/mquandalle/jade) и [spacebars](https://atmospherejs.com/meteor/spacebars). Просто у меня при знакомстве с версткой шаблонов в метеоре проблем не возникало, считаю, что они их, в самом деле, сделали очень удобными.\n\nВ общем все готово, открывайте форму аутентификации в шапке, входите в систему, и вместо заголовка \"Home\" на странице сразу же отобразится ваш профайл, без всяких перезагрузок.\n\n![profile](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/profile.png)\n\n* [Результат](http://hgsm-base-users.meteor.com/)\n* [Репозитарий](https://github.com/ovcharik/meteor-getting-started/tree/2110ed5168155893fbaf27a29df0675070765d81/todo-list)\n\nЕсли вам что-то не понятно до текущего момента, то советую ознакомится с текущим состоянием проекта в [репозитарии](https://github.com/ovcharik/meteor-getting-started/tree/b1067c219e591de6d6eb387d10a107cfff180e69/todo-list), я старался комментировать в файлах все происходящее, также возможно стоит еще раз полистать написанное выше, может не совсем последовательно, но я старался уделить внимание всем ключевым моментам, и конечно же можно склонировать проект, на данном этапе и пощупать его руками. Дальше я собираюсь затронуть еще несколько тем, связанных с серверным кодом: как создавать свои собственные коллекции, как можно защищать данные в коллекциях от нежелательного редактирования, расскажу немного про использование RPC и использование библиотек `npm` на сервере.\n\n## Еще про коллекции и подписки\n\nПрежде чем приступим к созданию своих коллекций предлагаю создать механизм, который будет автоматически вычислять некоторые поля при вставки/изменении данных в бд. Для этого добавим пакет [aldeed:collection2](https://github.com/aldeed/meteor-collection2), в который входит [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema). Эти пакеты позволят нам легко валидировать данные, добавлять индексы к коллекции и прочее.\n\nДобавим к пакету `aldeed:simple-schema` немного новых возможностей.\n\n```coffeescript\n# lib/simple_schema.coffee\n_.extend SimpleSchema,\n\n  # Данный метод будет из нескольких переданных объектов\n  # собирать одну схему и возвращать ее\n  build: (objects...) -\u003e\n    result = {}\n    for obj in objects\n      _.extend result, obj\n    return new SimpleSchema result\n\n  # Если добавить к схеме данный объект,\n  # то у модели появится два поля которые будут автоматически\n  # вычисляться\n  timestamp:\n    createdAt:\n      type: Date\n      denyUpdate: true\n      autoValue: -\u003e\n        if @isInsert\n          return new Date\n        if @isUpsert\n          return { $setOnInsert: new Date }\n        @unset()\n\n    updatedAt:\n      type: Date\n      autoValue: -\u003e\n        new Date\n```\n\nИ создадим новую коллекцию\n\n```coffeescript\n# collections/boards.coffee\n# схема данных\nboardsSchema = SimpleSchema.build SimpleSchema.timestamp,\n  'name':\n    type: String\n    index: true\n\n  'description':\n    type: String\n    optional: true # не обязательное поле\n\n  # автоматически генерируем автора доски\n  'owner':\n    type: String\n    autoValue: (doc) -\u003e\n      if @isInsert\n        return @userId\n      if @isUpsert\n        return { $setOnInsert: @userId }\n      @unset()\n\n  # список пользователей доски\n  'users':\n    type: [String]\n    defaultValue: []\n\n  'users.$':\n    type: String\n    regEx: SimpleSchema.RegEx.Id\n\n\n# регистрируем коллекцию и добавляем схему\nBoards = new Mongo.Collection 'boards'\nBoards.attachSchema boardsSchema\n\n\n# защита данных\nBoards.allow\n  # создавать доски может любой авторизованный пользователь\n  insert: (userId, doc) -\u003e\n    userId \u0026\u0026 true\n  # обновлять данные может только создатель доски\n  update: (userId, doc) -\u003e\n    userId \u0026\u0026 userId == doc.owner\n\n\n# статические методы\n_.extend Boards,\n  findByUser: (userId = Meteor.userId(), options) -\u003e\n    Boards.find\n      $or: [\n        { users: userId }\n        { owner: userId }\n      ]\n    , options\n\n  create: (data, cb) -\u003e\n    Boards.insert data, cb\n\n# методы объектов\nBoards.helpers\n  update: (data, cb) -\u003e\n    Boards.update @_id, data, cb\n\n  addUser: (user, cb) -\u003e\n    user = user._id if _.isObject(user)\n    @update\n      $addToSet:\n        users: user\n    , cb\n\n  removeUser: (user, cb) -\u003e\n    user = user._id if _.isObject(user)\n    @update\n      $pop:\n        users: user\n    , cb\n\n  updateName: (name, cb) -\u003e\n    @update { $set: {name: name} }, cb\n\n  updateDescription: (desc, cb) -\u003e\n    @update { $set: {description: desc} }, cb\n\n  # joins\n  getOwner: -\u003e\n    UsersCollection.findOne @owner\n\n  getUsers: (options) -\u003e\n    UsersCollection.find\n      $or: [\n        { _id: @owner }\n        { _id: { $in: @users } }\n      ]\n    , options\n\n  urlData: -\u003e\n    id: @_id\n\n\n# экспорт\n@BoardsCollection = Boards\n```\n\nПервым делом при создании коллекции мы определили схему, это позволит нам валидировать данные и автоматически вычислять некоторые поля. Подробнее о валидации можно почитать на странице пакета [aldeed:simple-schema](https://github.com/aldeed/meteor-simple-schema), там достаточно богатый функционал, а при установки дополнительного пакета `aldeed:autoform`, от тоже автора, можно генерировать формы, которые сразу же будут оповещать об ошибках, при создании записи.\n\nНовую коллекцию в бд мы создаем вызовом `Boards = new Mongo.Collection 'boards'`, если ее нет, либо подключаемся к существующей. В принципе это весь необходимый функционал для создания новых коллекций, там есть [еще пара](https://docs.meteor.com/#/full/mongo_collection) опций, которые можно указать при создании.\n\nС помощью метода `allow` у коллекции мы можем контролировать доступ к изменению данных в коллекции. В текущем примере мы запрещаем создавать новые записи в коллекции для всех неавторизованных пользователей, и разрешаем изменять данные только для создателя доски. Эти проверки будут осуществляться на сервере и можно не переживать, что какой-нибудь кулцхакер поменяет эту логику на клиенте. Также в вашем распоряжении есть практически аналогичный метод `deny`, думаю суть его ясна. Подробнее про [allow](https://docs.meteor.com/#/full/allow) и [deny](https://docs.meteor.com/#/full/deny).\n\nПри выводе карточки доски я хочу сразу отображать данные о создателе доски. Но если мы подпишемся только на доски, то эти данные поступать на клиент не будут. Однако публикации в метеоре дают возможность подписки на любые данные, даже автоматически вычисляемые, типа счетчиков коллекций и прочего.\n\n```coffeescript\n# server/publications/boards.coffee\nMeteor.publish 'boards', (userId, limit = 20) -\u003e\n  findOptions =\n    limit: limit\n    sort: { createdAt: -1 }\n\n  if userId\n    # доски конкретного пользователя\n    cursor = BoardsCollection.findByUser userId, findOptions\n  else\n    # все доски\n    cursor = BoardsCollection.find {}, findOptions\n\n  inited = false\n  userFindOptions =\n    fields:\n      service: 1\n      username: 1\n      profile: 1\n\n  # колбек для добавления создателя доски к подписке\n  addUser = (id, fields) =\u003e\n    if inited\n      userId = fields.owner\n      @added 'users', userId, UsersCollection.findOne(userId, userFindOptions)\n\n  # отслеживаем изменения в коллекции,\n  # что бы добавлять пользователей к подписке\n  handle = cursor.observeChanges\n    added: addUser\n    changed: addUser\n\n  inited = true\n  # при инициализации сразу же добавляем пользователей,\n  # при помощи одного запроса в бд\n  userIds = cursor.map (b) -\u003e b.owner\n  UsersCollection.find({_id: { $in: userIds }}, userFindOptions).forEach (u) =\u003e\n    @added 'users', u._id, u\n\n  # перестаем слушать курсор коллекции, при остановке подписки\n  @onStop -\u003e\n    handle.stop()\n\n  return cursor\n```\n\nТак как монга не умеет делать запросы через несколько коллекций и выдавать уже обработанные данные, как это происходит в реляционных бд, нам придется доставать данные о создателей досок при помощи еще одного запроса, да и так удобнее работать в рамках моделей данных.\n\nПервым делом в зависимости от запроса мы достаем из базы нужные доски, после этого нам необходимо еще одним запросом достать пользователей. Методы `added`, `changed` и `removed` в контексте публикации могут управлять данными передаваемыми на клиент. Если мы в публикации возвращаем курсор коллекции, то эти методы будут вызываться автоматически в зависимости от состояния коллекции, поэтому мы и возвращаем курсор, но дополнительно в самой публикации подписываемся на изменения данных в коллекции досок, и высылаем на клиент данные о пользователях по мере необходимости.\n\nС помощью логов соединения по веб-сокетам либо при помощи [данной](https://github.com/arunoda/meteor-ddp-analyzer) утилиты, можно убедиться, что подобный подход будет работать оптимально. И тут важно понимать, что в нашем случае изменения в коллекции пользователей не будут синхронизироваться с клиентом, но так и задумывалось. Кстати для простого \"джоина\" можно просто возвращать массив курсоров в результате подписки.\n\nДля отображения досок пользователей, я добавил новые подписки в роутеры и заверстал необходимые шаблоны, но все эти моменты мы уже рассмотрели выше, если вам интересны все изменения, то их можно увидеть [здесь](https://github.com/ovcharik/meteor-getting-started/commit/548ae3c4a6b8a752fa07c69e21ac3da929e1a70e). А в итоге мы должны получить следующее, правда доски придется создавать через консоль, что бы проверить работоспособность.\n\n![boards](https://raw.githubusercontent.com/ovcharik/meteor-getting-started/master/images/boards.png)\n\n\u003e Для создания реактивных публикаций также можно использовать пакет [mrt:reactive-publish](https://github.com/Diggsey/meteor-reactive-publish).\n\n## Дорабатываем сервер\n\nДавайте для досок добавим возможность задавать фоновое изображение, для этого нам необходимо настроить сервер, чтобы он смог принимать файлы, обрабатывать их, сохранять и отдавать при запросе.\n\n### NPM\n\nДля обработки изображений я привык использовать [ImageMagick](http://www.imagemagick.org/), и для нода есть соответствующие пакеты, которые предоставляют интерфейс к данной библиотеке. Чтобы дать метеору возможность использовать `npm` пакеты нужно добавить `meteorhacks:npm`, после этого все необходимые пакеты можно описать в файле `packages.json`. Например мне нужен только пакет [gm](https://github.com/aheckmann/gm) и мой `packages.json` будет выглядеть следующим образом:\n\n```json\n{\n  \"gm\": \"1.17.0\"\n}\n```\n\nВсе пакеты `npm` подключенные через `meteorhacks:npm` буду оборачиваться в один метеоровский пакет, поэтому при сборке приложения через команду `meteor build` не возникнет никаких проблем и все зависимости автоматически разрешаться.\n\nПодключать `npm` пакеты на сервере нужно через команду `Meteor.npmRequire(\u003cpkg-name\u003e)`, работает она также как и функция `require` в ноде.\n\n### RPC и синхронные вызовы асинхронных функций\n\nДля загрузки и обработки изображения создадим серверный метод, который можно будет вызывать с клиента.\n\n```coffeescript\n# server/lib/meteor.coffee\nMeteor.getUploadFilePath = (filename) -\u003e\n  \"#{process.env.PWD}/.uploads/#{filename}\"\n\n# server/methods/upload_board_image.coffee\n# подключаем библиотеку для обработки изображения\ngm = Meteor.npmRequire 'gm'\n\n# ресайз и сохранение изображения\nresizeAndWriteAsync = (buffer, path, w, h, cb) -\u003e\n  gm(buffer)\n  .options({imageMagick: true})\n  .resize(w, \"#{h}^\", \"\u003e\")\n  .gravity('Center')\n  .crop(w, h, 0, 0)\n  .noProfile()\n  .write(path, cb)\n\n# делаем обработку изображения синхронной\nresizeAndWrite = Meteor.wrapAsync resizeAndWriteAsync\n\n# регистрируем метод для загрузки изображения к доске\nMeteor.methods\n  uploadBoardImage: (boardId, data) -\u003e\n    board = BoardsCollection.findOne(boardId)\n    if board.owner != @userId\n      throw new Meteor.Error('notAuthorized', 'Not authorized')\n\n    data  = new Buffer data, 'binary'\n    name  = Meteor.uuid() # уникальное имя для изображения\n    path  = Meteor.getUploadFilePath name\n\n    resizeAndWrite data, \"#{path}.jpg\", 1920, 1080\n    resizeAndWrite data, \"#{path}_thumb.jpg\", 600, 400\n\n    # сохраняем данные к доске\n    BoardsCollection.update { _id: boardId },\n      $set:\n        background:\n          url:   \"/uploads/#{name}.jpg\"\n          thumb: \"/uploads/#{name}_thumb.jpg\"\n    return\n```\n\nВ методе `uploadBoardImage` мы принимаем идентификатор доски, к которой добавляется изображение и строку с бинарными данными этого изображения.\n\nЕсли в методе будет брошено исключение, то оно передастся пользователю на клиент, первым параметром коллбека. А данные возвращенные методом придут на клиент вторым параметром коллбека.\n\nЧтобы можно было использовать исключения и возвраты функций при асинхронном стиле программирования, в серверной части метеора есть метод оборачивающий асинхронные функции в синхронные, через библиотеку [fibers](https://github.com/laverdet/node-fibers). Если в кратце, благодаря этой библиотеке, вызовы обернутых функций не будут занимать очередь выполнения, так что на сервере можно писать синхронный код и не беспокоится о неправильной последовательности выполнения кода. Методом `Meteor.wrapAsync(\u003casync-func\u003e)` оборачиваются функции, которые последним параметром принимают коллбек. В этом коллбеке первым параметром должна идти ошибка, а вторым результат, такой формат параметров у всех стандартных библиотек в ноде. Если придет ошибка, то обернутая функция бросит исключение с этой ошибкой, иначе из функции вернется второй параметр переданный в коллбек.\n\n### Роутинг\n\n\u003e Я понимаю, что для выдачи статики с сервера лучше использовать готовые и обкатанные решения по многим причинам, но тут я собираюсь отдавать статику нодом.\n\nВ метеоре для серверного роутинга есть стандартный пакет [webapp](https://docs.meteor.com/#/full/webapp), но у нас уже установлено гораздо более удобное решение в виде `iron:router`. Аналогично, как и на клиенте, создадим серверный маршрут.\n\n```coffeescript\n# server/routes/uploads.coffee\nfs = Meteor.npmRequire 'fs'\n\nRouter.route '/uploads/:file',\n  where: 'server'\n  action: -\u003e\n    try\n      filepath = Meteor.getUploadFilePath(@params.file)\n      file = fs.readFileSync(filepath)\n      @response.writeHead 200, { 'Content-Type': 'image/jpg' }\n      @response.end file, 'binary'\n    catch e\n      @response.writeHead 404, { 'Content-Type': 'text/plain' }\n      @response.end '404. Not found.'\n```\n\nЗдесь главное роуту передать свойство `where: 'server'`, иначе он не будет работать. В действии мы пытаемся считать с диска указанный файл, так как в этой директории будут только изображения одного формата, я максимально упростил данный метод.\n\nОбъекты `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) соответственно. \n\n\u003e В `iron:router` есть еще [интерфейс](https://github.com/EventedMind/iron-router/blob/devel/Guide.md#server-routing) для создания REST API.\n\n### Использование RPC\n\nДля использования давайте создадим форму добавления новой доски.\n\n\u003e Тут я еще создал автокомплит для добавления пользователей к доске, там также используется RPC, подробнее с реализацией можно ознакомится в [репозитарии](https://github.com/ovcharik/meteor-getting-started/tree/master/todo-list/client/components/users_autocomplete).\n\n```jade\n//- client/components/new_board_form/new_board_form.jade\ntemplate(name='newBoardForm')\n  //- панель с динамическими стилями\n  .panel.panel-default.new-board-panel(style='{{panelStyle}}')\n    .panel-body\n      h1 New board\n      form(action='#')\n        .form-group\n          input.form-control(type='text',placeholder='Board name',name='board[name]')\n        .form-group\n          textarea.form-control(placeholder='Description',name='board[description]')\n        .form-group\n          //- прячем инпут с файлом, но оставляем метку на этот инпут, для красоты\n          label.btn.btn-default(for='newBoardImage') Board image\n          .hide\n            input#newBoardImage(type='file', accept='image/*')\n        button.btn.btn-primary(type='submit') Submit\n```\n\n```coffeescript\n# client/components/new_board_form/new_board_form.coffee\n# переменные для текущего пользовательского изображения\ncurrentImage = null\ncurrentImageUrl = null\ncurrentImageDepend = new Tracker.Dependency\n\n# сброс пользовательского изображения\nresetImage = -\u003e\n  currentImage = null\n  currentImageUrl = null\n  currentImageDepend.changed()\n\n# загрузка изображения на сервер\nuploadImage = (boardId) -\u003e\n  if currentImage\n    reader = new FileReader\n    reader.onload = (e) -\u003e\n      # вызов серверного метода\n      Meteor.call 'uploadBoardImage', boardId, e.target.result, (error) -\u003e\n        if error\n          alertify.error error.message\n        else\n          alertify.success 'Image uploaded'\n    reader.readAsBinaryString currentImage\n\n# хелперы шаблона формы\nTemplate.newBoardForm.helpers\n  # задаем фоновое изображение для формы,\n  # функция будет вызываться автоматически, так как имеет зависимость\n  panelStyle: -\u003e\n    currentImageDepend.depend()\n    currentImageUrl \u0026\u0026 \"background-image: url(#{currentImageUrl})\" || ''\n\n# данный колбек срабатывает каждый раз, когда форма рендерится на страницу\nTemplate.newBoardForm.rendered = -\u003e\n  resetImage()\n\n# события формы\nTemplate.newBoardForm.events\n  # при отправки фор","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fovcharik%2Fmeteor-getting-started","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fovcharik%2Fmeteor-getting-started","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fovcharik%2Fmeteor-getting-started/lists"}