使用 Rust 編寫更快的 React 元件

語言: CN / TW / HK

大家好,我是 ConardLi ,上週發了一篇 Wasm 的文章,主要分析的是今年 Google 開發者大會上的 Wasm 主題:

Wasm 為 Web 開發帶來無限可能

其實主要還是我個人對 Rust 比較感興趣,在今天的文章中,我將帶大家完成一個將 Rust 實際應用到 React 專案中的小 Demo

Wasm

在開始之前,我們還是先來回顧下 Wasm :

WebAssembly 是一種二進位制指令格式,簡稱為 Wsam ,它可以執行在適用於堆疊的虛擬機器上。

WebAssembly 存在的意義就是成為程式語言的可移植編譯目標,讓在 Web 上部署客戶端和服務端應用成為可能。

Wsam 具有緊湊的二進位制格式,可為我們提供近乎原生的網路效能。隨著它變得越來越流行,許多語言都編寫了編譯成 Web 程式集的繫結工具。

為什麼是 Rust

Rust 是一個快速、可靠二期又節約記憶體的程式語言。在過去六年的 stackoverflow 的最受喜愛的程式語言中,它一直蟬聯榜首的位置,主要還是這個語言本身擁有眾多的優點,比如:

  • 記憶體安全

  • 型別安全

  • 消除資料競爭

  • 使用前編譯

  • 建立(並且鼓勵)在零抽象之上

  • 最小的執行時(無停止世界的垃圾蒐集器,無 JIT 編譯器,無 VM
  • 低記憶體佔用(程式可以執行在資源受限的環境,比如小的微控制器)

  • 針對裸機(比如,寫一個 OS 核心或者裝置驅動,把 Rust 當一個 ‘高層’彙編器使用)”

另外, RustWebAssembly 領域的貢獻非常大的,使用 Rust 編寫 WebAssembly 非常簡單。

但是, Rust 存在的目的不是為了替代 JavaScript 而是和他形成互補,因為 Rust 語言的學習曲線是非常陡峭的,用它去完全替代 Web 開發幾乎是不可能的。

所以,我們一般會在 Web 開發的工具鏈,或者前端頁面中一些非常大量的資料計算中的操作用到它。

前置知識

在開始開發之前,你需要了解一些前置知識, React 相關的就不多說了,我們來看看 Rust 相關的幾個重要概念。

cargo

cargorust 的程式碼組織和包管理工具,你可以將它類比為 node.js 中的 npm

cargo 提供了一系列強大的功能,從專案的建立、構建到測試、執行直至部署,為 rust 專案的管理提供儘可能完整的手段。同時,它也與 rust 語言及其編譯器 rustc 本身的各種特性緊密結合。

rustup

rustupRust 的安裝和工具鏈管理工具,並且官網推薦使用 rustup 安裝 Rust

rustuprustc (rust編譯器) 和 cargo 等工具安裝在 Cargobin 目錄,但這些工具只是 Rust 工具鏈中元件的代理,真正工作的是工具鏈中的元件。通過 rustup 的命令可以指定使用不同版本的工具鏈。

wasm-bindgen

wasm-bindgen 提供了 JSRust 型別之間的橋樑,它允許 JS 使用字串呼叫 Rust API ,或者使用 Rust 函式來捕獲 JS 異常。

wasm-bindgen 的核心是促進 javascriptRust 之間使用 wasm 進行通訊。它允許開發者直接使用 Rust 的結構體、 javascript 的類、字串等型別,而不僅僅是 wasm 支援的整數或浮點數型別。

wasm-pack

wasm-packRust / Wasm 工作組開發維護,是現在最為活躍的 WebAssembly 應用開發工具。

wasm-pack 支援將程式碼打包成 npm 模組,並且附帶 Webpack 外掛( wasm-pack-plugin ),藉助它,我們可以輕鬆的將 Rust 與已有的 JavaScript 應用結合。

wasm32-unknown-unknown

通過 rustuptarget 命令可以指定編譯的目標平臺,也就是編譯後的程式在哪種作業系統上執行。

wasm-pack 使用 wasm32-unknown-unknown 目標編譯程式碼。

好了,瞭解了 Rust 相關的一些知識,我們一起來完成這個 Demo 吧。

一起來做個 Demo

在開始之前,要確保你的電腦上已經安裝了 NodeRust ,可以在命令列分別輸入 npmrustup 看看能否找到命令,如果沒安裝的話自己先安裝一下。

初始化一個簡單 React 程式

首先,我們來初始化一個 React 專案,命令列執行 npm init

然後,我們安裝一些開發專案必備的包:

$ npm i react react-dom
$ npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
$ npm i -D babel-core babel-loader @babel/preset-env @babel/preset-react

然後,我們在專案中建立一些常用的資料夾: srcpagepublicbuild 、和 dist

我們在 page 資料夾中建立一個 index.jsx ,編寫一些測試程式碼:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>code祕密花園 Hello, world!</h1>, document.getElementById('root'));

然後,我們為 babelwebpack 建立兩個配置檔案:

.babelrc

{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

const path = require('path');

module.exports = {
entry: './page/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[hash].js',
},
devServer: {
compress: true,
port: 8080,
hot: true,
static: './dist',
historyApiFallback: true,
open: true,
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: `${__dirname }/public/index.html`,
filename: 'index.html',
}),
],
mode: 'development',
devtool: 'inline-source-map',
};

然後,在 public 下建立一個 index.html

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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>code祕密花園</title>
</head>

<body>
<div id="root"></div>
</body>

</html>

下面檢查下你的 package.json ,看看和我的是不是一樣:

{
"name": "react-wasm",
"version": "1.0.0",
"description": "一個 Rust 編寫 React 元件的 Demo",
"main": "src/index.jsx",
"scripts": {
"dev": "webpack server"
},
"keywords": [],
"author": "ConardLi",
"license": "MIT",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"babel-loader": "^8.2.3",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.64.2",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.5.0"
}
}

下面,執行 npm install ,然後 npm run dev ,你就可以跑起來一個非常簡單的 React 應用:

引入 Rust

好了,下面我們來編寫我們的 Rust 元件(別忘了回顧下上面提到的 Rust 前置知識),首先我們使用 Rust 的包管理工具 cargo 來初始化一個簡單的 Rust 應用程式:

cargo init --lib .

執行完之後,會建立一個 Cargo.toml 和一個 src/lib.rc 檔案。

然後,我們在 Cargo.toml 中引入 wasm-bindgen 這個包,另外我們還需要告訴編譯器這個包是一個 cdylib

[package]
name = "react-wasm"
version = "1.0.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"

現在,你可以先嚐試執行下 cargo build

第一次執行可能會比較慢,可以 Google 搜一下怎麼將 cargo 配置為國內源。

好了,上面只是測試一下構建,它現在還派不上用場,我們下面還要執行一下編譯目標,執行:

$ rustup target add wasm32-unknown-unknown

指定好 wasm32-unknown-unknown 這個編譯目標,我們才能把它應用到我們的 React 程式中,下面我們給我們的 src/lib.rs 寫兩個簡單的函式:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}

#[wasm_bindgen]
pub fn big_computation() {
alert("這個是一個超級耗時的複雜計算邏輯");
}

#[wasm_bindgen]
pub fn welcome(name: &str) {
alert(&format!("Hi 我是 {} ,我在 code祕密花園 !", name));
}

為了確保我們的 Rust 應用程式正常工作,我們重新用 wasm32-unknown-unknown 編譯一下:

$ cargo build --target wasm32-unknown-unknown

然後我們安裝一下 wasm-bindgen-cli 這個命令列工具,以便我們能利用我們建立的 WebAssembly 程式碼:

$ cargo install -f wasm-bindgen-cli

安裝後,我們可以使用 Rust 生成的 WebAssembly 給我們的 React 程式碼建立一個包:

$ wasm-bindgen target/wasm32-unknown-unknown/debug/react_wasm.wasm --out-dir build

執行完成後,編譯好的 JavaScript 包和優化好的 Wasm 程式碼會儲存到我們的 build 目錄中,以供 React 程式使用。

在 React 程式中應用 Wasm

下面,我們嘗試一下在我們的 React 程式中用上這些 Wasm 程式碼,我們現在 package.json 中新增一些常用的 npm 指令碼:

  "build:wasm": "cargo build --target wasm32-unknown-unknown",
"build:bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/rusty_react.wasm --out-dir build",
"build": "npm run build:wasm && npm run build:bindgen && npx webpack",

然後我們執行 npm run build 就可以打包所有程式碼啦。

下面,我們還需要安裝一下上面我們提到的 wasm-packWebpack 外掛,它可以幫助我們把 Wasm 程式碼打包成 NPM 模組:

npm i -D @wasm-tool/wasm-pack-plugin

最後更新一下我們的 webpack.config.js ,新增下面的配置:

const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

...

plugins: [
...
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
...
experiments: {
asyncWebAssembly: true
}

下面,執行一下這幾個命令: npm run build:wasm、npm run build:bindgen、npm run build ,應該都不會報錯。

最後,我們在我們的 React 元件中呼叫一下我們剛剛生成的 Wasm 模組:

import React, { useState } from "react";
import ReactDOM from "react-dom";

const wasm = import("../build/rusty_react");

wasm.then(m => {
const App = () => {
const [name, setName] = useState("");
const handleChange = (e) => {
setName(e.target.value);
}
const handleClick = () => {
m.welcome(name);
}

return (
<>
<div>
<h1>Hi there</h1>
<button onClick={m.big_computation}>Run Computation</button>
</div>
<div>
<input type="text" onChange={handleChange} />
<button onClick={handleClick}>Say hello!</button>
</div>
</>

);
};

ReactDOM.render(<App />, document.getElementById("root"));
});

下面,你就可以在 React 元件中愉快的使用 Rust 了!

參考

  • https://www.rust-lang.org/learn

  • https://rustwasm.github.io/

  • https://www.joshfinnie.com/blog/using-webassembly-created-in-rust-for-fast-react-components/

最後

遷移公眾號後之前的星標就丟失啦,而且之前的一些長讀記錄也會丟失,所以你可能會經常收不到我公眾號的訊息推送,大家可以點 公眾號右上角的更多(...)— 設為星標

抖音前端正急缺人才,如果你想加入我們,歡迎加我微信和我聯絡。另外如果你想加入高質量前端交流群,或者你有任何其他事情想和我交流也可以新增我的個人微信 ConardLi 。

文中如有錯誤,歡迎在後臺和我留言,如果這篇文章幫助到了你,歡迎點贊、在看和關注。你的點贊、在看和關注是對我最大的支援!

創作不易,你的每一個 點贊、在看、分享、留言 都是對我最大的支援!:heart: