بناء واجهة مستخدم للعقد الخاص بك
لقد وجدت ميزة نحتاجها في نظام إيثريوم البيئي. لقد كتبت العقود الذكية لتنفيذها، وربما حتى بعض النصوص البرمجية ذات الصلة التي تعمل خارج السلسلة. هذا رائع! للأسف، بدون واجهة مستخدم لن يكون لديك أي مستخدمين، وفي آخر مرة كتبت فيها موقعًا على شبكة الإنترنت استخدم الناس أجهزة مودم الطلب الهاتفي وكانت جافا سكريبت جديدة.
هذا المقال لك. أفترض أنك تعرف البرمجة، وربما القليل من جافا سكريبت وHTML، ولكن مهاراتك في واجهة المستخدم قديمة وغير محدّثة. معًا سوف نتناول تطبيقًا حديثًا وبسيطًا حتى ترى كيف يتم ذلك في هذه الأيام.
لماذا هذا مهم
نظريًا، يمكنك فقط جعل الناس يستخدمون إيثرسكان (opens in a new tab) أو بلوك سكوت (opens in a new tab) للتفاعل مع عقودك. سيكون ذلك رائعًا بالنسبة لمستخدمي إيثريوم ذوي الخبرة. لكننا نحاول خدمة مليار شخص آخر (opens in a new tab). لن يحدث هذا بدون تجربة مستخدم رائعة، وواجهة المستخدم السهلة هي جزء كبير من ذلك.
تطبيق Greeter
هناك الكثير من النظريات وراء عمل واجهة المستخدم الحديثة، والكثير من المواقع الجيدة (opens in a new tab) التي تشرحها (opens in a new tab). بدلاً من تكرار العمل الجيد الذي قامت به تلك المواقع، سأفترض أنك تفضل التعلم بالممارسة والبدء بتطبيق يمكنك تجربته. ما زلت بحاجة إلى النظرية لإنجاز الأمور، وسنصل إليها - سنتناول فقط ملف المصدر تلو الآخر، ونناقش الأمور عند وصولنا إليها.
التثبيت
-
إذا لزم الأمر، أضف بلوكتشين هوليسكي (opens in a new tab) إلى محفظتك واحصل على ETH تجريبي (opens in a new tab).
-
استنسخ مستودع غيت هاب.
1git clone https://github.com/qbzzt/20230801-modern-ui.git -
ثبّت الحزم اللازمة.
1cd 20230801-modern-ui2pnpm install -
ابدأ التطبيق.
1pnpm dev -
تصفح عنوان URL الذي يعرضه التطبيق. في معظم الحالات، هو http://localhost:5173/ (opens in a new tab).
-
يمكنك رؤية النص البرمجي المصدر للعقد، وهو نسخة معدلة قليلاً من Greeter الخاص بـ هارد هات، على مستكشف بلوكتشين (opens in a new tab).
استعراض الملفات
index.html
هذا الملف هو قالب HTML قياسي باستثناء هذا السطر، الذي يستورد ملف البرنامج النصي.
1<script type="module" src="/src/main.tsx"></script>src/main.tsx
يخبرنا امتداد الملف أن هذا الملف هو مكون رياكت (opens in a new tab) مكتوب بـ تايب سكريبت (opens in a new tab)، وهو امتداد لـ جافا سكريبت يدعم التحقق من النوع (opens in a new tab). يتم تجميع تايب سكريبت إلى جافا سكريبت، لذا يمكننا استخدامه للتنفيذ من جانب العميل.
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'استيراد النص البرمجي للمكتبة التي نحتاجها.
1import { App } from './App'استيراد مكون رياكت الذي ينفذ التطبيق (انظر أدناه).
1ReactDOM.createRoot(document.getElementById('root')!).render(إنشاء مكون رياكت الجذري. المعلمة إلى render هي JSX (opens in a new tab)، وهي لغة امتداد تستخدم كلاً من HTML و جافا سكريبت/تايب سكريبت. تخبر علامة التعجب هنا مكون تايب سكريبت: "أنت لا تعرف أن document.getElementById('root') ستكون معلمة صالحة لـ ReactDOM.createRoot، ولكن لا تقلق - أنا المبرمج وأنا أقول لك إنها ستكون كذلك".
1 <React.StrictMode>سيتم وضع التطبيق داخل مكون React.StrictMode (opens in a new tab). يخبر هذا المكون مكتبة رياكت بإدراج فحوصات تصحيح أخطاء إضافية، وهو أمر مفيد أثناء التطوير.
1 <WagmiConfig config={config}>التطبيق موجود أيضًا داخل مكون WagmiConfig (opens in a new tab). مكتبة wagmi (we are going to make it) (opens in a new tab) تربط تعريفات واجهة مستخدم رياكت مع مكتبة viem (opens in a new tab) لكتابة تطبيق إيثريوم لامركزي.
1 <RainbowKitProvider chains={chains}>وأخيرًا، مكون RainbowKitProvider (opens in a new tab). يتعامل هذا المكون مع تسجيل الدخول والاتصال بين المحفظة والتطبيق.
1 <App />الآن يمكننا الحصول على مكون للتطبيق، والذي ينفذ واجهة المستخدم بالفعل. تخبر /> في نهاية المكون رياكت أن هذا المكون لا يحتوي على أي تعريفات بداخله، وفقًا لمعيار XML.
1 </RainbowKitProvider>2 </WagmiConfig>3 </React.StrictMode>,4)بالطبع، علينا إغلاق المكونات الأخرى.
src/App.tsx
1import { ConnectButton } from '@rainbow-me/rainbowkit'2import { useAccount } from 'wagmi'3import { Greeter } from './components/Greeter'45export function App() {هذه هي الطريقة القياسية لإنشاء مكون رياكت - تحديد دالة يتم استدعاؤها في كل مرة تحتاج فيها إلى عرضها. عادةً ما تحتوي هذه الدالة على بعض النصوص البرمجية لـ تايب سكريبت أو جافا سكريبت في الأعلى، متبوعة بعبارة return التي تُرجع النص البرمجي JSX.
1 const { isConnected } = useAccount()هنا نستخدم useAccount (opens in a new tab) للتحقق مما إذا كنا متصلين ببلوكتشين من خلال محفظة أم لا.
حسب الاصطلاح، في رياكت، الدوال التي تسمى use... هي hooks (opens in a new tab) التي تُرجع نوعًا من البيانات. عند استخدام مثل هذه الخطافات (hooks)، لا يحصل المكون الخاص بك على البيانات فحسب، بل عند تغيير هذه البيانات، يتم إعادة عرض المكون بالمعلومات المحدثة.
1 return (2 <>يجب أن تُرجع JSX لمكون رياكت مكونًا واحدًا. عندما يكون لدينا مكونات متعددة وليس لدينا أي شيء يغلفها "بشكل طبيعي" نستخدم مكونًا فارغًا (<> ... </>) لتحويلها إلى مكون واحد.
1 <h1>Greeter</h1>2 <ConnectButton />نحصل على مكون ConnectButton (opens in a new tab) من رينبو كيت. عندما لا نكون متصلين، فإنه يعطينا زر Connect Wallet الذي يفتح نافذة تشرح المحافظ وتتيح لك اختيار المحفظة التي تستخدمها. عندما نكون متصلين، فإنه يعرض البلوكتشين الذي نستخدمه، وعنوان حسابنا، ورصيدنا من ETH. يمكننا استخدام هذه الشاشات لتبديل الشبكة أو لقطع الاتصال.
1 {isConnected && (عندما نحتاج إلى إدراج جافا سكريبت فعلي (أو تايب سكريبت الذي سيتم تجميعه إلى جافا سكريبت) في JSX، فإننا نستخدم الأقواس المعقوفة ({}).
الصيغة a && b هي اختصار لـ a ? b : a (opens in a new tab). أي، إذا كانت a صحيحة، فسيتم تقييمها إلى b وإلا فسيتم تقييمها إلى a (والتي يمكن أن تكون false أو 0، إلخ). هذه طريقة سهلة لإخبار رياكت بأنه يجب عرض مكون فقط إذا تم استيفاء شرط معين.
في هذه الحالة، نريد فقط أن نظهر للمستخدم Greeter إذا كان المستخدم متصلاً ببلوكتشين.
1 <Greeter />2 )}3 </>4 )5}src/components/Greeter.tsx
يحتوي هذا الملف على معظم وظائف واجهة المستخدم. إنه يتضمن تعريفات تكون عادة في ملفات متعددة، ولكن نظرًا لأن هذا برنامج تعليمي، فقد تم تحسين البرنامج ليكون سهل الفهم في المرة الأولى، بدلاً من الأداء أو سهولة الصيانة.
1import { useState, ChangeEventHandler } from 'react'2import { useNetwork,3 useReadContract,4 usePrepareContractWrite,5 useContractWrite,6 useContractEvent7 } from 'wagmi'نحن نستخدم هذه الدوال المكتبية. مرة أخرى، يتم شرحها أدناه حيث يتم استخدامها.
1import { AddressType } from 'abitype'توفر لنا مكتبة abitype (opens in a new tab) تعريفات تايب سكريبت لأنواع بيانات إيثريوم المختلفة، مثل AddressType (opens in a new tab).
1let greeterABI = [2 .3 .4 .5] as const // greeterABIواجهة التطبيق الثنائية (ABI) لعقد Greeter.
إذا كنت تقوم بتطوير العقود وواجهة المستخدم في نفس الوقت، فعادةً ما تضعها في نفس المستودع وتستخدم واجهة التطبيق الثنائية (ABI) التي تم إنشاؤها بواسطة مترجم سوليديتي كملف في تطبيقك. ومع ذلك، هذا ليس ضروريًا هنا لأن العقد قد تم تطويره بالفعل ولن يتغير.
1type AddressPerBlockchainType = {2 [key: number]: AddressType3}تايب سكريبت لغة ذات أنواع قوية. نستخدم هذا التعريف لتحديد العنوان الذي يتم فيه نشر عقد Greeter على سلاسل مختلفة. المفتاح هو رقم (chainId)، والقيمة هي AddressType (عنوان).
1const contractAddrs: AddressPerBlockchainType = {2 // Holesky3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Sepolia6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'7}عنوان العقد على الشبكتين المدعومتين: هوليسكي (opens in a new tab) و سيبوليا (opens in a new tab).
ملاحظة: يوجد بالفعل تعريف ثالث، لـ Redstone هوليسكي، سيتم شرحه أدناه.
1type ShowObjectAttrsType = {2 name: string,3 object: any4}يُستخدم هذا النوع كمعلمة لمكون ShowObject (سيتم شرحه لاحقًا). يتضمن اسم الكائن وقيمته، والتي يتم عرضها لأغراض تصحيح الأخطاء.
1type ShowGreetingAttrsType = {2 greeting: string | undefined3}في أي لحظة من الزمن قد نعرف ما هي التحية (لأننا قرأناها من البلوكتشين) أو لا نعرف (لأننا لم نستلمها بعد). لذا من المفيد أن يكون لديك نوع يمكن أن يكون إما سلسلة نصية أو لا شيء.
مكون Greeter
1const Greeter = () => {أخيرًا، نصل إلى تحديد المكون.
1 const { chain } = useNetwork()معلومات حول السلسلة التي نستخدمها، بفضل wagmi (opens in a new tab).
نظرًا لأن هذا خطاف (hook) (use...)، ففي كل مرة تتغير فيها هذه المعلومات، تتم إعادة رسم المكون.
1 const greeterAddr = chain && contractAddrs[chain.id]عنوان عقد Greeter، الذي يختلف باختلاف السلسلة (والذي يكون undefined إذا لم تكن لدينا معلومات عن السلسلة أو كنا على سلسلة لا تحتوي على هذا العقد).
1 const readResults = useReadContract({2 address: greeterAddr,3 abi: greeterABI,4 functionName: "greet" , // لا توجد وسائط5 watch: true6 })خطاف useReadContract (opens in a new tab) يقرأ المعلومات من العقد. يمكنك أن ترى بالضبط ما هي المعلومات التي يُرجعها من خلال توسيع readResults في واجهة المستخدم. في هذه الحالة، نريده أن يستمر في البحث حتى نكون على علم عند تغيير التحية.
ملاحظة: يمكننا الاستماع إلى أحداث setGreeting (opens in a new tab) لمعرفة متى تتغير التحية وتحديثها بهذه الطريقة. ومع ذلك، على الرغم من أنها قد تكون أكثر كفاءة، إلا أنها لن تنطبق في جميع الحالات. عندما ينتقل المستخدم إلى سلسلة مختلفة، تتغير التحية أيضًا، ولكن هذا التغيير لا يرافقه حدث. يمكن أن يكون لدينا جزء من النص البرمجي يستمع للأحداث وآخر لتحديد تغييرات السلسلة، ولكن هذا سيكون أكثر تعقيدًا من مجرد تعيين معلمة watch (opens in a new tab).
1 const [ newGreeting, setNewGreeting ] = useState("")خطاف useState (opens in a new tab) في رياكت يتيح لنا تحديد متغير حالة، والذي تستمر قيمته من عرض للمكون إلى آخر. القيمة الأولية هي المعلمة، وفي هذه الحالة السلسلة الفارغة.
يُرجع خطاف useState قائمة بقيمتين:
- القيمة الحالية لمتغير الحالة.
- دالة لتعديل متغير الحالة عند الحاجة. بما أن هذا خطاف (hook)، ففي كل مرة يتم استدعاؤه، يتم عرض المكون مرة أخرى.
في هذه الحالة، نستخدم متغير حالة للتحية الجديدة التي يريد المستخدم تعيينها.
1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>2 setNewGreeting(evt.target.value)هذا هو معالج الأحداث عند تغيير حقل إدخال التحية الجديدة. النوع، ChangeEventHandler<HTMLInputElement> (opens in a new tab)، يحدد أن هذا معالج لتغيير قيمة عنصر إدخال HTML. يتم استخدام جزء <HTMLInputElement> لأن هذا نوع عام (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)هذه هي عملية إرسال معاملة بلوكتشين من منظور العميل:
- أرسل المعاملة إلى عقدة في البلوكتشين باستخدام
eth_estimateGas(opens in a new tab). - انتظر الرد من العقدة.
- عند تلقي الرد، اطلب من المستخدم توقيع المعاملة من خلال المحفظة. يجب أن تحدث هذه الخطوة بعد تلقي رد العقدة لأنه يتم عرض تكلفة الغاز للمعاملة للمستخدم قبل توقيعها.
- انتظر موافقة المستخدم.
- أرسل المعاملة مرة أخرى، وهذه المرة باستخدام
eth_sendRawTransaction(opens in a new tab).
من المحتمل أن تستغرق الخطوة 2 قدرًا ملحوظًا من الوقت، سيتساءل المستخدمون خلاله عما إذا كان أمرهم قد تم استلامه بالفعل من قبل واجهة المستخدم ولماذا لم يُطلب منهم توقيع المعاملة بالفعل. وهذا يؤدي إلى تجربة مستخدم (UX) سيئة.
الحل هو استخدام خطافات التحضير (prepare hooks) (opens in a new tab). في كل مرة يتغير فيها أحد المعلمات، أرسل فورًا طلب eth_estimateGas إلى العقدة. بعد ذلك، عندما يرغب المستخدم بالفعل في إرسال المعاملة (في هذه الحالة عن طريق الضغط على تحديث التحية)، تكون تكلفة الغاز معروفة ويمكن للمستخدم رؤية صفحة المحفظة على الفور.
1 return (الآن يمكننا أخيرًا إنشاء كود HTML الفعلي لإعادته.
1 <>2 <h2>Greeter</h2>3 {4 !readResults.isError && !readResults.isLoading &&5 <ShowGreeting greeting={readResults.data} />6 }7 <hr />قم بإنشاء مكون ShowGreeting (سيتم شرحه أدناه)، ولكن فقط إذا تمت قراءة التحية بنجاح من البلوكتشين.
1 <input type="text"2 value={newGreeting}3 onChange={greetingChange}4 />هذا هو حقل إدخال النص حيث يمكن للمستخدم تعيين تحية جديدة. في كل مرة يضغط فيها المستخدم على مفتاح، نستدعي greetingChange التي تستدعي setNewGreeting. نظرًا لأن setNewGreeting تأتي من خطاف useState، فإنها تتسبب في إعادة عرض مكون Greeter مرة أخرى. هذا يعني أن:
- نحتاج إلى تحديد
valueللحفاظ على قيمة التحية الجديدة، لأنه بخلاف ذلك ستعود إلى القيمة الافتراضية، وهي السلسلة الفارغة. - يتم استدعاء
usePrepareContractWriteفي كل مرة يتغير فيهاnewGreeting، مما يعني أنه سيحتوي دائمًا على أحدثnewGreetingفي المعاملة المُعدة.
1 <button disabled={!workingTx.write}2 onClick={workingTx.write}3 >4 تحديث التحية5 </button>إذا لم يكن هناك workingTx.write، فإننا ما زلنا ننتظر المعلومات اللازمة لإرسال تحديث التحية، لذلك يتم تعطيل الزر. إذا كانت هناك قيمة workingTx.write، فهذه هي الدالة التي يجب استدعاؤها لإرسال المعاملة.
1 <hr />2 <ShowObject name="readResults" object={readResults} />3 <ShowObject name="preparedTx" object={preparedTx} />4 <ShowObject name="workingTx" object={workingTx} />5 </>6 )7}أخيرًا، لمساعدتك على رؤية ما نفعله، نعرض الكائنات الثلاثة التي نستخدمها:
readResultspreparedTxworkingTx
مكون ShowGreeting
يعرض هذا المكون
1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {تتلقى دالة المكون معلمة تحتوي على جميع سمات المكون.
1 return <b>{attrs.greeting}</b>2}مكون ShowObject
لأغراض المعلومات، نستخدم مكون ShowObject لإظهار الكائنات المهمة (readResults لقراءة التحية و preparedTx و workingTx للمعاملات التي ننشئها).
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>لا نريد ازدحام واجهة المستخدم بكل المعلومات، لذلك لجعل من الممكن عرضها أو إغلاقها، نستخدم علامة details (opens in a new tab).
1 <summary>{attrs.name}</summary>2 <pre>3 {JSON.stringify(attrs.object, null, 2)}يتم عرض معظم الحقول باستخدام JSON.stringify (opens in a new tab).
1 </pre>2 { funs.length > 0 &&3 <>4 Functions:5 <ul>الاستثناء هو الدوال، التي ليست جزءًا من معيار JSON (opens in a new tab)، لذلك يجب عرضها بشكل منفصل.
1 {funs.map((f, i) =>ضمن JSX، يتم تفسير الكود داخل الأقواس المعقوفة {} على أنه جافا سكريبت. بعد ذلك، يتم تفسير الكود الموجود داخل الأقواس العادية () مرة أخرى على أنه JSX.
1 (<li key={i}>{f}</li>)2 )}يتطلب رياكت أن يكون للعلامات في شجرة DOM (opens in a new tab) معرّفات مميزة. هذا يعني أن العناصر الفرعية لنفس العلامة (في هذه الحالة، القائمة غير المرتبة (opens in a new tab)) تحتاج إلى سمات key مختلفة.
1 </ul>2 </>3 }4 </details>5 </>6}أغلق علامات HTML المختلفة.
التصدير النهائي
1export { Greeter }مكون Greeter هو المكون الذي نحتاج إلى تصديره للتطبيق.
src/wagmi.ts
أخيرًا، توجد تعريفات مختلفة متعلقة بـ WAGMI في src/wagmi.ts. لن أشرح كل شيء هنا، لأن معظمه عبارة عن قالب جاهز من غير المرجح أن تحتاج إلى تغييره.
النص البرمجي هنا ليس تمامًا مثل الموجود على github (opens in a new tab) لأنه في وقت لاحق من المقالة نضيف سلسلة أخرى (Redstone هوليسكي (opens in a new tab)).
1import { getDefaultWallets } from '@rainbow-me/rainbowkit'2import { configureChains, createConfig } from 'wagmi'3import { holesky, sepolia } from 'wagmi/chains'استيراد سلاسل الكتل التي يدعمها التطبيق. يمكنك رؤية قائمة السلاسل المدعومة في viem github (opens in a new tab).
1import { publicProvider } from 'wagmi/providers/public'23const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'لتتمكن من استخدام واليت كونكت (opens in a new tab)، تحتاج إلى معرف مشروع لتطبيقك. يمكنك الحصول عليه على cloud.walletconnect.com (opens in a new tab).
1const { chains, publicClient, webSocketPublicClient } = configureChains(2 [ holesky, sepolia ],3 [4 publicProvider(),5 ],6)78const { connectors } = getDefaultWallets({9 appName: 'تطبيقي wagmi + RainbowKit',10 chains,11 projectId: walletConnectProjectId,12})1314export const config = createConfig({15 autoConnect: true,16 connectors,17 publicClient,18 webSocketPublicClient,19})2021export { chains }إظهار الكلإضافة بلوكتشين آخر
في هذه الأيام، هناك الكثير من حلول توسيع الطبقة الثانية، وقد ترغب في دعم بعضها الذي لا يدعمه viem بعد. للقيام بذلك، قم بتعديل src/wagmi.ts. تشرح هذه التعليمات كيفية إضافة Redstone هوليسكي (opens in a new tab).
-
استورد نوع
defineChainمن viem.1import { defineChain } from 'viem' -
أضف تعريف الشبكة.
1const redstoneHolesky = defineChain({2 id: 17_001,3 name: 'Redstone هوليسكي',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: 'المستكشف', url: 'https://explorer.holesky.redstone.xyz' },22 },23})إظهار الكل -
أضف السلسلة الجديدة إلى استدعاء
configureChains.1 const { chains, publicClient, webSocketPublicClient } = configureChains(2 [ holesky, sepolia, redstoneHolesky ],3 [ publicProvider(), ],4 ) -
تأكد من أن التطبيق يعرف عنوان عقودك على الشبكة الجديدة. في هذه الحالة، نقوم بتعديل
src/components/Greeter.tsx:1const contractAddrs : AddressPerBlockchainType = {2 // هوليسكي3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Redstone هوليسكي6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',78 // سيبوليا9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'10}إظهار الكل
الخلاصة
بالطبع، أنت لا تهتم حقًا بتوفير واجهة مستخدم لـ Greeter. تريد إنشاء واجهة مستخدم لعقودك الخاصة. لإنشاء تطبيقك الخاص، قم بتشغيل هذه الخطوات:
-
حدد لإنشاء تطبيق wagmi.
1pnpm create wagmi -
قم بتسمية التطبيق.
-
حدد إطار عمل رياكت.
-
حدد متغير فيت.
الآن اذهب واجعل عقودك قابلة للاستخدام للعالم بأسره.
انظر هنا لمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 مارس 2026