Rust 和 Wasm 的融合,使用 yew 构建 web 前端(3)- 资源文件及小重构

前两篇文章《起步及 crate 选择》《组件和路由》中,我们介绍了选型原因,搭建了 yew 的基本开发环境,并进行了最基础的组件和路由编码。并且和 yew 中文文档的翻译者 sansx 老师及一些感兴趣的朋友进行了友好而热烈的交流。

关于交流心得,笔者感觉有必要提及一下,作为一个即要走路也要看路的技术认知:

  • 关于 html! 宏中的 <>……</>,这是因为 html! 宏仅能有一个根标签元素。<>……</> 充当了一个根标签,输出实际上是空的。另外,html! 宏中的标签必须闭合,即使 html5 标准中不需要 /> 的自闭合标签,也不能省略 />。如 <img src="path" />
  • yew 生产环境的应用。笔者仅是 yew 的初学者,理解不很恰当。根据对官方 API 文档的理解,个人认为当前版本用于生产环境,是一个不小的挑战(包括开发和维护)。但从项目源码、issues 讨论,以及路线规划来看,个人认为下个版本差强人意,待发布后,yew 用于生产环境是可以接受的。笔者也有此计划。
  • ssr 或者 seo 方面,yew 官方有计划,但未有实质进度。但笔者认为影响不大,网上几年前就有文章给出了结论:新时代的搜索引擎(Google、Yahoo、Bing、DuckDuckGo 等),能够像现代浏览器一样访问网站,能很好的抓取动态渲染后的内容,不用担心使用 yew 之类的框架而导致 seo 出现问题。几年过去了,搜索引擎的技术进步应该很大。再者,笔者认为现在信息传播的方式已经有所改变,国内尤为明显。最后,当国外搜索引擎已经收录大量中文站点的内容时,某些国内搜索引擎,却仅是首页甚至是未有收录;这样的情形,即使技术方面对 seo 很适配,估计也是不能解决收录问题的。
  • Rust 官方周报 393 期中有一篇技术,是关于 Rust + WebAssembly 为 deno 开发插件系统的,看起来前景很不错。基于 WebAssembly 的性能和特性,如果插件足够通用,说不定可发展为一个独立的职业。

前两篇文章中,我们实现的界面是非常简陋的,没有引入任何样式、图像等 web 应用必不可少的资源文件。本篇文章中,我们将实践如何对 yew 组件使用样式,组件包含图片等。严格来说,这部分是属于构建工具 trunk 的知识。trunk 工具在首篇文章《起步及 crate 选择》中已经提及,是完全的 Rust 技术栈开发,不同于 wasm-pack 那样需要 node 环境。其在样式方面,支持 css/sass/scss(scss 实质是 sass3 及之后的升级版,目前使用更广一些),我们都将进行实践。图像方面,笔者分别引入 icon 和在组件中放置 <img> 标签以作示例。其它 js 和数据等资源文件,未有设计,但使用和图像是类同的。

引入样式表

笔者在 frontend-yew 目录中,创建如下目录和结构,放置资源文件:

mkdir -p assets/{css, imgs, js, data}
cd assets/css
touch style.css style.sass style.scss

css 代码

我们分别有 css、sass,以及 scss,仅是为验证 trunk 对其都可以编译。

style.css

.home {
    background-color: red;
}

style.sass

$users-color: blue

.users
    background-color: $users-color;

style.scss

.logo-title {
    line-height: 40px;
    display:flex;
    align-items: center;
}

.nav {
    line-height: 30px;
    display:flex;
    align-items: center;
    margin-bottom: 20px;
    font-weight: bold;
}

$projects-color: green;

.projects {
    background-color: $projects-color;
}

将样式表加入 trunk 构建路径

trunk 工具构建时,资源文件通过 <link> 标签引入,但需要声明 data-trunk。我们要将上述三个样式表加入构建路径,在 index.html 文件中的 <head> 标签内,加入它们的路径:

<!doctype html>
<html lang="zh">

    <head>
        <title>tide-async-graphql-mongodb - frontend - yew</title>

        <meta charset="utf-8">

        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="author" content="zzy, https://github.com/zzy/tide-async-graphql-mongodb">

        <link data-trunk rel="css" href="assets/css/style.css">
        <link data-trunk rel="sass" href="assets/css/style.sass">
        <link data-trunk rel="scss" href="assets/css/style.scss">
    </head>

</html>

组件中使用 css

重要:以下均为代码片段,请注意文件名,以及不同的样式表压入方法。

使用 &str 字符串字面量

如在 main.rs 中的应用入口组件上,使用 style.scss 声明的样式:


#![allow(unused)]
fn main() {
    fn view(&self) -> Html {
        type Anchor = RouterAnchor<Route>;

        let home_cls = "nav";

        html! {
            <>
            <div class="logo-title">
                { "tide-async-graphql-mongodb / frontend-yew" }
            </div>
            <div class=home_cls>
                <Anchor route=Route::Users>
                    { "用户列表" }
                </Anchor>
                { " - " }
                ……
                ……
                ……
            </>
        }
    }
}

如在 users.rs 中的用户列表组件上,使用 style.sass 声明的样式:


#![allow(unused)]
fn main() {
    fn view(&self) -> Html {
        html! {
            <div class="users">
            { "用户列表 - 蓝色" }
            </div>
        }
    }
}

使用 classes!

yew 的近期版本中,新增了 classes! 宏,让样式表的压入更灵活,扩展性更强。

如在 home.rs 中的主界面组件上,使用 style.css 声明的样式:


#![allow(unused)]
fn main() {
    fn view(&self) -> Html {
        let home_cls = "home";

        html! {
            <div class=classes!(home_cls)>
               { "主界面 - 红色" }
            </div>
        }
    }
}

如在 projects.rs 中的项目列表组件上,使用 style.scss 声明的样式:


#![allow(unused)]
fn main() {
    fn view(&self) -> Html {
        html! {
            <div class=classes!("projects")>
            { "项目列表 - 绿色" }
            </div>
        }
    }
}

引入图像

笔者向 assets 目录中放入一个 favicon.png 图像,向 assets/imgs 目录中放入一个 budshome.png 图像。

icon 和 <img> 都是通过 <link> 标签加入到构建路径,但 rel 属性则不同:icon 图像的引入,定义为 rel="icon",而 <img> 使用的图像资源,则要在构建时复制:可以选择复制单个文件,也可以复制文件夹。

<link data-trunk rel="icon" href="assets/favicon.png">

<link data-trunk rel="copy-dir" href="assets/imgs"> 
# 或者复制单个文件
<link data-trunk rel="copy-file" href="assets/imgs/budshome.png">

笔者使用的是复制文件夹。至此,index.html 文件完整内容为:

<!doctype html>
<html lang="zh">

    <head>
        <title>tide-async-graphql-mongodb - frontend - yew</title>

        <meta charset="utf-8">

        <link data-trunk rel="icon" href="assets/favicon.png">

        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="author" content="zzy, https://github.com/zzy/tide-async-graphql-mongodb">

        <link data-trunk rel="css" href="assets/css/style.css">
        <link data-trunk rel="sass" href="assets/css/style.sass">
        <link data-trunk rel="scss" href="assets/css/style.scss">

        <link data-trunk rel="copy-dir" href="assets/imgs">
    </head>

</html>

在 yew 组件代码中,我们直接嵌入图像元素,注意此时图像路径从的根目录为 imgs

注意html! 宏中的标签必须闭合,即使 html5 标准中不需要 /> 的自闭合标签,也不能省略 />


#![allow(unused)]
fn main() {
    fn view(&self) -> Html {
        type Anchor = RouterAnchor<Route>;

        let home_cls = "nav";

        html! {
            <>
            <div class="logo-title">
                <img src="imgs/budshome.png" />
                { "tide-async-graphql-mongodb / frontend-yew" }
            </div>
            <div class=home_cls>
                <Anchor route=Route::Users>
                    { "用户列表" }
                ……
                ……
                ……
            </>
        }
    }
}

运行和测试

执行 trunk serve 命令,浏览器会自动打开一个页面,或者手动在浏览器中访问 http://127.0.0.1:3001。如果你未按照上篇 trunk.toml 所介绍的配置,请访问你自定义的端口(默认为 8080)。

点击导航菜单,可以看到页面内容有了一些基础的样式,也显示了图像元素,当然还是很简陋。但本文是示例说明资源文件的引入和构建,目标已经达成。

代码重构:精简 html! 宏中代码,提取为函数

有朋友联系,讨论 main.rs 文件中的 <main> 标签内代码是否为好的实践?是否应当提取为一个函数之类的?以保持 html! 宏中代码尽量精简。

笔者深以为然,函数相对来说是较好的实践。同时引申一下:yew 的新版本,增加了 yew-functional 函数组件包,目前还未发布为独立的 crate。

我们简单对其重构,增加一个 switch 函数,返回值为 yew 中的 Html 类型,实质上是 VNode 枚举。


#![allow(unused)]
fn main() {
fn switch(switch: Route) -> Html {
    match switch {
        Route::Users => {
            html! { <Users/> }
        }
        Route::Projects => {
            html! { <Projects/> }
        }
        Route::Home => {
            html! { <Home /> }
        }
    }
}
}

此时,main.rs 文件中的 <main> 标签内代码可精简为:

    <main>
        <Router<Route> render=Router::render(switch) />
    </main>

谢谢您的阅读!