Форум программистов, компьютерный форум, киберфорум
Наши страницы
loothood
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Сайт на Rust с использованием фреймворка Rocket

Запись от loothood размещена 19.11.2018 в 15:30

Всем привет.



О чем

У меня давно была задумка.
Сделать шутливый сайт для друзей. Все что этот сайт делает - для друга генерирует случайное прилагательное. Эта генерация сохраняется в базу. На сайте можно глянуть топ-10 сочетаний. Так же, по клику на друга или прилагательное, можно будет глянуть топ-10 сочетаний для этого прилагательного или друга.
Звучит очень просто. Потому это отличная задачка для изучения новых технологий. Я какое-то время уже пишу на rust в свободное от работы время - просто для изучения новых технологий и развлечения.
Но гуру Rust я не являюсь.
Так же, я довольно далек от веба, потому эту часть тоже пришлось учить.
Эту статью я пишу как продолжение обучения. Чтобы для себя разложить все по полочкам.
Если она кому-то чем-то поможет, буду только рад.
Чтож, цель есть, время выбрать фреймворк.



Фреймворк
Сейчас для Rust есть около десятка веб-фреймворков, это Iron, Tower Web, Gotham, Rocket и другие. Вообще, вы можете посмотреть на arewewebyet.org какую инфраструктуру rust предлагает на данный момент для веба.
Критерии для фреймворка были простые:
1) коммиты в репозитрии чем свежее тем лучше.
2) примеры, которые указаны на сайте/в репозитории, работают без лишних приседаний.
3) если сообщество, которое ипользует этот фреймворк, большое, то это очевидный плюс.
Чтож, почти все фреймворки проходили по пункту 1. Переходим к пункту 2.
К сожалению, или я криворукий, или примеры старые, но без приседаний, мне удалось завести примеры только для Iron и Tower Web. Так же, мне удалось завести пример для Rocket, но пришлось указать ночную сборку rust.
Третий этап оказался простым - Rocket гораздо больше распространен чем Iron и Tower Web. У него отличная документация. Потому выбор был очевиден.



Архитектура
По моему личному мнению, для любого проекта, даже такого маленького, продумать архитектуру заранее - хорошая идея.
Начнем с базы.
У нас будет три таблицы: таблица с друзьями(я назвал ее active-user), таблица с прилагательными(adjective), таблица с сочетаниями друг-прилагательное(user_adjective). Схему можно нарисовать такую
Дальше - архитектура кода.
Так как мы учимся, нет никаких ограничений, я решил сделать все по-взрослому: работа с базой через ORM. Несколько слоев проекта - бэкенд, миддл, фронтенд.
Максимальное разделение по логике (поможет при росте проекта). В общем, дерево проекта выглядит так:
Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
$ tree -L 10 -I target
.
|--- Cargo.lock
|--- Cargo.toml
|--- Rocket.toml
|--- migrations
|** |--- 00000000000000_diesel_initial_setup
|** |** |--- down.sql
|** |** |--- up.sql
|** |--- 2018-08-15-131959_create_user
|** |** |--- down.sql
|** |** |--- up.sql
|** |--- 2018-08-15-132408_create_adjective
|** |** |--- down.sql
|** |** |--- up.sql
|** |--- 2018-08-15-132416_create_user_adjective
|** |** |--- down.sql
|** |** |--- up.sql
|** |--- 2018-08-15-132833_fill_user
|** |** |--- down.sql
|** |** |--- up.sql
|** |--- 2018-08-15-132837_fill_adjective
|**     |--- down.sql
|**     |--- up.sql
|--- src
|** |--- backend
|** |** |--- handler.rs
|** |** |--- mod.rs
|** |** |--- routes.rs
|** |--- database
|** |** |--- connection.rs
|** |** |--- dbimpl.rs
|** |** |--- mod.rs
|** |** |--- models
|** |** |** |--- adjective.rs
|** |** |** |--- mod.rs
|** |** |** |--- user.rs
|** |** |** |--- user_adjective.rs
|** |** |--- schema.rs
|** |--- main.rs
|** |--- middleware
|**     |--- display_data.rs
|**     |--- mod.rs
|**     |--- randomize.rs
|--- static
|** |--- generate.js
|** |--- img
|** |** |--- logo.jpg
|** |--- spectre
|**     |--- bootstrap-theme.css
|**     |--- bootstrap-theme.css.map
|**     |--- bootstrap-theme.min.css
|**     |--- bootstrap-theme.min.css.map
|**     |--- bootstrap.css
|**     |--- bootstrap.css.map
|**     |--- bootstrap.min.css
|**     |--- bootstrap.min.css.map
|**     |--- spectre-exp.css
|**     |--- spectre-exp.min.css
|**     |--- spectre-icons.css
|**     |--- spectre-icons.min.css
|**     |--- spectre.css
|**     |--- spectre.min.css
|--- templates
    |--- adjective.html.hbs
    |--- common
    |** |--- footer.hbs
    |** |--- meta
    |**     |--- footer.hbs
    |**     |--- header.hbs
    |--- error
    |** |--- 404.html.hbs
    |--- index.html.hbs
    |--- user.html.hbs
 
19 directories, 55 files


Первые шаги
Честно говоря, описывать первые шаги - довольно скучное занятие, которое описано уже в тысячах туториалах.
Как установить rust, написано на сайте rust
Надо создать проект. Я использую Intellij IDEA с плагином, потому создание нового проекта - дело трех кликов и выбора имени проекта.
Вы же можете воспользоваться такой командой:
Bash
1
cargo new hello_world --bin
--bin значит что мы создаем не библиотеку(то есть, будет файлик с именем main.rs). Подробнее в документации.



Работа с базой
Я давно работаю с PostgreSQL, потому вопрос какую БД использовать не возникал.
Тем более, есть замечательная ORM библиотека(в rust мире библиотеки называют крэйтами(crates)) diesel. С ее помощью можно работать с PostgreSQL.
Хорошим тоном считается работа с базой через пулы(pools). Потому, я выбрал связку r2d2+diesel.
Первым делом, надо в проект подключить крэйты. Потому в Cargo.toml мы пишем:
JSON
1
2
3
4
[dependencies]
diesel =  { version = "1.3.2", features = ["postgres"] }
r2d2 = "0.8.2"
r2d2-diesel = "1.0.0"
Текущие версии крэйтов можно посмотреть на crates.io
Прежде чем продолжить работу, установим тулу под названием diesel_cli:
Bash
1
cargo install diesel_cli
Далее, надо инициализировать базу. Делается это просто: в файл .env в переменной DATABASE_URL, указываем как коннектиться к базе. Выглядит это примено так:
Bash
1
DATABASE_URL=postgres://mylogin:mypassword@myhost/mydb
В консоли пишем
Bash
1
diesel setup
База создана, можно создавать миграции. Делается это тоже из консоли. Подробней о миграциях и как работать с diesel, можно прочитать тут.
После выполнения миграций, которые создадут таблицы, надо создать схему базы, чтобы наш проект знал как работать с базой. Схему можно сделать руками. Но проще воспользоваться diesel_cli:
Bash
1
diesel print-schema > src/schema.rs
У вас будет файлик примерно такого содержания:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
table! {
    active_user (id) {
        id -> Int4,
        user_name -> Varchar,
    }
}
 
table! {
    adjective (id) {
        id -> Int4,
        adjective_value -> Varchar,
    }
}
 
table! {
    user_adjective (id) {
        id -> Int4,
        user_id -> Int4,
        adjective_id -> Int4,
        count -> Nullable<Int4>,
    }
}
 
allow_tables_to_appear_in_same_query!(
    active_user,
    adjective,
    user_adjective,
);
Все! Можно программировать на Rust!
Перво-наперво, давайте создадим Модели. В терминах diesel, модель - это описание таблицы, с которой мы будем работать. Моя модель таблицы user:
Haskell
1
2
3
4
5
#[derive(Clone, Queryable, Serialize, Deserialize, FromForm)]
pub struct User {
    pub id : i32,
    pub user_name : String,
}
Тут все очевидно, кроме, наверное, первой строчки
Haskell
1
#[derive(Clone, Queryable, Serialize, Deserialize, FromForm)]
Здесь мы применяем очень мощную штуку. Называется она макрос. Если быть точным, то процедурный макрос. Используются эти макросы в основном для кодогенерации. Работает это так: у нас есть тип данных i32. Для него уже описан ряд методов и типажей. Мы создаем структуру, внутри которой есть данные типа i32. И тут мы захотели эту структуру клонировать (вызвать для этой структуры метод clone()). Чтобы не писать этот метод специально для этой структуры, мы убеждаемся что для всех типов данных внутри этой структуры уже написан метод clone. и просто заимствуем (derive) существующий метод. Так как у меня в структуре данные типа i32 и String, для которых уже реализован типаж Clone, внутри которого есть метод clone, все что мне надо сделать - это заимствовать типаж Clone.

Вот код, который демонстрирует как создать коннект к базе:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use diesel::pg::PgConnection;
use r2d2;
use r2d2_diesel::ConnectionManager;
use std::env;
 
type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
 
pub fn init_pool() -> Pool {
    dotenv().ok();
    let manager = ConnectionManager::<PgConnection>::new(database_url());
    Pool::new(manager).expect("Cannot initialize db pool")
}
 
fn database_url() -> String {
    env::var("DATABASE_URL").expect("DATABASE_URL must be set")
}
Тут тоже все довольно просто: init_pool - функция которую мы будем вызывать чтобы передать фреймворку пул для коннекта к базе с которым он будет работать. Все остальное - типы и вспомогательные функции для создания пула.

Далее, нам надо как-то читать/писать/обновлять данные в базе. Я создал отдельную прослойку на среднем уровне для работы с базой:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use diesel;
use diesel::{ pg::PgConnection
              , RunQueryDsl
              , prelude::*
};
use database::schema::*;
use database::models::{ user::User
                        , adjective::Adjective
                        , user_adjective::{ UserAdjective
                                            , NewUsAdRecord }
};
use failure::Error;
 
pub fn all_users(connection: &PgConnection) -> QueryResult<Vec<User>> {
    active_user::table.load::<User>(&*connection)
}
 
pub fn all_adjectives(connection: &PgConnection) -> QueryResult<Vec<Adjective>> {
    adjective::table.load::<Adjective>(&*connection)
}
 
pub fn get_user_by_id(connection : &PgConnection, id : &i32) -> QueryResult<User> {
    active_user::table
        .find(id)
        .get_result::<User>(connection)
}
 
pub fn get_user_by_name(connection : &PgConnection, name : &str) -> QueryResult<User> {
    active_user::table
        .filter(active_user::user_name.eq(name))
        .get_result(connection)
}
 
pub fn get_adjective_by_id(connection : &PgConnection, id : &i32) -> QueryResult<Adjective> {
    adjective::table
        .find(id)
        .get_result::<Adjective>(connection)
}
 
pub fn get_adjective_by_name(connection : &PgConnection, name : &str) -> QueryResult<Adjective> {
    adjective::table
        .filter(adjective::adjective_value.eq(name))
        .get_result(connection)
}
 
pub fn get_top10_all(conn : &PgConnection) -> Result<Vec<UserAdjective>, Error> {
    let query = user_adjective::table
        .order(user_adjective::user_id)
        .order(user_adjective::count.desc())
        .limit(10);
 
    let us_adj = query.load::<UserAdjective>(conn)?;
 
    Ok(us_adj)
}
 
pub fn get_count_of_repeats(conn : &PgConnection, user_id : &i32, adjective_id : &i32) -> Result<Vec<UserAdjective>, Error> {
    let query = user_adjective::table
        .filter(user_adjective::user_id.eq(user_id))
        .filter(user_adjective::adjective_id.eq(adjective_id));
 
    let us_adj = query.load::<UserAdjective>(conn)?;
 
    Ok(us_adj)
}
 
 
pub fn get_count(conn : &PgConnection, user : &User, adjective : &Adjective) -> Result<Vec<UserAdjective>, Error> {
    let query = user_adjective::table
        .filter(user_adjective::user_id.eq(user.id))
        .filter(user_adjective::adjective_id.eq(adjective.id));
 
    let us_adj = query.load::<UserAdjective>(conn)?;
 
    Ok(us_adj)
}
 
 
pub fn update_us_adj_record<'a> (conn : &PgConnection, user_id : &i32, adjective_id : &i32, count_of_repeats : &i32) -> UserAdjective {
    let count = count_of_repeats + 1;
    diesel::update(user_adjective::table
        .filter(user_adjective::user_id.eq(user_id))
        .filter(user_adjective::adjective_id.eq(adjective_id)))
        .set(user_adjective::count.eq(count))
        .get_result(conn)
        .expect("Error updating data in the users_adjectives table!")
}
 
 
pub fn create_us_adj_record<'a>(conn : &PgConnection, user_id : &'a i32, adjective_id : &'a i32) -> UserAdjective {
    let new_usad_record = NewUsAdRecord {
        user_id,
        adjective_id,
        count : &1i32
    };
 
    diesel::insert_into(user_adjective::table)
        .values(&new_usad_record)
        .get_result(conn)
        .expect("Error saving new record in the users_adjectives table!")
}
 
 
pub fn get_top10_us_adj_for_user(connection : &PgConnection, user : &User) -> Result<Vec<UserAdjective>, Error> {
    let query = user_adjective::table
        .filter(user_adjective::user_id.eq(user.id))
        .order(user_adjective::adjective_id)
        .order(user_adjective::count.desc())
        .limit(10);
 
    let us_adj : Vec<UserAdjective> = query.load::<UserAdjective>(connection)?;
 
    Ok(us_adj)
}
 
pub fn get_top10_us_adj_for_adjective(connection : &PgConnection, adjective : &Adjective) -> Result<Vec<UserAdjective>, Error> {
    let query = user_adjective::table
        .filter(user_adjective::adjective_id.eq(adjective.id))
        .order(user_adjective::user_id)
        .order(user_adjective::count.desc())
        .limit(10);
 
    let us_adj : Vec<UserAdjective> = query.load::<UserAdjective>(connection)?;
 
    Ok(us_adj)
}
Здесь:
Haskell
1
2
3
4
5
use database::schema::*;
use database::models::{ user::User
                        , adjective::Adjective
                        , user_adjective::{ UserAdjective
                                            , NewUsAdRecord }
мы берем информацию о ранее созданной схеме и описанных моделях.
Этот код можно было бы сократить. Например, функции get_top10_us_adj_for_adjective и get_top10_us_adj_for_user можно было бы объединить в одну - сделать это можно с помощью макросов, но, к сожалению, с первого раза я не осилил макросы. Очень уж сложными они мне показались. Возможно, когда до конца разберусь с макросами, перепишу этот код.
Хотел бы остановиться еще на нескольких вещах:
для некоторых функций мы возвращаем результат в виде
Haskell
1
Result<Vec<UserAdjective>, Error>
Тут использован крэйт failure. На 12ой строке написано use failure::Error;. Это сделано для удобства. Почти каждая функция в моем случае будет возвращать свой тип Error. Раньше с этим боролись создавая enum и перечисляя все типы ошибок, которые могут возникнуть. Этот enum использовали в Result. Теперь же появился крэйт failure который делает все за вас.
Вообще, этот Result нужен лишь для того, чтобы не обрабатывать возможную ошибку здесь и сразу, а хотите прокинуть информацию на уровень выше.
Если же вы вообще не хотите возвращать Result на уровень выше, то можете поступить как я сделал в методе update_us_adj_record - я добавил метод expect в котором написал сообщение, которое будет показано в трейсе в случае, если на этом шаге произойдет любая ошибка выполнения.
Что лучше использовать - думаю как и для всего, зависит от ситуации и идеального решения нет.



Middleware
На этом уровне все довольно просто - нам нужно уметь выбирать из списков рандомные значения. Так же, нам надо уметь получать и обрабатывать данные, которые мы получаем из базы.
Про рандом просто - есть крэйт rand
С его помощью мы из списка вытаскиваем случайный элемент:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use rand::{ thread_rng, Rng };
 
#[derive(Debug)]
pub struct RandomData<T> {
    pub value : T,
}
 
impl <T : Clone> RandomData<T> {
 
    pub fn new(vector : &[T]) -> Self {
        let mut rng = thread_rng();
        let value = rng.choose(&vector).unwrap();
        RandomData { value : value.clone() }
    }
}
Здесь мы используем генерики. T - это обобщенный тип. За ним может скрываться что угодно - i32, String, enum, struct или что вы еще пожелаете. Главное чтобы они реализовывали поведения Clone и Debug. impl <T : Clone> RandomData<T> { означает что для структуры RandomData я имплементирую следующие методы(в моем случае один метод - new). По-хорошему, unwrap() можно было бы заменить на expect() или вообще возвращать Result из метода.

Второе - работа с данными из базы. Чтобы отобразить их на странице, надо привести их в соответсвующий вид. Вот пример как я преобразую данные для типа User:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#[derive(Serialize)]
pub struct DisplayUser {
    pub user : User,
    pub adj_count : Vec<AdjCount>,
}
 
impl DisplayUser {
    pub fn by_id(connection : &DbConn, id : &i32) -> DisplayUser {
        let user = dbimpl::get_user_by_id(&connection, &id)
            .expect(&format!("User with id: {} no found!", id));
        DisplayUser::by_user(connection, user)
    }
 
    pub fn by_name(connection : &DbConn, name : &String) -> DisplayUser {
        let user = dbimpl::get_user_by_name(&connection, &name)
            .expect(&format!("User with name: {} no found!", name));
        DisplayUser::by_user(connection, user)
    }
 
    fn by_user(connection : &DbConn, user : User) -> DisplayUser {
        let top10 = dbimpl::get_top10_us_adj_for_user(connection, &user).
            expect(&format!("User not found in the database. User name is : {}", user.user_name));
        let mut adj_counts : Vec<AdjCount> = Vec::new();
        for top in top10 {
            let adjective = dbimpl::get_adjective_by_id(&connection, &top.adjective_id)
                .expect(&format!("Adjective not found in the database. adjective_id : {}", top.adjective_id));
            let count = dbimpl::get_count(&connection, &user, &adjective)
                .expect(&format!("Count not found for user_id: {}  and adjective_id: {}", user.id, adjective.id));
            let adj_count = AdjCount { adjective, count : count.first().unwrap().count.unwrap() };
            adj_counts.push(adj_count)
        }
        DisplayUser { user, adj_count : adj_counts }
    }
 
}
На чем хотелось бы заострить внимание в среднем уровне, на этом коде:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[derive(Serialize)]
pub struct GenerateData {
    random_user_name : String,
    random_adjective_value : String,
}
 
 
impl GenerateData {
    pub fn new(connection : &DbConn, allDbData : &AllDbData) -> GenerateData {
        let random_adjective = RandomData::new(&allDbData.adjectives);
        let random_user = RandomData::new(&allDbData.users);
        create_or_update_us_adj(&connection, &random_user.value, &random_adjective.value);
        GenerateData {
            random_user_name : random_user.value.user_name,
            random_adjective_value : random_adjective.value.adjective_value,
        }
    }
}
 
impl<'r> Responder<'r> for GenerateData {
    fn respond_to(self, _: &Request) -> response::Result<'r> {
        Response::build()
            .sized_body(
                Cursor::new(
                    format!("<a href=\"/user/{user}\">{user}</a> cегодня <a href=\"/adjective/{adjective}\">{adjective}</a>",
                            user = self.random_user_name, adjective = self.random_adjective_value)))
            .ok()
    }
}
Я хочу GenerateData передавать сразу на фронтенд без каких-либо изменений. Чтобы rocket смог обработать и отобразить эти данные, я должен имплементировать метод respond_to для своей структуры. В принципе, что я и сделал.



Backend
Здесь все стандартно для веб-приложений - роутинг и хэндлеры. Начнем с роутинга:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use rocket;
use database::connection;
use backend::handler;
use rocket_contrib::Template;
use middleware::display_data::AllDbData;
 
pub fn create_routes() {
    rocket::ignite()
        .manage(connection::init_pool())
        .manage(AllDbData::new())
        .mount("/",
               routes![ handler::index
                             , handler::all_users
                             , handler::all_adjectives
                             , handler::get_user_by_id
                             , handler::get_user_by_name
                             , handler::get_adjective_by_name
                             , handler::get_adjective_by_id
                             , handler::redirect
                             , handler::static_content
                             , handler::generate_new_one
                             ],
        )
        .attach(Template::fairing())
        .catch(catchers![handler::not_found])
        .launch();
}
Первым делом, передаем пул коннекта к базе рокету manage(connection::init_pool()), потом вытаскиваем все данные из базы (потому что не очень хорошая идея ходить на каждый чих в базу за чтением данных). Ну а дальше перечисляем все хэндлеры которые есть в нашем приложении.
Строчка .attach(Template::fairing()) означает что я хочу использовать шаблоны для фронтенда. А строчка .catch(catchers![handler::not_found]) говорит какую страницу показывать если пользователь вдруг пойдет не туда. Проще говоря, 404ая страница. Ну и .launch(); - стартуем Rocket.
С хэндлерами тоже все просто:
Haskell
1
2
3
4
5
#[get("/")]
fn index(connection : DbConn, allDbData : State<AllDbData>) -> Template {
    let context = IndexData::new(&connection, &allDbData);
    Template::render("index", &context)
}
#[get("/")] - юзер должен зайти сюда, чтобы увидеть шаблон, который описан в файлике под названием index.
Еще пример из хэндлеров:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
#[get("/user/<id>")]
fn get_user_by_id(id: i32, connection: DbConn) -> Template {
    let context = DisplayUser::by_id(&connection, &id);
    Template::render("user", &context)
}
 
 
#[get("/user/<name>", rank = 2)]
fn get_user_by_name(name: String, connection: DbConn) -> Template {
    let context = DisplayUser::by_name(&connection, &name);
    Template::render("user", &context)
}
Здесь мы показываем данные юзера, если пользователь зайдет на страницу по id юзера #[get("/user/<id>")] или по его имени #[get("/user/<name>", rank = 2)]. rank=2 значит что у способа идентификации юзера по имени приоритет ниже.
Очень и очень много времени у меня заняло как сделать так, чтобы при заходе на страницу, пользователю показывались не только данные от rust кода, но и красивости от фронтенда. Это было по-настоящему сложно понять, но решение простое:
Haskell
1
2
3
4
#[get("/static/<file..>")]
fn static_content(file: PathBuf) -> Result<NamedFile, StdIoError> {
     NamedFile::open(Path::new("static/").join(file))
}
Этот метод необходимо прописать в хэндлерах чтобы из папки static у нас начали тянуться css, js и прочие статик-файлы.



Frontend
Как я уже говорил, я имею довольно мало отношения к вебу. Я отлично понимаю как работают веб-приложения на бекенде, как их задеплоить на сервере, как настроить почти любой веб-сервер, но что происходит на фронтенде - для меня всегда было большой загадкой. Чтож, у Rocket довольно мало инструментов для работы с фронтендом. Честно говоря, по началу я думал что придется использовать фреймоврк YEW(придется - потому что для моих целей это как из пушки по воробьям). Но внимательней прочитав документацию Rocket, я увидел что есть поддержка Handlebars. Изучив этот инструмент, я понял что это то что нужно! Handlebars - клиентский шаблонизатор для JavaScript.
Вот так выглядит index.html.hbs:
HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Кто какой</title>
        {{> common/meta/header }}
    </head>
    <body>
        <script src="/static/generate.js"></script>
        <br>
        <span class="h2 text-success flex-centered">Нарандомили:</span>
        <p align="center" id="display_random_data"><a href="/user/{{random_user_name}}">{{random_user_name}}</a> сегодня <a href="/adjective/{{random_adjective_value}}">{{random_adjective_value}}</a></p>
        <button class="btn btn-primary centered" onclick="generate()">Сгенерируй снова!</button>
        <br>
        <br>
        <h2 class="text-gray">Топ 10 сочетаний:</h2>
        <table class="table table-striped text-center">
            <thead>
                <tr>
                    <th>Кто</th>
                    <th>Какой</th>
                    <th>Количество совпадений</th>
                </tr>
            </thead>
            <tbody>
                {{#each usadjcounts}}
                    <tr class="active">
                        <td><a href="/user/{{this.user.id}}">{{this.user.user_name}}</a></td>
                        <td><a href="/adjective/{{this.adjective.id}}">{{this.adjective.adjective_value}}</a></td>
                        <td>{{this.count}}</td>
                    </tr>
 
                {{/each}}
            </tbody>
        </table>
        {{> common/footer }}
    </body>
</html>
И generate.js:
Javascript
1
2
3
4
5
6
7
8
9
function generate() {
    const Http = new XMLHttpRequest();
    const url='./generate_new_one';
    Http.open("POST", url);
    Http.send();
    Http.onreadystatechange=(e)=>{
        document.getElementById("display_random_data").innerHTML = Http.responseText
    }
}
Здесь я просто в хэндлерах на странице generate_new_one генерирую новое сочетание user-adjective и вывожу на странице получившиеся данные.
А вот этот участок кода:
HTML5
1
2
3
4
5
6
7
8
 {{#each usadjcounts}}
                    <tr class="active">
                        <td><a href="/user/{{this.user.id}}">{{this.user.user_name}}</a></td>
                        <td><a href="/adjective/{{this.adjective.id}}">{{this.adjective.adjective_value}}</a></td>
                        <td>{{this.count}}</td>
                    </tr>
 
                {{/each}}
Заполняется довольно просто:
в хэндлерах написано:
Haskell
1
2
3
4
5
#[get("/")]
fn index(connection : DbConn, allDbData : State<AllDbData>) -> Template {
    let context = IndexData::new(&connection, &allDbData);
    Template::render("index", &context)
}
Смотрим определение IndexData:
Haskell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#[derive(Serialize)]
pub struct IndexData {
    random_user_name : String,
    random_adjective_value : String,
    usadjcounts : Vec<UsAdjCount>,
}
 
impl IndexData {
    pub fn new(connection : &DbConn, allDbData : &AllDbData) -> IndexData {
        let most_frequent = dbimpl::get_top10_all(&connection)
            .unwrap_or(Vec::new());
        let mut usadjcounts = Vec::new();
        for most in most_frequent.iter() {
            let user = dbimpl::get_user_by_id(&connection, &most.user_id)
                .expect(&format!("User not found in the database. user_id : {}", most.user_id));
            let adjective = dbimpl::get_adjective_by_id(&connection, &most.adjective_id)
                .expect(&format!("Adjective not found in the database. adjective_id : {}", most.adjective_id));
            let count = most.count
                .expect(&format!("Cannot get count from database for user_id : {}", most.user_id));
            let usadjcount = UsAdjCount { user, adjective, count };
            usadjcounts.push(usadjcount);
        }
        let random_adjective = RandomData::new(&allDbData.adjectives);
        let random_user = RandomData::new(&allDbData.users);
        create_or_update_us_adj(&connection, &random_user.value, &random_adjective.value);
        IndexData {
            random_user_name : random_user.value.user_name,
            random_adjective_value : random_adjective.value.adjective_value,
            usadjcounts,
        }
    }
}
то есть, шаблону я передаю структуру с содержимым: random_user_name : String, random_adjective_value : String, usadjcounts : Vec<UsAdjCount>, а внутри шаблона я оперирую теми же данными: {{#each usadjcounts}}.
Осталось навести красоту с помощью css. Я выбрал Spectre CSS - просто закинул в папку которая будет отдавать статику все файлики этого CSS фреймворка.



Опции Rocket
Rocket позволяет настроить работу с окружением своим конфигурационным toml-файлом.
Вот он в моем случае:
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[global.limits]
forms = 32768
 
[development]
address = "localhost"
port = 8000
workers = 4
keep_alive = 5
log = "normal"
template_dir = "templates/"
 
 
[staging]
address = "0.0.0.0"
port = 8000
workers = 8
keep_alive = 5
log = "normal"
template_dir = "templates/"
 
[production]
address = "0.0.0.0"
port = 8000
workers = 12
keep_alive = 5
log = "critical"
template_dir = "templates/"
Здесь прописаны варианты работы в различных режимах (параметры в которых будет сбилжено/запущено приложение с помощью cargo).
Подробнее о конфигурационном файле Rocket написано здесь



Итого
Я написал сайт пару месяцев назад. Для старой версии rustc - компилятора rust. И с запуском этого приложения с новой версией компилятора могут возникнуть проблемы. Но cargo - отличная вещь! Достаточно указать версию компилятора с которой ты хочешь запустить приложение и это будет работать!
В моем случае команда выглядит так:
Bash
1
cargo +nightly-2018-07-24 run
Я очень доволен Rocket. Так же, отличные возможности показал diesel.
Rust - великолепный язык. Писать на нем сплошное удовольствие. Советую и вам попробовать.
Как работает мой сайт-шутка можно глянуть здесь. Код приложения доступен здесь.
Размещено в Без категории
Просмотров 157 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru