Создание движка для форума с помощью Meteor и AngularJS

Меня часто просят создать небольшие HTML форумы для посетителей сайта и службы поддержки. Поэтому я решил рассказать о том, как это сделать с помощью Meteor и AngularJS:

Для начала установим Meteor! Откройте командную строку и вставьте следующую команду:

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

Если же вы используете Windows, загрузите официальную программу установки Meteor.

Теперь создадим новое приложение Meteor.

$ meteor create forum

Откройте каталог forum и удалите автоматически сгенерированные файлы: forum.css, forum.html и forum.js.

$ cd forum
$ rm forum.css forum.html forum.js

Я хочу установить Angular-Meteor, удалив ненужные пакеты - blaze-html-templates, ecmascript, autopublish и insecure. Начиная с Meteor v1.2, нужно вручную добавить check, чтобы выполнить базовую санитизацию ввода:

$ meteor remove blaze-html-templates ecmascript autopublish insecure
$ meteor add angular check

Создадим каталоги client и views в корне проекта. Первый представляет собой специальный каталог Meteor, который содержит код, используемый только на стороне клиента. Хотя Meteor позволяет написать код, выполняемый как на стороне клиента, так и на стороне сервера. Это довольно хорошая идея отделить бизнес-логику по соображениям безопасности. Давайте поместим HTML-шаблоны для верстки страниц и каталогов AngularJS в папку views:

$ mkdir client
$ mkdir views

Продолжим реализацию форума для сайта HTML. Создайте файл index.html в корневом каталоге проекта и разместите приведенный ниже код внутри него:

<head>
    <title>Angular-Meteor Forum</title>
</head>
<body ng-app="forum">
    <div ng-include="'views/forum.html'"></div>
</body>

Это будет загрузочный модуль forum AngularJS, который еще не существует. Он включает в себя шаблон views/forum.htm, который также еще не существует. Давайте исправим это.

Для начала исправим модуль AngularJS. Создайте файл client/app.js и определите модуль:

angular.module('forum', ['angular-meteor']);

Далее создайте шаблон форума views/forum.html. Это будет общий макет, используемый всеми страницами:

<div class="forum">
    FORUM
</div>

На этом этапе можно запустить приложение.

$ meteor

Откройте браузер и перейдите по адресу http://localhost:3000. Вы должны увидеть текст FORUM. Давайте создадим шаблон views/pages/topics.html для отображения тем HTML форума:

<div class="page">
    <h1>Topics</h1>
    <div ng-repeat="topic in topics">
        <a ui-sref="topic({topicId: topic._id})">{{ topic.name }}</a>
    </div>
</div>

Чтобы увидеть этот шаблон в действии, мы установим решение для маршрутизации в AngularJS - angular-ui-router, и определим основные маршруты. Выйдите из Meteor с помощью Ctrl+C или вновь откройте командную строку и перейдите к корневой папке приложения:

meteor add angularui:angular-ui-router

Созданный нами app.js - отличное место для настройки маршрутизации и контроллеров. Тем не менее, вы можете поместить их в разные файлы. Если сделаете это, то убедитесь, что Meteor загружает файлы. Поместите определение модуля в файл client/lib/, иначе он будет загружен после контроллера и каталога, вызывая исключение в AngularJS.

Замените содержимое файла app.js:

angular.module('forum', ['angular-meteor', 'ui.router'])
.config(function($urlRouterProvider, $stateProvider){

    // Установка маршрута по умолчанию 
    $urlRouterProvider
        .when('/', '/topics')
        .otherwise('/topics');

    // Добавление состояния
    $stateProvider.state('topics', {
        url: '/topics',
        templateUrl: 'views/pages/topics.html'
    });

})
.run(function($state){
    // Мы вводим $state здесь, чтобы инициализировать ui.router 
})

Прежде чем список тем отобразится в браузере, нужно доработать файл views/forum.html. Нам необходим элемент с атрибутом ui-view для отрисовки шаблона форума HTML, связанного с текущим маршрутом:

<div class="forum">
    <div class="page-container" ui-view>
    </div>
</div>

Перейдите сейчас по адресу http://localhost:3000 и обратите внимание, что вы мгновенно перенаправляетесь на страницу тем. Но у нас еще нет тем. Поскольку мы захотим добавить или удалить темы, будет хорошей идеей сохранить их в базе данных.

Meteor использует СУБД MongoDB. Можно определить коллекции MongoDB с помощью new Meteor.Collection (collectionName). Этот код должен присутствовать и на стороне клиента, и на стороне сервера, поэтому создадим каталог common и поместим данную строку в common/db.js:

Topics = new Meteor.Collection('Topics');

В Meteor публикация - это способ построения набора данных для отправки клиенту. Вы вызываете Meteor.publish с именем набора данных, и функция обратного вызова возвращает курсор в mongodb. Meteor считывает изменения из mongodb oplog (или опрашивает базу данных, если oplog не установлен должным образом) и обновляет курсор, когда данные добавлены, удалены или изменены.

Публикация должна быть определена в файле только для сервера, поэтому создадим папку server. Каждый JavaScript-файл, который вы помещаете сюда, будет работать только на сервере.

Создадим файл server/publications.js:

Meteor.publish('topics', function(){
    return Topics.find();
});

Meteor.publish('topic', function(id){
    check(id, String);
    return Topics.find({_id: id});
});

Это позволит определить две публикации в форуме для сайта HTML: одну для всех тем и одну для той, которую запрашивает идентификатор. Кроме этого, давайте вставим некоторые темы по умолчанию, если коллекция пуста. Поместите приведенный ниже код в server/defaults.js:

if (Topics.find().count() === 0) {
    _.each(['General Discussion', 'Tutorials', 'Help'], function(topicName){
        Topics.insert({name: topicName});
    });
}

Клиент инициирует подписку, которая подключается к публикации, и получает эти данные. Давайте посмотрим, как это сделать с помощью Angular-Meteor. В client/app.js добавьте параметр контроллера в определение состояния topics, называемого TopicsContoller, и определите контроллер после .run():

// ...
        $stateProvider.state('topics', {
            url: '/topics',
            templateUrl: 'views/pages/topics.html',
            controller: 'TopicsContoller'
        });
// ...
.controller('TopicsContoller', function($scope){
    $scope.subscribe('topics');
    $scope.helpers({
        topics: function() {
            return Topics.find({}, {sort: {name:1}});
        }
    });
})

Здесь мы присоединились к публикации topics, а также добавили помощника с именем topics, который возвращает курсор MongoDB на список тем, отсортированных по именам. С точки зрения AngularJS это простой массив, доступный в переменной $scope.topics.

Просмотрите HTML форум в браузере. Вы должны увидеть три темы в соответствии с заголовком. Тем не менее, если кликнуть по ним, ничего не произойдет. Это потому, что мы не определили состояние topic. Давайте сделаем это и определим TopicController:

// ...
        $stateProvider.state('topic', {
            url: '/topic/:topicId',
            templateUrl: 'views/pages/topic.html',
            controller: 'TopicContoller'
        });
// ...
.controller('TopicContoller', function($scope, $stateParams){
    $scope.subscribe('topic', function(){ return [$stateParams.topicId]; });
    $scope.helpers({
        topic: function() {
            return Topics.findOne({_id: $stateParams.topicId});
        }
    });
})

Это довольно просто, но учтите, что нужно использовать функцию, чтобы передать аргумент идентификатора topics публикации и вернуть одну тему с findOne вместо find.

Шаблон views/pages/topic.html будет довольно простым, но не волнуйтесь, мы скоро добавим список ветвей обсуждения:

<div class="page">
    <h1>{{ topic.name }}</h1>
</div>

Чтобы использовать HTML код форума для сайта, нужно идентифицировать пользователей. У Meteor есть полезные пакеты для аутентификации:

  • accounts-base: Этот пакет реализует основные функции, необходимые для учетных записей пользователей и позволяет другим пакетам использовать службы входа в систему;
  • accounts-password: служба, которая включает безопасный, основанный на пароле, вход в систему;
  • dotansimha:accounts-ui-angular: AngularJS «контейнер» для пакета UI учетной записи Meteor.

Давайте установим их:

$ meteor add accounts-base accounts-password dotansimha:accounts-ui-angular

Для использования dotansimha:accounts-ui-angular модуль AngularJS должен указать accounts.ui в качестве зависимости:

angular.module('forum', ['angular-meteor', 'ui.router', 'accounts.ui'])

Теперь можно добавить аутентификацию, регистрацию, функции: forgot password и change password с помощью одной строки. Лучше всего прописать все это в views/forum.html, поскольку это корневое расположение для всех страниц шаблона форума HTML:

<div class="forum">
    <login-buttons></login-buttons>
    <div ui-view>
    </div>
</div>

Пора создать потоки выполнения:

  • Определить коллекцию common/db.js:
Threads = new Meteor.Collection('Threads');
  • Опубликовать треды (ветви обсуждения) в server/publications.js:
Meteor.publish('threads', function(topicId){
    check(topicId, String);
    return Threads.find({topicId: topicId});
});

Meteor.publish('thread', function(id){
    check(id, String);
    return Threads.find({_id: id});
});
  • Отредактировать views/pages/topic.html, чтобы перечислить треды темы и добавить форму, где пользователи могут создавать новые ветви обсуждения:
<div class="page">
    <h1>{{ topic.name }}</h1>
    <ul>
        <li ng-repeat="thread in threads">
            <a ui-sref="thread({threadId: thread._id})">{{ thread.content }}</a>
            by {{ thread.author }} at {{ thread.createdAt | date }}
        </li>
    </ul>

    <h2>Create a new thread</h2>
    <form ng-submit="createThread(thread)">
        <input type="text" placeholder="Start discussion..." ng-model="thread.content">
        <button type="submit">Create</button>
    </form>
</div>
  • Изменить TopicController в client/app.js так, чтобы он подписался к списку тредов, которые принадлежат текущей теме, и управлял созданием ветвей обсуждения:
.controller('TopicContoller', function($scope, $stateParams, $meteor){
    $scope.subscribe('topic', function(){ return [$stateParams.topicId]; });
    $scope.subscribe('threads', function(){ return [$stateParams.topicId]; });
    $scope.helpers({
        topic: function() {
            return Topics.findOne({_id: $stateParams.topicId});
        },
        threads: function() {
            return Threads.find({topicId: $stateParams.topicId});
        }
    });
    $scope.createThread = function(thread){
        $meteor.call("createThread", $stateParams.topicId, thread.content).then(function(){
            thread.content = '';
        }).catch(function(){
            alert("An error occured while creating the thread!");
        });
    };
})

Увидели вложенную службу $meteor? Мы используем ее для вызова серверного метода createThread, который еще предстоит создать.

Вставьте приведенный ниже код в файл HTML форума server/methods.js:

Meteor.methods({
    createThread: function(topicId, content){
        check(topicId, String);
        check(content, String);
        var user = Meteor.user();
        if (!user) {
            throw new Meteor.Error("You are not logged in!");
        }
        if (!content){
            throw new Meteor.Error("Content is required!");
        }
        var thread = {
            author: user.emails[0].address,
            createdAt: new Date(),
            topicId: topicId,
            content: content
        };
        return Threads.insert(thread);
    }       
});

Meteor.methods ожидает объект, в котором ключи - имена методов, а значения - определения методов. Методы могут иметь аргументы и могут возвратить что-либо, что может быть сериализовано в формате JSON. Курсоры Meteor не сериализуются, поэтому постарайтесь избежать возвращения collection.find(...), поскольку это приведет к краху приложения.

Давайте подведем итоги. Мы можем:

  • Перечислить темы;
  • Открыть темы со списком тредов;
  • Создать треды;
  • Зарегистрироваться / войти / выйти, сбросить забытый пароль и изменить пароль.

Что осталось сделать:

  • Определить коллекцию Posts и опубликовать ее так же, как это сделали с Threads;
  • Создать маршрут для одной темы, чтобы перечислить сообщения так же, как это сделали с topic;
  • Создать форму, чтобы публиковать тему, как это сделали с topic;
  • Написать метод для создания записи, как мы это сделали с createThread.

Вы готовы к реализации проекта? Не волнуйтесь, вы всегда можете посмотреть пример создания форума на сайте HTML в репозитории GitHub.

Пожалуйста, оставьте ваши комментарии по текущей теме статьи. За комментарии, подписки, дизлайки, отклики, лайки низкий вам поклон!