針對(duì)JavaScript開(kāi)發(fā)人員的Rust簡(jiǎn)介
Rust是2010年起源于Mozilla Research的一種編程語(yǔ)言。如今,所有大公司都在使用它。
亞馬遜和微軟都認(rèn)可它是其系統(tǒng)中C / C ++的最佳替代品,但是Rust并不止于此。像Figma和Discord這樣的公司現(xiàn)在也通過(guò)在客戶(hù)端應(yīng)用中使用Rust來(lái)引領(lǐng)潮流。
本篇Rust教程旨在簡(jiǎn)要介紹Rust,如何在瀏覽器中使用它,以及何時(shí)應(yīng)該考慮使用它。我將從比較Rust和JavaScript開(kāi)始,然后引導(dǎo)你完成Rust在瀏覽器中運(yùn)行的步驟。最后,我將介紹一個(gè)使用Rust和JavaScript的COVID simulator web應(yīng)用程序的快速性能評(píng)估。
簡(jiǎn)而言之
Rust在概念上與JavaScript非常不同。但也有相似之處需要指出,讓我們來(lái)看看問(wèn)題的兩面。
相似之處
這兩種語(yǔ)言都有一個(gè)現(xiàn)代化的包管理系統(tǒng)。JavaScript有npm,Rust有Cargo。Rust有 Cargo.toml 來(lái)代替 package.json 進(jìn)行依賴(lài)管理。要?jiǎng)?chuàng)建一個(gè)新的項(xiàng)目,使用 cargo init,要運(yùn)行它,使用 cargo run。不太陌生吧?
Rust中有許多很酷的功能,你已經(jīng)從JavaScript中知道了,只是語(yǔ)法略有不同。利用這個(gè)常見(jiàn)的JavaScript模式,對(duì)數(shù)組中的每個(gè)元素都應(yīng)用一個(gè)閉包:
- let staff = [
- {name: "George", money: 0},
- {name: "Lea", money: 500000},
- ];
- let salary = 1000;
- staff.forEach( (employee) => { employee.money += salary; } );
在Rust中,我們可以這樣寫(xiě):
- let salary = 1000;
- staff.iter_mut().for_each(
- |employee| { employee.money += salary; }
- );
誠(chéng)然,習(xí)慣這種語(yǔ)法需要時(shí)間,用管子( | )代替括號(hào)。但在克服了最初的尷尬之后,我發(fā)現(xiàn)它比另一組括號(hào)讀起來(lái)更清晰。
再舉一個(gè)例子,這是JavaScript中的對(duì)象解構(gòu):
- let point = { x: 5, y: 10 };
- let {x,y} = point;
同樣在Rust中:
- let point = Point { x: 5, y: 10 };
- let Point { x, y } = point;
主要的區(qū)別是,在Rust中我們必須指定類(lèi)型(Point),更普遍的是,Rust需要在編譯時(shí)知道所有類(lèi)型。但與大多數(shù)其他編譯語(yǔ)言不同的是,編譯器盡可能自己推斷類(lèi)型。
為了進(jìn)一步解釋這個(gè)問(wèn)題,下面是在C++和許多其他語(yǔ)言中有效的代碼。每個(gè)變量都需要明確的類(lèi)型聲明。
- 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;
共享功能不勝枚舉:
- Rust具有 async + await 語(yǔ)法。
- 數(shù)組可以像讓 let array = [1,2,3] 一樣簡(jiǎn)單地創(chuàng)建。
- 代碼按模塊組織,有明確的導(dǎo)入和導(dǎo)出。
- 字符串是用Unicode編碼的,處理特殊字符沒(méi)有問(wèn)題。
我可以繼續(xù)列舉下去,但我想我的觀點(diǎn)現(xiàn)在已經(jīng)很清楚了。Rust有一系列豐富的功能,這些功能在現(xiàn)代JavaScript中也有使用。
不同點(diǎn)
Rust是一種編譯語(yǔ)言,這意味著沒(méi)有運(yùn)行時(shí)可以執(zhí)行Rust代碼。一個(gè)應(yīng)用程序只能在編譯器(rustc)完成它的魔法之后運(yùn)行。這種方法的好處通常是更好的性能。
幸運(yùn)的是,Cargo為我們解決了調(diào)用編譯器的問(wèn)題。而有了webpack,我們還可以將 Cargo 隱藏在 npm run build 后面。有了這個(gè)指南,只要為項(xiàng)目設(shè)置好Rust,就可以保留Web開(kāi)發(fā)者的正常工作流程。
Rust 是一種強(qiáng)類(lèi)型語(yǔ)言,這意味著在編譯時(shí)所有類(lèi)型必須匹配。例如,你不能調(diào)用一個(gè)參數(shù)類(lèi)型錯(cuò)誤或參數(shù)數(shù)量錯(cuò)誤的函數(shù)。編譯器會(huì)在你運(yùn)行時(shí)遇到這個(gè)錯(cuò)誤之前為你捕捉到它。顯而易見(jiàn)的比較是TypeScript,如果你喜歡TypeScript,那么你很可能會(huì)喜歡Rust。
但別擔(dān)心:如果你不喜歡TypeScript,Rust可能還是適合你。Rust 是近幾年從頭開(kāi)始構(gòu)建的,它考慮到了過(guò)去幾十年來(lái)人類(lèi)在編程語(yǔ)言設(shè)計(jì)方面所學(xué)到的一切。其結(jié)果是一種令人耳目一新的簡(jiǎn)潔語(yǔ)言。
Rust中的模式匹配是我最喜歡的一個(gè)特征,其他語(yǔ)言有 switch 和 case 來(lái)避免像這樣的長(zhǎng)鏈:
- if ( x == 1) {
- // ...
- } else if ( x == 2 ) {
- // ...
- }
- else if ( x == 3 || x == 4 ) {
- // ...
- } // ...
Rust使用了如下更優(yōu)雅的匹配項(xiàng):
- 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 */ }
- }
我認(rèn)為這是非常整潔的,我希望JavaScript開(kāi)發(fā)人員也能欣賞這種語(yǔ)法擴(kuò)展。
不幸的是,我們還得談?wù)凴ust的黑暗面。直言不諱地說(shuō),使用嚴(yán)格的類(lèi)型系統(tǒng)有時(shí)會(huì)讓人感覺(jué)非常繁瑣。如果你認(rèn)為C++或Java的類(lèi)型系統(tǒng)很?chē)?yán)格,那么請(qǐng)準(zhǔn)備好迎接Rust的艱難之旅吧。
就我個(gè)人而言,我很喜歡Rust這部分。我依賴(lài)于嚴(yán)格的類(lèi)型系統(tǒng),因此可以關(guān)閉大腦的一部分——每當(dāng)我發(fā)現(xiàn)自己在編寫(xiě)JavaScript時(shí),大腦的一部分就會(huì)劇烈地興奮起來(lái)。但是我知道對(duì)于初學(xué)者來(lái)說(shuō),總是和編譯器作對(duì)是很煩人的。我們將在稍后的Rust教程中看到一些。
Hello Rust
現(xiàn)在,讓我們用Rust在瀏覽器中運(yùn)行一個(gè) hello world ,我們首先要確保所有必要的工具都已安裝。
工具
使用rustup安裝Cargo + rustc。 Rustup是推薦的安裝Rust的方法,它將安裝最新的穩(wěn)定版Rust的編譯器(rustc)和包管理器(Cargo)。它將安裝Rust最新穩(wěn)定版本的編譯器(rustc)和包管理器(Cargo)。它還可以管理beta版和每夜構(gòu)建版,但對(duì)于本例來(lái)說(shuō),這不是必需的。
- 在終端機(jī)上輸入 cargo --version 來(lái)檢查安裝情況,你應(yīng)該可以看到 cargo 1.48.0 (65cbdd2dc 2020-10-14) 這樣的內(nèi)容。
- 還要檢查Rustup:rustup --version 應(yīng)該產(chǎn)生 rustup 1.23.0(00924c9ba 2020-11-27)。
安裝wasm-pack。 這是為了將編譯器與npm集成。
- 通過(guò)輸入 wasm-pack --version 來(lái)檢查安裝,這應(yīng)該為您提供 wasm-pack 0.9.1 之類(lèi)的東西。
我們還需要Node和npm。我們有一篇完整的文章[1]解釋了安裝這兩個(gè)的最佳方法。
編寫(xiě)Rust代碼
現(xiàn)在一切都安裝好了,讓我們來(lái)創(chuàng)建項(xiàng)目。最終的代碼也可以在這個(gè)GitHub倉(cāng)庫(kù)[2]中找到。我們從一個(gè)可以編譯成npm包的Rust項(xiàng)目開(kāi)始,之后會(huì)有導(dǎo)入該包的JavaScript代碼。
要?jiǎng)?chuàng)建一個(gè)名為 hello-world 的Rust項(xiàng)目,請(qǐng)使用 cargo init --lib hello-world。這將創(chuàng)建一個(gè)新目錄并生成Rust庫(kù)所需的所有文件:
- ├──hello-world
- ├── Cargo.toml
- ├── src
- ├── lib.rs
Rust代碼將放在 lib.rs 中,在此之前我們必須調(diào)整 Cargo.toml。它使用 TOML[3] 定義了依賴(lài)關(guān)系和其他包的信息。如果想在瀏覽器中看到hello world,請(qǐng)?jiān)?Cargo.toml 中的某個(gè)地方添加以下行數(shù)(例如,在文件的最后)。
- [lib]
- crate-type = ["cdylib"]
這告訴編譯器在C兼容模式下創(chuàng)建一個(gè)庫(kù)。顯然我們?cè)谖覀兊睦又袥](méi)有使用C。C-compatible只是意味著不是Rust專(zhuān)用的,這是我們使用JavaScript中的庫(kù)所需要的。
我們還需要兩個(gè)外部庫(kù),將它們作為單獨(dú)的一行添加到依賴(lài)關(guān)系部分。
- [dependencies]
- wasm-bindgen = "0.2.68"
- web-sys = {version = "0.3.45", features = ["console"]}
這些都是來(lái)自 crates.io[4] 的依賴(lài)項(xiàng),它是 Cargo 使用的默認(rèn)包倉(cāng)庫(kù)。
wasm-bindgen[5]是必要的,以創(chuàng)建一個(gè)我們以后可以從JavaScript中調(diào)用的入口點(diǎn)。(你可以在這里找到完整的文檔。)值 ”0.2.68" 指定了版本。
web-sys[6]包含了所有Web API的Rust綁定,它將使我們能夠訪(fǎng)問(wèn)瀏覽器控制臺(tái)。請(qǐng)注意,我們必須明確地選擇控制臺(tái)功能,我們最終的二進(jìn)制文件將只包含這樣選擇的Web API綁定。
接下來(lái)是 lib.rs 內(nèi)部的實(shí)際代碼。自動(dòng)生成的單元測(cè)試可以刪除。只需使用以下代碼替換文件的內(nèi)容:
- use wasm_bindgen::prelude::*;
- use web_sys::console;
- #[wasm_bindgen]
- pub fn hello_world() {
- console::log_1("Hello world");
- }
頂部的 use 語(yǔ)句是用于從其他模塊導(dǎo)入項(xiàng)目。這與JavaScript中的 import 類(lèi)似)。
pub fn hello_world(){...} 聲明一個(gè)函數(shù)。pub 修飾符是“public”的縮寫(xiě),作用類(lèi)似于JavaScript中的 export。注釋 #[wasm_bindgen] 特定于Rust編譯為[WebAssembly (Wasm)](https://webassembly.org/ "wasm_bindgen] 特定于Rust編譯為[WebAssembly (Wasm "wasm_bindgen] 特定于Rust編譯為[WebAssembly (Wasm)")")。我們?cè)谶@里需要它來(lái)確保編譯器將包裝函數(shù)公開(kāi)給JavaScript。
在功能主體中,“Hello world”被打印到控制臺(tái)上。Rust中的 console :: log_1() 是對(duì) console.log() 的調(diào)用的包裝。
你是否注意到函數(shù)調(diào)用中的 _1 后綴?這是因?yàn)镴avaScript允許使用可變數(shù)量的參數(shù),而Rust不允許。為了解決這個(gè)問(wèn)題, wasm_bindgen 為每種參數(shù)數(shù)量生成一個(gè)函數(shù)。是的,這很快就會(huì)變得丑陋!但這有效。在web-sys文檔[7]中提供了一個(gè)可以在Rust控制臺(tái)中調(diào)用的完整函數(shù)列表。
現(xiàn)在我們應(yīng)該已經(jīng)一切就緒,試著用下面的命令編譯它。這將下載所有的依賴(lài)項(xiàng)并編譯項(xiàng)目,第一次可能會(huì)花一些時(shí)間。
- cd hello-world
- wasm-pack build
哈!Rust編譯器對(duì)我們不滿(mǎn)意。
- 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
注意:如果您看到其他錯(cuò)誤(error: linking with cc failed: exit code: 1)并且你使用的是Linux,則說(shuō)明缺少交叉編譯依賴(lài)性。sudo apt install gcc-multilib 應(yīng)該可以解決此問(wèn)題。
正如我前面提到的,編譯器很?chē)?yán)格。當(dāng)它期望一個(gè) JsValue 的引用作為一個(gè)函數(shù)的參數(shù)時(shí),它不會(huì)接受一個(gè)靜態(tài)字符串。為了滿(mǎn)足編譯器的要求,必須進(jìn)行顯式轉(zhuǎn)換。
- console::log_1(&"Hello world".into());
方法 [into()](https://doc.rust-lang.org/std/convert/trait.Into.html "into( "into()")") 將一個(gè)值轉(zhuǎn)換為另一個(gè)值。Rust 編譯器很聰明,它可以推遲哪些類(lèi)型參與轉(zhuǎn)換,因?yàn)楹瘮?shù)簽名只留下了一種可能性。在這種情況下,它將轉(zhuǎn)換為 JsValue,這是一個(gè)由JavaScript管理的值的包裝類(lèi)型。然后,我們還得加上 &,通過(guò)引用而不是通過(guò)值來(lái)傳遞,否則編譯器又會(huì)抱怨。
嘗試再次運(yùn)行 wasm-pack build,如果一切順利,則最后一行應(yīng)如下所示:
- [INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.
如果你能走到這一步,你現(xiàn)在就可以手動(dòng)編譯Rust了。下一步,我們將把它與npm和webpack集成,后者將自動(dòng)為我們完成這項(xiàng)工作。
JavaScript整合
在這個(gè)例子中,我決定將 package.json 放在 hello-world 目錄內(nèi)。我們也可以為Rust項(xiàng)目和JavaScript項(xiàng)目使用不同的目錄,這是個(gè)口味問(wèn)題。
以下是我的 package.json 文件。遵循的最簡(jiǎn)單方法是將其復(fù)制并運(yùn)行 npm install,或者運(yùn)行 npm init 并僅復(fù)制 dev 依賴(lài)項(xiàng):
- {
- "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"
- }
- }
如你所見(jiàn),我們使用的是webpack 5。Wasm-pack也可以和舊版本的webpack一起使用,甚至可以不使用捆綁程序。但每個(gè)設(shè)置的工作方式都有些不同,我建議你在跟隨這個(gè)Rust教程時(shí)使用完全相同的版本。
另一個(gè)重要的依賴(lài)項(xiàng)是 wasm-pack-plugin。這是一個(gè)Webpack插件,專(zhuān)門(mén)用于加載使用wasm-pack構(gòu)建的Rust軟件包。
繼續(xù),我們還需要?jiǎng)?chuàng)建 webpack.config.js 文件來(lái)配置webpack。它應(yīng)該是這樣的:
- 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。如果你喜歡不同的設(shè)置,可以隨時(shí)調(diào)整這些。
你還會(huì)注意到,我們使用webpack experiments[8],這是webpack 5引入的新選項(xiàng)。請(qǐng)確保將 syncWebAssembly 設(shè)置為true。
最后,我們必須創(chuàng)建JavaScript入口點(diǎn) src/index.js:
- import("../pkg").catch(e => console.error("Failed loading Wasm module:", e)).then(
- rust =>
- rust.hello_world()
- );
我們必須異步加載Rust模塊。調(diào)用 rust.hello_world() 會(huì)調(diào)用一個(gè)生成的封裝函數(shù),而這個(gè)函數(shù)又會(huì)調(diào)用 lib.rs 中定義的Rust函數(shù) hello_world。
現(xiàn)在,運(yùn)行 npm run serve 應(yīng)該可以編譯所有內(nèi)容并啟動(dòng)開(kāi)發(fā)服務(wù)器。我們沒(méi)有定義HTML文件,因此頁(yè)面上沒(méi)有任何顯示。你可能還必須手動(dòng)轉(zhuǎn)到 http://localhost:8080/index,因?yàn)閔ttp://localhost:8080只是列出文件而不執(zhí)行任何代碼。
打開(kāi)空白頁(yè)后,打開(kāi)開(kāi)發(fā)人員控制臺(tái)。Hello World應(yīng)該有一個(gè)日志條目。
好吧,對(duì)于一個(gè)簡(jiǎn)單的hello world來(lái)說(shuō),這是相當(dāng)多的工作。但現(xiàn)在一切都到位了,我們可以輕松地?cái)U(kuò)展Rust代碼,而不用擔(dān)心這些。保存對(duì) lib.rs 的修改后,你應(yīng)該會(huì)自動(dòng)看到重新編譯和瀏覽器中的實(shí)時(shí)更新,就像JavaScript一樣。
何時(shí)使用Rust
Rust不是JavaScript的一般替代品。它只能通過(guò)Wasm在瀏覽器中運(yùn)行,這在很大程度上限制了它的作用。即使你可以用Rust替換幾乎所有的JavaScript代碼,如果你真的想的話(huà),那是一個(gè)壞主意,而且不是Wasm的目的。例如,Rust并不適合與你網(wǎng)站的UI進(jìn)行交互。
我認(rèn)為Rust + Wasm是一個(gè)額外的選項(xiàng),可以用來(lái)更有效地運(yùn)行CPU重的工作負(fù)載。以較大的下載量為代價(jià),Wasm避免了JavaScript代碼面臨的解析和編譯開(kāi)銷(xiāo)。這一點(diǎn),再加上編譯器的強(qiáng)力優(yōu)化,可能會(huì)帶來(lái)更好的性能。這通常是公司為特定項(xiàng)目選擇Rust的原因。選擇Rust的另一個(gè)原因可能是語(yǔ)言偏好,但這是一個(gè)完全不同的討論,我不會(huì)在這里討論。
參考資料
[1]文章: https://www.sitepoint.com/quick-tip-multiple-versions-node-nvm/
[2]GitHub倉(cāng)庫(kù): https://github.com/sitepoint-editors/rust-wasm-hello-world
[3]TOML: https://github.com/toml-lang/toml
[4]crates.io: https://crates.io/
[5]wasm-bindgen: https://crates.io/crates/wasm-bindgen
[6]web-sys: https://crates.io/crates/web-sys
[7]web-sys文檔: https://rustwasm.github.io/wasm-bindgen/api/web_sys/console/index.html
[8]webpack experiments: https://webpack.js.org/configuration/experiments/
本文轉(zhuǎn)載自微信公眾號(hào)「前端全棧開(kāi)發(fā)者」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端全棧開(kāi)發(fā)者公眾號(hào)。