使用 Rust 編寫更快的 React 組件

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

Wasm

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

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

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

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

爲什麼是 Rust

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

另外,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,可以在命令行分別輸入 npm、rustup 看看能否找到命令,如果沒安裝的話自己先安裝一下。

初始化一個簡單 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

然後,我們在項目中創建一些常用的文件夾:src、page、public、build、和 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 >
    <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 了!

參考

最後

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/MYgQ12ptL53ye_uj71_7VA