Доброго времени суток, Хабр! Если Вы давно хотели сделать себе, соседке или её собаке сайт, но пока не сделали, то эта статья для Вас! В этой серии статей я покажу основы работы с vibe для создания сайтов на примере простого блога.
В первой части мы разберём базовые моменты и добавим к получившемуся приложению шифрование.
0. Начнём с самого начала
- устанавливаем dmd
- устанавливаем dub
- создаём проект, использующий vibe следующими командами
mkdir yourblogname
cd yourblogname
dub init -t vibe.d
дальше пойдут вопросы dub о создаваемом приложении. Большинство полей будут иметь значения по умолчанию, но я советую выбирать формат sdl, так как он читабельней и проще чем json, а возможности одинаковые.
1. Простое веб-приложение
Итак у нас уже есть заготовка от Людвига и нам нужно расширить её. Первое что мы сделаем, это создадим класс нашего приложения и добавим пару страниц.
import vibe.d;
shared static this()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "127.0.0.1"];
auto router = new URLRouter; // отвечает за обработку путей в запросе
router.registerWebInterface(new MyBlog); // реализация нашего приложения
router.rebuild(); // должно немного ускорить обработку =)
listenHTTP(settings, router); // заменяем адрес функции на роутер
}
class MyBlog
{
@path("/") void getHome(scope HTTPServerRequest req, scope HTTPServerResponse res)
{
res.writeBody("Hello, World! <a href='/page2'>go to page2</a>", "text/html; charset=UTF-8");
}
void getPage2(scope HTTPServerRequest req, scope HTTPServerResponse res)
{
res.writeBody("Page2 <a href='/'>go to index</a>", "text/html; charset=UTF-8");
}
}
Как можно заметить, метод getHome
имеет UDA @path("/")
, который указывает на то, что это корневая страница. UDA — user defined attributes — атрибуты объявляемые пользователем. По сути это любые объекты языка: enum'ы, структуры, простые типы данных. Используя сущьность, имеющую UDA мы можем проверить как наличие атрибута, его значение, а так же получить все поля и методы в классе/структуре, помеченные каким-то UDA. vibe, в данном случае, использует это для построения кода роутинга. Метод getPage2
использует соглашение именования и будет отрабатывать по get запросу по пути http://127.0.0.1:8080/page2
. Таким же образом себя будут вести @property
методы. Сейчас методы своей сигнатурой не отличаются от функции hello
в шаблоне и пишем мы тела страниц руками. Не комильфо, добавим представлений в папку views
и сразу по уму.
Создадим несколько файлов:
Шаблон для всех страниц сайта
doctype html
html
head
// вставляем блок head
block head
// печатаем значение переменной title в соответствующий тег, объявляться она должна в блоке head
title #{title}
body
header
// запись без имени тега создаёт div, в данном случае '<div class="mrow">'
.mrow
// включаем полностью файл views/header.dt
include header
main
.mrow
// вставляем блок main
block main
footer
.mrow
// включаем views/footer.dt
include footer
Отступы комментариев должны соответствовать отступам блоков. Комментарии попадают в результирующий html, если Вы этого не хотите используйте // -
(со знаком минус).
Тут будет шапка сайта
div Мой блог
Тут будет подвал сайта
div контакты, копирайты и тд
Главная страница, первой строкой мы указываем какой шаблон хотим расширять (без пути, ибо в той же папке и без расширения .dt
)
extends layout
block head
// как раз тут мы объявляем содержимое блока, который будет использоваться в главном шаблоне
// а содержимого и нет, только D код, который скомпилируется и будет исполняться при конструировании страницы сервером в ответ на запрос
- auto title = "Главная";
block main
// этот блок уже содержит какой-то html, который вставится в шаблон
div Контент
a(href="/page2") на вторую страницу
Вторая страница аналогична главной
extends layout
block head
- auto title = "Вторая страница";
block main
div Контент страницы 2
a(href="/") на главную
И поправим код нашего блога
class MyBlog
{
@path("/") void getHome() { render!("index.dt"); }
void getPage2() { render!("page2.dt"); }
}
Стало лаконичней, нам не нужно теперь напрямую писать тело ответа. Добавим стиля!
* {
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
font-size: 13px;
text-align: center;
width: 100%;
}
.mrow {
margin: 0 auto;
width: 1170px;
text-align: left;
}
header {
height: 60px;
width: 100%;
background: #e2e2e2;
border-top: none;
border-right: none;
border-left: none;
border-bottom: 1px solid;
border-radius: 0;
border-color: #aaa;
margin-bottom: 20px;
font-size: 24px;
}
header div {
padding-top: 10px;
}
main {
margin-top: 10px;
}
footer {
height: 60px;
width: 100%;
background: #333;
color: #bbb;
position: absolute;
margin-top: 20px;
padding: 0;
bottom: inherit;
border-top: 2px solid #888;
}
footer div {
padding-top: 10px;
}
Нужно включить раздачу статических файлов в нашем приложении
shared static this()
{
...
router.get("*", serveStaticFiles("public/"));
...
}
И, наконец, добавить стиль в head нашего layout.dt
...
html
head
link(rel="stylesheet", href="style.css")
...
После сборки и запуска должно выглядеть примерно так:
Не особо, но, надеюсь, что глаза не выпадают и с этим будет уже приятней работать)
2. Обезопасимся
У нас появился какой-то базис, теперь пора задуматься о дальнейшем развитии. И прежде чем добавлять какую-либо функциональность позаботимся о безопасности: добавим шифрование. О создании сертификатов можно найти много информации, например. Тут приведу основные шаги без описания:
Корневой ключ
openssl genrsa -out rootCA.key 2048
Корневой сертификат
openssl req -x509 -new -key rootCA.key -days 9999 -out rootCA.crt
Ответы на вопросы не важны, мы заставим наш браузер ему доверять)
Ещё ключ
openssl genrsa -out server.key 2048
Запрос на сертификат (важно указать имя домена)
openssl req -new -key server.key -out server.csr
Подписываем новый сертификат
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 9998
Добавляем в браузер rootCA.crt
, rootCA.key
содержим в тайне.
Теперь включим шифрование в нашем приложении.
shared static this()
{
...
settings.tlsContext = createTLSContext(TLSContextKind.server);
settings.tlsContext.useCertificateChainFile("server.crt");
settings.tlsContext.usePrivateKeyFile("server.key");
...
}
Теперь, если Вы всё сделали правильно, сайт перестанет быть доступным по http
и станет доступным без вопросов по https
.
3. Простейшая авторизация
Для начала создадим сессию в нашем приложении.
shared static this()
{
...
settings.sessionStore = new MemorySessionStore;
...
}
Добавим немного кода. Вне нашего класса будет тип переменной сессии.
struct UserSettings
{
bool loggedIn = false;
string email;
}
Для неразрывного восприятия, наверное, будет лучше показать новый класс целиком.
class MyBlog
{
mixin PrivateAccessProxy;
private SessionVar!(UserSettings, "settings") userSettings; // переменная сессии
// специальная переменная _error будет заполняться другими методами в случае необходимости
@path("/") void getHome(string _error)
{
auto error = _error;
auto settings = userSettings;
render!("index.dt", settings, error);
}
/+ @errorDisplay позволяет, при возникновении внутри этого метода
исключения передать его msg в метод getHome, в качестве
параметра _error, а тот уже в свою очередь должен правильно
отобразить ошибку
@auth объявлен ниже, смысл его в том, что перед выполнением
этого метода, будет выполнен метод, указанный как auth,
там, как раз, и производится проверка на авторизованность и,
в случае успеха, в этот метод передастся email пользователя
+/
@auth @errorDisplay!getHome void getWritepost(string _email)
{
auto email = _email;
render!("writepost.dt", email);
}
// ValidEmail ведёт себя почти как string, только проверяет валидность почтового адреса
@errorDisplay!getHome void postLogin(ValidEmail email, string pass)
{
enforce(pass == "secret", "Неверный пароль"); // нормальную авторизацию оставим для следующей статьи
userSettings = UserSettings(true, email); // вот тут мы и изменяем переменную сессии и пользователь становится авторизованным
redirect("./");
}
void postLogout(scope HTTPServerResponse res)
{
userSettings = UserSettings.init;
res.terminateSession();
redirect("./");
}
private:
/+ будем использовать в качестве метода проверки ensureAuth,
а в случае успеха будем возвращать результат ensureAuth в
параметр _email декорируемого метода
+/
enum auth = before!ensureAuth("_email");
/+ здесь мы проверяем переменную сессии и, если всё в порядке,
возвращаем email пользователя
+/
string ensureAuth(scope HTTPServerRequest req, scope HTTPServerResponse res)
{
if (!userSettings.loggedIn)
redirect("/");
return userSettings.email;
}
}
Все публичные методы класса попадают в роутинг, поэтому ensureAuth
создан как private
. При этом vibe'у нужно знать об этом методе для его вызова перед помеченными @auth
методами, поэтому нужно использовать mixin PrivateAccessProxy
. Атрибут @auth
создаётся с помощью шаблона before
, выполняя роль хука, вызывает указанную функцию до помеченной и передаёт ей в качестве аргумента результат функции хука. Так же существует @after
, который заставит выполняться функцию после и требует чтобы хук-функция принимала в качестве аргумента рузультат помечаемой. К сожалению документации я не нашёл на сайте vibe, но в код документация по этим моментам есть. Так же мы переименовали (удалили) метод page2. На его место встал getWritepost, который будет возвращать страницу создания новой записи.
Также следует немного доработать остальные файлы.
В
views/layout.dt
вставить блок, который будет содержать отображение ошибкиblock error
(в layout он будет вставляться из других файлов)
В
views/index.dt
добавить блок отображения ошибок, он может выглядеть так
... block error - if (error.length) div#error #{error} ...
Как Вы, скорее всего, поняли после знака '-' должен идти код на D, а
#{value}
экранирует и вставляет в html значение переменнойvalue
.
Создать файл
views/logindesk.dt
, куда добавить форму для логина
div form#loginform(action="/login", method="POST") div input(class="form-control",name="email",placeholder="Email",type="email",required) div input(class="form-control",name="pass",placeholder="Пароль",type="password",required) div button(type="submit") Войти
Необходимо внимательно относится к отступам, они работают как в python (а тут форматирование иногда ест пробелы).
В файле
views/index.dt
изменить блок main
... block main - if (!settings.loggedIn) include logindesk - else div Контент
Переменные
settings
иerror
передаются в шаблон функциейrender!("index.dt", settings, error);
в методеgetHome
. Каждый темплейт представления принимает свой набор переменных.
- В файл
views/writepost.dt
записать форму, которая пока ничего не будет делать
extends layout
block head
- auto title = "Новый пост";
block main
div Новый пост от #{email}
form#postform(action="/posts", method="POST")
div Заголовок
input(class="from-control", name="title")
div Текст
textarea(class="form-contorl", name="text")
div
button(type="submit") Опубликовать
- Создать ссылку на "Новый пост" и "Главная" по своему усмотрению.
- Оформить стили всего этого по своему усмотрению (свой код я выложу в конечном виде в конце)
Теперь у нас есть сайт, на котором можно залогиниться и есть страница на сайте (writepost), на которую нельзя перейти, если не залогинен.
4. Простейшие записи блога
Статья и так получилась достаточно объёмной, но хочется довести её до какой-то логической запятой)
Добавим запись новых статей в обычное поле класса, а тип поля будет массив структур
struct Post
{
string author;
string title;
string text;
}
Добавляем в наш класс этот массив, передаём его в параметрах в функцию render
метода getHome
и добавляем метод, записывающий эти посты в массив.
class MyBlog
{
...
public:
Post[] posts;
@path("/") void getHome(string _error)
{
...
render!("index.dt", posts, settings, error);
}
...
// ошибки этого метода уже будут отображаться методом getWritepost
@auth @errorDisplay!getWritepost void postPosts(string title, string text, string _email)
{
posts ~= Post(_email, title, text);
redirect("./");
}
...
}
Будьте внимательны — имена параметров методов должны совпадать с именами полей формы! Осталось главное изменение, которое позволит нам видеть посты. В файле views/index.dt
заменим запись "Контент" на код
...
- if (!settings.loggedIn)
include logindesk
- else
- foreach(post; posts)
div.post
div.title #{post.title}
div.text #{post.text}
div.author #{post.author}
К этому моменты у Вас уже должно быть создано приложение в котором есть:
- простейшая форма авторизации
- шифрование трафика
- проверка авторизации
- добавление и отображение постов
У меня получилось так:
Исходники этой части лежат тут.
В следующих частях я расскажу как использовать mongo, сделать страницы для постов (url будет содержать индекс или имя поста) и коментирование, покажу более адекватную авторизацию и ещё некоторые мелочи.
Пишите в коментариях что было не до конца ясно, буду рад доработать эту часть.