Разработка клиент-серверной инфраструктуры на 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
Вот и все, проект в облаке, все щасливы. Как всегда, рад критике и предложениям.
ссылка на оригинал статьи http://habrahabr.ru/post/200356/
Добавить комментарий