使用 Rust 編寫更快的 React 組件


Wasm

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

 

 

WebAssembly 是一種二進制指令格式,簡稱為 Wasm,它可以運行在適用於堆棧的虛擬機上。

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

 

 

Wasm 具有緊湊的二進制格式,可為我們提供近乎原生的網絡性能。隨着它變得越來越流行,許多語言都編寫了編譯成 Web 程序集的綁定工具。

為什么是 Rust

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

  • 內存安全
  • 類型安全
  • 消除數據競爭
  • 使用前編譯
  • 建立(並且鼓勵)在零抽象之上
  • 最小的運行時(無停止世界的垃圾搜集器,無 JIT 編譯器,無 VM
  • 低內存占用(程序可以運行在資源受限的環境,比如小的微控制器)
  • 針對裸機(比如,寫一個 OS 內核或者設備驅動,把 Rust 當一個 ‘高層’匯編器使用)”

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

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

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

前置知識

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

cargo

 

 

cargo 是 rust 的代碼組織和包管理工具,你可以將它類比為 node.js 中的 npm

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

rustup

 

 

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

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

wasm-bindgen

 

 

wasm-bindgen 提供了 JS 和 Rust 類型之間的橋梁,它允許 JS 使用字符串調用 Rust API,或者使用 Rust 函數來捕獲 JS 異常。

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

wasm-pack

 

 

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

wasm-pack 支持將代碼打包成 npm 模塊,並且附帶 Webpack 插件(wasm-pack-plugin),借助它,我們可以輕松的將 Rust 與已有的 JavaScript 應用結合。

wasm32-unknown-unknown

通過 rustup 的 target 命令可以指定編譯的目標平台,也就是編譯后的程序在哪種操作系統上運行。

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

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

一起來做個 Demo

在開始之前,要確保你的電腦上已經安裝了 Node 和 Rust,可以在命令行分別輸入 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'));

然后,我們為 babel 和 webpack 創建兩個配置文件:

.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-pack 的 Webpack 插件,它可以幫助我們把 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/
  •  

轉自https://mp.weixin.qq.com/s/dknF5W-Quh_o4fqTlptCCA


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM