Доброго времени суток! Если Вы давно хотели сделать себе, соседке или её собаке сайт, но пока не сделали, то эта статья для Вас! В этой серии статей я покажу основы работы с 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("/")
, который указывает на то, что это корневая страница. Метод getPage2
использует соглашение именования и будет отрабатывать по get запросу по пути http://127.0.0.1:8080/page2
. Методы своей сигнатурой не отличаются от функции 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
. Так же мы переименовали (удалили) метод 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 будет содержать индекс или имя поста) и коментирование, покажу более адекватную авторизацию и ещё некоторые мелочи.
Пишите в коментариях что было не до конца ясно, буду рад доработать эту часть.
Автор: deviator