Bài 2: Các Bước Thiết Kế Giao Diện DApp Cơ Bản Với React Và Ant Design

Ấn tượng đầu tiên luôn là quan trọng nhất. Để xây dựng một DApp hoàn chỉnh, được nhiều người dùng lựa chọn sử dụng, bạn cần chú ý tới “bộ mặt” - giao diện của DApp. Vậy, làm thế nào để thiết kế giao diện cơ bản cho DApp? Tham khảo ngay bài viết dưới đây!

Bài 2: Các Bước Thiết Kế Giao Diện DApp Cơ Bản Với React Và Ant Design

Hãy cùng tìm hiểu về ReactJS và Ant Design - các thư viện UI đang được những ứng dụng toàn cầu như Facebook, Shopee, Lazada,… sử dụng cho giao diện của họ.

Xây dựng giao diện bằng ReactJS

Hiện nay, hầu hết lập trình viên Frontend đều biết đến hoặc đã từng nghe qua về ReactJS. ReactJS là một thư viện Javascript được phát triển tại Facebook nhằm tăng tốc và giảm bug trong quá trình xây dựng giao diện, đồng thời giúp việc phát triển và bảo trì mã nguồn trở nên dễ dàng hơn.

Các khái niệm cơ bản

Trước khi học cách sử dụng ReactJS, bạn cần nắm một số khái niệm cơ bản gồm:

  • Virtual DOM: Ở DOM tree truyền thống, khi một node thay đổi, toàn bộ node sẽ tái cấu trúc. Như vậy đồng nghĩa với việc DOM tree cũng sẽ phải thay đổi một phần, điều này sẽ ảnh hưởng đến tốc độ xử lý. ReactJS sử dụng Virtual DOM (DOM ảo) để cải thiện vấn đề này. Công nghệ DOM ảo giúp tăng hiệu năng cho ứng dụng bằng cách tính toán tối ưu hoá việc re-render DOM tree thật khi dữ liệu thay đổi.
  • JSX: Một dạng ngôn ngữ cho phép viết các mã HTML trong Javascript.
  • Components: ReactJS được xây dựng xung quanh các component, chúng ta có thể tái sử dụng component ở nhiều nơi. Việc này giúp dễ dàng bảo trì mã code.
  • Props: Input của Component nhận gọi là props, props được truyền vào và không thể thay đổi.
  • State: thể hiện trạng thái của component, khi state thay đổi bằng các phương thức setState thì component đồng thời render lại để cập nhật UI.

Các bước cài đặt và khởi chạy ReactJS

Bước 1. Cài đặt NodeJS và NPM

Để cài đặt môi trường chạy ReactJS, trước tiên bạn phải cài đặt NodeJS và NPM - đây là nền tảng bắt buộc.

1. Truy cập https://nodejs.org/en/download/, chọn và tải phiên bản phù hợp với hệ điều hành của bạn. Tiến hành cài đặt theo mặc định.

2. Hãy kiểm tra lại phiên bản để xác nhận đã cài thành công bằng cách mở Terminal và gõ dòng lệnh sau:


user-pc ~ % node -v
v14.17.4
user-pc ~ % npm -v
6.14.14

Bước 2. Khởi tạo ReactJS App

Để khởi tạo một dự án sử dụng ReactJS vào trong thư mục chứa dự án, bạn hãy mở Terminal và gõ dòng lệnh sau:

npx create-react-app my-app --template typescript 
cd my-app

Trong đó, my-app là tên thư mục chứa dự án của bạn.

Bước 3. Cài đặt package + webpack

1. Cập nhật nội dung file package.json như sau:

{
 "name": "my-app",
 "dependencies": {
   "@emotion/react": "^11.8.1",
   "@emotion/styled": "^11.8.1",
   "@saberhq/use-solana": "^1.12.48",
   "@gokiprotocol/walletkit": "^1.6.4",
   "@reduxjs/toolkit": "^1.7.2",
   "@solana/buffer-layout": "^4.0.0",
   "antd": "^4.18.2",
   "react": "^17.0.2",
   "react-dom": "^17.0.2",
   "react-router-dom": "^5.3.0",
   "web-vitals": "^2.1.4",
   "@testing-library/jest-dom": "^5.16.3",
   "@testing-library/react": "^12.1.4",
   "@testing-library/user-event": "^13.5.0",
   "@types/jest": "^27.4.1"
 },
 "scripts": {
   "start": "craco start",
   "build": "craco build",
   "test": "craco test"
 },
 "browserslist": {
   "production": [
     ">0.2%",
     "not dead",
     "not op_mini all"
   ],
   "development": [
     "last 1 chrome version",
     "last 1 firefox version",
     "last 1 safari version"
   ]
 },
 "devDependencies": {
   "@craco/craco": "^6.4.0",
   "@types/node": "^16.11.26",
   "@types/react": "^17.0.39",
   "@types/react-dom": "^17.0.11",
   "@types/react-router-dom": "^5.3.3",
   "assert": "^2.0.0",
   "dotenv-cra": "^3.0.2",
   "gh-pages": "^3.2.3",
   "less": "^4.1.2",
   "less-loader": "^10.2.0",
   "react-scripts": "^5.0.0",
   "typescript": "^4.5.2"
 }
}

2. Tạo folder plugins và tạo mới 3 file với nội dung như sau:

plugins/craco-compatibility.js

/**
* Maintain Webpack 5 compatibility
*/
const path = require('path')
const webpack = require('webpack')

const overrideWebpackConfig = ({ context, webpackConfig }) => {
 // Add buffer to Webpack 5 polyfill
 // https://github.com/diegomura/react-pdf/issues/1029
 webpackConfig.plugins.push(
   new webpack.ProvidePlugin({
     Buffer: ['buffer', 'Buffer'],
   }),
 )
 // Add polyfill libraries
 webpackConfig.resolve.fallback = {
   // For Ethereum Web3
   assert: require.resolve('assert/'),
 }

 // Fix unrecognized change / caching problem
 webpackConfig.cache.buildDependencies.config.push(
   path.join(context.paths.appPath, './craco.config.js'),
 )
 // Fix "fully specified"
 // https://github.com/webpack/webpack/issues/11467#issuecomment-808618999/
 webpackConfig.module.rules.push({
   test: /\.m?js/,
   resolve: {
     fullySpecified: false,
   },
 })
 return webpackConfig
}

module.exports = { overrideWebpackConfig }

plugins/craco-silence.js

/**
* Disable warnings & Minimal logs
*/

const path = require('path')

const overrideWebpackConfig = ({ context, webpackConfig, pluginOptions }) => {
 // Disable verbose stat
 webpackConfig.stats = 'errors-only'
 // Disable "Failed to parse source map"
 const pathSep = path.sep
 webpackConfig.module.rules.forEach((rule) => {
   if (rule.loader?.includes(`${pathSep}source-map-loader${pathSep}`)) {
     const { exclude } = rule
     if (typeof exclude === 'array') exclude.push(/node_modules/)
     else rule.exclude = [exclude, /node_modules/]
   }
 })
 return webpackConfig
}

const overrideDevServerConfig = ({
 devServerConfig,
 cracoConfig,
 pluginOptions,
 context,
}) => {
 devServerConfig.client.overlay = {
   warnings: false,
   errors: true,
 }
 return devServerConfig
}

module.exports = { overrideWebpackConfig, overrideDevServerConfig }

plugins/craco-wasm.js

/**
* WebAssembly loader for Webpack 5
*/

const overrideWebpackConfig = ({ context, webpackConfig, pluginOptions }) => {
 const wasmExtensionRegExp = /\.wasm$/
 // Add additional extension for WASM and enable WASM
 webpackConfig.resolve.extensions.push('.wasm')
 webpackConfig.experiments = { asyncWebAssembly: true }
 // Exclude the extension from asset/resource
 const oneOfRule = webpackConfig.module.rules.find((rule) => rule.oneOf)
 if (!oneOfRule) {
   throw new Error(
     `Can't find a 'oneOf' rule under module.rules in the ${context.env} webpack config!`,
     'webpack+rules+oneOf',
   )
 }
 let assetResourceIndex = oneOfRule.oneOf.findIndex(
   (rule) => rule.type === 'asset/resource',
 )
 if (!oneOfRule.oneOf[assetResourceIndex].exclude)
   oneOfRule.oneOf[assetResourceIndex].exclude = []
 oneOfRule.oneOf[assetResourceIndex].exclude.push(wasmExtensionRegExp)
 // Add the wasm loader
 const wasmLoader = {
   test: wasmExtensionRegExp,
   exclude: /node_modules/,
   use: [{ loader: 'wasm-loader' }], // Webpack 5 natively supports wasm-loader
   type: 'webassembly/async',
 }
 oneOfRule.oneOf.splice(assetResourceIndex, 0, wasmLoader)
 return webpackConfig
}

module.exports = { overrideWebpackConfig }

3. Tạo file craco.config.js với nội dung:

require("dotenv-cra").config();

const CracoWasm = require("./plugins/craco-wasm");
const CracoSilence = require("./plugins/craco-silence");
const CracoCompatibility = require("./plugins/craco-compatibility");

module.exports = {
 plugins: [
   {
     plugin: CracoCompatibility,
   },
   {
     plugin: CracoWasm,
   },
   {
     plugin: CracoSilence,
   },
 ],
};



Bước 4. Khởi chạy ứng dụng

Sau khi cài đặt cấu hình package + webpack, bạn cần install lại để project cài lại node-module. Hãy chạy dòng lệnh sau:

npm install --force

Sau khi install thành công, bạn hãy khởi động ứng dụng bằng câu lệnh sau:

npm start

Lúc này, ReactJS sẽ khởi chạy dự án mặc định ở port 3000, chúng ta có thể mở trình duyệt và truy cập đường dẫn http://localhost:3000

Để xây dựng ứng dụng ReactJS đầu tiên, bạn hãy truy cập vào thư mục src trong dự án, tìm đến file src/App.tsx và sửa thành:

import "./App.css";

function App() {
 return <div className="App">Đây là MyApp</div>;
}

export default App;

Chúng ta sẽ thấy kết quả thay đổi trên giao diện.

Lưu ý: Sau khi khởi chạy dự án thành công, nếu bạn sửa đổi các file thì ReactJS sẽ tự động reload các thay đổi đó. Bạn không cần phải khởi động lại server bằng cách thủ công nữa.

Xây dựng giao diện bằng Ant Design

Ant Design (AntD) là thư viện tập hợp các component của React. AntD cung cấp hầu hết các component thông dụng trong ứng dụng web hiện đại như Layout, Button, Icon, DatePicker, vân vân.

Bạn có thể tham khảo và học cách sử dụng các component của AntD tại đây: https://ant.design/components/overview/

Xây dựng ứng dụng đầu tiên

Bước 1. Cập nhật file src/App.css
@import "~antd/dist/antd.css";

Bước 2. Cập nhật file src/App.tsx
import { Button } from "antd";
import "./App.css";

function App() {
 return (
   <div className="App">
     <Button type="primary">Button</Button>
   </div>
 );
}

export default App;

Khi đã khởi chạy,  ứng dụng ReactJS sẽ tự động cập nhật khi có thay đổi. Đây là kết quả:

Đoạn code ở file src/App.tsx có sử dụng Button được import từ AntD. Đây được gọi là component. Component có thể dễ dàng được tái sử dụng ở  nhiều nơi.

 <Button type="primary">Button</Button>

Button là một component. Typepropscomponent nhận vào với giá trị là “primary”.

Xây dựng component

Tiếp theo, chúng ta sẽ xây dựng một component.

Bước 1. Tạo file src/components/walletInfo.tsx

import { Col, Row } from "antd";

const WalletInfo = ({ address, balance }: { address: string; balance: number }) => {
 return (
   <Row gutter={[24, 24]}>
     {/* Wallet address */}
     <Col span={24}>
       <Row gutter={[12, 12]}>
         <Col>Wallet Address:</Col>
         <Col>{address}</Col>
       </Row>
     </Col>
     {/* Wallet balance */}
     <Col span={24}>
       <Row gutter={[12, 12]}>
         <Col>Balance:</Col>
         <Col>{balance}</Col>
       </Row>
     </Col>
   </Row>
 );
};

export default WalletInfo;

Component Wallet Info sẽ hiển thị thông tin address (địa chỉ) và balance (số dư) của ví. Component này sẽ nhận vào 2 props là: address với kiểu dữ liệu string, và balance với kiểu dữ liệu number.

Lưu ý: Trong một component, bạn có thể sử dụng một component khác.

Bước 2. Sử dụng component vừa tạo, cập nhật nội dung file src/App.tsx

import { Button, Col, Row } from "antd";
import WalletInfo from "components/walletInfo";
import "./App.css";

function App() {
 return (
   <Row justify="center">
     <Col>
       <WalletInfo address="Đây là wallet address" balance={0} />
     </Col>
     {/* Button connect wallet */}
     <Col span={24} style={{ textAlign: "center" }}>
       <Button type="primary">Kết nối ví</Button>
     </Col>
   </Row>
 );
}

export default App;

Bước 3. Quay lại trang http://localhost:3000 để xem kết quả:

Tìm hiểu về State

State được dùng để lưu trữ trạng thái dữ liệu hiện tại.

Để hiểu rõ hơn, chúng ta hãy cùng tìm hiểu qua ví dụ sau: Khi click vào button “Kết nối ví”, hành động được thực hiện là lấy địa chỉ ví từ một bên thứ 3, sau đó lưu địa chỉ ví lại (state) để hiển thị lên giao diện.

Để thực thi ví dụ trên, hãy cập nhật file src/App.tsx như sau:

import { useState } from "react";

import { Button, Col, Row } from "antd";
import WalletInfo from "components/walletInfo";

import "./App.css";

function App() {
 // state: wallet address (type = string, default value = '')
 const [walletAddress, setWalletAddress] = useState<string>("");

 const connectWallet = async () => {
   // TODO: fetch wallet address
   const newWalletAddress = "1234567890";
   setWalletAddress(newWalletAddress);
 };

 return (
   <Row justify="center">
     <Col>
       <WalletInfo address={walletAddress} balance={0} />
     </Col>
     {/* Button connect wallet */}
     <Col span={24} style={{ textAlign: "center" }}>
       {walletAddress !== "" ? (
         <Button type="primary" disabled>
           Đã kết nối
         </Button>
       ) : (
         // Call connectWallet function when click Button
         <Button type="primary" onClick={connectWallet}>
           Kết nối ví
         </Button>
       )}
     </Col>
   </Row>
 );
}

export default App;



Sau khi đã hoàn thành, bạn hãy quay lại trang http://localhost:3000, click vào button và xem kết quả:

Tìm hiểu về UseEffect và UseCallback

UseEffect được dùng để quản lý vòng đời của component.

UseEffect thường được sử dụng để thực hiện một số hành động như: khi khởi tạo component, các giá trị phụ thuộc (được khai báo ở ngoặc vuông cuối hàm, có thể là Props và State) thay đổi. Ngoài ra, UseEffect có thể được sử dụng cho một vài trường hợp khác nhưng mình sẽ không đề cập ở đây.

UseCallback được sử dụng để tối ưu quá trình render, ngăn hàm bị tạo lại.

Để hiểu rõ hơn về UseEffect, chúng ta sẽ cùng đi đến ví dụ tiếp theo: Khi walletAddress thay đổi, App tự động lấy balance của walletAddress đó và cập nhật vào state để hiển thị lên giao diện.

Để thực thi ví dụ trên, hãy cập nhật file src/App.tsx như sau:

import { useCallback, useEffect, useState } from "react";

import { Button, Col, Row } from "antd";
import WalletInfo from "components/walletInfo";

import "./App.css";

function App() {
 // state: wallet address (type = string, default value = '')
 const [walletAddress, setWalletAddress] = useState<string>("");
 // state: balance (type = number, default value = 0)
 const [balance, setBalance] = useState<number>(0);

 const connectWallet = async () => {
   // TODO: fetch wallet address
   const newWalletAddress = "1234567890";
   setWalletAddress(newWalletAddress);
 };

 const fetchBalance = useCallback(async () => {
   // TODO: fetch balance
   let balance = walletAddress.length;
   setBalance(balance);
 }, [walletAddress]);

 useEffect(() => {
   fetchBalance();
 }, [fetchBalance]);

 return (
   <Row justify="center">
     <Col>
       <WalletInfo address={walletAddress} balance={balance} />
     </Col>
     {/* Button connect wallet */}
     <Col span={24} style={{ textAlign: "center" }}>
       {walletAddress !== "" ? (
         <Button type="primary" disabled>
           Đã kết nối
         </Button>
       ) : (
         // Call connectWallet function when click Button
         <Button type="primary" onClick={connectWallet}>
           Kết nối ví
         </Button>
       )}
     </Col>
   </Row>
 );
}

export default App;

Sau khi đã hoàn thành, bạn hãy quay lại trang http://localhost:3000 và xem kết quả:

Vậy là chúng ta đã tìm hiểu qua các khái niệm căn bản, cách tạo và sử dụng Component, Props, State, UseEffect, và UseCallback. Các bạn có thể tham khảo thêm ở trang chủ của ReactJS tại: https://reactjs.org/docs/hooks-intro.html

Các bước tạo và kết nối ví tiền điện tử lên DApp

Crypto Wallet (Ví tiền điện tử)

Để tham gia sử dụng và phát triển trên mạng lưới blockchain Solana, bạn cần sở hữu một crypto wallet (ví tiền điện tử) để quản lý tiền điện tử. Đó có thể là ví C98, Phantom, hay Slope…

Để nhận và chuyển tiền, người gửi và người nhận sẽ được định danh bằng wallet address (địa chỉ ví).

PublicKey là một cách hiển thị khác của Address, tương tự với cách Momo cho phép nhận và chuyển tiền bằng số điện thoại của khách hàng.

Balance là số dư của các loại tiền (Token, Coin) hiện có trong ví.

Goki - Hỗ trợ kết nối ví điện tử

Cùng sự phát triển của công nghệ, ngày càng nhiều loại ví ra đời phục vụ nhu cầu của người dùng. Goki được xây dựng để giúp developer hỗ trợ nhiều ví nhất trên DApp.

Sau đây, chúng ta sẽ học cách cài đặt và kết nối ví Phantom.

Cài đặt ví Phantom

Bước 1: Đầu tiên, bạn hãy tìm tiện ích Phantom trên Chrome tại Đây và chọn “Thêm vào Chrome”.

Bước 2: Bạn hãy chọn Pin để ghim ứng dụng lên góc trên bên trái trang trình duyệt.

Bước 3: Giờ chúng ta sẽ tiến hành tạo ví.

  • Chọn biểu tượng Phantom trên trình duyệt, bạn sẽ thấy giao diện như hình dưới.
  • Chọn “Create a new wallet” để tạo ví mới.

Bước 4: Bạn sẽ được cấp 12 từ khóa. Đây là cụm từ khóa bí mật để khôi phục ví trên thiết bị khác. Hãy nhấn “Copy” để sao chép và lưu lại cụm từ khóa này sang một file khác, hoặc viết ra giấy để lưu giữ.

Bước 5: Khi đã lưu trữ cụm từ khoá xong, chọn “I saved my Secret Recovery Phrase” → Continue → Finish. Tới đây, bạn đã tạo thành công ví Phantom cho bản thân rồi đấy!

Kết nối ví Phantom bằng Goki

Bước 1. Cập nhật src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { WalletKitProvider } from "@gokiprotocol/walletkit";

import App from "./App";
import "./index.css";

ReactDOM.render(
 <React.StrictMode>
   <WalletKitProvider
     defaultNetwork="devnet"
     app={{
       name: "My App",
     }}
   >
     <App />
   </WalletKitProvider>
 </React.StrictMode>,
 document.getElementById("root")
);

Bước 2. Cập nhật file src/App.tsx như sau:
import { useCallback, useEffect, useState } from "react";
import { useWalletKit, useSolana, useConnectedWallet } from "@gokiprotocol/walletkit";

import { Button, Col, Row } from "antd";
import WalletInfo from "components/walletInfo";

import "./App.css";

function App() {
 // State: balance (type = number, default value = 0)
 const [balance, setBalance] = useState<number>(0);
 // Goki hooks
 const wallet = useConnectedWallet();
 const { connect } = useWalletKit();
 const { disconnect, providerMut } = useSolana();

 const fetchBalance = useCallback(async () => {
   // TODO: fetch balance
   if (wallet && providerMut) {
     let balance = await providerMut.connection.getBalance(wallet.publicKey);
     return setBalance(balance);
   }
   setBalance(0);
 }, [providerMut, wallet]);

 useEffect(() => {
   fetchBalance();
 }, [fetchBalance]);

 return (
   <Row justify="center">
     <Col>
       <WalletInfo address={wallet?.publicKey.toBase58() || ""} balance={balance} />
     </Col>
     {/* Button connect wallet */}
     <Col span={24} style={{ textAlign: "center" }}>
       {wallet ? (
         <Button type="primary" onClick={disconnect}>
           Disconnect
         </Button>
       ) : (
         // Call connectWallet function when click Button
         <Button type="primary" onClick={connect}>
           Connect Wallet
         </Button>
       )}
     </Col>
   </Row>
 );
}

export default App;



Bước 3:

Quay lại trang http://localhost:3000 và click vào Button “Connect Wallet”. Module kết nối ví sẽ hiện lên như hình dưới. Bạn chọn Continue, sau đó chọn loại ví đã cài đặt (ở đây là Phantom).

Sau khi kết nối ví thành công. Thông tin địa chỉ ví và số dư sẽ tự động được cập nhật.

Tham khảo project mẫu tại:

https://github.com/DescartesNetwork/solana-academy/tree/init-dapp-ui

Tài liệu tham khảo

https://create-react-app.dev/docs/getting-started

https://create-react-app.dev/docs/adding-typescript/

Về Sentre Protocol

Sentre Protocol là nền tảng mở All-in-One hoạt động trên mạng lưới Solana, cung cấp DApp Store và giao thức mở giúp tích lũy thanh khoản. Với Sentre:

  • Người dùng có thể cài đặt các DApp yêu thích của mình và trải nghiệm thế giới DeFi trên một nền tảng duy nhất;
  • Lập trình viên và đối tác có thể phát hành DApp thông qua Sen Store, tận dụng tài nguyên có sẵn và tự do đóng góp cho nền tảng.