提升 Yew 開發體驗的方案

預覽效果

TLDR; 本文先後講了 Yew 框架開發過程中引入 tailwindcss、使用 DarkMode、使用 Cargo-watch 進行熱重載和模擬 dangerouslySetInnerHtml。

友情提示:本文提到的內容均可以通過最下方的閱讀原文預覽效果。

特別說明:網站樣式取自 antfu.me[1]。

鄭重聲明:筆者乃前端小白,這篇文章僅代表筆者在學習 Wasm 過程中的進行的一些探索。如果讀者有更好的方法,可以使用下方的小程序留言或者掃描文末二維碼與筆者交流給出建議。

TailwindCSS

像筆者這樣對 CSS 一竅不通的後端一筋工程師在進行頁面開發的時候無疑是一件非常痛苦的事情。

幸好開源社區提供了很多開箱即用的 CSS 框架。而筆者對其中的 TailwindCSS[2] 則情有獨鍾。

TailwindCSS 提倡 utility-first 的理念,提供了各種見名知義的樣式名稱——使用者只需要將其組合使用就可以輕鬆實現觀感極佳的響應式頁面效果。

官方效果圖

此外,該框架還提供了 TreeShaking[3] 的功能,實現了樣式按需引入編譯的效果,節省樣式文件的體積。

說了這麼多,要怎麼和 Yew 結合使用呢?

環境準備

需要下載安裝 npmjs[4].

需要安裝 Rust 工具鏈和 Yew 框架——如果你對這句話一無所知建議從頭閱讀筆者的《Rust 學習筆記》

PostCSS

在 TailwindCSS 官網中提供了好幾種框架使用場景,這些框架會在使用過程中進行樣式的最終編譯和 TreeShaking。但還沒有 Yew 的。

不過不用擔心,官方提供了一種缺省的使用方式,那就是通過 PostCSS[5] 這個 js 工具對 css 進行轉換編譯。

配置

首先在我們的 Yew 項目根目錄下創建一個 styles 文件夾,執行下列命令:

cd styles
npm init
npm install -d postcss autoprefixer postcss-cli tailwindcss

等待安裝完成。

接着創建一個tailwind.config.js作爲 TailwindCSS 的配置文件:

module.exports = {
  purge: [
      '../src/**/*.rs'
  ],
  darkMode: 'class',
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

其中purge字段用於配置進行 Treeshaking 的文件,我們選擇了根目錄下的src文件夾中所有 rs 後綴的文件。

然後創建一個postcss.config.js作爲 postcss-cli 的配置文件:

module.exports = {
    plugins: {
        tailwindcss: { config: './tailwind.config.js' },
        autoprefixer: {},
        'postcss-nested': {},
    }
}

可以看到,使用了三個插件,tailwindcss並指定了剛纔的配置文件,autoprefixer用於針對不同瀏覽器添加差異化的樣式前綴,postcss-nested則是允許使用嵌套 css 的寫法。

最後再創建一個main.pcss作爲我們的樣式源碼文件,裏面首先使用了 TailwindCSS 的樣式,後續可以續寫一些自定義的樣式:

@tailwind base;
@tailwind components;
@tailwind utilities;

編譯

盡情在rs文件中寫下需要使用的樣式名,然後在styles目錄執行:

NODE_ENV=production node_modules/.bin/postcss main.pcss -o ../static/app.css

postcss-cli 就會按需引入編譯樣式並輸出到根目錄下的static文件夾中。

而我們只需要在static/index.html中引入這個app.css文件就可以使用樣式了。

每次都要輸出這麼一大串命令自然是麻煩又難記,所以我們可以在package.jsonscripts字段寫入:

{
  "scripts": {
    "build": "NODE_ENV=production postcss main.pcss -o ../static/app.css"
  },
}

Dark Mode

近年來 Dark Mode[6] 成爲一個很流行的主題,假如你開源了一個博客皮膚或者靜態網站生成器,那麼肯定有人會請求實現 Dark Mode 這個特性。

實現方式

實現這個特性的方法有好幾種,因爲使用的是 TailwindCSS,本文主要講兩種:

media就是自動根據系統的設置來決定使用明暗主題 •class是通過手動添加樣式的方式來實現明暗主題

media的缺陷在於無法手動切換,只能完全根據系統設置來決定,所以我們最後使用class的方式。

細心的讀者應該有注意到在上一節styles/tailwind.config.js配置文件裏有個darkMode被我們設置成了class,這就是爲 DarkMode 做的準備。

實現步驟

樣式配置

styles/main.pcss文件中,使用 CSS Variable[7] 給定幾種顏色變量和指定初始值:

// 省略上文
:root {
    --c-bg: #fff;
    --c-scrollbar: #eee;
    --c-scrollbar-hover: #bbb;
}
html {
    background-color: var(--c-bg);
    @apply text-gray-700;
}

可以看到緊接着我們在 html 裏指定了背景顏色使用預設的顏色變量--c-bg

爲什麼要這麼做呢?

這是因爲使用變量可以方便地在切換成 Dark Mode 之後控制樣式顏色變換:

// 省略上文
html.dark {
    --c-bg: #050505;
    --c-scrollbar: #111;
    --c-scrollbar-hover: #222;
    @apply text-gray-200;
}

就像這樣,在html被加上dark類後,將三個變量的值改成適合黑色主題的顏色。

代碼配置

樣式的思路已經確定,接下來就是在 Yew 框架中實現主題的切換。

通過 RustWasm 的庫web_sys提供的 Dom 操作 Api,可以實現對 html 標籤的樣式添加與刪除操作。

首先在Cargo.toml添加相關依賴:

// 省略上文
[dependencies]
wasm-bindgen = "0.2.67"
[dependencies.web-sys]
version = "0.3.4"
features = [
    'Document',
    'Element',
    'HtmlElement',
    'Node',
    'Window',
    'MediaQueryList',
]
[dependencies.js-sys]
version = "0.3.47"

然後編寫一個組件,名叫ToggleTheme

use wasm_bindgen::prelude::*;
use yew::prelude::*;
use yew_functional::*;
fn set_class(is_dark: bool) {
    // 下面的操作展示了獲取html元素並添加`dark`class的過程
    let window = web_sys::window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    let element = document
        .document_element()
        .expect("should hav a element on document");
    // js_sys提供了數組數據結構用於添加到Element結構體中
    let arr = js_sys::Array::new_with_length(1);
    arr.set(0, JsValue::from_str("dark"));
    let class_list = element.class_list();
    if is_dark {
        class_list.add(&arr).expect("should add dark class success");
    } else {
        class_list
            .remove(&arr)
            .expect("should remove dark class success");
    }
}
#[function_component(ToggleTheme)]
pub fn toggle_theme() -> Html {
    let (is_dark, set_is_dark) = use_state(|| false);
    let onclick = {
        let (is_dark, set_is_dark) = (s_dark.clone(), set_is_dark.clone());
        Callback::from(move |_| {
            set_is_dark(!*is_dark);
            set_class(!*is_dark);
        })
    };
    html! {
      <button onclick=onclick>{"切換主題"}</button>
    }
}

將這個組件掛載到頁面合適的地方,然後編譯成 Wasm 並使用簡易 http 服務器進行瀏覽,點擊之後就實現了黑色和亮色主題的切換效果。

toggle theme

結合系統偏好

實現了手動切換,但是有一個缺點:用戶首次進來永遠是亮色主題,想要使用黑色主題只能手動。

有沒有辦法默認使用系統設置,並允許用戶手動切換呢?

答案自然是有的,我們只需要使用window對象提供的matchMedia方法,獲取系統偏好設置就行了。

// 省略上文
fn use_prefered_dark() -> bool {
    let window = web_sys::window().expect("no global `window` exists");
    let mut is_perfered_dark = false;
    match window.match_media("(prefers-color-scheme: dark)") {
        Ok(option_media_query_list) => match option_media_query_list {
            Some(media_query_list) => {
                is_perfered_dark = media_query_list.matches();
            }
            None => {}
        },
        Err(_) => {}
    };
    is_perfered_dark
}
#[function_component(ToggleTheme)]
pub fn toggle_theme() -> Html {
    let (is_dark, set_is_dark) = use_state(|| use_prefered_dark());
    use_effect({
        let is_dark = is_dark.clone();
        move || {
            set_class(*is_dark);
            || {}
        }
    });
    // 省略下文
}

我們添加了一個use_prefered_dark方法,通過window.match_media去匹配prefers-color-scheme:來得到系統默認顏色是否爲黑色,並置爲初始值。

然後使用了use_effect這個 Hook 在組件一加載的時候應用主題。

其他改進?

就在剛纔,我們實現了一個勉強可用的 Dark Mode,現在思考一下還有什麼可以改進呢?

• 通過事件監聽系統隨着時間流逝切換主題,實時應用主題變更 • 通過local_storage記憶用戶的設置,並在下次訪問時進行應用

讀者可以自行拓展,筆者這裏不再贅述。

熱重載

不知道在前兩節中,讀者有沒有對時而編譯 CSS 時而編譯 Wasm 而感到手忙腳亂?

在使用 React 或者 Vue 進行開發的時候框架往往自帶很方便的熱重載功能,要是這裏也能用上將會極大地提升開發體驗。

cargo-watch[8] 就是一個提供熱重載的全局工具。

它默認根據.gitignore的配置進行文件監聽,只要監聽的文件發生了變動就會重複執行預先設置的指令,從而達到熱重載的效果。

針對當前場景,我們只需要在項目根目錄執行:

 cargo-watch -s "yarn --cwd styles build" \ 
  -s "wasm-pack build --target web --out-name wasm --out-dir ./dist"

就可以放手開發而不管編譯操作。

使用拓展

上述操作雖然簡單,但是重載過程需要一定時間。

讀者可以自行閱讀cargo-watch的使用手冊,實現分別監聽 css 變化和 rs 文件變化熱重載,進一步提高使用體驗。

此外,如果有前端讀者知道如何使用vite或者其他工具實現熱重載自動刷新頁面也請告知筆者。

筆者嘗試過使用 vite+ts 引用 wasm 的方式進行開發,但是遇到了兩個問題:

  1. 自動刷新頁面後變成空白

  2. 構建後無法正確讀取 Wasm 文件。

dangerouslySetInnerHtml

如果你和筆者一樣,想用 Yew 開發一個讀取 Markdown[9] 文件並生成文章的靜態博客網站生成器,那麼一定會需要類似於 React 的dangerouslySetInnerHtml或 Vue 的v-html指令。

這個指令的用途是無轉義地將 Markdown 渲染生成的 html 內容嵌入到組件中。

很遺憾目前 Yew 並沒有提供一個直接設置的指令,但是官方給出了一個間接實現的方案 [10]:

use yew::prelude::*;
use yew::web_sys::Element;
use yew_functional::*;
#[derive(Debug, Clone, Eq, PartialEq, Properties)]
pub struct Props {
    pub inner_html: String,
}
#[function_component(Post)]
pub fn post(props: &Props) -> Html {
    let node_ref = NodeRef::default();
    {
        let inner_html = props.inner_html.clone();
        let node_ref = node_ref.clone();
        use_effect(move || {
            let el = node_ref.cast::<Element>().unwrap();
            el.set_inner_html(inner_html.as_str());
            || {}
        });
    }
    html! {
      <>
        <div>
          <h1>{"Yew Tailwindcss"}</h1>
        </div>
        <div ref=node_ref.clone() />
      </>
    }
}

通過NodeRef來調用元素自身,然後進行inner_html的設置,將渲染內容填充進去。

接下來

關於 Wasm 和 Yew 系列的文章即將到達尾聲。

筆者學習 Wasm 的目的在於想要實現一個類似於 VuepressGatsbyjs 的靜態網站生成器,目前看來只剩下最後一個需要解決的問題:

爲每個路由生成一個服務端渲染後的靜態頁面並輸出,解決 SEO 問題。

讀者也許注意到了,筆者在以往的文章中故意沒有提及yew-router的使用。其實就是爲了在最後一篇文章中一起講解。

關於源碼

本文中描述的相關代碼可以在 yuchanns/rustbyexample[11] 找到。

這是一個筆者創建的學習 Rust 過程中記錄各種 demo 的 git 倉庫。歡迎各位觀衆 star 關注,以及 fork 和 pr 添加新的 demo,大家一起學習進步

如果你覺得這篇文章不錯,有更多心得想和筆者交流,歡迎添加我的微信,備註【來自 代碼鍊金工坊】!

引用鏈接

[1] antfu.me: https://antfu.me/
[2] TailwindCSS: https://tailwindcss.com/
[3] TreeShaking: https://en.wikipedia.org/wiki/Tree_shaking
[4] npmjs: https://www.npmjs.com/
[5] PostCSS: https://postcss.org/
[6] Dark Mode: https://en.wikipedia.org/wiki/Light-on-dark_color_scheme
[7] CSS Variable: https://www.w3schools.com/css/css3_variables.asp
[8] cargo-watch: https://github.com/passcod/cargo-watch
[9] Markdown: https://en.wikipedia.org/wiki/Markdown
[10] 間接實現的方案: https://github.com/yewstack/yew/issues/189
[11] yuchanns/rustbyexample: https://github.com/yuchanns/rustbyexample

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FJt7YW0P1zzPpVVDEoY1uQ