Здравствуйте, в этой статье я расскажу о серверной части и опишу процесс размещения приложения на облачном
Разработка клиент-серверной инфраструктуры на 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'});
}
}));
Локально и на моем
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 закончено. Как только проект начал обретать готовый вид я задумался, куда его разместить. Сначала он жил на моем
Хотя я и описываю в этой статье openshift, но сам я к ним никак не отношусь и процедуры размещения на других хостингах будут очень сильно похожи(все сводится к git push). Выбрал я именно этот
Итак, сначала нужно зарегистрироватся, если вы этого еще не сделали. Потом создать проект, в их документации это подробно описано. Особенно я хочу обратить ваше внимание на некоторые вещи. Во-первых, если вы думаете хостить проект в облаке, то лучше сразу выберите где именно и заточите проект под именно этот
Я переместил 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