На этот раз мы создаем базовый HTML и придаем нашему приложению уникальный стиль.

Статьи в серии:

Мой первоначальный план состоял в том, чтобы сделать наши Rusty Clock интерактивными в этом сегменте. Однако с тех пор я понял, что взаимодействие с некоторыми WEB API в Rust может представлять собой серьезную проблему, особенно для новичков в Rust. Итак, для простоты в этой статье мы сосредоточимся исключительно на HTML и стилях. Интерактивность получит особое внимание в будущем выпуске.

Выбор веб-фреймворка

По правде говоря, я не тратил много времени на изучение всех доступных фреймворков Rust. Мой типичный подход при тестировании новой библиотеки или фреймворка — зайти на GitHub и проверить количество звезд. Но что еще более важно, мне нравится заглядывать в Insights › Contributors, чтобы проверить активность проекта:

И до сих пор ничто не было даже близко к Тису, который мы будем использовать на протяжении всей этой серии. Тем не менее, я хотел бы упомянуть несколько интересных альтернатив, а именно Dioxus и Iced. Оба выглядят очень интересно и активно разрабатываются, но оба нацелены на несколько платформ с разными рендерами для каждой, в то время как я хотел сосредоточиться исключительно на WEB как на альтернативной веб-инфраструктуре, такой как React или Vue.

Создать проект

Я буду считать само собой разумеющимся, что вы уже установили все необходимые предварительные условия. Если нет, обратитесь к первой части этой серии «Создание веб-приложений с помощью Rust: практическое руководство, часть 1 — введение».

Давайте начнем с создания нового проекта. В командной строке введите команду cargo new rusty-clock. Это создаст новое приложение, готовое к работе. Затем введите cd rusty-clock, а затем cargo run, чтобы начать работу.

Вы должны увидеть что-то похожее на то, что показано на скриншоте ниже. На данный момент это, вероятно, напомнит вам о проекте «create-react-app» или «vite-based».

Добавление зависимостей

Теперь мы приступим к добавлению всего, что не связано с исходным кодом.

Во-первых, мы добавим статические ресурсы в корневую папку. На данном этапе у нас есть только два файла:

  • public/images/background.png — это фоновое изображение для нашего приложения. Я поделился ссылкой на свой репозиторий, но изначально я взял это изображение с https://www.transparenttextures.com/. У них есть сотни изображений, на случай, если вы захотите разнообразить мой дизайн.
  • public/images/clockface.png — это циферблат нашего приложения.

Далее мы добавим папку styles в корень нашего проекта. Из того, что я видел, Yew не предлагает решений CSS-in-JS, подобных эмоциям или стежкам. Но благодаря Trunk мы можем использовать CSS или SCSS прямо из коробки.

Давайте также добавим index.scss в папку styles. Без него Транк будет жаловаться.

Наконец, давайте добавим файл index.html в корень нашего проекта:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link data-trunk rel="scss" href="styles/index.scss"/>
        <link data-trunk rel="copy-dir" href="public/images"/>
        <title>Rusty Clock</title>
    </head>
</html>

Как показано, мы можем напрямую ссылаться на SCSS, а папки относятся к структуре проекта. Trunk автоматически все скомпилирует и скопирует в папку dist.

Наконец, давайте добавим зависимости проекта, которые мы будем использовать в этой части, в файл Cargo.toml. Как я отметил во введении, Cargo.toml сродни package.json. После добавления зависимостей ваша окончательная версия должна выглядеть примерно так:

[package]
name = "rusty-clock"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
chrono = "0.4.26"
chrono-tz = "0.8.2"

Где:

  • yew — это Yew Web Framework, который мы будем использовать для этого приложения.
  • chrono — это библиотека, которая позволяет нам манипулировать датами и временем.
  • chrono-tz — надстройка для хроно, упрощающая работу с разными часовыми поясами.

После добавления этих зависимостей просто запустите trunk serve в командной строке, и он позаботится обо всем остальном. На этом этапе структура вашего проекта должна выглядеть примерно так:

Добавление базового HTML

Давайте углубимся в добавление нашего базового HTML. Если вы веб-разработчик с опытом работы с React, это должно быть захватывающим шагом, поскольку структура нашего приложения очень похожа на структуру, настроенную с помощью Vite или create-react-app.

Мы начнем с создания сразу всех необходимых файлов. Таким образом, я могу объяснить некоторые концепции, характерные для Rust. Создадим следующие файлы:

  • ./компоненты/часы.рс
  • ./components/plate.rs
  • ./компоненты/mod.rs
  • ./представления/app.rs
  • ./просмотры/mod.rs

Вот некоторые концепции, которые нам необходимо прояснить:

  • Каталог components содержит компоненты, которые являются «тупыми», то есть они являются общими и не знают об общей логике приложения. Например, Button может обрабатывать клики или иметь заголовок, но он не знает, какой тип приложения мы создаем. Поэтому, если вы хотите локализовать свое приложение, кнопка будет ожидать, что вы предоставите заголовок, но приложение будет нести ответственность за предоставление переведенной строки.
  • Наш компонент Clock следует этому принципу. Он может отображать время, но не знает часовой пояс; наше приложение вычисляет время для определенного часового пояса.
  • Каталог views предназначен для наших «умных» компонентов. Это те, кто знает логику и особенности приложения. Итак, если мы локализуем наше приложение, наши представления будут отвечать за получение переводов и затем передачу этих переводов нашим «тупым» компонентам.

Наконец, файл mod.rs в каждой папке является чем-то весьма специфичным для Rust, и он может сбивать с толку новичков в Rust. Для получения более подробной информации обратитесь к Rust Book и Rust By Example, но чтобы сосредоточиться на нашей цели, я дам упрощенное объяснение:

Rust рассматривает все исходники как независимые модули. Вы не можете просто импортировать файлы, как в JavaScript. Вместо этого вам придется импортировать модули, а затем использовать из них структуры и методы. Итак, чтобы использовать наш компонент Clock, нам нужно сделать что-то вроде этого:

mod clock // It is act
use clock::Clock

Когда вы имеете дело с большим количеством компонентов, импортировать и использовать их по отдельности было бы неудобно. Вместо этого мы можем сгруппировать все файлы в папке как часть единого модуля с mod.rs в корне и экспортировать их все сразу. В качестве примера возьмем папку components.

На данный момент эта папка содержит три файла: clock.rs, plate.rs и mod.rs , где mod.rs — это корень нашего модуля components.

Начнем с добавления содержимого в файл clock.rs:

use yew::prelude::*;
use chrono::{DateTime, Timelike};
use chrono_tz::Tz;

const HOUR_DEGREE: f32 = 30.0; // 360 / 12 = 30
const MINUTE_DEGREE: f32 = 6.0; // 360 / 60 = 6
const SECOND_DEGREE: f32 = 6.0; // 360 / 60 = 6
const HOUR_MINUTE_FACTOR: f32 = 0.5; // 30 / 60 = 0.5
const MINUTE_SECOND_FACTOR: f32 = 0.1; // 6 / 60 = 0.1

#[derive(Properties, PartialEq)]
pub struct Props {
    pub time: DateTime<Tz>,
}

#[function_component]
pub fn Clock(props: &Props) -> Html {
    let time = props.time.time();

    html! {
        <div class="clock">
            <div style={make_css_rotation(calc_hour_degree(time.hour(), time.minute()))} class="hand main hours" />
            <div style={make_css_rotation(calc_minute_degree(time.minute(), time.second()))} class="hand main minutes" />
            <div style={make_css_rotation(calc_second_degree(time.second()))} class="hand seconds" />
            <div class="point"></div>
            <div class="inner" />
        </div>
    }
}

fn make_css_rotation(degrees: f32) -> String {
    format!("rotate: {}deg;", degrees)
}

fn calc_hour_degree(hours: u32, minutes: u32) -> f32 {
    hours as f32 * HOUR_DEGREE + minutes as f32 * HOUR_MINUTE_FACTOR
}

fn calc_minute_degree(minutes: u32, seconds: u32) -> f32 {
    minutes as f32 * MINUTE_DEGREE + seconds as f32 * MINUTE_SECOND_FACTOR
}

fn calc_second_degree(seconds: u32) -> f32 {
    seconds as f32 * SECOND_DEGREE
}

Не волнуйтесь, мы обсудим некоторые детали реализации позже, а пока давайте сосредоточимся на модулях.

Теперь давайте заполним plate.rs :

use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
    pub children: Children,
}

#[function_component]
pub fn Plate(props: &Props) -> Html {
    html! {
        <div class="plate">{for props.children.iter()}</div>
    }
}

И наконец mod.rs :

mod clock;
mod plate;

pub use clock::*;
pub use plate::*;

Будем читать как import modules clock and plate and export everythig they provide .

Теперь добрались до main.rs и измените его на:

mod components;

fn main() {
    println!("Hello, world!");
}

Пока мы только импортировали наш модуль components. Но это не значит, что мы пока что что-то из этого используем. Мы просто сообщили нашему приложению, что намерены его использовать — это похоже на добавление зависимости в файл package.json. И поскольку мы сделали это в нашем основном файле (корень всего), теперь мы можем использовать эти компоненты во всем нашем приложении без необходимости каждый раз импортировать их.

Теперь давайте перейдем к добавлению наших представлений. На данный момент у нас есть только представление App, поэтому мы можем импортировать его напрямую. Однако я предполагаю, что в будущем мы добавим больше представлений, так что давайте будем последовательны.

Добавьте следующий контент в ./view/app.rs

use yew::prelude::*;
use chrono::Utc;
use chrono_tz::{
    Asia::Tokyo,
    America::New_York,
    Europe::Paris,
};

use crate::components::*;

#[function_component]
pub fn App() -> Html {
    html! {
        <div class="board">
            <div>
                <Clock time={Utc::now().with_timezone(&Tokyo)} />
                <Plate>{"Tokyo"}</Plate>
            </div>
            <div>
                <Clock time={Utc::now().with_timezone(&New_York)} />
                <Plate>{"New York"}</Plate>
            </div>

            <div>
                <Clock time={Utc::now().with_timezone(&Paris)} />
                <Plate>{"Paris"}</Plate>
            </div>
        </div>
    }
}

Мы вернемся к этому позже, но обратите внимание на use create::components::* . Поскольку мы уже «зарегистрировали» наши компоненты в main.rs, теперь мы можем использовать компоненты без записи mod components. На самом деле я даже не уверен, что вы сможете использовать компоненты внутри представлений каким-либо другим способом, потому что компоненты - это родственная папка, а не дочерняя.

Затем добавьте контент в ./views/mod.rs:

mod app;

pub use app::*;

И, наконец, измените main.rs, чтобы он выглядел следующим образом:

mod components;
mod views;

use crate::views::App;

fn main() {
    yew::Renderer::<App>::new().render();
}

Здесь yew::Renderer — наша альтернатива createRoot в React.

Запустите trunk serve еще раз. Возможно, он у вас уже запущен, и благодаря горячей перезагрузке он уже все обновил. Теперь вы сможете увидеть что-то не очень впечатляющее, как на изображении ниже:

Далее, давайте добавим к этому немного стиля — это начинает казаться немного монотонным. Я надеюсь, что с добавлением привлекательного дизайна вы останетесь заинтересованными и взволнованными в предстоящем путешествии.

Добавление стилей

./styles/_clock.scss

.clock {
  width: 160px;
  height: 160px;
  position: relative;
  box-sizing: border-box;
  border-radius: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: -10px 15px 3px rgba(0, 0, 0, 0.4), 3px -3px 2px rgb(255, 255, 255);

  > .inner {
    width: 90%;
    height: 90%;
    border-radius: 50%;
    position: relative;
    background: #eee no-repeat url("images/clock-face.svg") center center;
    background-size: calc(100% - 4px);
    box-shadow: inset -10px 15px 3px rgba(0, 0, 0, 0.2), inset 3px -3px 2px rgba(255, 255, 255, 1);
  }

  > .hand {
    position: absolute;
    transform-origin: bottom;
    bottom: 50%;
    z-index: 200;
  }

  > .main {
    width: 0;
    height: 27%;

    &::before {
      content: "";
      position: absolute;
      border: 3px solid transparent;
      border-bottom: 15px solid #111;
      left: -3px;
      top: 0px;
    }

    &:after {
      content: "";
      position: absolute;
      left: -3px;
      top: 18px;
      width: 0;
      height: 0;
      border: 3px solid transparent;
      border-top: 28px solid #111;
    }
  }

  > .hours {
    transform: scaleY(0.9);
  }

  > .minutes {
    background: #111;
    transform: scaleY(1.4);
  }

  > .seconds {
    width: 1%;
    height: 39%;
    background: #c00;
  }

  > .point {
    position: absolute;
    width: 7%;
    height: 7%;
    border-radius: 50%;
    background: #000;
    z-index: 300;
  }
}

./styles/_plate.scss

.plate {
  text-align: center;
  font-weight: bold;
  margin-top: 40px;
  padding: 12px;
  font-size: 16px;
  text-transform: uppercase;
  font-family: Verdana, Arial, Helvetica, sans-serif;
  background: linear-gradient(to top, #c4c5c7 0%, #dcdddf 52%, #ebebeb 100%);
  box-shadow: -1px 2px 2px rgba(0,0,0,0.5), 1px -1px rgba(255,255,255, 1)
}

./styles/_bord.scss

.board {
  width: 100vw;
  height: 100vh;
  gap: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;

  background: url("images/background.png");
}

И, наконец, давайте обновим наш index.scss.

@import 'board';
@import 'clock';
@import 'plate';

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  position: relative;
}

Наконец-то мы это сделали!

Надеюсь, это вызвало волнение! Нет ничего лучше, чем видеть, как ваша работа оживает. Теперь давайте углубимся в детали реализации. Если вы хотите, чтобы я углубился в стилизацию, просто дайте мне знать, но, как я уже говорил ранее, я действую исходя из того, что у вас уже есть некоторый опыт работы с CSS. Итак, пока давайте сосредоточимся на стороне Rust.

Детали реализации

Как видите, приложение Yew очень похоже на React и говорит само за себя, поэтому давайте выберем самый сложный компонент — clock.rs :

use yew::prelude::*;
use chrono::{DateTime, Timelike};
use chrono_tz::Tz;

Здесь мы начнем с импорта. Учитывая, что они поступают из других ящиков, мы можем использовать их как есть. Нам не нужно использовать mod перед использованием предметов из других библиотек (ящиков). Чтобы импортировать материал из наших собственных модулей, мы используем префикс crate:: и проверяем, правильно ли импортирован модуль, как мы сделали в нашем main.rs.

Далее у нас есть props, которые снова поразительно похожи на React.

#[derive(Properties, PartialEq)]
pub struct Props {
    pub time: DateTime<Tz>,
}

Rust предлагает функцию под названием «черты», которую на высоком уровне можно сравнить с интерфейсами в таких языках, как TypeScript.

В нашем контексте #[derive(Properties, PartialEq)] автоматически реализует два из этих трейтов для нашей структуры Props. Properties — это специфичная черта Yew, которая позволяет Yew использовать нашу структуру Props в компонентах Yew, а PartialEq — это стандартная черта Rust, которая означает, что наша структура Props должна быть сопоставимой. Это позволяет Yew повторно отображать компоненты, если текущие свойства не равны следующим свойствам — концепция, аналогичная React.

В приведенном выше примере мы не реализуем эти свойства вручную, а получаем их. Сейчас это может показаться немного абстрактным, но не переусердствуйте — если вам интересно, как работает «вывод», я рекомендую вам ознакомиться с официальной документацией Rust для более подробного объяснения.

Теперь давайте подробнее рассмотрим сам наш компонент:

#[function_component]
pub fn Clock(props: &Props) -> Html {
    println!("{}", props.time);

    let time = props.time.time();

    html! {
        <div class="clock">
            <div style={make_css_rotation(calc_hour_degree(time.hour(), time.minute()))} class="hand main hours" />
            <div style={make_css_rotation(calc_minute_degree(time.minute(), time.second()))} class="hand main minutes" />
            <div style={make_css_rotation(calc_second_degree(time.second()))} class="hand seconds" />
            <div class="point"></div>
            <div class="inner" />
        </div>
    }
}

Как видите, он очень похож на JSX React. Единственное, что вам может быть непонятно, это html!, который является макросом Rust.

На данный момент проще думать о макросах Rust, таких как предустановки Babel, которые применяют определенные преобразования или генерируют совершенно новый код перед созданием окончательного файла JavaScript, который может интерпретировать браузер. Точно так же, как JavaScript по своей сути не понимает JSX, Rust не понимает синтаксис HTML Yew. Когда вы используете React, файл JSX выглядит следующим образом:

<div className="test">Hello, World!</div>

Будет преобразован одним из пресетов Babel в окончательный JavaScript, который сможет понять браузер:

React.createElement("div", { className: "test" }, "Hello, World!")

Точно так же в Rust макрос html! в Yew преобразует наши HTML-подобные инструкции в код Rust, который может понять компилятор Rust. Это преобразование происходит еще до того, как компилятор Rust начнет свою работу. Таким образом, благодаря макросу html! мы можем писать внутри нашего приложения на Rust то, что выглядит как HTML-код. Это один из ключевых элементов, который делает структуру компонентов Yew похожей на React JSX.

Действительно, это было довольно путешествие. Однако давайте также добавим несколько тестов, просто ради достижения.

Погружение в тестирование

В Rust набор для тестирования встроен непосредственно в язык, что невероятно удобно. Одна из особенностей заключается в том, что тесты пишутся в том же файле, что и сам исходный код.

Хотя наш компонент «Часы» еще не имеет интерактивности, мы хотим убедиться, что он точно отображает время. Для этого мы разработали три функции, вычисляющие углы для часовой, минутной и секундной стрелок. Внизу файла clock.rs напишем для них несколько тестов:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_second_degree() {
        assert_eq!(calc_second_degree(0), 0.0);
        assert_eq!(calc_second_degree(15), 90.0);
        assert_eq!(calc_second_degree(30), 180.0);
        assert_eq!(calc_second_degree(45), 270.0);
        assert_eq!(calc_second_degree(60), 360.0);
    }

    #[test]
    fn test_minute_degree() {
        assert_eq!(calc_minute_degree(0, 0), 0.0);
        assert_eq!(calc_minute_degree(15, 0), 90.0);
        assert_eq!(calc_minute_degree(30, 0), 180.0);
        assert_eq!(calc_minute_degree(45, 0), 270.0);
        assert_eq!(calc_minute_degree(60, 0), 360.0);

        assert_eq!(calc_minute_degree(0, 15), 1.5);
        assert_eq!(calc_minute_degree(15, 15), 91.5);
        assert_eq!(calc_minute_degree(30, 30), 183.0);
        assert_eq!(calc_minute_degree(45, 45), 274.5);
    }

    #[test]
    fn test_hour_degree() {
        assert_eq!(calc_hour_degree(0, 0), 0.0);
        assert_eq!(calc_hour_degree(3, 0), 90.0);
        assert_eq!(calc_hour_degree(6, 0), 180.0);
        assert_eq!(calc_hour_degree(9, 0), 270.0);
        assert_eq!(calc_hour_degree(12, 0), 360.0);

        assert_eq!(calc_hour_degree(0, 15), 7.5);
        assert_eq!(calc_hour_degree(3, 15), 97.5);
        assert_eq!(calc_hour_degree(6, 30), 195.0);
        assert_eq!(calc_hour_degree(9, 45), 292.5);
    }
}

Для тех, кто использует Visual Studio Code, IDE удобно предложит установить все необходимые расширения Rust. Эти дополнения позволяют запускать тесты непосредственно в самой среде IDE:

Заключение

И вот оно! В этой части мы рассмотрели довольно много, и я понимаю, что некоторые аспекты могут показаться элементарными. Тем не менее, для тех, кто заинтригован Rust, но мало знаком с ним, эти основы необходимы.

По мере того, как мы продвигаемся в этой серии, мы многое будем изучать. Итак, я очень хочу услышать от вас:

  • Какие аспекты вы хотели бы уточнить или прояснить?
  • Есть ли какие-то особенности, о которых вам интересно узнать?

В следующей части мы анимируем наши часы и сделаем их интерактивными. Хотя Rust может похвастаться одной из лучших экосистем для разработки веб-приложений на основе WASM, даже использование тривиальных функций, таких как setTimer или setInterval, может вызвать проблемы.

Исходник этой статьи вы можете найти на Github

Следите за новостями,
Ура!