Fronted for Web3 Dapp: روشهای خوب

1. اتحادیه های تبعیض آمیز ایمنی نوع
با استفاده از اتحادیه های تبعیض آمیز در TypeScript kind
یک رویکرد ایمن از نوع برای الگوبرداری از steets ، رویدادها و دستورات مختلف ایجاد می کند:
// State modeling with discriminated unions
type TxNormalStateIdle = { kind: 'idle' };
type TxNormalStateSending = { kind: 'sending' };
type TxNormalStateSuccess = { kind: 'success'; txHash: Hash };
type TxNormalStateError = { kind: 'error'; error: BaseError };
export type TxNormalState =
| TxNormalStateIdle
| TxNormalStateSending
| TxNormalStateSuccess
| TxNormalStateError;
2. الگوی فرمان-ایالتی
جداسازی اقدامات کاربر (دستورات) ، رویدادهای سیستم (رویدادها) و وضعیت برنامه (STAT) به شما امکان می دهد تا یک جریان داده یک طرفه واضح و روشن ایجاد کنید:
// Commands - user actions
type Submit = {
kind: 'submit';
data: { tx: () => WriteContractParameters };
};
// Events - system responses
type SuccessEvent = {
kind: 'success';
data: { hash: Hash };
};
// State - application state
type TxAllowanceStateApproveNeeded = {
kind: 'approve-needed';
amount: bigint;
};
3. دستگاه های حالت محدود
عملکرد مدل سازی از طریق دستگاه های حالت محدود (FSM) باعث می شود که دولتها از انتقال حالتها آشکار شده و از انتقال غیرقانونی آمار جلوگیری می کنند:
reducer: (state, event) => {
switch (event.kind) {
case 'check':
return {
...state,
kind: 'checking-allowance',
amount: event.data.amount,
};
case 'enough-allowance':
switch (state.kind) {
case 'rechecking-allowance':
return { ...state, kind: 'sending' };
default:
return {
...state,
kind: 'has-allowance',
amount: event.data.amount,
};
}
// ...
}
}
4. برنامه نویسی واکنشی с rxjs
استفاده از مشاهدات برای عملیات ناهمزمان بهترین ترکیب و امکان سانسی جریان را در صورت لزوم فراهم می کند (متأسفانه ، در زیر جعبه ، تمام API های Web3 هنوز در آنجا پیچیده اند):
return from(
this.client.simulateContract(<SimulateContractParameters>cmd.data.tx())
).pipe(
switchMap(response =>
from(this.client.writeContract(response.request)).pipe(
switchMap(txHash => {
return from(
this.client.waitForTransactionReceipt({
hash: txHash,
confirmations: 1,
})
).pipe(
map(() =>
txAllowanceEvent({
kind: 'success',
data: { hash: txHash },
})
)
);
}),
catchError(err => of(txAllowanceEvent({ kind: 'error', data: err })))
)
),
startWith(txAllowanceEvent({ kind: 'submitted' })),
take(2)
);
5. خطا به عنوان داده ، نه استثنا
تمام خطاها به صورت داده های معمولی ارائه می شود ، نه exepshenes. این به شما امکان می دهد تا کد را تا حد ممکن واضح و قابل پیش بینی کنید ، زیرا آنها به لایه ای از منطق تجارت نشت نمی کنند:
// Error is just another type of state
type TxAllowanceStateError = {
kind: 'error';
amount: bigint;
error: BaseError;
};
// Error is handled through the normal event flow
catchError(err =>
of(txAllowanceEvent({
kind: 'error',
data: err.cause ?? err
}))
)
// Inside business logic there is no exception handling, only data handling
tap(result => {
if (result.kind === 'success') {
// do something
} else {
// do something else, show alert for example
}
})
6. معماری مبتنی بر افزونه
جداسازی عملکرد پیچیده به افزونه های ترکیبی و خودمختار:
@Injectable()
export class TxAllowanceStore extends FeatureStore<
TxAllowanceCommand,
TxAllowanceEvent,
TxAllowanceState,
TxAllowanceStateIdle
> {
// Implementation
}
7. کلاسهای پایه انتزاعی قراردادهای رابط
استفاده از کلاسهای اساسی اساسی برای تعیین قراردادهای روشن:
@Injectable()
export abstract class WalletBase {
public abstract requestConnect(): void;
public abstract getCurrentAddress(): Observable<Hash | null>;
public abstract getBalance(): Observable<string>;
}
8. تایپ سازی قوی در برنامه
استفاده از ژنرال ها برای ارائه ایمنی نوع:
export class TxNormalStore extends FeatureStore<
TxNormalCommand,
TxNormalEvent,
TxNormalState,
TxNormalStateIdle
> {
// Implementation
}
9. تعریف صریح از حالتهای اولیه
نقطه شروع منطق تجارت برای هر افزونه:
initialValue: {
kind: 'idle',
amount: 0n,
}
10. کاهش دهنده های خالص انتقال دولت
استفاده از توابع خالص برای به روزرسانی حالت برای حفظ پیش بینی:
reducer: (state, event) => {
switch (event.kind) {
case 'reset':
return { kind: 'idle', amount: 0n };
case 'success':
return { ...state, kind: 'success', txHash: event.data.hash };
// ...
}
}
مثال 1: جریان تأیید توکن با FSM
این مثال جریان کامل برای تأیید توکن را نشان می دهد ، که یک الگوی مشترک در برنامه های DEFI است. اجرای ظریف پردازش های پیچیده حالت را پردازش می کند:
// From tx-with-allowance.ts
function checkAllowance(
client: PublicClient & WalletClient,
data: Check['data']
) {
return from(
client.readContract({
address: data.token,
abi: erc20Abi,
functionName: 'allowance',
args: [data.userAddress, data.spender],
})
).pipe(
map(actualAllowance => {
const isEnoughAllowance = actualAllowance >= data.amount;
if (isEnoughAllowance) {
return txAllowanceEvent({
kind: 'enough-allowance',
data: {
spender: data.spender,
token: data.token,
amount: data.amount,
},
});
} else {
return txAllowanceEvent({
kind: 'not-enough-allowance',
data: {
spender: data.spender,
token: data.token,
amount: data.amount,
},
});
}
}),
catchError(() =>
of(
txAllowanceEvent({
kind: 'not-enough-allowance',
data: {
spender: data.spender,
token: data.token,
amount: data.amount,
},
})
)
),
take(1)
);
}
مثال 2: خط لوله اجرای معامله
این مثال نشان می دهد پیپلان برای معاملات با پردازش خطای بعدی:
// From tx-normal.ts
submit: cmd => {
const tx = <SimulateContractParameters>cmd.data.tx();
console.log('tx: ', tx);
return from(this.client.simulateContract(tx)).pipe(
switchMap(response =>
from(this.client.writeContract(response.request)).pipe(
switchMap(txHash => {
return from(
this.client.waitForTransactionReceipt({
hash: txHash,
confirmations: 1,
})
).pipe(
map(() =>
txNormalEvent({ kind: 'success', data: { hash: txHash } })
)
);
}),
catchError(err => {
return of(txNormalEvent({ kind: 'error', data: err }));
})
)
),
catchError(err => {
return of(
txNormalEvent({
kind: 'error',
data: err.cause ?? err,
})
);
}),
startWith(txNormalEvent({ kind: 'submitted' })),
take(2)
);
}
مثال 3: کنترل کننده تأیید توکن
این مثال نحوه پردازش فرآیند تأیید توکن را با شبیه سازی + اجرا نشان می دهد:
// From tx-with-allowance.ts
approve: (cmd: Approve) => {
return defer(() =>
from(
this.client.simulateContract({
account: cmd.data.userAddress,
address: cmd.data.token,
abi: erc20Abi,
functionName: 'approve',
args: [cmd.data.spender, MAX_UINT],
gas: 65000n,
})
)
).pipe(
switchMap(response =>
from(this.client.writeContract(response.request)).pipe(
switchMap(value => {
return from(
this.client.waitForTransactionReceipt({
hash: value,
confirmations: 1,
})
).pipe(switchMap(() => checkAllowance(this.client, cmd.data)));
}),
catchError(err => {
return of(
txAllowanceEvent({
kind: 'approve-fail',
data: err.cause ?? err,
})
);
})
)
),
catchError(err =>
of(
txAllowanceEvent({
kind: 'approve-fail',
data: err.cause ?? err,
})
)
),
startWith(
txAllowanceEvent({
kind: 'approve-sent',
data: {
spender: cmd.data.spender,
token: cmd.data.token,
amount: cmd.data.amount,
},
})
),
take(2)
);
}
مثال 4: انتقال دولت
// From tx-allowance.ts
reducer: (state, event) => {
console.log('event: ', event);
switch (event.kind) {
case 'reset':
return { kind: 'idle', amount: 0n };
case 'success':
return { ...state, kind: 'success', txHash: event.data.hash };
case 'error':
return { ...state, error: event.data, kind: 'error' };
case 'check':
return {
...state,
kind: 'checking-allowance',
amount: event.data.amount,
};
case 'enough-allowance':
switch (state.kind) {
case 'rechecking-allowance':
return { ...state, kind: 'sending' };
default:
return {
...state,
kind: 'has-allowance',
amount: event.data.amount,
};
}
// Additional cases omitted for brevity
}
}
در اصل ، من از تمام این رویکردها برای هر برنامه کاربردی استفاده می کنم ، اما در Web3 این امر به ویژه مرتبط است ، زیرا وقتی کیف پول کاربر از بسیاری از ایالت ها عبور می کند (چه در زمان اتصال و چه در زمان معامله) ، پس از آن بدون الگوی عادی تطبیق ، اگر دیگر به دست آمده ، از انبوهی از انباشت بپاشید. رویکرد فوق به شما امکان می دهد تا همه چیز را بسازید تا کامپایلر تمام حالتهای ممکن و انتقال بین آنها را بررسی کند و بار را روی توسعه دهنده کاهش دهد.