Разработка клиент-серверной инфраструктуры на javascript (часть 1 — клиент)

в 9:20, , рубрики: backbone.js, javascript, marionette, phonegap, Мобильный веб, метки: , ,

image

О чем эта статья. Я хочу поделиться опытом разработки мобильного приложения на phonegap. В итоге получился целый програмный комплекс с RESTfull сервером, клиентами, да еще хостится на PaaS. Поэтому я опишу отдельно архитектуру клиентского приложения (html5 single page app, завернутое в phonegap), серверного (nodejs с swagger-node-express + node-orm2), и как разместить все это на openshift PaaS.

Для нетерпеливых:
Страница проекта
Исходный код

Думаю, сайт сразу ляжет от хабраэффекта. Это триальный аккаунт с всего-лишт двумя слотами для ноды. Но об этом в конце.

Хочу сразу уточнить — проект не закончен, но уже стабильно работает, плюс можно показать основные части и архитектурные решения.

Начнем, пожалуй, с самого главного — клиента. Я использовал (ex)twitter bootstrap, я понимаю что шаблон пока не очень — но главное это js логика. Само приложение построено на require.js, хотя я сам не против минимизации всего проекта. Дело в том, что на телефоне файлы быстро будут подгружатся, а для сайта, вообще-то, планируется отдельное приложение в дальнейшем. В качестве javascript фреймворка я выбрал marionette.
Сейчас реализовано два основных модуля: Auth и Conferences.
Отключил автостарт:

this.startWithParent = false;

И вручную запускаю их после инициализации главного модуля.

require(
    [
        'css!bootstrap_css',
        'bootstrap',
        'app/modules/conferences',
        'app/modules/auth',
    ],
    function () {
        app.start();
    }
);   

MyConference.addInitializer(function(options){
    mainLayout = new MainLayout;
    MyConference.mainView.show(mainLayout);
    var headerView = new HeaderView;
    headerView.MyConference = MyConference;
    mainLayout.header.show(headerView);

    MyConference.Conferences.start();
    MyConference.Auth.start();
});

Auth — регистрация/авторизация. Хочу обратить внимание на социальную авторизацию. Я всегда сам реализую авторизацию и не пользуюсь сторонними агрегаторами, незнаю хорошо это или плохо. Реализовано Google, LinkedIn, Facebook, Twitter, можете просто взять мой код, если вам нужно реализовать у себя что-то похожее. Суть социальной авторизации в том, что я с помощью js на клиенте получаю Api key, а потом передаю его на сервер для проверки. Например facebook:

var afterInit = function(){
    var sendAccessToken = function(response){
        $.post(
            cfg.baseUrl + 'auth.json/facebook',
            {FacebookKEY: response.authResponse.accessToken},
            function(data, message, xhr){
                process_social_resporce(model, data, xhr);
            },
            "text"
        );
    }

    FB.getLoginStatus(function(response) {
        if(response.status == "not_authorized" || response.status == "unknown"){
            FB.login(function(response, a) {
                if (response.authResponse) {
                    sendAccessToken(response);
                } else {
                    console.log(response, a)
                }
            }, {scope:'email'});
        }else{
            sendAccessToken(response);
        }
    });
}

window.fbAsyncInit = function() {
    FB.init({
      appId      : cfg.facebookAppId, // App ID
      status     : true, // check login status
      cookie     : true, // enable cookies to allow the server to access the session
      xfbml      : true  // parse XFBML
    });
    afterInit();
};

// Load the SDK asynchronously
(function(d){
     var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
     if (d.getElementById(id)) {return afterInit();}
     js = d.createElement('script'); js.id = id; js.async = true;
     js.src = "//connect.facebook.net/en_US/all.js";
     ref.parentNode.insertBefore(js, ref);
}(document));

Для постов/лайков мне этот ключ не нужен, только узнать что это за пользователь. Так что, если вам нужно использовать offline доступ, то такой метод может не сработать из-за того, что нужно получать отдельный ключ, который js клиентам не выдают.
Отдельная история с twitter. У него нет браузерной клиентской авторизации, поэтому я реализовал серверную. Открывается окошко, там пользователь авторизируется и потом родительское окно считывает ответ с дочернего. Это может работать не во всех браузерах, так что, скорее всего, придется немного его изменить. Но в android проложении работает нормально.

var childWin = window.open(cfg.baseUrl + 'auth.json/twitter/'+Storage.get('API_KEY'), 'Twitter Auth', "height=640,width=480");
childWin.onunload = function(){
    var check = function(){
        if(childWin.document){
            var body = childWin.document.getElementsByTagName("body")[0];
            if(!model.isNew() || body.textContent.length > 0){
                process_social_resporce(model, body.textContent);
                childWin.close();
            }else{
                setTimeout(check, 100);
            }
        }else{
            setTimeout(check, 100);
        }
    }
    setTimeout(check, 100);
}

Теперь перейдем к основному модулю — Conferences. Здесь, на самом деле, все очень просто. Описываю контролер з роутами.

var ConferencesController = Marionette.Controller.extend(new function(){
    return {
        main: function(){
            MyConference.mainView.currentView.header.currentView.setHeader('Conferences');

            var conferencesCollection = new ConferencesCollection;

            var spinnerView = new SpinnerView();
            spinnerView.render();
            conferencesCollection.fetch({
                error: function(){
                    console.log('error');
                },
                success: function(collection){
                    var mainView = new MainView;
                    mainView.collection = conferencesCollection;
                    MyConference.mainView.currentView.content.show(mainView);
                    spinnerView.remove();
                }
            })
        },
        conference: function(id){
            var conferenceModel = new ConferenceModel;
            conferenceModel.set('id', id);
            conferenceModel.fetch({
                error: function(){
                    var conferenceNotFoundView = new ConferenceNotFoundView;
                    MyConference.mainView.currentView.content.show(conferenceNotFoundView);
                },
                success: function(conference){
                    var conferenceFullView = new ConferenceFullView;
                    conferenceFullView.model = conference;
                    MyConference.mainView.currentView.content.show(conferenceFullView);
                }
            });
        },
        streams: function(conference_id){
            ShowStreams(
                conference_id,
                function(){
                    ShowStream(
                        MyConference.
                            mainView.
                                currentView.
                                    content.
                                        currentView.
                                            model.
                                                streams.
                                                    at(0).
                                                        get('id')
                    );
                }
            );
        },
        stream: function(id){
            ShowStream(id);
        }
    }
});

var MainRouter = Backbone.Marionette.AppRouter.extend({
    appRoutes: {
        "conference/:id": "conference",
        "conferences": "main",
        "": "main",
        "streams/:conference_id": "streams",
        "stream/:id": "stream"
    },
    controller: new ConferencesController
});

Как видно, создается обычная бекбоновская модель/коллекция, получаются данные и передается в вьюху. Список конференций это обычный CollectionView. Детальнее остановлюсь для View подробного описания конференции. Поддержка OpenStreetMap и GoogleMaps реализована вручную. Можно, конечно, использовать Leaflet, но я не уверен, что гуглу нравится прямой доступ к их картинкам. Так же там картинка/pdf отображается или ссылка на файл, если есть. И вверху справа ссылка на список докладов.
Если пользователь залогинен, он видит три кнопки, «пойти» на конференцию, в избранное и отказаться. Детальное описание я наследовал не от ItemView, а от Layout, поэтому просто определил блок, куда рендерить эти три кнопки.

regions: {
    decision: "#decision"
},

И в зависимости от статуса пользователя, показываю ту или иную вьюху.

if(MyConference.Auth.getUser().isNew()){
    var view = new GuestDecisionView;
}else{
    var desisionModel = new DecisionModel(view.model.get('decision'));
    var view = new LoggedInDecisionView({model: desisionModel});
    view.parent = this;
}
this.decision.show(view);

Осталось только собрать и запустить проет на телефоне. Если нужно додать платформу выполните «cordova platform add android»

cordova platform add android

Потом

cordova build android

Для сборки или

cordova run android

Чтобы посмотреть у себя на телефоне.

Конечной целью является размещение приложений в Google play, Apple App Store и Windows Store. Но основной моей деятельностью является web, а не мобильная, разработка, поэтому я еще не зарегистрирован как разработчик ни в одном из этих магазинов.

Надеюсь кому-то будет полезна эта статья. Я пытался не сильно ее раздувать, но обратить внимание на все основные моменты. Буду рад критике, пожеланиям, pull request'ам в репозиторий. Из-за того, что материала вышло много, я его разбил на две статьи — клиент и сервер. В следующей статье я опишу создание restfull сервера на nodejs с orm’ом автодокументацией и memcached’ом. И как я деплоил все это на PaaS от RedHat — openshift.

Автор: peinguin

Источник

Поделиться