4

Dapp 前端工具: Drizzle Store

 2 years ago
source link: https://learnblockchain.cn/article/3454
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如果你最近一直在尝试用 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启动初始化过程,如下图:

    Drizzle初始化过程

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

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

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

Web3实例化

调用和交易

当你想要从以太坊区块链读取数据时,你可以使用 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 <Context.Provider value={drizzle}>{children}</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(
  <React.StrictMode>
    <ReduxProvider store={drizzle.store}>
      <DrizzleProvider drizzle={drizzle}>
        <App />
      </DrizzleProvider>
    </ReduxProvider>
  </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 (
        <main className="container loading-screen">
          <div className="pure-g">
            <div className="pure-u-1-1">
              <h1>⚠️</h1>
              <h3>
                Please use the Chrome/FireFox extension MetaMask, or dedicated
                Ethereum browsers, and make sure to connect one of your accounts
                to the dapp.
              </h3>
            </div>
          </div>
        </main>
      );
    }

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

<LoadingContainer>
  <App/>
</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 (
    <div>
      <h1>Data in contract storage:</h1>
      {dataCall[cacheKey] && dataCall[cacheKey].value && (
        <p>{dataCall[cacheKey].value}</p>
      )}
      <form onSubmit={onSubmit}>
        <input value={data} onChange={onChange} />
        <input type="submit" value="submit" />
      </form>
    </div>
  );
}

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

export default connect(mapStateToProps)(App);

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK