- PVSM.RU - https://www.pvsm.ru -
Доброго времени суток! Если Вы давно хотели сделать себе, соседке или её собаке сайт, но пока не сделали, то эта статья для Вас! В этой серии статей я покажу основы работы с vibe для создания сайтов на примере простого блога.
В первой части мы разберём базовые моменты и добавим к получившемуся приложению шифрование.
mkdir yourblogname
cd yourblogname
dub init -t vibe.d
Итак у нас уже есть заготовка от Людвига [3] и нам нужно расширить её. Первое что мы сделаем, это создадим класс нашего приложения и добавим пару страниц.
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
использует соглашение именования [4] и будет отрабатывать по 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")
...
После сборки и запуска должно выглядеть примерно так:
Не особо, но, надеюсь, что глаза не выпадают [5] и с этим будет уже приятней работать)
У нас появился какой-то базис, теперь пора задуматься о дальнейшем развитии. И прежде чем добавлять какую-либо функциональность позаботимся о безопасности: добавим шифрование. О создании сертификатов можно найти много информации, например [6]. Тут приведу основные шаги без описания:
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
.
Для начала создадим сессию в нашем приложении.
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), на которую нельзя перейти, если не залогинен.
Статья и так получилась достаточно объёмной, но хочется довести её до какой-то логической запятой)
Добавим запись новых статей в обычное поле класса, а тип поля будет массив структур
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}
К этому моменты у Вас уже должно быть создано приложение в котором есть:
У меня получилось так:
Исходники этой части лежат тут [7].
В следующих частях я расскажу как использовать mongo, сделать страницы для постов (url будет содержать индекс или имя поста) и коментирование, покажу более адекватную авторизацию и ещё некоторые мелочи.
Пишите в коментариях что было не до конца ясно, буду рад доработать эту часть.
Автор: deviator
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/web-razrabotka/190074
Ссылки в тексте:
[1] dmd: http://dlang.org/download.html
[2] dub: https://code.dlang.org/download
[3] Людвига: https://github.com/s-ludwig
[4] соглашение именования: http://vibed.org/api/vibe.web.web/registerWebInterface
[5] глаза не выпадают: https://habrastorage.org/files/bc2/ea3/dca/bc2ea3dcaced4350a35555957194ae4b.png
[6] например: https://habrahabr.ru/post/192446/
[7] тут: https://github.com/deviator/blogexample/tree/part1
[8] Источник: https://habrahabr.ru/post/310436/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.