web3前端最佳实践指南
众所周知,目前前端开发有三大主流框架:
React
Vue
Angular
在 Web2
中,这三个框架都或多或少的被使用,尤其是 Vue
,在国内前端开发者中更为受欢迎。
然而,在 Web3
领域,React
算是一家独大。无论是去中心化应用(dApps)、生态项目的官网,还是区块链浏览器,React
都是开发者的首选框架。可以说,超过 90% 的区块链项目前端都采用 React
进行开发,究其原因是因为在Web3
中,React
拥有着更广泛的生态系统和丰富社区资源。
同时,为了优化用户体验,近年来前端开发者越来越倾向于使用服务端渲染(SSR),将需要展示的 HTML 数据提前在服务器端生成。这也造就了 React
服务端渲染框架 Next.js
的流行, 尽管推上有很多人吐槽 Next.js
,但无疑其目前是最好的React
服务端渲染框架。
而且Next.js
也可以用来写后端 api, 集成数据库等。所以框架的最佳选择非 Next.js
莫属。
服务端组件 or 客户端组件
Next.js
支持服务端组件和客户端组件
- 服务端组件:服务端渲染的组件,在服务端生成 HTML
- 客户端组件: 客户端渲染的组件, 通常用于展示具有交互和动态数据的组件上,由客户端加载 js 后执行渲染, 需要在文件顶部添加
"use client";
作为开发者,需要正确的区分是使用服务端组件还是客户端组件。不然可能会出现意外的 水合(hydration) 错误。这里有一个简单的判断标准,凡是涉及到连接的用户钱包地址的组件都使用客户端组件。例如
- 连接钱包的按钮
- 展示用户地址下的
token
数量 - 用户进行链上交互等
其他的诸如,展示智能合约的余额、读取智能合约的变量等用户地址无关的组件则使用服务端组件。而且在请求数据时,还可以利用服务端组件的缓存能力。例如在读取智能合约的某个变量时,设置缓存时间为 10 秒钟,在这 10 秒内,任何用户请求都不会再次发起 RPC 请求,如果使用的是付费 RPC 节点,则可以很大程度上减少 RPC 的用量。
链上交互
作为 web3
项目,首先要做的就是在页面上添加连接钱包的按钮。以 EVM
系的生态为例,可供选择的库有
rabinbowkit
connectkit
web3modal
这三者并无较大的不同,区别在于 UI 上,底层都用到了 wagmi
wagmi
是一套用于链上交互的 React hooks, 基于 viem
构建。例如
- 连接钱包
useConnect
- 获取当前连接的用户地址
useAccount
- 发送交易
useSendTransaction
- 读取合约变量
useReadContract
如果当前组件是服务端组件,无法使用 hook
, 则建议直接使用 viem
,其无需另外安装,wagmi
已默认导出了 viem
的所有方法。
请求状态跟踪
对于服务端组件而言,本身就具有异步的特性,对于 RPC 请求来说,可以轻易的跟踪请求状态
import { createPublicClient, erc20Abi, http } from 'viem'
import { mainnet } from 'viem/chains'
export const revalidate = 10 // 缓存10秒
export const runtime = 'edge'
export const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_ENDPOINT)
})
// 异步组件
export default async function TotalSupply() {
// 读取合约数据
const data = await publicClient.readContract({
address: '0x123...',
abi: erc20Abi,
functionName: 'totalSupply'
})
return <div>{data.toString()}</div>
}
上面是一个展示 erc20 合约中 totalSupply
变量值的组件,设置了组件缓存时间是 10 秒。意味着,这 10 秒内有用户访问这个组件时不会再次发送 RPC 请求。如果你要读取的是一个永远不会变的值,则可以将缓存设置为**'force-cache'
**
在父组件中展示请求中的状态
<Suspense fallback={<div>Loading</div>}>
<TotalSupply />
</Suspense>
对于客户端组件来说,由于 wagmi
内部使用 Tanstack Query
对于每一次的链上交互都可以轻易的获取请求状态。例如
'use client'
import { erc20Abi } from 'viem'
import { useReadContract } from 'wagmi'
export default function TotalSupply() {
const { data, isLoading, isError } = useReadContract({
address: '0x123...',
abi: erc20Abi,
functionName: 'totalSupply',
query: {
staleTime: 10 * 1000 // 缓存10秒
}
})
if (isLoading) return <div>Loading...</div>
if (isError) return <div>Error</div>
return <div>{data?.toString()}</div>
}
善用 MultiCall
经常出现的业务场景是,需要在页面展示多个合约的数据。例如
-
用户持有的 token 数量
-
A 合约中用户的质押 token 数量
-
B 合约中用户可领取的奖励数量
如果对于每个数据都进行一次查询,首先会多消耗 RPC 的请求额度,其次会增加页面最终展示数据的时间。使用 multiCall
则可以一次性查询所有数据。
import { multicall } from 'wagmi/actions'
export const App = async () => {
const result = await multicall(config, {
contracts: [
{
address: '0x122',
abi: [],
functionName: 'balanceOf',
args: [userAddress]
},
{
address: '0x122',
abi: [],
functionName: 'staked',
args: [userAddress]
},
{
address: '0x122',
abi: [],
functionName: 'reward',
args: [userAddress]
}
]
})
}
multiCall 是一个智能合约,通过合约的方式进行一次性调用
样式库的选择
React 中有几种书写 CSS 样式的方法
- 内联样式
- 通过
className
添加外部样式 - css module: 创建一个跟组件同名的文件
组件名.module.css
- 样式组件:使用库
styled-components
创建具有 CSS 样式的组件
相比于上面的几种方案更加推荐使用 tailwind
。
tailwind
是一款移动端优先的原子化 CSS 框架。内置了多个标准化的 CSS 样式,具有高度可定制化的特点。
而且 web3
项目通常都要做移动端兼容,如果不使用 tailwind
,这就意味着你需要写一大堆的 media query
, 类似于 @media (min-width: 640px)
用来监听浏览器宽度的变化,并对不同的宽度设置不同的样式。但使用 tailwind
则可以轻易的对不同的浏览器宽度使用不同的样式。tailwind
内置了 5 种浏览器宽度
sm
: 640pxmd
: 768pxlg
: 2014pxxl
: 1280px2xl
: 1536px
例如想要一个 div 在不同的浏览器宽度下显示不同的颜色,则可以
<div className="bg-red-600 md:bg-green-600 w-8 h-8"></div>
由于 tailwind
是一款移动端优先的 CSS 框架。 在浏览器宽度低于 sm
时,将显示red-600
。 浏览器宽度达到 md
或大于 md
后将显示green-600
组件库
由于 web3
项目通常都具有很强的个性化。因此对于一些样式固定的组件库而言就显得不太适合。即使组件库中的组件提供了一些外部参数可以让开发者动态的调整样式,但总归有一些无法满足的情况。
因此就不得不重新去写相关的基础组件。但从头开始写又过于麻烦。所以我们需要一种包含基础样式的组件库。
那么不得不推荐 shadcn/ui
这个组件库。作为目前最受欢迎的组件库,不同于其他组件库的全局安装模式。shadcn/ui
可以分组件安装。而且安装的是组件源码。你可以轻松的根据自己的项目需要去调整样式。亦或者是做一些定制化的修改。
# 安装 shadcn-ui
npx shadcn-ui@latest init
# 安装 button 组件
npx shadcn-ui@latest add button
使用组件
import { Button } from '@/components/ui/button'
export function App() {
// 通过 className 进行样式定制
return <Button className="bg-orange-600 w-8 h-8">Button</Button>
}
或者可以使用别人提供的酷炫的组件效果:
链接
- Next.js: https://nextjs.org/
- wagmi: https://wagmi.sh/
- viem: https://viem.sh/
- RainbowKit: https://www.rainbowkit.com/
- ConnectKit: https://docs.family.co/connectkit
- Tailwind: https://tailwindcss.com/
- shadcn/ui: https://ui.shadcn.com/