如何仅使用 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 新手,而且技术也有些生疏(开个玩笑),所以我觉得对于一个新框架和新语言的初学者来说,几个小时已经相当不错了。
在本教程中,我将向你展示我最初制作的生命游戏和种子生命游戏之间的区别。
建议你在学习本教程之前先了解一些基础知识。
- 你已经了解了Rust。
- 您已阅读过有关 Javascript 的内容。
- 你已经了解了Wasm
- 如果你能先按照原版人生游戏教程操作一遍,那就太好了。
注:
- 我将向您展示一个非常简陋且直接的转换方法。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"
好的,这里有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", "--${@}"]
添加 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>
我们导入了一个生成的 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);
}
在你的 .gitignore 文件中
/target
Cargo.lock
/pkg
至少在 Linux 系统上,我需要这样做,否则/pkg每次编译后 Cargo 都会检测到更改并重新构建 :P 也许这是个小 bug 需要修复 :D
我们现在有:
seeded-game-of-life/
├── .gitignore
├── Cargo.toml
├── index.html
├── MakeFile.toml
└── src
├── lib.rs
然后
cargo make watch在一个终端中 -> 更改时编译
cargo make serve在另一个文件中 -> 提供文件
你应该有个漂亮的柜台。
除了测试和发布构建以及使用之外,无需进行其他设置。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(())
}
}
我们只保留关于宇宙的部分。我们将两种实现方式合并,并创建一个新文件。universe.rs
注意:我的原始 lib.rs 文件包含的内容比教程仓库中的内容更多,因为我完成了作者给出的一些不错的练习。
我已经实现了随机宇宙和终极死亡。
我没能成功地让随机宇宙在原版人生游戏中运行 :(
以下是一些生成随机内容所需的额外依赖项。请将它们添加[dependencies]到您的……Cargo.toml
rand = "0.7.3"
rand_core = "0.5.1"
我承认我超级懒。肯定有不用依赖就能手动完成的简单方法吧 :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(())
}
}
我们可以看到这方面有显著的改进:
- 无需使用
#[wasm_bindgen] - 无需使用
sys_web - 无需使用
log,Seed我们可以根据需要使用另一个。 - 无需使用
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();
这是原文。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>
内容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,
}
让我们开始吧
// `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,
}
}
让我们列出应用中消息将要发生的事情!
// `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
}
编写消息/事件的这一步骤让我想起了我在 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 => {}
}
}
让我们来编写视图
// `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)
})
],
]
}
以下几点需要注意:
- 作为 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();
}
您需要在文件顶部更新导入内容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
}
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) => {}
}
}
两种情况下,我们都只是按照代码执行操作。简单易行。
刷新页面(热刷新功能即将推出),您应该就能看到画面开始移动了。
我们需要一些 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>
我们需要定位到该部分,因为我们在视图中使用了它,瞧!
细胞应该在移动 :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);
}
}
}
现在你可以尝试修复导入问题,但最终会遇到错误。
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
嗯……那里发生了什么事……?
- 奇怪的是,编译器在 canvas 中找不到这个方法。
- 你在那里也看不到这个方法,https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlCanvasElement.html
我来告诉你为什么 :D
- 您可以在此处找到该方法:https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Element.html#method.get_bounding_client_rect,位于 Element 类中。
- Rust 不使用继承,因为这个概念存在太多问题,我想。我们
traits在 Rust 中使用了(剧透:它们很棒!!!) - 实际上,您需要激活 web_sys 的功能才能使用,正如我之前链接的页面上提到的那样。
请记住,Rust 的设计目标是高性能,因此在这种情况下,它可以让你选择要编译使用的内容。
这样一来,你的二进制文件更小,你的 IDE 搜索内容也更快,你的 Web 浏览器不会因为成千上万个软件包/文件(参见 node_modules)而耗尽你的 PC 内存,关于这种策略还有很多其他好处。
Cargo.toml
#stuff from before still there
[dependencies.web-sys]
version = "0.3"
features=[ "DomRect", "Element"]
现在可以了。程序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)
})
],
]
}
处理范围和其中状态还有更好的方法。稍后我会展示推荐的方法。现在我们先用简单粗暴的方法。
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);
}
}
}
由于 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();
}
};
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);
};
那么我们也这样做。让我们在内部使用 fps Msg::Draw,并且Class不要使用 a,而是使用 astruct和impl。
编写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,
}
为了随时可以访问,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,
}
然后,我们可以在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()));
我们需要一些 CSS,就用原教程里的吧,所以放在 `<div>`<style>标签里。index.html
#fps {
white-space: pre;
font-family: monospace;
}
你现在应该收到一些好东西了吧 :)
好,让我们根据现有信息,总结一下与原始源代码相比的优缺点:
好处 :
- 一种语言,包罗万象 <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


