Přeskočit na hlavní obsah

Vytvoření uživatelského rozhraní pro váš kontrakt

typescript
react
vite
wagmi
frontend
Začátečník
Ori Pomerantz
1. listopadu 2023
13 minuta čtení

Našli jste funkci, kterou v ekosystému Etherea potřebujeme. Napsali jste chytré kontrakty k jeho implementaci a možná i nějaký související kód, který běží offchain. To je skvělé! Bohužel bez uživatelského rozhraní nebudete mít žádné uživatele a naposledy, když jste psali webové stránky, lidé používali dial-up modemy a JavaScript byl novinkou.

Tento článek je pro vás. Předpokládám, že umíte programovat a možná trochu JavaScript a HTML, ale vaše dovednosti v oblasti uživatelského rozhraní jsou zrezivělé a zastaralé. Společně si projdeme jednoduchou moderní aplikaci, abyste viděli, jak se to dnes dělá.

Proč je to důležité

Teoreticky byste mohli nechat lidi používat Etherscan (opens in a new tab) nebo Blockscout (opens in a new tab) k interakci s vašimi kontrakty. To bude skvělé pro zkušené příznivce Etherea. My se ale snažíme sloužit další miliardě lidí (opens in a new tab). To se nestane bez skvělého uživatelského zážitku a přátelské uživatelské rozhraní je jeho velkou součástí.

Aplikace Greeter

Za moderním uživatelským rozhraním je spousta teorie a mnoho dobrých stránek (opens in a new tab), které to vysvětlují (opens in a new tab). Místo opakování skvělé práce, kterou odvedly tyto stránky, budu předpokládat, že se raději učíte praxí a začnete s aplikací, se kterou si můžete hrát. Stále potřebujete teorii, abyste mohli věci dotáhnout do konce, a k tomu se dostaneme - projdeme si zdrojový soubor po zdrojovém souboru a probereme věci, jakmile na ně narazíme.

Instalace

  1. V případě potřeby si přidejte blockchain Holesky (opens in a new tab) do své peněženky a získejte testovací ETH (opens in a new tab).

  2. Klonujte repozitář z githubu.

    1git clone https://github.com/qbzzt/20230801-modern-ui.git
  3. Nainstalujte potřebné balíčky.

    1cd 20230801-modern-ui
    2pnpm install
  4. Spusťte aplikaci.

    1pnpm dev
  5. Přejděte na adresu URL zobrazenou aplikací. Ve většině případů je to http://localhost:5173/ (opens in a new tab).

  6. Zdrojový kód kontraktu, mírně upravenou verzi Greeter od Hardhat, si můžete prohlédnout v průzkumníku blockchainu (opens in a new tab).

Procházení souborů

index.html

Tento soubor je standardní HTML šablona s výjimkou tohoto řádku, který importuje soubor se skriptem.

1<script type="module" src="/src/main.tsx"></script>

src/main.tsx

Přípona souboru nám říká, že tento soubor je komponenta React (opens in a new tab) napsaná v TypeScriptu (opens in a new tab), rozšíření JavaScriptu, které podporuje kontrolu typů (opens in a new tab). TypeScript je kompilován do JavaScriptu, takže ho můžeme použít pro spuštění na straně klienta.

1import '@rainbow-me/rainbowkit/styles.css'
2import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
3import * as React from 'react'
4import * as ReactDOM from 'react-dom/client'
5import { WagmiConfig } from 'wagmi'
6import { chains, config } from './wagmi'

Importujte kód knihovny, který potřebujeme.

1import { App } from './App'

Importujte komponentu React, která implementuje aplikaci (viz níže).

1ReactDOM.createRoot(document.getElementById('root')!).render(

Vytvořte kořenovou komponentu React. Parametrem render je JSX (opens in a new tab), jazykové rozšíření, které používá jak HTML, tak JavaScript/TypeScript. Vykřičník zde říká komponentě TypeScript: "nevíte, že document.getElementById('root') bude platný parametr pro ReactDOM.createRoot, ale nebojte se – já jsem vývojář a říkám vám, že bude".

1 <React.StrictMode>

Aplikace se nachází uvnitř komponenty React.StrictMode (opens in a new tab). Tato komponenta říká knihovně React, aby vložila další kontrolu ladění, což je užitečné během vývoje.

1 <WagmiConfig config={config}>

Aplikace je také uvnitř komponenty WagmiConfig (opens in a new tab). Knihovna wagmi (we are going to make it) (opens in a new tab) propojuje definice UI v Reactu s knihovnou viem (opens in a new tab) pro psaní decentralizovaných aplikací na Ethereu.

1 <RainbowKitProvider chains={chains}>

A nakonec komponenta RainbowKitProvider (opens in a new tab). Tato komponenta zpracovává přihlašování a komunikaci mezi peněženkou a aplikací.

1 <App />

Nyní můžeme mít komponentu pro aplikaci, která skutečně implementuje UI. Znak /> na konci komponenty říká Reactu, že tato komponenta v sobě nemá žádné definice, podle standardu XML.

1 </RainbowKitProvider>
2 </WagmiConfig>
3 </React.StrictMode>,
4)

Samozřejmě musíme uzavřít i ostatní komponenty.

src/App.tsx

1import { ConnectButton } from '@rainbow-me/rainbowkit'
2import { useAccount } from 'wagmi'
3import { Greeter } from './components/Greeter'
4
5export function App() {

Toto je standardní způsob, jak vytvořit komponentu React – definovat funkci, která se volá pokaždé, když je třeba ji vykreslit. Tato funkce má obvykle nahoře nějaký kód v TypeScriptu nebo JavaScriptu, za nímž následuje příkaz return, který vrací kód JSX.

1 const { isConnected } = useAccount()

Zde používáme useAccount (opens in a new tab) ke kontrole, zda jsme připojeni k blockchainu prostřednictvím peněženky.

Podle konvence jsou v Reactu funkce nazvané use... hooky (opens in a new tab), které vracejí nějaký druh dat. Když použijete takové hooky, vaše komponenta nejenže získá data, ale když se tato data změní, komponenta se znovu vykreslí s aktualizovanými informacemi.

1 return (
2 <>

JSX komponenty React musí vracet jednu komponentu. Když máme více komponent a nemáme nic, co by je "přirozeně" zabalilo, použijeme prázdnou komponentu (<> ... </>), abychom z nich vytvořili jedinou komponentu.

1 <h1>Greeter</h1>
2 <ConnectButton />

Komponentu ConnectButton (opens in a new tab) získáváme z RainbowKit. Když nejsme připojeni, zobrazí se nám tlačítko Připojit peněženku, které otevře modální okno, které vysvětluje peněženky a umožňuje vám vybrat, kterou používáte. Když jsme připojeni, zobrazí se používaný blockchain, adresa našeho účtu a náš zůstatek ETH. Tato zobrazení můžeme použít k přepnutí sítě nebo k odpojení.

1 {isConnected && (

Když potřebujeme vložit skutečný JavaScript (nebo TypeScript, který bude zkompilován do JavaScriptu) do JSX, použijeme závorky ({}).

Syntaxe a && b je zkratka pro [a ? b : a](https://www.w3schools.com/react/react_es6_ternary.asp). To znamená, že pokud je apravda, vyhodnotí se jakoba jinak se vyhodnotí jakoa(což může býtfalse, 0` atd.). To je snadný způsob, jak sdělit Reactu, že se komponenta má zobrazit pouze v případě, že je splněna určitá podmínka.

V tomto případě chceme uživateli zobrazit Greeter pouze v případě, že je uživatel připojen k blockchainu.

1 <Greeter />
2 )}
3 </>
4 )
5}

src/components/Greeter.tsx

Tento soubor obsahuje většinu funkcí uživatelského rozhraní. Obsahuje definice, které by se normálně nacházely ve více souborech, ale jelikož se jedná o tutoriál, program je optimalizován pro snadné pochopení na první pokus, spíše než pro výkon nebo snadnou údržbu.

1import { useState, ChangeEventHandler } from 'react'
2import { useNetwork,
3 useReadContract,
4 usePrepareContractWrite,
5 useContractWrite,
6 useContractEvent
7 } from 'wagmi'

Používáme tyto funkce knihovny. Opět jsou vysvětleny níže, kde jsou použity.

1import { AddressType } from 'abitype'

Knihovna abitype (opens in a new tab) nám poskytuje definice TypeScriptu pro různé datové typy Etherea, jako je AddressType (opens in a new tab).

1let greeterABI = [
2 .
3 .
4 .
5] as const // greeterABI

ABI pro kontrakt Greeter. Pokud vyvíjíte kontrakty a UI zároveň, obvykle je umístíte do stejného repozitáře a ABI vygenerované kompilátorem Solidity použijete jako soubor ve vaší aplikaci. To však zde není nutné, protože kontrakt je již vyvinut a nebude se měnit.

1type AddressPerBlockchainType = {
2 [key: number]: AddressType
3}

TypeScript je silně typovaný. Tuto definici používáme k určení adresy, na které je kontrakt Greeter nasazen na různých řetězcích. Klíčem je číslo (ID řetězce) a hodnotou je AddressType (adresa).

1const contractAddrs: AddressPerBlockchainType = {
2 // Holesky
3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
4
5 // Sepolia
6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
7}

Adresa kontraktu na dvou podporovaných sítích: Holesky (opens in a new tab) a Sepolia (opens in a new tab).

Poznámka: Ve skutečnosti existuje ještě třetí definice pro Redstone Holesky, která bude vysvětlena níže.

1type ShowObjectAttrsType = {
2 name: string,
3 object: any
4}

Tento typ se používá jako parametr pro komponentu ShowObject (vysvětleno později). Obsahuje název objektu a jeho hodnotu, které se zobrazují pro účely ladění.

1type ShowGreetingAttrsType = {
2 greeting: string | undefined
3}

V každém okamžiku můžeme buď vědět, jaký je pozdrav (protože jsme ho přečetli z blockchainu), nebo nevědět (protože jsme ho ještě neobdrželi). Je tedy užitečné mít typ, který může být buď řetězec, nebo nic.

Komponenta Greeter
1const Greeter = () => {

Konečně se dostáváme k definici komponenty.

1 const { chain } = useNetwork()

Informace o řetězci, který používáme, s laskavým svolením wagmi (opens in a new tab). Protože se jedná o hook (use...), při každé změně této informace se komponenta překreslí.

1 const greeterAddr = chain && contractAddrs[chain.id]

Adresa kontraktu Greeter, která se liší podle řetězce (a která je undefined, pokud nemáme informace o řetězci nebo jsme na řetězci bez tohoto kontraktu).

1 const readResults = useReadContract({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: "greet" , // No arguments
5 watch: true
6 })

Hook useReadContract (opens in a new tab) čte informace z kontraktu. Přesné informace, které vrací, uvidíte, když v UI rozbalíte readResults. V tomto případě chceme, aby stále hledal, takže budeme informováni, když se pozdrav změní.

Poznámka: Mohli bychom naslouchat událostem setGreeting (opens in a new tab), abychom věděli, kdy se pozdrav změní, a tímto způsobem ho aktualizovali. Ačkoli to může být efektivnější, nebude to platit ve všech případech. Když uživatel přepne na jiný řetězec, pozdrav se také změní, ale tato změna není doprovázena událostí. Mohli bychom mít jednu část kódu, která naslouchá událostem, a druhou, která identifikuje změny řetězce, ale to by bylo složitější než jen nastavit parametr watch (opens in a new tab).

1 const [ newGreeting, setNewGreeting ] = useState("")

Hook useState v Reactu (opens in a new tab) nám umožňuje specifikovat proměnnou stavu, jejíž hodnota přetrvává z jednoho vykreslení komponenty na druhé. Počáteční hodnota je parametr, v tomto případě prázdný řetězec.

Hook useState vrací seznam se dvěma hodnotami:

  1. Aktuální hodnota proměnné stavu.
  2. Funkce pro úpravu proměnné stavu v případě potřeby. Protože se jedná o hook, pokaždé, když je volán, komponenta se znovu vykreslí.

V tomto případě používáme proměnnou stavu pro nový pozdrav, který chce uživatel nastavit.

1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
2 setNewGreeting(evt.target.value)

Toto je obsluha události, která se spustí při změně vstupního pole pro nový pozdrav. Typ ChangeEventHandler<HTMLInputElement> (opens in a new tab) specifikuje, že se jedná o obsluhu pro změnu hodnoty vstupního prvku HTML. Část <HTMLInputElement> se používá, protože se jedná o generický typ (opens in a new tab).

1 const preparedTx = usePrepareContractWrite({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: 'setGreeting',
5 args: [ newGreeting ]
6 })
7 const workingTx = useContractWrite(preparedTx.config)

Toto je proces odeslání blockchainové transakce z pohledu klienta:

  1. Odešlete transakci uzlu v blockchainu pomocí eth_estimateGas (opens in a new tab).
  2. Počkejte na odpověď z uzlu.
  3. Po obdržení odpovědi požádejte uživatele o podepsání transakce prostřednictvím peněženky. Tento krok se musí provést až po obdržení odpovědi z uzlu, protože uživateli se před podpisem zobrazí náklady na transakci za palivo.
  4. Počkejte na schválení uživatele.
  5. Odešlete transakci znovu, tentokrát pomocí eth_sendRawTransaction (opens in a new tab).

Krok 2 pravděpodobně zabere znatelné množství času, během kterého by se uživatelé divili, zda byl jejich příkaz skutečně přijat uživatelským rozhraním a proč ještě nejsou požádáni o podepsání transakce. To vede ke špatnému uživatelskému zážitku (UX).

Řešením je použití přípravných hooků (opens in a new tab). Pokaždé, když se změní parametr, okamžitě odešlete uzlu požadavek eth_estimateGas. Poté, když uživatel skutečně chce odeslat transakci (v tomto případě stisknutím Aktualizovat pozdrav), náklady na palivo jsou známé a uživatel může okamžitě vidět stránku peněženky.

1 return (

Nyní můžeme konečně vytvořit skutečné HTML, které se má vrátit.

1 <>
2 <h2>Greeter</h2>
3 {
4 !readResults.isError && !readResults.isLoading &&
5 <ShowGreeting greeting={readResults.data} />
6 }
7 <hr />

Vytvořte komponentu ShowGreeting (vysvětleno níže), ale pouze pokud byl pozdrav úspěšně přečten z blockchainu.

1 <input type="text"
2 value={newGreeting}
3 onChange={greetingChange}
4 />

Toto je vstupní textové pole, kde si uživatel může nastavit nový pozdrav. Pokaždé, když uživatel stiskne klávesu, zavoláme greetingChange, které zavolá setNewGreeting. Protože setNewGreeting pochází z hooku useState, způsobí to opětovné vykreslení komponenty Greeter. To znamená, že:

  • Musíme specifikovat value, abychom zachovali hodnotu nového pozdravu, protože jinak by se vrátila zpět na výchozí hodnotu, prázdný řetězec.
  • usePrepareContractWrite se volá pokaždé, když se newGreeting změní, což znamená, že v připravené transakci bude vždy nejnovější newGreeting.
1 <button disabled={!workingTx.write}
2 onClick={workingTx.write}
3 >
4 Update greeting
5 </button>

Pokud workingTx.write neexistuje, stále čekáme na informace potřebné k odeslání aktualizace pozdravu, takže je tlačítko zakázáno. Pokud hodnota workingTx.write existuje, je to funkce, která se má volat k odeslání transakce.

1 <hr />
2 <ShowObject name="readResults" object={readResults} />
3 <ShowObject name="preparedTx" object={preparedTx} />
4 <ShowObject name="workingTx" object={workingTx} />
5 </>
6 )
7}

Nakonec, abychom vám pomohli vidět, co děláme, ukážeme tři objekty, které používáme:

  • readResults
  • preparedTx
  • workingTx
Komponenta ShowGreeting

Tato komponenta zobrazuje

1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

Funkce komponenty přijímá parametr se všemi atributy komponenty.

1 return <b>{attrs.greeting}</b>
2}
Komponenta ShowObject

Pro informační účely používáme komponentu ShowObject k zobrazení důležitých objektů (readResults pro čtení pozdravu a preparedTx a workingTx pro transakce, které vytváříme).

1const ShowObject = (attrs: ShowObjectAttrsType ) => {
2 const keys = Object.keys(attrs.object)
3 const funs = keys.filter(k => typeof attrs.object[k] == "function")
4 return <>
5 <details>

Nechceme zaplnit UI všemi informacemi, takže abychom je mohli zobrazit nebo zavřít, používáme značku details (opens in a new tab).

1 <summary>{attrs.name}</summary>
2 <pre>
3 {JSON.stringify(attrs.object, null, 2)}

Většina polí se zobrazuje pomocí JSON.stringify (opens in a new tab).

1 </pre>
2 { funs.length > 0 &&
3 <>
4 Functions:
5 <ul>

Výjimkou jsou funkce, které nejsou součástí standardu JSON (opens in a new tab), takže se musí zobrazovat samostatně.

1 {funs.map((f, i) =>

V rámci JSX je kód uvnitř { složených závorek } interpretován jako JavaScript. Poté je kód uvnitř ( kulatých závorek ) interpretován znovu jako JSX.

1 (<li key={i}>{f}</li>)
2 )}

React vyžaduje, aby značky ve stromu DOM (opens in a new tab) měly jedinečné identifikátory. To znamená, že potomci stejné značky (v tomto případě neuspořádaný seznam (opens in a new tab)) potřebují různé atributy key.

1 </ul>
2 </>
3 }
4 </details>
5 </>
6}

Ukončete různé značky HTML.

Konečný export
1export { Greeter }

Komponenta Greeter je ta, kterou potřebujeme exportovat pro aplikaci.

src/wagmi.ts

Nakonec jsou různé definice související s WAGMI v src/wagmi.ts. Nebudu zde vše vysvětlovat, protože většina z toho je šablona, kterou pravděpodobně nebudete muset měnit.

Kód zde není úplně stejný jako na githubu (opens in a new tab), protože později v článku přidáme další řetězec (Redstone Holesky (opens in a new tab)).

1import { getDefaultWallets } from '@rainbow-me/rainbowkit'
2import { configureChains, createConfig } from 'wagmi'
3import { holesky, sepolia } from 'wagmi/chains'

Importujte blockchainy, které aplikace podporuje. Seznam podporovaných řetězců si můžete prohlédnout v githubu viem (opens in a new tab).

1import { publicProvider } from 'wagmi/providers/public'
2
3const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'

Abyste mohli používat WalletConnect (opens in a new tab), potřebujete ID projektu pro vaši aplikaci. Můžete jej získat na cloud.walletconnect.com (opens in a new tab).

1const { chains, publicClient, webSocketPublicClient } = configureChains(
2 [ holesky, sepolia ],
3 [
4 publicProvider(),
5 ],
6)
7
8const { connectors } = getDefaultWallets({
9 appName: 'My wagmi + RainbowKit App',
10 chains,
11 projectId: walletConnectProjectId,
12})
13
14export const config = createConfig({
15 autoConnect: true,
16 connectors,
17 publicClient,
18 webSocketPublicClient,
19})
20
21export { chains }
Zobrazit vše

Přidání dalšího blockchainu

V dnešní době existuje mnoho řešení pro škálování L2 a možná budete chtít podporovat některá, která viem ještě nepodporuje. K tomu upravte src/wagmi.ts. Tyto pokyny vysvětlují, jak přidat Redstone Holesky (opens in a new tab).

  1. Importujte typ defineChain z viem.

    1import { defineChain } from 'viem'
  2. Přidejte definici sítě.

    1const redstoneHolesky = defineChain({
    2 id: 17_001,
    3 name: 'Redstone Holesky',
    4 network: 'redstone-holesky',
    5 nativeCurrency: {
    6 decimals: 18,
    7 name: 'Ether',
    8 symbol: 'ETH',
    9 },
    10 rpcUrls: {
    11 default: {
    12 http: ['https://rpc.holesky.redstone.xyz'],
    13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    14 },
    15 public: {
    16 http: ['https://rpc.holesky.redstone.xyz'],
    17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    18 },
    19 },
    20 blockExplorers: {
    21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },
    22 },
    23})
    Zobrazit vše
  3. Přidejte nový řetězec do volání configureChains.

    1 const { chains, publicClient, webSocketPublicClient } = configureChains(
    2 [ holesky, sepolia, redstoneHolesky ],
    3 [ publicProvider(), ],
    4 )
  4. Zajistěte, aby aplikace znala adresu vašich kontraktů na nové síti. V tomto případě upravíme src/components/Greeter.tsx:

    1const contractAddrs : AddressPerBlockchainType = {
    2 // Holesky
    3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
    4
    5 // Redstone Holesky
    6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',
    7
    8 // Sepolia
    9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
    10}
    Zobrazit vše

Závěr

Samozřejmě vás nezajímá poskytování uživatelského rozhraní pro Greeter. Chcete vytvořit uživatelské rozhraní pro své vlastní kontrakty. Chcete-li vytvořit vlastní aplikaci, proveďte tyto kroky:

  1. Určete, že chcete vytvořit aplikaci wagmi.

    1pnpm create wagmi
  2. Pojmenujte aplikaci.

  3. Vyberte framework React.

  4. Vyberte variantu Vite.

  5. Můžete přidat Rainbow kit (opens in a new tab).

Nyní jděte a zpřístupněte své kontrakty širokému světu.

Více z mé práce najdete zde (opens in a new tab).

Stránka naposledy aktualizována: 3. března 2026

Byl tento tutoriál užitečný?