Advertisement

How to Build an Ethereum DApp from Scratch using React.

阅读量:

作者:禅与计算机程序设计艺术

1.简介

在本文中, 我旨在指导您搭建一个基于ReactJS的Ethereum DApp. 您将能够从头开始学习如何编写ReactJS代码, 并开发属于自己的去中心化应用(DApp)。

如果对于React或其他任何开发技术尚不熟悉,并且对Solidity、Web3.js、Truffle等技术细节存有疑问,请不要感到焦虑!本文详尽地阐述了整个流程,在此您可以按照步骤逐一实现目标。

2.准备工作

首先,您需要安装以下工具:

如果您想掌握前端开发的基础知识,请学习包括但不限于HTML、CSS和JavaScript等技术。然而,在本文中将不涵盖这些知识点。但对于那些希望深入学习的朋友,则建议参考相关的高级教程或资源。

3.项目结构

复制代码
    ├── app
    │   ├── src
    │   │   └── components
    │   │       ├── App.tsx    # 根组件
    │   │       ├── Button.tsx # 按钮组件
    │   │       ├── Card.tsx   # 卡片组件
    │   │       └── Input.tsx  # 输入框组件
    │   ├── index.css            # 全局样式文件
    │   ├── index.html           # HTML模板文件
    │   └── index.tsx            # 入口文件
    └── test                      # 测试用例目录
    └── contracts             # Solidity合约源文件目录
        ├── SimpleStorage.sol # 简单的存储合约
    
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

我们将会创建一个React项目,这个项目主要由三个部分组成:

  1. src目录:主要用于存储所有React组件的源代码文件;
  2. index.*文件:主要用于存储静态页面模板、全局样式表以及项目的入口文件;
  3. test目录:用于存储测试用例,并后续将阐述如何编写测试用例。

4.项目环境配置

接下来,我们将设置项目的开发环境。

创建项目

首先,我们需要创建一个空目录,然后进入该目录执行如下命令:

复制代码
    npm init -y
    touch README.md package.json tsconfig.json.gitignore
    mkdir app && cd app
    mkdir src && touch src/.gitkeep
    echo "module.exports = { \"compilerOptions\": { \"esModuleInterop\": true } }" > jsconfig.json
    
      
      
      
      
    
    代码解读

上述命令执行后,会生成如下文件:

  • package.json: 作为项目的包清单存在;

    • .gitignore: 列出了应被排除于Git版本控制之外的文件夹或目录。
    • README.md: 项目详细技术说明文档。
    • tsconfig.json: 存储TypeScript编译所需的配置信息。
    • jsconfig.json: JavaScript项目的编译设置存放于此。
    • app/: 应用项目的源码管理位于此目录内。
    • src/: 源码的主要开发部分存放在该目录中。
    • .gitkeep: 防止Git误判空目录的一个占位符文件。
  • 在索引位置设置的[HTML|CSS]标记表示网页模板和统一样式文档的位置;

  • 指定为项目入口点的[.tsx]标记位于项目根目录中,并指示用于构建和运行React应用程序的位置。

安装依赖

为了使项目正常运行,我们需要安装一些必要的依赖包:

复制代码
    npm install --save react react-dom @types/react @types/react-dom typescript webpack webpack-cli html-webpack-plugin css-loader style-loader sass-loader node-sass @babel/core babel-loader @babel/preset-env eslint eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin
    
    
    代码解读

上述命令会安装以下依赖包:

  • reactreact-dom: 负责构建用户界面组件。
    • @types/react@types/react-dom: 包含React相关的类型定义文件。
    • typescript: 提供静态类型编译器。
    • webpackwebpack-cli: 提供打包引擎用于构建Web应用。
    • html-webpack-plugin: 提供功能用于生成用于构建Web应用的HTML文件插件。
    • css-loader/style-loader: 提供功能用于处理CSS文件。
    • sass-loader/node-sass: 支持SCSS格式的样式表文件导入与解析。
    • @babel/corebabel-loader: 提供功能作为JavaScript转ES5兼容模式工具。
    • @babel/preset-env: 提供特定规则集作为JavaScript转ES5迁移方案指南。
    • eslinteslint-plugin-react: 提供功能用于执行代码格式化检查以确保代码可读性。
    • @typescript-eslint/parser@typescript-eslint/eslint-plugin: 提供功能用于执行代码风格检查以确保项目遵循统一编码规范。

初始化项目

接下来,我们初始化项目,新建一下配置文件:

复制代码
    npx truffle unbox metacoin
    mv contracts/*./src/contracts
    rm -rf build contracts migrations
    sed's/SimpleStorage/MyEthereumDApp/' -i $(grep -rl SimpleStorage.)
    
      
      
      
    
    代码解读

上述命令执行后,会自动完成以下几件事情:

  1. 转移该Solidity代码至指定存储位置。
  2. 移除不必要的文件。
  3. 在所有引用中更换相关变量名。

配置webpack

在打包项目时建议采用Webpack,并在其中修改webpack.config.js文件如下所示

复制代码
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      mode: 'development',
      entry: './src/index.tsx', // 入口文件
      output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'), // 输出路径
      },
      resolve: {
    extensions: ['.ts', '.tsx', '.js'],
      },
      module: {
    rules: [
      {
        test: /\.(j|t)sx?$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader','sass-loader']
      },
    ],
      },
      plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
      devServer: {
    contentBase: './dist', // 静态文件目录
    open: true, // 启动浏览器
    hot: true, // 启用热更新
      },
    };
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

该配置决定了项目入口文件的位置,并将打包后的输出目录一并指定。借助babelClassLoader解析jsx代码,并分别利用styleClassLoader、cssClassLoader和sassClassLoader导入scss样式文件。

设置Babel

为了实现对最新JavaScript特性的支持,在开发过程中需要借助Babel技术将当前项目中的ES6代码转换为兼容的ES5代码。编辑.babelrc文件如下:

复制代码
    {
      "presets": ["@babel/preset-env", "@babel/preset-react"],
      "plugins": []
    }
    
      
      
      
    
    代码解读

上述配置指定了ES6->ES5的转换规则和插件。

设置ESLint

"extends": [
"schema": "https://eslint.org/schemas/v6.0.0/east-gcc Kraken.json", "ignore": ["**/*.ts"], // 不显示类型脚本文件
"rules": {
"indent": {
"functionName": "mustBeAtLeast",
"minimumValue": 2,
"maximumValue": 8
},
// 添加其他相关规则
}
]

复制代码
    {
      "env": {
    "browser": true,
    "commonjs": true,
    "es6": true
      },
      "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
      ],
      "globals": {},
      "parser": "@typescript-eslint/parser",
      "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "project": "./tsconfig.json"
      },
      "plugins": ["@typescript-eslint"]
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

该参数设置决定了项目运行环境、推荐规则集、解析器设置以及插件组件。

添加脚本命令

为了让大家更为简便地执行各种任务, 我们将向项目中添加一些脚本命令至.... 编辑完成后, 文件内容如上所示.

复制代码
    {
      "name": "my-ethereum-dapp",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
    "start": "webpack serve",
    "build": "webpack --mode=production",
    "lint": "eslint src/**/*.{ts,tsx}"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
    "@babel/core": "^7.12.10",
    "@babel/preset-env": "^7.12.11",
    "@babel/preset-react": "^7.12.5",
    "@types/jest": "^26.0.20",
    "@types/node": "^14.14.19",
    "@types/react": "^16.9.53",
    "@types/react-dom": "^16.9.8",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^5.0.2",
    "eslint": "^7.17.0",
    "eslint-plugin-react": "^7.22.0",
    "file-loader": "^6.2.0",
    "fork-ts-checker-webpack-plugin": "^6.1.0",
    "html-webpack-plugin": "^5.0.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^26.6.3",
    "mini-css-extract-plugin": "^1.3.6",
    "node-sass": "^5.0.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^5.0.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "rimraf": "^3.0.2",
    "sass-loader": "^10.1.0",
    "style-loader": "^2.0.0",
    "terser-webpack-plugin": "^5.0.3",
    "ts-jest": "^26.4.4",
    "ts-loader": "^8.0.11",
    "typescript": "^4.1.3",
    "url-loader": "^4.1.1",
    "webpack": "^5.11.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.2"
      },
      "dependencies": {}
    }
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

上述配置增加了两个新的命令:

  1. start: 启动开发服务器,实时刷新浏览器;
  2. build: 打包生产环境的代码。

5.编写React组件

现在,我们已经设置好了开发环境,接下来就可以编写React组件了。

编写Card组件

我们先编写一个简单的卡片组件,显示当前账户的地址和余额。

复制代码
    import React, { useState, useEffect } from'react';
    import Web3 from 'web3';
    
    interface Props {
      account?: string;
    }
    
    function Card(props: Props) {
      const web3 = new Web3();
    
      return (
    <div className="card">
      <p>Address:</p>
      <code>{props.account}</code>
      <br />
      <p>Balance:</p>
      <code>{web3.utils.fromWei(String(props.balance), 'ether')} ETH</code>
    </div>
      );
    }
    
    export default Card;
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

在这里, 我们通过调用Web3.js的方法获取当前账户的地址和余额. 该组件定义了一个名为Props的接口, 传入的对象必须包含account属性. 利用JavaScript Web APIs自动生成对应的交易信息, 并将其绑定到组件变量上.

编写Button组件

我们再编写一个按钮组件,用来连接钱包和发布合约。

复制代码
    import React, { useState, useEffect } from'react';
    import Web3 from 'web3';
    
    interface Props {
      onClick: () => void;
    }
    
    function Button(props: Props) {
      const [isConnected, setIsConnected] = useState<boolean>(false);
      const web3 = new Web3(window.ethereum);
    
      useEffect(() => {
    async function connect() {
      if (!window.ethereum) {
        alert('Metamask is not installed!');
        return;
      }
    
      try {
        await window.ethereum.request({ method: 'eth_requestAccounts' });
        setIsConnected(true);
      } catch (error) {
        console.log(error);
      }
    }
    
    connect();
      }, []);
    
      return (
    <button disabled={!isConnected || props.onClick === undefined} onClick={() => props.onClick?.()}>
      Connect Wallet and Publish Contract
    </button>
      );
    }
    
    export default Button;
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

在这里, 我们创建了一个名为Props的接口, 其中包含了 onClick 这个回调函数. 当用户点击按钮时, 我们会通过调用 Web3() 函数来构造一个 Web3 对象, 从而实现与钱包的连接. 为了记录当前是否已连接钱包, 我们定义了一个 useState 钩子. 在 useEffect 钩子中, 我们会尝试连接钱包. 如果连接成功的话, 则会将 isConnected 状态设置为 true; 如果出现错误则会打印错误日志. 最后我们会返回一个按钮元素, 并根据 isConnected 和 onClick 属性来判断按钮是否被禁用. 当按钮被点击时, 如果对应的 onClick 回叫函数存在, 则该回叫函数将会被执行.

编写Input组件

我们再编写一个输入框组件,用来保存合约发布者的名称。

复制代码
    import React, { useState } from'react';
    
    interface Props {
      value?: string;
      onChange: (event: React.FormEvent<HTMLInputElement>) => void;
    }
    
    function Input(props: Props) {
      return (
    <input type="text" value={props.value} placeholder="Enter a name for your contract" onChange={props.onChange} />
      );
    }
    
    export default Input;
    
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

在这里的基础上,我们创建了一个名为Props的接口类型。该接口包含了数值属性以及一个响应文本修改事件的回调函数。它将被用来绑定到某个文本编辑区域,并在编辑发生时触发特定的行为。该组件生成一个带有placeholder字段的输入区域,并将通过props onChange机制来处理编辑后的反馈信息。

编写ContractInfo组件

我们再编写一个展示合约信息的组件。

复制代码
    import React, { useState, useEffect } from'react';
    import Web3 from 'web3';
    
    interface Props {
      address: string | null;
    }
    
    function ContractInfo(props: Props) {
      const [contractName, setContractName] = useState('');
      const web3 = new Web3();
    
      useEffect(() => {
    if (!props.address) {
      return;
    }
    
    const getContractName = async () => {
      try {
        const provider = new web3.providers.WebsocketProvider('ws://localhost:8545');
    
        const instance = new web3.eth.Contract(
          JSON.parse(
            '{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"","type":"string"}],"name":"SetMessage","type":"event"}]}'
          ),
          props.address,
          {
            from: (await provider.listAccounts())[0],
            gasPrice: web3.utils.toHex((await provider.getGasPrice()) * 1.5),
          }
        );
    
        const name = await instance.methods.getMessage().call();
    
        setContractName(name as any);
      } catch (error) {
        console.log(error);
      }
    };
    
    getContractName();
      }, [props]);
    
      return (
    <div className="card">
      <h2>Contract Info</h2>
      <p><strong>Address:</strong></p>
      <code>{props.address}</code>
      <br />
      <p><strong>Name:</strong></p>
      <code>{contractName}</code>
    </div>
      );
    }
    
    export default ContractInfo;
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

在这里, 我们创建了一个名为Props的接口, 其中包含有一个address属性, 该属性指定与要展示信息相关联的合约地址. 我们注册了两个useState hook, 分别用于保存合约名称以及当前账户的余额. 在异步操作中, 我们加载了合约ABI数据并构建了完整的合约对象. 接下来, 我们调用钩子函数完成操作, 获取存储于链上字符串并将其保存到contractName状态. 最后, 我们渲染一个Card组件以显示该合约地址及其名称.

全部评论 (0)

还没有任何评论哟~