Dapp 前端工具: Drizzle Store

速成指南:在 React Dapp 中使用 drizzle store

如果你最近一直在尝试用 React 和 Redux 构建 dapp 前端,估计你已经意识到不能只专注于 dapp 做什么,为了使用 web3 组件和合约实例,以及从区块链同步数据,这些事情上,你已经在配置上花了大量时间。现在我告诉你,你来对地方了,Drizzle 库正好是你现在所需要的。我们来看看它是怎么工作的,怎么用它来构建 dapp 前端。

什么是 drizzle store?它是怎么工作的?

drizzle store 的主要目的是提供一个可用的 redux store 版本,可以通过配置来管理所有与 web3 实例、合约实例、事件、交易和调用相关的事情。

使用 Drizzle Store ,你需要先建一个 drizzle 实例。

drizzle 实例负责保存以下属性:

  • web3 实例
  • 合约实例:包含 drizzle 合约实例的对象,用合约名称作为key。
  • 合约列表:drizzle 合约的数组
  • Redux store
  • 选项(Options):用于配置 drizzle store

其中加粗的是在 React 组件中会用到的属性。

你需要为 drizzle 实例配置合适的选项,让 drizzle 按照你的想法管理存储和跟踪数据。这里有可配置选项的完整列表和描述。

drizzle store 的状态

drizzle store 管理下面这些状态:

Drizzle state: source

accounts / accountBalances:账户列表用web3.eth.getAccounts获取,相应余额用web3.eth.getBalance获取,在初始化 drizzle 过程中存储在 state 中。如果在选项中定义了账户拉取间隔,那么在达到间隔时间时,将会再次获取账户和其余额。

contracts :用于存储事件和调用结果。如果一个新区块被广播,合约对象的synced属性会被设为false,表示合约准备同步,当合约被同步后,synced属性设为true(所有合约已经重新调用)

当初始化合约时,通过 web3 实例构建 ( new web3.eth.Contract()) 的合约对象会被修改,会在callsend方法的之上添加cacheCallcacheSend方法。一旦这个过程完成,所有在选项中为合约指定的事件将被订阅,所有传入的事件将被添加到合约的事件属性下的 state 中。

调用的结果会被在使用cacheCall时获取的参数哈希索引。

currentBlock:最新的区块,由web3.eth.getBlock()生成的对象。

在 drizzle 初始化过程中,创建一个 saga 事件通道,通过web3.eth.subscribe('newBlockHeaders', (error, result)=>{})来监听每个新传入的区块,并将其保存在 state 中。如果把syncAlways选项设为true,那么当接收到一个新区块时所有合约调用都会重新执行。如果syncAlways设为fase,并且如果保存的任一合约与区块中现存的某个交易有关,那么所有相关合约的调用都会重新执行。

drizzleStatus :包含 drizzle 状态信息的对象。这个对象唯一的属性就是initialized,这个属性会在初始化过程完成时被设为 true(初始化 web3 和合约,获取账户和对应的余额)。

transactions :用于通过交易哈希key或者临时key(交易哈希还不可用时)存储交易结果。在创建交易时,事件通道与通过[contractName].methods.[methodName]获取的交易对象关联,以便可以监听如“transactionHash”, “confirmation”, “receipt” 和 “error”的事件,然后创建一个临时 key 并保存在transactionStack中,这个临时 key 的索引会作为cacheSend方法的结果返回。

transactionStack :用于存储交易 key。创建交易时,交易哈希还不可用,临时 key 会被 push 到这个交易堆栈,所以如果交易失败,用户可以通过这个临时 key 从 state 中的transactions对象得到错误信息。一旦交易哈希可用了,临时 key 就会被替代。

web3 :包含 web3 实例状态的对象。如果可用则会获取networkId,如果用户钱包的网络没有在networkWhiteList选项中定义的,则networkMismatch会设为 true ,如果网络没有配置错误,那么这个属性不定义。

web3 可能有的状态:initializing , initializedfailed

当你创建一个 drizzle 实例的时候发生了什么?

当你第一次创建drizzle实例时,构造函数会如下开展:

  • 首先为构造函数提供的选项会与默认选项合并,这意味着如果在默认选项中没有定义值,则会使用默认值

    默认选项如下:

web3: {                              
  fallback: {                           
    type: 'ws',                         
    url: 'ws://127.0.0.1:8545'              
  }                      
},                        
contracts: [],               
events: {},          
polls: {          
   blocks: 3000         
},                     
syncAlways: false,                 
networkWhitelist: []
  • 一旦环境准备好了(在 web 浏览器的情况下,window对象是可用的、可加载的),构造函数立即分配DRIZZLE_INITIALIZING启动初始化过程,如下图:

    <center>Drizzle初始化过程</center>

如上面的流程图,初始化过程首先从初始化 web3 实例开始,并且这是通过提供给 drizzle 构造函数的选项中的web3字段来完成的。

// options regarding web3 instantiation
web3: {
    customProvider,
    fallback: {
      type
      url
    }
  },

web3 实例化可以用下面的图描述:

<center>Web3实例化</center>

调用和交易

调用:

当你想要从以太坊区块链读取数据时,你可以使用 web3 合约的call或者用drizzle 添加的cacheCall。两者的不同在于cacheCall会返回参数hash(用于调用 state 中的存储结果的索引),会同步区块链上最新的可用数据,而call只会返回调用时区块链上当时可用的数据。

交易

像调用一样,你可以用send或者cacheSend

cacheSend方法返回用于引用交易结果的 key 的索引,这个索引会存储在 state 中的transactions对象里。而这个 key 则可以通过cacheSend返回的索引从transactionStack数组恢复。

动态的添加和删除合约

添加合约

如果你想要添加一个合约,你可以用drizzle.addContract() 或者使用动作ADD_CONTRACT

删除合约

你也可以用drizzle.deleteContract() 或者使用动作 DELETE_CONTRACT来删除合约

Generate Store

如果你想要定制 drizzle store ,你应该用generateStore,它可以构建 store 实例并配置它,可以添加你的应用 reducers , sagas 和 中间件(middlewares)。默认情况下,如果你没有在 drizzle 构造函数中指定 store ,那么 drizzle 会自己创建一个。

import {generateStore, Drizzle} from @drizzle/store
const store = generateStore({
   drizzleOptions,
   appReducers,
   appSagas,
   appMiddlewares
})
const drizzle = new Drizzle(drizzleOptions, store)

generateStore有一个对象有如下属性:

  • drizzleOptions : 用于配置 drizzle store 的选项;
  • appReducers : 包含所有应用reducer的对象,会通过Redux的combineReducers添加到drizzle reducer;
  • appSagas : 包含应用sagas的数组;
  • appMiddlewares : 包含要添加到存储区的中间件的数组。

唯一需要的属性就是drizzleOptions,其他属性都有默认值。

举例:简单存储

在这个例子中,我们将会构建一个简单的dapp,它可以从合约存储读取并且更新数据。

1. 创建一个 truffle 项目并部署合约

首先,在目录中创建一个空项目“drizzle-example”,用truffle init来实例化这个项目。

> mkdir drizzle-example
> cd drizzle-example && truffle init

用 VScode 或者任何你喜欢的代码编辑器打开这个项目,创建合约 SimpleStorage 和它的迁移文件。

更新truffle-config.js文件,用 ganache 作为开发网络,设置编译器版本,然后将下面的内容添加到到 simplestage 合约中:

contract SimpleStorage {
  string public data = "hello";
  event DataChanged(string indexed data);
  function setData(string calldata _data) external {
     data = _data;
     emit DataChanged(_data);
  }
}

打开 truffle 控制台,然后编译并部署这个合约。

> truffle console
> compile
> migrate

2、初始化 React 应用

在同一个项目中,用create-react-app创建一个名为 client 的新文件夹。

> npx create-react-app client

安装依赖:web3, drizzle store, redux, react-redux, redux-logger:

> cd client
> npm i web3 @drizzle/store redux react-redux redux-logger --save

在client/src drizzle和 components 下创建三个新文件夹:

> cd src
> mkdir components drizzle contracts

返回到truffle-config.js文件并在 module.exports 下面添加这行代码:

contracts_build_directory: path.join(__dirname, "/client/src/contracts"),

还需要在文件的开头导入 path 模块:

const path = require("path");

现在我们需要重新部署项目,以便让合约的 json 接口在合约文件夹中可用。

3、配置 drizzle :

在 drizzle 文件夹下,创建两个文件:drizzleOptions.js 和 drizzleContext.js

配置应用程序,我们需要 drizzle 的两样东西:

  • drizzle 实例,它将为我们提供 web3 和合约实例。
  • drizzle store。

我们将使用 React Context API,让 drizzle 实例在组件中可用:

import React, { createContext, useContext } from "react";

const Context = createContext();

export function DrizzleProvider({ drizzle, children }) {
  return &lt;Context.Provider value={drizzle}>{children}&lt;/Context.Provider>;
}

export function useDrizzleContext() {
  const context = useContext(Context);
  return context;
}

接下来我们在 drizzleOptions.js 文件中添加以下配置:

import SimpleStorage from "../contracts/SimpleStorage.json";
const options = {
   contracts: [SimpleStorage],
   events: {
      SimpleStorage: ["DataChanged"],
   },
};
export default options;

4. 应用程序与 drizzle store 建立链接

首先,我们将把 redux-logger 中间件添加到存储中,它将被提供给 drizzle 实例,然后使用我们在 drizzleContext.js 文件中构建的 drizzle provider 和 redux provider 为应用程序提供存储和 drizzle 实例。

下面就是 index.js 文件:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { DrizzleProvider } from "./drizzle/drizzleContext";
import { Provider as ReduxProvider } from "react-redux";
import options from "./drizzle/drizzleOptions";
import { Drizzle, generateStore } from "@drizzle/store";
import logger from "redux-logger";

const store = generateStore({
  drizzleOptions: options,
  appMiddlewares: [logger],
});

const drizzle = new Drizzle(options, store);

ReactDOM.render(
  &lt;React.StrictMode>
    &lt;ReduxProvider store={drizzle.store}>
      &lt;DrizzleProvider drizzle={drizzle}>
        &lt;App />
      &lt;/DrizzleProvider>
    &lt;/ReduxProvider>
  &lt;/React.StrictMode>,
  document.getElementById("root")
);

5. 创建 Loading container 组件

在组件文件夹下创建一个新文件,名为 LoadingContainer.js :

import React from "react";
import { connect } from "react-redux";

const LoadingContainer = ({ web3, accounts, initialized, children }) => {
  if (initialized) {
    if (web3.status === "failed") {
      return (
        &lt;main className="container loading-screen">
          &lt;div className="pure-g">
            &lt;div className="pure-u-1-1">
              &lt;h1>⚠️&lt;/h1>
              &lt;h3>
                Please use the Chrome/FireFox extension MetaMask, or dedicated
                Ethereum browsers, and make sure to connect one of your accounts
                to the dapp.
              &lt;/h3>
            &lt;/div>
          &lt;/div>
        &lt;/main>
      );
    }

    if (web3.status === "initialized" && Object.keys(accounts).length === 0) {
      return (
        &lt;main className="container loading-screen">
          &lt;div className="pure-g">
            &lt;div className="pure-u-1-1">
              &lt;h1>🦊&lt;/h1>
              &lt;h3>
                &lt;strong>{"We can't find any Ethereum accounts!"}&lt;/strong>
                Please check and make sure Metamask or your browser Ethereum
                wallet is pointed at the correct network and your account is
                unlocked.
              &lt;/h3>
            &lt;/div>
          &lt;/div>
        &lt;/main>
      );
    }

    if (web3.status === "initialized") {
      return children;
    }
  }

  return "Loading...";
};

const mapStateToProps = (state) => {
  return {
    web3: state.web3,
    accounts: state.accounts,
    initialized: state.drizzleStatus.initialized,
  };
};

export default connect(mapStateToProps)(LoadingContainer);

一旦创建了 LoadingContainer 组件,我们需要立即把它添加到 index.js 文件中:

&lt;LoadingContainer>
  &lt;App/>
&lt;/LoadingContainer>

6. 与合约交互

现在该修改 App.js 文件了:

import React, { useEffect, useState } from "react";
import { useDrizzleContext } from "./drizzle/drizzleContext";
import { connect } from "react-redux";

function App({ dataCall, account }) {
  const drizzle = useDrizzleContext();

  const [data, setData] = useState("");
  const [cacheKey, setCacheKey] = useState(null);

  useEffect(() => {
    // call the simpleStorage contract data method
    const cacheKey = drizzle.contracts.SimpleStorage.methods.data.cacheCall();
    setCacheKey(cacheKey);
  }, []);

  const onSubmit = (event) => {
    event.preventDefault();
    drizzle.contracts.SimpleStorage.methods
      .setData(data)
      .send({ from: account, gas: 30400 })
      .then((receipt) => {
        console.log(receipt);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const onChange = (event) => {
    setData(event.target.value);
  };

  return (
    &lt;div>
      &lt;h1>Data in contract storage:&lt;/h1>
      {dataCall[cacheKey] && dataCall[cacheKey].value && (
        &lt;p>{dataCall[cacheKey].value}&lt;/p>
      )}
      &lt;form onSubmit={onSubmit}>
        &lt;input value={data} onChange={onChange} />
        &lt;input type="submit" value="submit" />
      &lt;/form>
    &lt;/div>
  );
}

const mapStateToProps = (state) => {
  return {
    dataCall: state.contracts.SimpleStorage.data,
    account: state.accounts[0],
  };
};

export default connect(mapStateToProps)(App);

好了,现在你可以在浏览器中试一下了。 github)有本文中的完整项目。

点赞 1
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO