发布于 2026-01-06 0 阅读
0

如何仅使用 Rust 进行 Web 前端开发?Mux 主办的全球展示与分享挑战赛:展示你的项目!

如何仅使用 Rust 开发 Web 前端

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

大家好!

我是 Arn,我是一名程序员,主要使用front_end & full_stack React/Angular/Redux/Ngrx/Javascript/Typescript + Node, 以前是现在肯定是 Rust/ seed-rs full_stack

今天我写了我人生中第一篇长篇教程,会向大家展示我上周发现的一些很棒的东西。我是Rust新手,但我很喜欢它,所以如果有经验丰富的 Rust 程序员看到我写的丑陋代码,欢迎指正 :D。

总之,我几个月前发现了Wasm,然后就开始尝试学习这个教程:

https://rustwasm.github.io/docs/book/introduction.html

这本书讲解了如何使用 Rust 开发前端,以及如何将编译后的代码与 JavaScript 结合使用。我对此非常感兴趣,尤其因为 Rust 本身就很好用,而且我的电脑在开发大型前端应用时速度很慢……主要是因为 Angular 中文件过多导致的内存不足问题。

我上周开始认真写教程,然后脑子里突然冒出一个念头:“我的天哪!”有了 Rust 工具链,有了强大的智能编译器,前端开发工作就能轻松应对,性能也……

仅仅是用一点 Rust 语言来制作前端,就让我感到非常满足、快乐和充满动力。

看完这个教程后,我就想尝试用 WebGL 而不是 Canvas 渲染,也许有人已经用 WebGL 做过类似的东西了。然后我又想,也许有人开发了一个 Web 框架,这样我们就可以只用 Rust 而不用 JavaScript 了。那想想就觉得很棒。用最稳定、最健壮、最安全、最高效的编程语言来做前端开发。

然后我看到了Seed,然后我的脑袋就炸开了!

在克隆了 Seed 的一个快速入门应用程序的第二天,我决定将生命游戏教程改编成纯 Rust代码。

我花了几个小时才完成。因为我是 Rust 新手,而且技术也有些生疏(开个玩笑),所以我觉得对于一个新框架和新语言的初学者来说,几个小时已经相当不错了。

在本教程中,我将向你展示我最初制作的生命游戏和种子生命游戏之间的区别。
建议你在学习本教程之前先了解一些基础知识。

注:

  • 我将向您展示一个非常简陋且直接的转换方法。Seed 中有很多很棒的功能,例如从文档引用元素 =>,这些功能可以极大地改进代码ElRef<T>。但我现在暂时先略过它们。

概括

1 - 设置
2 - 使用原教程中的 Rust 代码Universe
3 - 将应用程序的核心从 JavaScript 构建为 Rust
4 - 进行测试和基准测试
5 - 如何提高性能
6 - 总结

我们走吧 !

1 - 设置

如果您克隆了“ https://github.com/seed-rs/seed-quickstart ”上的快速入门指南,则可以跳过此部分,但以下显示了从头开始创建项目的快速步骤。

cargo new seeded-game-of-life --lib

我们的项目是一个图书馆。

我们将添加以下内容:

让我们更新一下我们的Cargo.toml

Cargo.toml

[package]
version = "0.1.0"
name = "seeded-game-of-life"
repository = "https://github.com/seed-rs/seed-quickstart"
authors = ["Your Name <email@address.com>"]
description = "App Description"
categories = ["category"]
license = "MIT"
readme = "./README.md"
edition = "2018"

[lib]
crate-type = ["cdylib", ,"rlib"]

[dependencies]
seed = { git = "https://github.com/seed-rs/seed", rev = "0a538f0" }
[dependencies.web-sys]
version = "0.3"
Enter fullscreen mode Exit fullscreen mode

好的,这里有3个要点:

  • cdylib据我所知,它是用来编译成 .wasm 文件的。如果不用它编译,就会出错。

  • seed = { git = "https://github.com/seed-rs/seed", rev = "0a538f0" }

这是我们的主要依赖项,因为它是 Web 框架 :D

  • [dependencies.web-sys] version = "0.3"

web-sys将大部分 Web 浏览器 API 暴露给 Rust。根据你的需求,你可能需要启用一些功能。

我们需要运行一些任务watch and compile并处理一些serve文件。

cargo install cargo-make

然后创建此文件MakeFile.toml

我们还可以用它来构建发布版本。目前无需了解它的内部结构 :P。

MakeFile.toml

[env]
PORT = "8000"

[config]
skip_core_tasks = true

# ---- BASIC ----

[tasks.watch]
description = "Watch files and recompile the project on change"
run_task = [
    { name = "build" },
]
watch = true

[tasks.serve]
description = "Start server"
install_crate = { crate_name = "microserver", binary = "microserver", test_arg = "-h" }
command = "microserver"
args = ["--port", "${PORT}"]

[tasks.verify]
description = "Format, lint with Clippy and run tests"
dependencies = ["fmt", "clippy", "test_h_firefox"]

# ---- BUILD ----

[tasks.build]
description = "Build with wasm-pack"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["build", "--target", "web", "--out-name", "package", "--dev"]

[tasks.build_release]
description = "Build with wasm-pack in release mode"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["build", "--target", "web", "--out-name", "package"]

# ---- LINT ----

[tasks.clippy]
description = "Lint with Clippy"
install_crate = { rustup_component_name = "clippy", binary = "cargo-clippy", test_arg = "--help" }
command = "cargo"
args = ["clippy", "--all-features", "--", "--deny", "warnings", "--deny", "clippy::pedantic", "--deny", "clippy::nursery"]

[tasks.fmt]
description = "Format with rustfmt"
install_crate = { rustup_component_name = "rustfmt", binary = "rustfmt", test_arg = "-V" }
command = "cargo"
args = ["fmt"]


# ---- TEST ----

[tasks.test_h]
description = "Run headless tests. Ex: 'cargo make test_h firefox'. Test envs: [chrome, firefox, safari]"
extend = "test"
args = ["test", "--headless", "--${@}"]

[tasks.test_h_firefox]
description = "Run headless tests with Firefox."
extend = "test"
args = ["test", "--headless", "--firefox"]

[tasks.test]
description = "Run tests. Ex: 'cargo make test firefox'. Test envs: [chrome, firefox, safari]"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["test", "--${@}"]

Enter fullscreen mode Exit fullscreen mode

添加 index.html 文件

我们的应用是一个单页应用。我们会向它提供一些代码Wasm,以及一些生成的 JavaScript 代码。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Seeded Game of life</title>
</head>

<body>
    <section id="app"></section>
    <script type="module">
        import init from '/pkg/package.js';
        init('/pkg/package_bg.wasm');
    </script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

我们导入了一个生成的 js 文件,其中包含 JS 和 WASM 之间的通信,因为在底层,我们有时会调用一些用于与 Web 浏览器通信的标准 js 函数。

这种情况未来应该会改变。

然后我们还有 package_bg.wasm,其中包含了我们可爱的Rust 编译后的二进制文件 :)

现在让我们添加一段示例代码lib.rs,看看它是否有效。

lib.rs

use seed::{prelude::*, *};

// `init` describes what should happen when your app started.
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
    Model::default()
}

// `Model` describes our app state.
type Model = i32;

// `Msg` describes the different events you can modify state with.
enum Msg {
    Increment,
}

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Increment => *model += 1,
    }
}

// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
    div![
        "This is a counter: ",
        C!["counter"],
        button![
            model,
            ev(Ev::Click, |_| Msg::Increment),
        ],
    ]
}

#[wasm_bindgen(start)]
pub fn start() {
    // Mount the `app` to the element with the `id` "app".
    App::start("app", init, update, view);
}
Enter fullscreen mode Exit fullscreen mode

在你的 .gitignore 文件中

/target
Cargo.lock
/pkg  
Enter fullscreen mode Exit fullscreen mode

至少在 Linux 系统上,我需要这样做,否则/pkg每次编译后 Cargo 都会检测到更改并重新构建 :P 也许这是个小 bug 需要修复 :D

我们现在有:

seeded-game-of-life/
├── .gitignore
├── Cargo.toml
├── index.html
├── MakeFile.toml
└── src
    ├── lib.rs

Enter fullscreen mode Exit fullscreen mode

然后

cargo make watch在一个终端中 -> 更改时编译

cargo make serve在另一个文件中 -> 提供文件

http://localhost:8000

你应该有个漂亮的柜台。

除了测试和发布构建以及使用之外,无需进行其他设置。web-sys

与生命游戏的原始设置相比,我们不需要www文件夹。

  • 没有 JSON 包
  • 没有 webpack
  • 没有 JavaScript 代码
  • 没有 node_modules 目录

基本上,有了 4 个文件和 2 个依赖项(sys-web + seed),我们就有了进行 Web 开发的基础。

这真是个巨大的进步!!!!我们可以轻松集中精力,提高效率 :)。
上周末我把这个展示给了我一个刚接触 Web 开发的朋友。他一直在努力学习 React,因为它依赖项太多,而且需要掌握 webpack、package.json 等等方面的知识。Angular 和 Vue 也一样。即使随着时间的推移,它们都会变得更加易于使用和配置,但对于不熟悉 JavaScript 且来自底层编程领域的人来说,它们仍然相当具有挑战性。到处都是文件 :P
而且需要理解的概念和抽象层次也很多。

在使用 Rust/Seed 的第一个小时里,我的朋友就更新了你将会看到并使用的代码,ElRef<T>取代了我现在使用的这种不太优雅的 DOM 调用方式。这令人印象深刻,因为:

  • 他以前从未接触过锈蚀。
  • 他以前从未接触过榆木,但仅凭反例就理解了其中的规律。
  • 他不是网页开发人员。
  • 他不是 JavaScript 专家,而是 Python 专家。

现在我们有了一个非常轻量级的配置,可以开始玩了 :D

2 - 从原始教程中获取 Rust 代码

我的原始代码 lib.rs来自https://github.com/arn-the-long-beard/wasm-game-of-life/blob/master/src/lib.rs


mod utils;

use rand_core::{OsRng, RngCore};
use std::fmt;
use wasm_bindgen::prelude::*;
use web_sys::console;
#[wasm_bindgen]
extern crate web_sys;
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
impl Cell {
    fn toggle(&mut self) {
        *self = match *self {
            Cell::Dead => Cell::Alive,
            Cell::Alive => Cell::Dead,
        };
    }
}
#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}

impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }
    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;

        let north = if row == 0 { self.height - 1 } else { row - 1 };

        let south = if row == self.height - 1 { 0 } else { row + 1 };

        let west = if column == 0 {
            self.width - 1
        } else {
            column - 1
        };

        let east = if column == self.width - 1 {
            0
        } else {
            column + 1
        };

        let nw = self.get_index(north, west);
        count += self.cells[nw] as u8;

        let n = self.get_index(north, column);
        count += self.cells[n] as u8;

        let ne = self.get_index(north, east);
        count += self.cells[ne] as u8;

        let w = self.get_index(row, west);
        count += self.cells[w] as u8;

        let e = self.get_index(row, east);
        count += self.cells[e] as u8;

        let sw = self.get_index(south, west);
        count += self.cells[sw] as u8;

        let s = self.get_index(south, column);
        count += self.cells[s] as u8;

        let se = self.get_index(south, east);
        count += self.cells[se] as u8;

        count
    }
}

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    pub fn new() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;

        let cells = (0..width * height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn reset(&mut self) {
        let cells = (0..self.width * self.height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        self.cells = cells;
    }
    pub fn death() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;

        let cells = (0..width * height).map(|i| Cell::Dead).collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn random() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;
        let mut key = [0u8; 16];
        OsRng.fill_bytes(&mut key);
        let random_u64 = OsRng.next_u64();
        let cells = (0..width * height)
            .map(|i| {
                if random_u64 % 2 == 0 || random_u64 % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
    ///
    /// Toggle a cell on specific coordinates
    pub fn toggle_cell(&mut self, row: u32, column: u32) {
        let idx = self.get_index(row, column);
        self.cells[idx].toggle();
    }
    pub fn render(&self) -> String {
        self.to_string()
    }

    pub fn tick(&mut self) {
        let mut next = { self.cells.clone() };

        {
            for row in 0..self.height {
                for col in 0..self.width {
                    let idx = self.get_index(row, col);
                    let cell = self.cells[idx];
                    let live_neighbors = self.live_neighbor_count(row, col);

                    let next_cell = match (cell, live_neighbors) {
                        // Rule 1: Any live cell with fewer than two live neighbours
                        // dies, as if caused by underpopulation.
                        (Cell::Alive, x) if x < 2 => Cell::Dead,
                        // Rule 2: Any live cell with two or three live neighbours
                        // lives on to the next generation.
                        (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                        // Rule 3: Any live cell with more than three live
                        // neighbours dies, as if by overpopulation.
                        (Cell::Alive, x) if x > 3 => Cell::Dead,
                        // Rule 4: Any dead cell with exactly three live neighbours
                        // becomes a live cell, as if by reproduction.
                        (Cell::Dead, 3) => Cell::Alive,
                        // All other cells remain in the same state.
                        (otherwise, _) => otherwise,
                    };

                    next[idx] = next_cell;
                }
            }
        }

        self.cells = next;
    }
    /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
    // ...
}

impl Universe {
    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }
}

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

我们只保留关于宇宙的部分。我们将两种实现方式合并,并创建一个新文件。universe.rs

注意:我的原始 lib.rs 文件包含的内容比教程仓库中的内容更多,因为我完成了作者给出的一些不错的练习。

我已经实现了随机宇宙和终极死亡。

我没能成功地让随机宇宙在原版人生游戏中运行 :(

以下是一些生成随机内容所需的额外依赖项。请将它们添加[dependencies]到您的……Cargo.toml

rand = "0.7.3"
rand_core = "0.5.1"
Enter fullscreen mode Exit fullscreen mode

我承认我超级懒。肯定有不用依赖就能手动完成的简单方法吧 :D

现在在/src

universe.rs

use rand_core::{OsRng, RngCore};
use std::fmt;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
impl Cell {
    fn toggle(&mut self) {
        *self = match *self {
            Cell::Dead => Cell::Alive,
            Cell::Alive => Cell::Dead,
        };
    }
}

pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
/// Public methods, exported to JavaScript.

impl Universe {
    pub fn new() -> Universe {
        let width = 64;
        let height = 64;

        let cells = (0..width * height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn reset(&mut self) {
        let cells = (0..self.width * self.height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        self.cells = cells;
    }
    /// Kill all the cells
    pub fn death() -> Universe {
        let width = 64;
        let height = 64;

        let cells = (0..width * height).map(|i| Cell::Dead).collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    /// Generate random state for cell
    pub fn random() -> Universe {
        let width = 64;
        let height = 64;
        let mut key = [0u8; 16];
        OsRng.fill_bytes(&mut key);

        let cells = (0..width * height)
            .map(|i| {
                if OsRng.next_u64() % 2 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }

    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }
    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;

        let north = if row == 0 { self.height - 1 } else { row - 1 };

        let south = if row == self.height - 1 { 0 } else { row + 1 };

        let west = if column == 0 {
            self.width - 1
        } else {
            column - 1
        };

        let east = if column == self.width - 1 {
            0
        } else {
            column + 1
        };

        let nw = self.get_index(north, west);
        count += self.cells[nw] as u8;

        let n = self.get_index(north, column);
        count += self.cells[n] as u8;

        let ne = self.get_index(north, east);
        count += self.cells[ne] as u8;

        let w = self.get_index(row, west);
        count += self.cells[w] as u8;

        let e = self.get_index(row, east);
        count += self.cells[e] as u8;

        let sw = self.get_index(south, west);
        count += self.cells[sw] as u8;

        let s = self.get_index(south, column);
        count += self.cells[s] as u8;

        let se = self.get_index(south, east);
        count += self.cells[se] as u8;

        count
    }
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
    ///
    /// Toggle a cell on specific coordinates
    pub fn toggle_cell(&mut self, row: u32, column: u32) {
        let idx = self.get_index(row, column);
        self.cells[idx].toggle();
    }
    pub fn render(&self) -> String {
        self.to_string()
    }

    pub fn cell_at_index(&self, index: usize) -> Cell {
        self.cells[index]
    }

    pub fn tick(&mut self) {
        let mut next = { self.cells.clone() };

        {
            for row in 0..self.height {
                for col in 0..self.width {
                    let idx = self.get_index(row, col);
                    let cell = self.cells[idx];
                    let live_neighbors = self.live_neighbor_count(row, col);

                    let next_cell = match (cell, live_neighbors) {
                        // Rule 1: Any live cell with fewer than two live neighbours
                        // dies, as if caused by underpopulation.
                        (Cell::Alive, x) if x < 2 => Cell::Dead,
                        // Rule 2: Any live cell with two or three live neighbours
                        // lives on to the next generation.
                        (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                        // Rule 3: Any live cell with more than three live
                        // neighbours dies, as if by overpopulation.
                        (Cell::Alive, x) if x > 3 => Cell::Dead,
                        // Rule 4: Any dead cell with exactly three live neighbours
                        // becomes a live cell, as if by reproduction.
                        (Cell::Dead, 3) => Cell::Alive,
                        // All other cells remain in the same state.
                        (otherwise, _) => otherwise,
                    };

                    next[idx] = next_cell;
                }
            }
        }

        self.cells = next;
    }
    /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
    // ...
}

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}


Enter fullscreen mode Exit fullscreen mode

我们可以看到这方面有显著的改进:

  • 无需使用#[wasm_bindgen]
  • 无需使用sys_web
  • 无需使用logSeed我们可以根据需要使用另一个。
  • 无需使用utils模块。尝试创建一个普通的程序panic!,你就能在浏览器控制台中看到 Rust 代码中出现错误的那一行了 :)
  • 我们将不同的impl宇宙融合在一起
  • Universe如果需要,我们可以直接进行单元测试。
  • Universe如果需要,我们可以直接进行基准测试。

现在让我们深入了解一下这款应用的核心。

3 - 让我们从 Js 到 Rust 构建应用程序的核心。

各位,请记住,我们正在翻译的这个仓库是我根据这篇很棒的教程(https://rustwasm.github.io/docs/book/introduction.html)编写的。

==>到 Rust/Seed

我最初的JS文件很丑,因为,嗯,我根本没在意让它美观,也没创建类或其他文件。即使在示例仓库中,JS文件也很混乱 :P。

这是我的原稿 index.js


import { Universe, Cell } from "wasm-game-of-life";
// Import the WebAssembly memory at the top of the file.
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";
const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";

// Construct the universe, and get its width and height.
let  universe = Universe.new();

// universe.set_height(100);
// universe.set_width(100);
universe.reset();
const width = universe.width();
const height = universe.height();


// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;

const ctx = canvas.getContext('2d');
let animationId = null;
const fps = new class {
    constructor() {
        this.fps = document.getElementById("fps");
        this.frames = [];
        this.lastFrameTimeStamp = performance.now();
    }

    render() {
        // Convert the delta time since the last frame render into a measure
        // of frames per second.
        const now = performance.now();
        const delta = now - this.lastFrameTimeStamp;
        this.lastFrameTimeStamp = now;
        const fps = 1 / delta * 1000;

        // Save only the latest 100 timings.
        this.frames.push(fps);
        if (this.frames.length > 100) {
            this.frames.shift();
        }

        // Find the max, min, and mean of our 100 latest timings.
        let min = Infinity;
        let max = -Infinity;
        let sum = 0;
        for (let i = 0; i < this.frames.length; i++) {
            sum += this.frames[i];
            min = Math.min(this.frames[i], min);
            max = Math.max(this.frames[i], max);
        }
        let mean = sum / this.frames.length;

        // Render the statistics.
        this.fps.textContent = `
Frames per Second:
         latest = ${Math.round(fps)}
avg of last 100 = ${Math.round(mean)}
min of last 100 = ${Math.round(min)}
max of last 100 = ${Math.round(max)}
`.trim();
    }
};
const renderLoop = () => {
    fps.render();
    let ticks = document.getElementById("ticks").value;
    for (let i = 0; i <  ticks ; i++) {
        universe.tick();
    }
    drawGrid();
    drawCells();

 animationId = requestAnimationFrame(renderLoop);
};


const ultimateDeath = document.getElementById("death");

ultimateDeath.addEventListener("click", event=> {
  universe = Universe.death();
})

// const reset = document.getElementById("reset");
//
// reset.addEventListener("click", event=> {
//     universe = Universe.random();
// })
const isPaused = () => {
    return animationId === null;
};
const playPauseButton = document.getElementById("play-pause");

const play = () => {
    playPauseButton.textContent = "";
    renderLoop();
};

const pause = () => {
    playPauseButton.textContent = "";
    cancelAnimationFrame(animationId);
    animationId = null;
};

playPauseButton.addEventListener("click", event => {
    if (isPaused()) {
        play();
    } else {
        pause();
    }
});
const drawGrid = () => {
    ctx.beginPath();
    ctx.strokeStyle = GRID_COLOR;

    // Vertical lines.
    for (let i = 0; i <= width; i++) {

        ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
        ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
    }

    // Horizontal lines.
    for (let j = 0; j <= height; j++) {

        ctx.moveTo(0,                           j * (CELL_SIZE + 1) + 1);
        ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
    }

    ctx.stroke();
};
const getIndex = (row, column) => {
    return row * width + column;
};

const drawCells = () => {
    const cellsPtr = universe.cells();
    const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

    ctx.beginPath();

    // Alive cells.
    ctx.fillStyle = ALIVE_COLOR;
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const idx = getIndex(row, col);
            if (cells[idx] !== Cell.Alive) {
                continue;
            }

            ctx.fillRect(
                col * (CELL_SIZE + 1) + 1,
                row * (CELL_SIZE + 1) + 1,
                CELL_SIZE,
                CELL_SIZE
            );
        }
    }

// Dead cells.
    ctx.fillStyle = DEAD_COLOR;
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const idx = getIndex(row, col);
            if (cells[idx] !== Cell.Dead) {
                continue;
            }

            ctx.fillRect(
                col * (CELL_SIZE + 1) + 1,
                row * (CELL_SIZE + 1) + 1,
                CELL_SIZE,
                CELL_SIZE
            );
        }
    }

    ctx.stroke();
};


canvas.addEventListener("click", event => {
    const boundingRect = canvas.getBoundingClientRect();

    const scaleX = canvas.width / boundingRect.width;
    const scaleY = canvas.height / boundingRect.height;

    const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
    const canvasTop = (event.clientY - boundingRect.top) * scaleY;

    const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
    const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);

    universe.toggle_cell(row, col);

    drawGrid();
    drawCells();
});



drawGrid();
drawCells();
play();

Enter fullscreen mode Exit fullscreen mode

这是原文。index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
    <style>
        body {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #fps {
            white-space: pre;
            font-family: monospace;
        }
    </style>
</head>
<body>
<div id="fps"></div>
<p>Tick settings:</p>

<div>
    <input type="range" id="ticks" name="ticks"
           min="0" max="10">
    <label for="ticks">Ticks</label>
</div>
<button id="reset">Random reset</button>
<button id="death">Ultimate death</button>
<button id="play-pause"></button>
<canvas id="game-of-life-canvas"></canvas>

<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<script src="./bootstrap.js"></script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

内容index.js和主体index.html将以纯 Rust 语言编写。

Seed顺便一提,它使用的图案与 Elm 的图案类似。

我们现在需要做什么

  • 定义内容(即状态,也称为模型)
  • 定义视图
  • 定义我们如何更改内容(更新)
  • 定义初始化

那我们走吧!

模型

我们需要 :

  • 设置画布的宽度和高度
  • 细胞大小
  • 拥有宇宙
  • 知道我们是否参赛
  • 显示有关帧率的信息
  • 为 range 设置一个值(我省略了这一步,因为本教程中我采用了不太规范的方法,文末的链接展示了正确的方法)。
  • 为生者和死者赋予色彩
  • 为网格着色

我们一开始的重点是展示宇宙,包括暂停/播放和点击单元格的功能,所以先把fps&符号留range到后面再说吧 :)

lib.rs

// `Model` describes our app state.
pub struct Model {
    cell_size: u32,
    grid_color: String,
    dead_color: String,
    alive_color: String,
    universe: Universe,
    pause: bool,
    canvas_height: u32,
    canvas_width: u32,
}

Enter fullscreen mode Exit fullscreen mode

让我们开始吧

// `init` describes what should happen when your app started.
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {

    let universe = Universe::new();
    let cell_size = 5;
    let canvas_width = (cell_size + 1) * universe.width() + 1;
    let canvas_height = (cell_size + 1) * universe.height() + 1;

    Model {
        cell_size,
        grid_color: "#CCCCCC".to_string(),
        dead_color: "#FFFFFF".to_string(),
        alive_color: "#000000".to_string(),
        pause: false,
        universe,
        canvas_height,
        canvas_width,
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们列出应用中消息将要发生的事情!


// `Msg` describes the different events you can modify state with.
enum Msg {
    /// We need to play the game
    Play,
    /// We need to pause
    Pause,
    /// We need to draw stuff
    Draw,
    /// We need to destroy the universe
    Destroy,
    /// We need to generate a random Universe
    Random,
    /// We need to click on a cell
    CellClick
}
Enter fullscreen mode Exit fullscreen mode

编写消息/事件的这一步骤让我想起了我在 Redux/Ngrx 中编写 actions/action_type 的情景。

强迫自己思考你正在做的事情以及将会发生什么,这是一个很好的方法。

为了好玩,我们来写一个包含空匹配项的更新语句。我们稍后会详细讨论这个问题。

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {},
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick => {}
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们来编写视图

// `view` describes what to display.


// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
   section![
     button![
            id!("random"),
            ev(Ev::Click, |_| Msg::Random),
            "Random Reset"
        ],
        button![
            id!("destroy"),
            ev(Ev::Click, |_| Msg::Destroy),
            "Ultimate Death"
        ],
        button![
            id!("play-pause"),
            if model.pause {
                ev(Ev::Click, |_| Msg::Play)
            } else {
                ev(Ev::Click, |_| Msg::Pause)
            },
            if model.pause { "▶" } else { "⏸" }
        ],
        canvas![
            id!("game-of-life-canvas"),
            ev(Ev::Click, |event| {
                let mouse_event: web_sys::MouseEvent = event.unchecked_into();
                Msg::CellClick(mouse_event)
            })
        ],
    ]
}
Enter fullscreen mode Exit fullscreen mode

以下几点需要注意:

  • 作为 Jetbrain Clion 的用户,我找不到任何智能感知或强色彩显示功能Macros,所以在那里编写代码比较困难。

Visual Studio Code 的用户在这方面稍微幸运一些,因为其内置的智能感知和颜色功能可以正常工作 :D

  • 目前我们始终需要将 Seed 的代码封装在一个单独的节点中。我选择了一个部分。最终我们将有两个部分。

  • 我们可以在那里使用 Rust 代码,这真是太棒了。所以即使我的智能感知和代码颜色没有显示出来,所有这些东西都有我们可爱的编译器来支持。

现在刷新页面,你应该会看到类似这样的内容。

替代文字

现在更新函数里还有一些工作要做。

绘制我们的画布 ####

正如开头所说,本教程是一个从 JavaScript 到 Rust 的彻底转换。我尽量避免使用太奇特的技术 :D。

我们画图的时候,实际上既画的是单元格,也画的是网格。

lib.rs


// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {
            if model.pause {
            } else {
                model.universe.tick();
                draw_grid(model);
                draw_cells(model);           
            }
        },
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick(event) => {}
    }
}

fn draw_grid(model: &mut Model) {
// equivalent to js const canvas = document.getElementById("game-of-life-canvas");
// could be written let canvas document().get_element_by_id("game-of-life-canvas");
// I used a shortcut here which is fine.
// There is even a better way that I will show you later
    let canvas = canvas("game-of-life-canvas").unwrap(); 
    canvas.set_width(model.canvas_width);
    canvas.set_height(model.canvas_height);
    let ctx = seed::canvas_context_2d(&canvas);
    ctx.begin_path();
    ctx.set_stroke_style(&JsValue::from_str(model.grid_color.as_str()));

    // Vertical lines.
    for i in 0..model.universe.width() {
        ctx.move_to((i * (model.cell_size + 1) + 1).into(), 0.);
        ctx.line_to(
            (i * (model.cell_size + 1) + 1).into(),
            ((model.cell_size + 1) * model.universe.height() + 1).into(),
        );
    }
    // Horizontal lines.
    for j in 0..model.universe.height() {
        ctx.move_to(0., (j * (model.cell_size + 1) + 1).into());
        ctx.line_to(
            ((model.cell_size + 1) * model.universe.width() + 1).into(),
            (j * (model.cell_size + 1) + 1).into(),
        )
    }

    ctx.stroke();
}

fn draw_cells(model: &mut Model) {
    let canvas = canvas("game-of-life-canvas").unwrap();
    let ctx = seed::canvas_context_2d(&canvas);
    ctx.begin_path();

    // Alive cells.
    ctx.set_fill_style(&JsValue::from_str(model.alive_color.as_str()));
    for row in 0..model.universe.height() {
        for col in 0..model.universe.width() {
            let idx = model.universe.get_index(row, col);
            if model.universe.cell_at_index(idx) != Cell::Alive {
                continue;
            }

            ctx.fill_rect(
                (col * (model.cell_size + 1) + 1).into(),
                (row * (model.cell_size + 1) + 1).into(),
                (model.cell_size).into(),
                (model.cell_size).into(),
            );
        }
    }

    // Dead cells.
    ctx.set_fill_style(&JsValue::from_str(model.dead_color.as_str()));
    for row in 0..model.universe.height() {
        for col in 0..model.universe.width() {
            let idx = model.universe.get_index(row, col);
            if model.universe.cell_at_index(idx) != Cell::Dead {
                continue;
            }

            ctx.fill_rect(
                (col * (model.cell_size + 1) + 1).into(),
                (row * (model.cell_size + 1) + 1).into(),
                (model.cell_size).into(),
                (model.cell_size).into(),
            );
        }
    }

    ctx.stroke();
}
Enter fullscreen mode Exit fullscreen mode

您需要在文件顶部更新导入内容use crate::universe::{Universe, Cell};

我只是偷懒没多想就把代码转换了一下。
正如注释里解释的,从 DOM 中获取 canvas 元素的方法有很多种。
稍后我会展示正确的方法。

注意:等等,那里有些奇怪的东西:

  • 理论上我们会绘制这些东西,但前提是我们收到消息。Msg::Draw
  • 在哪里animationId = requestAnimationFrame(renderLoop);?循环已经不存在了。

呵呵,从技术上讲,我们可以使用它request_animation_frame,只要你使用 Rust 风格的 snake_case 命名方式,你的智能感知就能找到你在 Javascript 中拥有的所有 API。

但你也会发现它已经被弃用了。即使我懒,我也不想使用已弃用的东西。

一些标准方法会产生一些副作用。Seed 一直在开发新的工具来适应这些副作用,并赋予我们更大的控制权。

让我们来介绍一下订单。

在我看来,这像是响应式编程。在我们的例子中,我们可以用它来对消息进行排队。

  • 初始化时开始绘制
  • 继续画

lib.rs


fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
    orders.after_next_render(|_| Msg::Draw);

//... the other stuff from before
}

Enter fullscreen mode Exit fullscreen mode

lib.rs


fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {
            if model.pause {
            } else {
                model.universe.tick();
                draw_grid(model);
                draw_cells(model);
                orders.after_next_render(|_| Msg::Draw);
            }
        },
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick(event) => {}
    }
}
Enter fullscreen mode Exit fullscreen mode

两种情况下,我们都只是按照代码执行操作。简单易行。

刷新页面(热刷新功能即将推出),您应该就能看到画面开始移动了。

我们需要一些 CSS 代码,可以将其放在 index.html 文件中。<head> INSERT THERE </head>


    <style>
        section {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
    </style>
Enter fullscreen mode Exit fullscreen mode

我们需要定位到该部分,因为我们在视图中使用了它,瞧!

替代文字

细胞应该在移动 :D

注:

  • 底部和右侧的边缘似乎被磨损了一些(可能是因为我正在进行格式转换)。
  • 这个网格看起来和原图有点不一样(我想原因和上面一样)。

我是Rust新手,所以写代码的时候比较粗糙。我的代码生成的值不准确。

在继续之前,我们先把绘图函数移到一个文件中,命名为 draw.rs。

这次没有代码可以展示,导入和其他相关问题你自己解决吧 :P

销毁并生成随机数

只需要在更新函数中调用universe::death()&universe::random()符号即可!

播放和暂停

你可以自己做。

记得msg::Draw在游戏时间再发一条消息哦 :P

点击单元格

我们依然偷懒,直接把旧的JS代码转换成Rust代码。


/// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::Draw => draw::draw(model, orders),
        Msg::Play => {
            model.pause = false;
            orders.after_next_render(|_| Msg::Draw);
        }
        Msg::Pause => model.pause = true,
        Msg::Destroy => model.universe = Universe::death(),
        Msg::Random => model.universe = Universe::random(),
        Msg::CellClick(event) => {
            let canvas = canvas("game-of-life-canvas").unwrap();
            let bounding_rect = canvas.get_bounding_client_rect();

            let scale_x: f64 = f64::from(canvas.width()) / bounding_rect.width();
            let scale_y: f64 = f64::from(canvas.height()) / bounding_rect.height();

            let canvas_left: f64 = (f64::from(event.client_x()) - bounding_rect.left()) * scale_x;
            let canvas_top: f64 = (f64::from(event.client_y()) - bounding_rect.top()) * scale_y;

            let row_pos: f64 = (canvas_top / f64::from(model.cell_size + 1)).floor();
            let col_pos: f64 = (canvas_left / f64::from(model.cell_size + 1)).floor();

            let row: u32 = cmp::min(row_pos as u32, model.universe.height() - 1);
            let col: u32 = cmp::min(col_pos as u32, model.universe.width() - 1);

            model.universe.toggle_cell(row, col);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

现在你可以尝试修复导入问题,但最终会遇到错误。

error[E0599]: no method named `get_bounding_client_rect` found for struct `seed::prelude::web_sys::HtmlCanvasElement` in the current scope
  --> src/lib.rs:78:40
   |
78 |             let bounding_rect = canvas.get_bounding_client_rect();
   |                                        ^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `seed::prelude::web_sys::HtmlCanvasElement

Enter fullscreen mode Exit fullscreen mode

嗯……那里发生了什么事……?

我来告诉你为什么 :D

请记住,Rust 的设计目标是高性能,因此在这种情况下,它可以让你选择要编译使用的内容。

这样一来,你的二进制文件更小,你的 IDE 搜索内容也更快,你的 Web 浏览器不会因为成千上万个软件包/文件(参见 node_modules)而耗尽你的 PC 内存,关于这种策略还有很多其他好处。

Cargo.toml

#stuff from before still there
[dependencies.web-sys]
version = "0.3"
features=[ "DomRect", "Element"]
Enter fullscreen mode Exit fullscreen mode

现在可以了。程序cargo make watch应该已经为你重新编译好了 :) 你可以点击单元格了。

让我们把点击单元格的代码移到draw.rs

我将 draw.rs 重命名为 canvas.rs。

我让你在那里做必要的修改 :P

关于进口,我有两种选择:cmp

  • use seed::prelude::wasm_bindgen::__rt::std::cmp;
  • use std::cmp;

我真的不知道它们之间的区别。我需要问问其他更有经验的Seed人。我们是园丁吗?我有很多没用的笑话 :( 。

选择每帧的刻度数。

好的,根据原教程,我已经实现了选择每帧渲染的刻度数的范围。

我们先从视图开始,因为它比较容易转换(也许吧)。
我需要参考Seed 代码库里的一些例子,因为对我来说,没有智能感知,宏操作并不容易。

lib.rs

// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
    section![
        p!["Ticks settings :"],
        div![
            input![
                id!("ticks"),
                1,
                attrs! {
                    At::Name => "ticks",
                    At::Type => "range",
                    At::Min =>"1",
                    At::Max =>"10"
                }
            ],
            label![attrs! { At::For => "ticks"}, "ticks"]
        ],
        button![
            id!("random"),
            ev(Ev::Click, |_| Msg::Random),
            "Random Reset"
        ],
        button![
            id!("destroy"),
            ev(Ev::Click, |_| Msg::Destroy),
            "Ultimate Death"
        ],
        button![
            id!("play-pause"),
            if model.pause {
                ev(Ev::Click, |_| Msg::Play)
            } else {
                ev(Ev::Click, |_| Msg::Pause)
            },
            if model.pause { "▶" } else { "⏸" }
        ],
        canvas![
            id!("game-of-life-canvas"),
            ev(Ev::Click, |event| {
                let mouse_event: web_sys::MouseEvent = event.unchecked_into();
                Msg::CellClick(mouse_event)
            })
        ],
    ]
}

Enter fullscreen mode Exit fullscreen mode

处理范围和其中状态还有更好的方法。稍后我会展示推荐的方法。现在我们先用简单粗暴的方法。

lib.rs

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {
            model.pause = false;
            orders.after_next_render(|_| Msg::Draw);
        }
        Msg::Pause => model.pause = true,
        Msg::Draw => {
            if model.pause {
            } else {
                let tick_input = document().get_element_by_id("ticks").unwrap();
                let tick_frequency = get_value(tick_input.as_ref()).unwrap();
                let tick_number = tick_frequency.parse::<u32>().unwrap();

                for i in 0..tick_number {
                    model.universe.tick();
                }
                canvas::draw_grid(model);
                canvas::draw_cells(model);
                orders.after_next_render(|_| Msg::Draw);
            }
        }
        Msg::Destroy => {
            model.universe = Universe::death();
        }
        Msg::Random => {
            model.universe = Universe::random();
        }
        Msg::CellClick(event) => {
            let position = canvas::find_cell_from_click(model, event);

            model.universe.toggle_cell(position.0, position.1);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

由于 Rust 的缘故,提取值所需的部分不像在 JavaScript 中那么小:

  • 没有继承权
  • 需要解包,因为我们有结果,而且有可能处理错误。
  • 需要将字符串转换为 u32 类型

即使我们在这方面还有一些工作要做,但我们也拥有更大的控制权。
请记住,我这种写法并非最佳或推荐方式。我们还有很大的改进空间。

你可以更改范围,看看效果如何。就我而言,我觉得种子人生游戏在标准模式下表现更好(或者说,如果打开多个标签页,效果会略好一些),至少比旧版游戏build好。--debug

显示帧率

现在让我们像原教程中那样显示每秒帧数。

div![id!["fps"]],fn view()正文之前添加即可p!["Ticks settings :"],

现在,在原始的 Javascript 文件中,我们有Class类似这样的代码。

const fps = new class {
    constructor() {
        this.fps = document.getElementById("fps");
        this.frames = [];
        this.lastFrameTimeStamp = performance.now();
    }

    render() {
        // Convert the delta time since the last frame render into a measure
        // of frames per second.
        const now = performance.now();
        const delta = now - this.lastFrameTimeStamp;
        this.lastFrameTimeStamp = now;
        const fps = 1 / delta * 1000;

        // Save only the latest 100 timings.
        this.frames.push(fps);
        if (this.frames.length > 100) {
            this.frames.shift();
        }

        // Find the max, min, and mean of our 100 latest timings.
        let min = Infinity;
        let max = -Infinity;
        let sum = 0;
        for (let i = 0; i < this.frames.length; i++) {
            sum += this.frames[i];
            min = Math.min(this.frames[i], min);
            max = Math.max(this.frames[i], max);
        }
        let mean = sum / this.frames.length;

        // Render the statistics.
        this.fps.textContent = `
Frames per Second:
         latest = ${Math.round(fps)}
avg of last 100 = ${Math.round(mean)}
min of last 100 = ${Math.round(min)}
max of last 100 = ${Math.round(max)}
`.trim();
    }
};

Enter fullscreen mode Exit fullscreen mode

index.js该变量实际上只在in中使用过一次。renderLoop()

const renderLoop = () => {
    fps.render();
    let ticks = document.getElementById("ticks").value;
    for (let i = 0; i <  ticks ; i++) {
        universe.tick();
    }
    drawGrid();
    drawCells();

 animationId = requestAnimationFrame(renderLoop);
};

Enter fullscreen mode Exit fullscreen mode

那么我们也这样做。让我们在内部使用 fps Msg::Draw,并且Class不要使用 a,而是使用 astructimpl

编写fps.rs并完善一些更好的代码。

既然我们想要公开 fps lib.rs,那就尽量减少对 Web 浏览器 API 的调用,专注于计算。

我们可以修改render方法并调用它calculate来返回统计数据。

fps.rs

use seed::window;
use std::cmp;

pub struct FpsCounter {
    frames: Vec<f64>,
    last_frame_timestamp: f64,
}

impl FpsCounter {
    pub fn new() -> FpsCounter {
        FpsCounter {
            frames: Vec::new(),
            last_frame_timestamp: window().performance().unwrap().now(), //should have it outside so it would be  more beautiful
        }
    }
    /// Ex- Render function
    /// Same as original in JS , I moved most of call to DOM outside to make it "cleaner"
    /// Maybe I could also have passed time as an argument to make it even better
    /// If we removed calls to window() we could make unit test and benchmark    
        pub fn calculate(&mut self) -> FpsStatistic {
        let now = window().performance().unwrap().now();
        let delta = now - self.last_frame_timestamp;
        self.last_frame_timestamp = now;

        let fps = 1. / delta * 1000.;

        self.frames.push(fps);

        if self.frames.len() > 100 {
            self.frames.remove(0);
        }

        let mut min = i32::MAX;
        let mut max = i32::MIN;

        let mut sum: f64 = 0.;

        for i in 0..self.frames.len() {
            sum = sum + self.frames[i] as f64;

            min = cmp::min(self.frames[i] as i32, min);

            max = cmp::max(self.frames[i] as i32, max);
        }
        let mean = sum / self.frames.len() as f64;

        FpsStatistic {
            fps: fps as u32,
            mean: mean as u32,
            min,
            max,
        }
    }
}

pub struct FpsStatistic {
    pub fps: u32,
    pub mean: u32,
    pub min: i32,
    pub max: i32,
}

Enter fullscreen mode Exit fullscreen mode

为了随时可以访问,fps我们把它添加到状态中吧 :)

lib.rs


// `init` describes what should happen when your app started.
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
    orders.after_next_render(|_| Msg::Draw);

    let universe = Universe::new();
    let cell_size = 5;
    let canvas_width = (cell_size + 1) * universe.width() + 1;
    let canvas_height = (cell_size + 1) * universe.height() + 1;

    Model {
        cell_size,
        grid_color: "#CCCCCC".to_string(),
        dead_color: "#FFFFFF".to_string(),
        alive_color: "#000000".to_string(),
        pause: false,
        universe,
        canvas_height,
        canvas_width,
        fps:FpsCounter::new()
    }
}

// `Model` describes our app state.
pub struct Model {
    cell_size: u32,
    grid_color: String,
    dead_color: String,
    alive_color: String,
    universe: Universe,
    pause: bool,
    canvas_height: u32,
    canvas_width: u32,
    fps: FpsCounter,
}
Enter fullscreen mode Exit fullscreen mode

然后,我们可以在Msg::Draw获取滴答频率值的代码之前添加这段代码。

lib.rs

 let fps = document().get_element_by_id("fps").unwrap();
                let stats = model.fps.calculate();
                let text = format!(
                    "\
                Frames per Second:
         latest = {:?}
avg of last 100 = {:?}
min of last 100 = {:?}
max of last 100 = {:?}
                \
                ",
                    stats.fps, stats.mean, stats.min, stats.max
                );

                fps.set_text_content(Some(text.as_str()));

Enter fullscreen mode Exit fullscreen mode

我们需要一些 CSS,就用原教程里的吧,所以放在 `<div>`<style>标签里。index.html

      #fps {
            white-space: pre;
            font-family: monospace;
        }
Enter fullscreen mode Exit fullscreen mode

你现在应该收到一些好东西了吧 :)

替代文字

好,让我们根据现有信息,总结一下与原始源代码相比的优缺点:

好处 :

  • 一种语言,包罗万象 <3
  • 依赖项很少
  • 你很快就能提高工作效率。
  • 代码更容易拆分和组织 => 代码看起来更美观
  • 对正在发生的事情有更多控制权(更多类型/语法/工具可用/IDE支持)
  • 我们无需额外的库/逻辑即可免费获得状态管理:它已包含在内且易于使用!
  • 编译器会检查所有内容,包括宏/类似 html 的内容。
  • 没有 nodes_modules 和 JavaScript 依赖项
  • 当我打开浏览器开发者工具时,我的浏览器不会占用太多内存。
  • 根据需要选择要编译的内容Cargo.toml
  • 还有更多积极的评价 <3

成本

  • 你至少需要是小小Rustacean才能做一些事情,或者身边有人在旁边。
  • IDE 的智能感知功能并非在所有情况下都能正常工作(但在 JavaScript 中,它也会因为捕获的内容过多而导致 IDE 过载)。
  • 类型并非总是来自 web_sys stuff/Seed
  • 我在网页浏览器中看不到 package.wasm 的内容,但在原教程中可以看到。
  • 我不确定能否在运行时进行调试。
  • 请在评论中写下你感受到的痛苦,我会把它添加进去 :)
  • 我们的内部生成了更多 J 代码。package.js

在本系列的下一部分中,我们将了解如何检查性能并完成从 wasm+js 到 rust/seed 的迁移。

文章来源:https://dev.to/arnthelongbeard/how-to-only-rust-for-web-frontend-1026