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

в 10:17, , рубрики: javascript, node.js, openshift, orm, paas, RESTful, хостинг, метки: , , , , ,

imageЗдравствуйте, в этой статье я расскажу о серверной части и опишу процесс размещения приложения на облачном хостинге openshift.

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

Сервер написан на nodejs с использованием swagger-node-express. Это дает преимущество автодокументации. Хочу отметить, что сервер создавался, скорее, в поддержку клиентской части, поэтому некоторые проверки и оптимизиции были намеренно опущены и оставлены на будущее.

Сервер состоит из набора модулей. В главном файле все модули инициализируются.

var express = require("express"),
 swagger = require("./swagger"),
 orm = require('./orm'),
 auth = require('./auth'),
 config = require('./config'),
 static_files = require('./static');

var app = express();

app.use(express.bodyParser());

auth.init(app);
orm.init(app);
swagger(app);
static_files(app);

app.listen(config.listen, config.ipaddr);

Основные модули. Для автентификации используется http заголовок, его имя можно задавать в параметрах. Сесия хранится в memcached, это просто соответствие api_key -> user_id. Код проверки пользователя.

var client = new memcache.Client(cfg.memcache.port, cfg.memcache.host);
...
app.use(function(req, res, next){
	client.get(
		req.headers[cfg.header.toLowerCase()],
		function(error, result){
			if(result){
				req.user = result;
			}
			next();
		}
	);

	req.memcache = client;
});

Для работы с базой данных используется node-orm2. Отмечу, что в package.json добавляйте только тот драйвер базы данных, который будете использовать.
Соединение с базой и пример описания модели.

app.use(orm.express(config.db, {
    define: function (db, models) {

        db.define("users",
          {
            id       : Number,
            email    : String,
            password : String,
            twitter  : Number,
            facebook : Number,
            google   : Number,
            linkedin : String
        },
        {
            validations: {
              email: [
                orm.enforce.unique("Email already taken!"),
                orm.enforce.unique({ ignoreCase: true }),
                  orm.enforce.notEmptyString()
                ],
                password: orm.enforce.notEmptyString()
            },
          id: "id",
          autoFetch: false
        }
      );
      var Conferences = db.define("conferences",
        {
            id          : Number,
            title       : String,
            description : String,
            datetime    : Date,
            place       : String,
            location    : String,
            site        : String,
            logo        : String,
            facebook    : String,
            twitter     : String,
            telephone   : String,
            cost        : String,
            file        : String
        },{
          id: "id",
          autoFetch: false
        }
      );
      var Decisions = db.define("decisions",
        {
          id            : Number,
          decision      : ['go', 'not go', 'favorite'],
          user          : Number,
          conference_id : Number
        },{
          id: "id",
          cache: false,
          autoFetch: false
        }
      );

      Decisions.hasOne('conference', Conferences, {reverse: 'decision'});
    }
}));

Локально и на моем VPS для статики используется nginx, но в случае PaaS сам nodejs статику и отдает, поэтому для нее я создал отдельный обработчик. Нужно отдавать документацию и сам клиент.

var static_handler = express.static(__dirname + '/../static/');
app.get(/^/static(/.*)?$/, function(req, res, next) {
	if (req.url === '/static') { // express static barfs on root url w/o trailing slash
		res.writeHead(302, { 'Location' : req.url + '/' });
		res.end();
                return;
	}

	req.url = req.url.substr('/static'.length);
	return static_handler(req, res, next);
});

var main_handler = express.static(__dirname + '/../client/www/');
app.get(/^/(.*)$/, function(req, res, next) {
	if(req.url == '/cordova.js'){
		return res.send('');
	}

	if(!req.url){
		req.url = 'index.html';
	}
	return main_handler(req, res, next);
});

Файл cordova.js создается phonegap`ом, содержит функции общения с железом и прочие, платформозависимые фичи. В браузер просто отдается заглушка, таким образом клиент знает, что функции ОС он использовать не может.
Теперь нужно инициализировать сам сервер.

app.use(function(req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
    res.header('Access-Control-Allow-Headers',  cfg.header+', Content-Type');
    res.header('Access-Control-Expose-Headers', cfg.header+', Content-Type');

    if (req.method == 'OPTIONS') {
        res.send(200);
    }
    else {
        next();
    }
});

swagger.setAppHandler(app);
swagger.addModels(models);
controllers.init(swagger);
swagger.configure(cfg.basePath, "0.1");
// Serve up swagger ui at /docs via static route
var docs_handler = express.static(__dirname + '/../documentation/swagger/');
app.get(/^/docs(/.*)?$/, function(req, res, next) {
	if (req.url === '/docs') { // express static barfs on root url w/o trailing slash
		res.writeHead(302, { 'Location' : req.url + '/' });
		res.end();
		return;
	}
	// take off leading /docs so that connect locates file correctly
	req.url = req.url.substr('/docs'.length);
	return docs_handler(req, res, next);
});

Чтобы из браузера можно было свободно пользоватся API отдаются cross-origin resource sharing headers. Потом инициализация свагера. Тут нужно обратить внимание на

swagger.addModels(models);
controllers.init(swagger);

Эти модели, что додаются, это, можно сказать, аналог ViewModel и нинкак не относятся к моделям, описываемых в orm.js. В controllers.js прописываются обработчики action’ов. Пример.

swagger.addGET(conferences.get);
swagger.addGET(conferences.list);
swagger.addPOST(conferences.decision);
swagger.addDELETE(conferences.reject);

Каким образом описывается action. Распишу пример обработчика получения конференции по id.

var get = {
  'spec': {
    "description" : "Get conference by id",
    "path" : "/conferences.{format}/{id}",
    "notes" : "Get conference",
    "summary" : "Get conference",
    "method": "GET",
    "responseClass" : "Conference",
    "nickname" : "conference"
  },
  'action': function (req,res) {

    if (!req.params.id) {
        throw swagger.errors.invalid('id'); }
      var id = parseInt(req.params.id);

    req.db.models.conferences.get(id, function(err, conference){
      if(err){
        res.send(500, JSON.stringify({code: 500, header: 'Internal Server Error', message: JSON.stringify(err)}));
      }else{
        if(conference){
          if(conference.file){
            conference.file = '/static/' + conference.file;
          }
          if(req.user){
            conference.getDecision({user: req.user}, function(err, decision){
              if(err){
                res.send(500, JSON.stringify(err));
              }else{
                conference.decision = decision.pop();
                res.send(200, JSON.stringify(conference));
              }
            });
          }else{
            res.send(200, JSON.stringify(conference));
          }
        }else{
          throw swagger.errors.notFound('conference');
        }
      }
    });
  }
};

В «spec» в основном информация для документации, но тут находится «path», тоесть url, по которому express будет вызывать обработчик. Основная часть это — action. Проверяется наличие параметров и запрос в базу данных. Кто не любит, когда колбеки разрастаются вширь, скажу сразу, что node-orm2 позволяет строить цепочки как запроса так и обработчиков. Если к конференции прикреплены файлы, то для них генерируется путь. Если пользователь залогинен ищется его решение относительно конференции. Это делается для экономии http запросов, но скажу чесно, что не знаю, как лучше и правильнее с точки зрение REST: прикреплять has-one модель к родительской или возвращать id, а клиент уже пусть сам шлет еще один запрос, если ему нужно.
На этом, думаю, описание работы с swagger-node-express закончено. Как только проект начал обретать готовый вид я задумался, куда его разместить. Сначала он жил на моем VPS, но я там постоянно все ламаю, значит было решено выложить его на PaaS. Потому что облака это модно, а еще весело. Кстати, многие облачные хостинги предлагают бесплатные аккаунты, причем многие из них без ограничений во времени. Таким образом можно свободно хостить с десяток проектов.
Хотя я и описываю в этой статье openshift, но сам я к ним никак не отношусь и процедуры размещения на других хостингах будут очень сильно похожи(все сводится к git push). Выбрал я именно этот хостинг совершенно случайно.
Итак, сначала нужно зарегистрироватся, если вы этого еще не сделали. Потом создать проект, в их документации это подробно описано. Особенно я хочу обратить ваше внимание на некоторые вещи. Во-первых, если вы думаете хостить проект в облаке, то лучше сразу выберите где именно и заточите проект под именно этот хостинг. Таким образом вы избавитесь от переделывания проекта перед деплоем, например мне нужно было переименовать главный файл в server.js, додать поддержку статики клиентского приложения и использовать внешний memcached сервер.
Я переместил client/www в server/www и туда же добавил каталог со статикой, прикрепленной к конференциям, и свагер. Вот как выглядит новый static.js.

var static_handler = express.static(__dirname + '/www/static/');
app.get(/^/static(/.*)?$/, function(req, res, next) {
  if (req.url === '/static') { // express static barfs on root url w/o trailing slash
    res.writeHead(302, { 'Location' : req.url + '/' });
    res.end();
    return;
  }
  // take off leading /docs so that connect locates file correctly
  req.url = req.url.substr('/docs'.length);
  return static_handler(req, res, next);
});

var main_handler = express.static(__dirname + '/www/');
app.get(/^/(.*)$/, function(req, res, next) {

  if(req.url == '/cordova.js'){
    return res.send('');
  }

  if(!req.url){
    req.url = 'index.html';
  }
  return main_handler(req, res, next);
});

Во-вторых, поинтересуйтесь насчет SQL/no-SQL баз, потому что в некоторых базы отдельно, а в некоторых, openshift например, занимают целый слот, а их и так мало. Memcached я отдал другому сервису garantiadata.com, где тоже создал триальный аккаунт. У меня осталось 2 слота, кстати, вчера второй экзепляр ноды тоже запускался, что меня порадовало, так как я не был уверен что автомасштабирование пройдет успешно.
И да, сам деплой.

git add --all; git commit -m 'commit'; git push origin master

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

exports.listen = process.env.OPENSHIFT_NODEJS_PORT || 8080;
exports.ipaddr = process.env.OPENSHIFT_NODEJS_IP || "127.0.0.1";

Состояние можно мониторить, при git push вы получаете лог. А также можно зайти по ssh и посмотреть tail_all.
Через ssh также можно подключится к БД, предварительно посмотев строку подключения.

echo $OPENSHIFT_POSTGRESQL_DB_URL

Вот и все, проект в облаке, все щасливы. Как всегда, рад критике и предложениям.

Автор: peinguin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js