Введение
Здраствуйте, дорогие хабровчане! Сегодня у нас в основном будут маленькие изменения, но изменений много. В этой части мы будем:
- Создавать свой логгер
- Записывать в лог запросы и время их обработки
- Исправлять ошибки, которые мы допустили в первой части.
- Разбираться с авторизациеей
- Разбираться с некоторыми классами
- Конфиги!
Логи
Для логов мы будем использовать самодельный модуль. Создадим папку logger
. В нем будет файл index.js
.
var stackTrace = require('stack-trace'); // Для получения имени родительского модуля
var util = require('util'); //util.inspect()
var path = require('path'); //path.relative() path.sep
var projectname = require('../package').name; //package.json -> project name
module.exports = class Logger // Класс логера :)
{
constructor()
{
function generateLogFunction(level) // Функция генератор функий логгера :)
{
return function(message,meta)
{
//var d = Date.now(); // Будем потом записовать время вызова
var mes = this.module + " -- ";
mes += level + " -- ";
mes += message; // прицепить сообщение
if(meta) mes += " " + util.inspect(meta) + " "; // Записать доп инфу (Object||Error)
mes += '\n'; // Конец строки :)
this.write(mes);
// Записать во все потоки наше сообщение
}
};
this.trace = stackTrace.get()[1]; // Получить стек вызова
this.filename = this.trace.getFileName(); // Получить имя файла которое вызвало конструктор
this.module = projectname + path.sep + path.relative('.',this.filename); // Записать име модуля
this.streams = [process.stdout]; // Потоки в которые мы будем записовать логи
// В дальнейшем здесь будет стрим к файлу
this.log = generateLogFunction('Log'); // Лог поведения
this.info = generateLogFunction('Info'); // Лог информативный
this.error = generateLogFunction('Error'); // Лог ошибок
this.warn = generateLogFunction('Warning'); // Лог предупреждений
}
write(d)
{
this.streams.forEach((stream)=>{
stream.write(d);
});
}
}
А теперь про синтаксис использования.
var logger = new require('./logger')();
//...
logger.info('Hello, world');
Почему мы используем new
? Для того что бы получить имя файла в котором был создан логер. Ибо запуск stack-trace каждый раз когда мы пишем в лог будет использовать много ресурсов. Заменим все console на logger. Оставлю все на волю вашего IDE :)
NOTE: В папке doc
и node_modules
есть файлы использующие console
. Будьте осторожны!
Так-же заменим в файле worker.js
console.error
на throw
. Вот так:
app.listen(3000,function(err){
if(err) throw err;
// Если есть ошибка сообщить об этом
logger.log(`Running server at port 3000!`)
// Иначе сообщить что мы успешно соединились с мастером
// И ждем сообщений от клиентов
});
Почему мы не используем winston
и другие модули для работы с логами? Ответ прост: winston
показывает маленькую производительность. И не только винстон. Тоже самое касается многих модулей. Как оказалось после некоторого тестирования наш самодельный модуль показывает 4-8 раза больше производительности чем многие другие модули :)
Время обработки запроса
Для того что бы видеть какие запросы пришли на сервер и сколько времени заняла ее обработка мы напишем свой middleware. В папке bin
создадим файл rt.js
var Logger = require('../logger');
var logger = new Logger();
module.exports = function(req,res,next)
{
// Засечь начало
var beginTime = Date.now();
// В конце ответа
res.on('finish',()=>{
var d = Date.now();// получить дату в мс
logger.log('Reponse time: ' + (d - beginTime),{
url:req.url, // записать в лог куда пришел запрос (Включает urlencode string :)
time:(d - beginTime) // сколько прошло времени
});
});
// Передать действие другому обработчику
next();
}
А в worker.js
до каких либо обработчиков добавим:
// Время ответа
app.use(require('./rt'));
Здесь мы используем собственный модуль потому что все остальные модули НЕ умеют логировать только гарантированно отправленные (хотя бы до ОС) запросы.
Разница приложения от роутера
В контроллере мы видели express()
для создания мини приложения и потом мы монтировали его в проект с помощью app.use()
но express не рекомендуют так делать. Мы заменим express()
на new express.Router()
в файле контроллера:
// Пример:
var Router = require('express').Router;
var app = new Router();
// app.use(....)
// app.get(....)
// etc
Какие проблемы возникнут с express()
? Самое важное. Мы не можем изменить настройки во всем приложении. К тому же не можем использовать app.locals
. И еще по какой-то не понятной причине оно НЕ передает куки (Почему так?).
Конфигурация
Создадим папку config
. В папке у нас будет файл index.js
, где мы будем получать все настройки, складывать, парсить и даже грабить корованы вставлять при необходимости нужные поля, если они отсутствуют.
module.exports = require('./config');
А в файле config.json
:
{
"port":8080,
"mongoUri":"mongodb://127.0.0.1/armleo-test"
}
NOTE: Если нет включенных сетей в windows то localhost
не работает и надо использовать 127.0.0.1
В файле worker.js
добавим в начале:
var config = require('../config');
А последние строки превращаются в зайца следующее:
// Запустим сервер на порту 3000 и сообщим об этом в консоли.
// Все Worker-ы должны иметь один и тот же порт
app.listen(config.port,function(err){
if(err) throw err;
// Если есть ошибка сообщить об этом
logger.log(`Running server at port ${config.port}!`);
// Иначе сообщить что мы успешно соединились с мастером
// И ждем сообщений от клиентов
});
Мы ведь помним, что нам еще нужно изменить строки в dbinit.js
? Так сделаем это.
// 10 line bin/dbinit.js
// Подключимся к серверу MongoDB
var config = require('../config');
mongoose.connect(config.mongoUri,{
server:{
poolSize: 10
// Поставим количество подключений в пуле
// 10 рекомендуемое количество для моего проекта.
// Вам возможно понадобится и то меньше...
}
});
ES6
Мы заменим все нужные var
на const
и let
. Маленькое изменение, но оно мне нравится!
Авторизация
Теперь к авторизации! Для авторизации будем использовать Passport.js
. В нашем проекте ПОКА не нужна регистрация ибо пользователь один и будет добавлен нами в датабазу в ручную. Создадим контроллер auth.js
. В контроллере нам нужны парсеры входных данных:
let app = new (require('express').Router)();
const models = require('./../models');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(
function(username, password, done) {
models.User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (user.password != password) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
passport.serializeUser(function(user, done) {
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
models.User.findById(id, function(err, user) {
done(err, user);
});
});
app.post('/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
})
);
app.get('/login',function(req,res,next)
{
if(req.user) return res.redirect('/');
res.render('login',{
user:req.user
});
});
module.exports = app;
Для авторизации мы в views/login.html
добавим форму.
<form action="/login" method="POST">
<input name="username"/>
<br/>
<input name="password"/>
<br/>
<input type="submit"/>
</form>
Для пользователей придумаем модель.
Посты!
Установим модули:
npm i mongoose-url-slugs --save
Создадим модель постов в папке models
файл post.js
:
// Загрузим mongoose т.к. нам требуется несколько классов или типов для нашей модели
const mongoose = require('mongoose');
const URLSlugs = require('mongoose-url-slugs');
// Создаем новую схему!
let postSchema = new mongoose.Schema({
title:{
type:String, // тип: String
required:[true,"titleRequired"],
// Данное поле обязательно. Если его нет вывести ошибку с текстом titleRequired
// Максимальная длинна 32 Юникод символа (Unicode symbol != byte)
minlength:[6,"tooShort"],
unique:true // Оно должно быть уникальным
},
text:{
type:String, // тип String
required:[true,"textRequired"]
// Думаю здесь все тоже очевидно
},
// Здесь будут и другие поля, но сейчас еще рано их сюда ставить!
// Например коментарии
// Оценки
// и тд
// slug:String
});
// Теперь подключим плагины (внешние модули)
// Подключим генератор на основе названия
postSchema.plugin(URLSlugs('title'));
// Компилируем и Экспортируем модель
module.exports = mongoose.model('Post',postSchema);
А в файле models/index.js
:
module.exports = {
// Загрузить модель юзера (пользователя)
// На *nix-ах все файлы чувствительны к регистру
User:require('./user'),
Post:require('./post')
};
// Не забудем точку с запятой!
Создадим контроллер для создания/редактирования постов! В файле controllers/index.js
:
const Logger = require('../logger');
const logger = new Logger();
let app = new (require('express').Router)();
app.use(require('./auth'));
app.use(require('./home'));
app.use(require('./post'));
module.exports = app;
А в файле controllers/post.js
:
let app = new (require('express').Router)();
const models = require("../models");
app.get('/post', function(req,res,next)
{
if(!req.user) return res.redirect('/login');
res.render('addpost',{
user:req.user
});
});
app.post('/post', function(req, res, next)
{
if(!req.user) return res.redirect('/login');
let post = new models.Post(req.body);
post.save()
.then(()=>{
res.redirect('/post/' + post.slug);
}).catch(next);
});
app.get('/post/:slug',(req, res, next)=>{
models.Post.findOne({
slug:req.params.slug
}).exec().then((post)=>{
if(!post) res.redirect('/#notfound');
res.render('post',{
user:req.user,
post
});
}).catch(next);
});
module.exports = app;
И соответсвенно образы! Создания views/addpost.html
<form method="POST" action="/post">
<input name="title"/>
<br/>
<input name="text"/>
<br/>
<input type="submit"/>
</form>
Отображения views/post.html
{{#post}}
<h1>{{title}}</h1>
<br/>
{{text}}
{{/post}}
Немного доделаем образ views/index.html
{{#user}}
Hello {{username}}
{{/user}}
{{^user}}
Login <a href="/login">here!</a>
{{/user}}
{{#posts}}
<br/><a href="/post/{{slug}}">{{title}}</a>
{{/posts}}
Доделаем контроллер controllers/home.js
:
let app = new (require('express').Router)();
const models = require('../models');
app.get('/',(req,res,next)=>{
//Создадим новый handler который сидит по пути `/`
models.Post.find({}).exec().then((posts)=>{
res.render('index',{
user:req.user,
posts
});
// Отправим рендер образа под именем index
}).catch(next);
});
module.exports = app;
В bin/worker.js
добавим парсер urlencode:
app.use(bodyParser.urlencoded());
GitHub
Наш проект вы можете найти на гитхабе вот тут
Конец второй части!
На этом конец второй части. В след. частях мы выделим немного времени HTTPS, сервисам в Ubuntu, Будем Хешировать пароль, Каптче (recaptcha) и комментариям с некоторой статистикой и введем поддержку markdown для постов, Заменим MongoDB для сессий на Redis, Добавим кеширования.
Комментарии (8)
RA_ZeroTech
02.12.2016 00:50если уж проводили рефакторинг, то в роутах уберите проверку «if(!req.user) return res.redirect('/login');»
и вынесите ее в отдельный middleware
пример:
const AuthCheck = require('path/to/authcheck.js'); ........ app.get('/post', AuthCheck, function(req,res,next) { ..... }); app.post('/post', AuthCheck, function(req, res, next) { ..... });
где файл authcheck.js с вашей проверкой…
'use strict'; module.exports = function(req, res, next) { if(!req.user) { return res.redirect('/login'); } next(); };
RA_ZeroTech
02.12.2016 03:56или еще проще вызывать так в вашем файле post.js:
app.use(AuthCheck); app.get('/post', function(req,res,next) { ..... }); app.post('/post', function(req, res, next) { ..... });
Armleo
02.12.2016 07:28Будет. Как видите статья разделена на две части. К счастью оно будет во второй части.
RA_ZeroTech
02.12.2016 04:10Какие проблемы возникнут с express()? Самое важное. Мы не можем изменить настройки во всем приложении.
о каких настройках приложения вы говорите? не встречал проблем с этим
К тому же не можем использовать app.locals.
от чего же не можете?
в доках все написано
app.locals
res.locals
И еще по какой-то не понятной причине оно НЕ передает куки (Почему так?).
может просто надо поставить cookieParser!?Armleo
02.12.2016 07:26Речь про setCookie. Чуть позже напишу про это подробнее.
RA_ZeroTech
02.12.2016 11:02для установки куки достаточно потом вызывать:
res.cookie(name, value, options);
Rapeed
Спасибо за публикацию. Скажите пожалуйста, а почему часть 2.1? Это исправленный вариант второй части? Буквально вчера второй части еще не было :)
Armleo
1 часть.
2.1 часть
2.2 часть.
3 часть.
Возможно будут другие части. (Например дополнения)