Rust 是 2010 年起源於 Mozilla Research 的一種編程語言。現在,全部大公司都在使用它。css
亞馬遜和微軟都承認它是其系統中 C / C ++的最佳替代品,可是 Rust 並不止於此。像 Figma 和 Discord 這樣的公司如今也經過在客戶端應用中使用 Rust 來引領潮流。html
本篇 Rust 教程旨在簡要介紹 Rust,如何在瀏覽器中使用它,以及什麼時候應該考慮使用它。我將從比較 Rust 和 JavaScript 開始,而後引導你完成 Rust 在瀏覽器中運行的步驟。最後,我將介紹一個使用 Rust 和 JavaScript 的 COVID simulator web 應用程序的快速性能評估。node
Rust 在概念上與 JavaScript 很是不一樣。但也有類似之處須要指出,讓咱們來看看問題的兩面。webpack
這兩種語言都有一個現代化的包管理系統。JavaScript 有 npm,Rust 有 Cargo。Rust 有 Cargo.toml
來代替 package.json
進行依賴管理。要建立一個新的項目,使用 cargo init
,要運行它,使用 cargo run
。不太陌生吧?c++
Rust 中有許多很酷的功能,你已經從 JavaScript 中知道了,只是語法略有不一樣。利用這個常見的 JavaScript 模式,對數組中的每一個元素都應用一個閉包:git
let staff = [ { name: "George", money: 0 }, { name: "Lea", money: 500000 }, ]; let salary = 1000; staff.forEach((employee) => { employee.money += salary; });
在 Rust 中,咱們能夠這樣寫:github
let salary = 1000; staff.iter_mut().for_each( |employee| { employee.money += salary; } );
誠然,習慣這種語法須要時間,用管子( |
)代替括號。但在克服了最初的尷尬以後,我發現它比另外一組括號讀起來更清晰。web
再舉一個例子,這是 JavaScript 中的對象解構:npm
let point = { x: 5, y: 10 }; let { x, y } = point;
一樣在 Rust 中:編程
let point = Point { x: 5, y: 10 }; let Point { x, y } = point;
主要的區別是,在 Rust 中咱們必須指定類型(Point
),更廣泛的是,Rust 須要在編譯時知道全部類型。但與大多數其餘編譯語言不一樣的是,編譯器儘量本身推斷類型。
爲了進一步解釋這個問題,下面是在 C++和許多其餘語言中有效的代碼。每一個變量都須要明確的類型聲明。
int a = 5; float b = 0.5; float c = 1.5 * a;
在 JavaScript 以及 Rust 中,這段代碼是有效的:
let a = 5; let b = 0.5; let c = 1.5 * a;
共享功能不勝枚舉:
async
+ await
語法。let array = [1,2,3]
同樣簡單地建立。我能夠繼續列舉下去,但我想個人觀點如今已經很清楚了。Rust 有一系列豐富的功能,這些功能在現代 JavaScript 中也有使用。
Rust 是一種編譯語言,這意味着沒有運行時能夠執行 Rust 代碼。一個應用程序只能在編譯器(rustc
)完成它的魔法以後運行。這種方法的好處一般是更好的性能。
幸運的是,Cargo 爲咱們解決了調用編譯器的問題。而有了 webpack,咱們還能夠將 Cargo
隱藏在 npm run build
後面。有了這個指南,只要爲項目設置好 Rust,就能夠保留 Web 開發者的正常工做流程。
Rust 是一種強類型語言,這意味着在編譯時全部類型必須匹配。例如,你不能調用一個參數類型錯誤或參數數量錯誤的函數。編譯器會在你運行時遇到這個錯誤以前爲你捕捉到它。顯而易見的比較是 TypeScript,若是你喜歡 TypeScript,那麼你極可能會喜歡 Rust。
但別擔憂:若是你不喜歡 TypeScript,Rust 可能仍是適合你。Rust 是近幾年從頭開始構建的,它考慮到了過去幾十年來人類在編程語言設計方面所學到的一切。其結果是一種使人耳目一新的簡潔語言。
Rust 中的模式匹配是我最喜歡的一個特徵,其餘語言有 switch
和 case
來避免像這樣的長鏈:
if (x == 1) { // ... } else if (x == 2) { // ... } else if (x == 3 || x == 4) { // ... } // ...
Rust 使用了以下更優雅的匹配項:
match x { 1 => { /* Do something if x == 1 */}, 2 => { /* Do something if x == 2 */}, 3 | 4 => { /* Do something if x == 3 || x == 4 */}, 5...10 => { /* Do something if x >= 5 && x <= 10 */}, _ => { /* Catch all other cases */ } }
我認爲這是很是整潔的,我但願 JavaScript 開發人員也能欣賞這種語法擴展。
不幸的是,咱們還得談談 Rust 的黑暗面。直言不諱地說,使用嚴格的類型系統有時會讓人感受很是繁瑣。若是你認爲 C++或 Java 的類型系統很嚴格,那麼請準備好迎接 Rust 的艱難之旅吧。
就我我的而言,我很喜歡 Rust 這部分。我依賴於嚴格的類型系統,所以能夠關閉大腦的一部分——每當我發現本身在編寫 JavaScript 時,大腦的一部分就會劇烈地興奮起來。可是我知道對於初學者來講,老是和編譯器做對是很煩人的。咱們將在稍後的 Rust 教程中看到一些。
如今,讓咱們用 Rust 在瀏覽器中運行一個 hello world
,咱們首先要確保全部必要的工具都已安裝。
使用 rustup 安裝 Cargo + rustc。 Rustup 是推薦的安裝 Rust 的方法,它將安裝最新的穩定版 Rust 的編譯器(rustc)和包管理器(Cargo)。它將安裝 Rust 最新穩定版本的編譯器(rustc)和包管理器(Cargo)。它還能夠管理 beta 版和每夜構建版,但對於本例來講,這不是必需的。
cargo --version
來檢查安裝狀況,你應該能夠看到 cargo 1.48.0 (65cbdd2dc 2020-10-14)
這樣的內容。rustup --version
應該產生 rustup 1.23.0(00924c9ba 2020-11-27)
。安裝 wasm-pack。 這是爲了將編譯器與 npm 集成。
wasm-pack --version
來檢查安裝,這應該爲您提供 wasm-pack 0.9.1
之類的東西。咱們還須要 Node 和 npm。咱們有一篇完整的文章解釋了安裝這兩個的最佳方法。
如今一切都安裝好了,讓咱們來建立項目。最終的代碼也能夠在這個GitHub 倉庫中找到。咱們從一個能夠編譯成 npm 包的 Rust 項目開始,以後會有導入該包的 JavaScript 代碼。
要建立一個名爲 hello-world
的 Rust 項目,請使用 cargo init --lib hello-world
。這將建立一個新目錄並生成 Rust 庫所需的全部文件:
├──hello-world ├── Cargo.toml ├── src ├── lib.rs
Rust 代碼將放在 lib.rs
中,在此以前咱們必須調整 Cargo.toml
。它使用 TOML 定義了依賴關係和其餘包的信息。若是想在瀏覽器中看到 hello world,請在 Cargo.toml
中的某個地方添加如下行數(例如,在文件的最後)。
[lib] crate-type = ["cdylib"]
這告訴編譯器在 C 兼容模式下建立一個庫。顯然咱們在咱們的例子中沒有使用 C。C-compatible 只是意味着不是 Rust 專用的,這是咱們使用 JavaScript 中的庫所須要的。
咱們還須要兩個外部庫,將它們做爲單獨的一行添加到依賴關係部分。
[dependencies] wasm-bindgen = "0.2.68" web-sys = {version = "0.3.45", features = ["console"]}
這些都是來自 crates.io 的依賴項,它是 Cargo 使用的默認包倉庫。
wasm-bindgen是必要的,以建立一個咱們之後能夠從 JavaScript 中調用的入口點。(你能夠在這裏找到完整的文檔。)值 」0.2.68"
指定了版本。
web-sys包含了全部 Web API 的 Rust 綁定,它將使咱們可以訪問瀏覽器控制檯。請注意,咱們必須明確地選擇控制檯功能,咱們最終的二進制文件將只包含這樣選擇的 Web API 綁定。
接下來是 lib.rs
內部的實際代碼。自動生成的單元測試能夠刪除。只需使用如下代碼替換文件的內容:
use wasm_bindgen::prelude::*; use web_sys::console; #[wasm_bindgen] pub fn hello_world() { console::log_1("Hello world"); }
頂部的 use
語句是用於從其餘模塊導入項目。這與 JavaScript 中的 import
相似)。
pub fn hello_world(){...}
聲明一個函數。 pub
修飾符是「public」的縮寫,做用相似於 JavaScript 中的 export
。註釋 #[wasm_bindgen]
特定於 Rust 編譯爲WebAssembly (Wasm)")")。咱們在這裏須要它來確保編譯器將包裝函數公開給 JavaScript。
在功能主體中,「Hello world」被打印到控制檯上。 Rust 中的 console :: log_1()
是對 console.log()
的調用的包裝。
你是否注意到函數調用中的 _1
後綴?這是由於 JavaScript 容許使用可變數量的參數,而 Rust 不容許。爲了解決這個問題, wasm_bindgen
爲每種參數數量生成一個函數。是的,這很快就會變得醜陋!但這有效。 在web-sys 文檔中提供了一個能夠在 Rust 控制檯中調用的完整函數列表。
如今咱們應該已經一切就緒,試着用下面的命令編譯它。這將下載全部的依賴項並編譯項目,第一次可能會花一些時間。
cd hello-world wasm-pack build
哈!Rust 編譯器對咱們不滿意。
error[E0308]: mismatched types --> src\lib.rs:6:20 | 6 | console::log_1("Hello world"); | ^^^^^^^^^^^^^ expected struct `JsValue`, found `str` | = note: expected reference `&JsValue` found reference `&'static str
注意:若是您看到其餘錯誤(error: linking with cc failed: exit code: 1
)而且你使用的是 Linux,則說明缺乏交叉編譯依賴性。 sudo apt install gcc-multilib
應該能夠解決此問題。
正如我前面提到的,編譯器很嚴格。當它指望一個 JsValue
的引用做爲一個函數的參數時,它不會接受一個靜態字符串。爲了知足編譯器的要求,必須進行顯式轉換。
console::log_1(&"Hello world".into());
方法 into()")") 將一個值轉換爲另外一個值。Rust 編譯器很聰明,它能夠推遲哪些類型參與轉換,由於函數簽名只留下了一種可能性。在這種狀況下,它將轉換爲 JsValue
,這是一個由 JavaScript 管理的值的包裝類型。而後,咱們還得加上 &
,經過引用而不是經過值來傳遞,不然編譯器又會抱怨。
嘗試再次運行 wasm-pack build
,若是一切順利,則最後一行應以下所示:
[INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.
若是你能走到這一步,你如今就能夠手動編譯 Rust 了。下一步,咱們將把它與 npm 和 webpack 集成,後者將自動爲咱們完成這項工做。
在這個例子中,我決定將 package.json
放在 hello-world
目錄內。咱們也能夠爲 Rust 項目和 JavaScript 項目使用不一樣的目錄,這是個口味問題。
如下是個人 package.json
文件。遵循的最簡單方法是將其複製並運行 npm install
,或者運行 npm init
並僅複製 dev
依賴項:
{ "name": "hello-world", "version": "1.0.0", "description": "Hello world app for Rust in the browser.", "main": "index.js", "scripts": { "build": "webpack", "serve": "webpack serve" }, "author": "Jakob Meier <inbox@jakobmeier.ch>", "license": "(MIT OR Apache-2.0)", "devDependencies": { "@wasm-tool/wasm-pack-plugin": "~1.3.1", "@webpack-cli/serve": "^1.1.0", "css-loader": "^5.0.1", "style-loader": "^2.0.0", "webpack": "~5.8.0", "webpack-cli": "~4.2.0", "webpack-dev-server": "~3.11.0" } }
如你所見,咱們使用的是 webpack 5。Wasm-pack 也能夠和舊版本的 webpack 一塊兒使用,甚至能夠不使用捆綁程序。但每一個設置的工做方式都有些不一樣,我建議你在跟隨這個 Rust 教程時使用徹底相同的版本。
另外一個重要的依賴項是 wasm-pack-plugin
。這是一個 Webpack 插件,專門用於加載使用 wasm-pack 構建的 Rust 軟件包。
繼續,咱們還須要建立 webpack.config.js
文件來配置 webpack。它應該是這樣的:
const path = require("path"); const webpack = require("webpack"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "index.js", }, plugins: [ new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "."), }), ], devServer: { contentBase: "./src", hot: true, }, module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"], }, ], }, experiments: { syncWebAssembly: true, }, };
全部的路徑都配置爲 Rust 代碼和 JavaScript 代碼並排。index.js
將在 src
文件夾中,緊挨着 lib.rs
。若是你喜歡不一樣的設置,能夠隨時調整這些。
你還會注意到,咱們使用webpack experiments,這是 webpack 5 引入的新選項。請確保將 syncWebAssembly
設置爲 true。
最後,咱們必須建立 JavaScript 入口點 src/index.js
:
import("../pkg") .catch((e) => console.error("Failed loading Wasm module:", e) ) .then((rust) => rust.hello_world());
咱們必須異步加載 Rust 模塊。調用 rust.hello_world()
會調用一個生成的封裝函數,而這個函數又會調用 lib.rs
中定義的 Rust 函數 hello_world
。
如今,運行 npm run serve
應該能夠編譯全部內容並啓動開發服務器。咱們沒有定義 HTML 文件,所以頁面上沒有任何顯示。你可能還必須手動轉到 http://localhost:8080/index
,由於http://localhost:8080
只是列出文件而不執行任何代碼。
打開空白頁後,打開開發人員控制檯。 Hello World 應該有一個日誌條目。
好吧,對於一個簡單的 hello world 來講,這是至關多的工做。但如今一切都到位了,咱們能夠輕鬆地擴展 Rust 代碼,而不用擔憂這些。保存對 lib.rs
的修改後,你應該會自動看到從新編譯和瀏覽器中的實時更新,就像 JavaScript 同樣。
Rust 不是 JavaScript 的通常替代品。它只能經過 Wasm 在瀏覽器中運行,這在很大程度上限制了它的做用。即便你能夠用 Rust 替換幾乎全部的 JavaScript 代碼,若是你真的想的話,那是一個壞主意,並且不是 Wasm 的目的。例如,Rust 並不適合與你網站的 UI 進行交互。
我認爲 Rust + Wasm 是一個額外的選項,能夠用來更有效地運行 CPU 重的工做負載。以較大的下載量爲代價,Wasm 避免了 JavaScript 代碼面臨的解析和編譯開銷。這一點,再加上編譯器的強力優化,可能會帶來更好的性能。這一般是公司爲特定項目選擇 Rust 的緣由。選擇 Rust 的另外一個緣由多是語言偏好,但這是一個徹底不一樣的討論,我不會在這裏討論。