跳转至

掌握 dApps

在掌握 dApps 的过程中,您将学习如何创建高级的分散式应用程序,这些应用程序使用我们在前面章节中看到的智能契约。我们将从头开始经历所有的步骤,包括计划、开发代码和测试应用程序。首先,您将从了解 dapp 的结构开始,这样您就可以从头开始高效地创建新的 dapp。你将通过以太坊和松露的安装来为你的产品使用它。然后,您将学习如何创建出色的用户界面,向人们展示正确的内容而不显得杂乱无章。最后,您将创建与 dApp 交互所需的智能合约,并且您将集成这些合约以允许用户从界面轻松地与合约交互。

在本章中,我们将讨论以下主题:

  • 介绍 dApp 架构
  • 安装以太坊和松露
  • 设置和配置以太坊和松露
  • 创建 dApps
  • 创建用户界面
  • 将智能合约连接到 web 应用程序

介绍 dApp 架构

构建一个分散的应用程序意味着做出高层次的软件决策来指导我们想法的设计。我们正在制定步骤,这样我们就可以流畅地创建 dApp,而不会陷入设计决策中。它还意味着计划智能合约将如何与 dApp 通信,用户将如何与 dApp 交互,以及我们希望最终产品具有什么样的功能。

在设计应用程序时,我们希望重点关注用户体验,以便他们在使用最终的 dApp 时感到舒适。这就是为什么在我们开始编码之前,对它的外观有一个清晰的愿景是很重要的,因为如果我们想拥有一个现代的 dApp,能够对技术用户做出响应,我们就必须更加专注于提供关于我们应用程序每个元素的广泛信息。

例如,让我们假设您想要创建一个博客 dApp,用户可以在那里发布关于特定主题的文章。你会如何设计这样的 dApp?你从哪里开始?事实上,没有一个完美的系统可以从头开始设计你理想的 dApp 这更像是一个互动的过程,在这个过程中,随着你的发展,你会回到绘图板来阐明你的想法。

就我个人而言,我喜欢从详细描述我的想法开始,尽可能多的澄清,并列出必须不惜一切代价实现的功能。例如,我想为 ICO 爱好者创建一个分散的博客,他们喜欢阅读新项目,并通过 ICO、TGE 或 STO 筹集资金,因为他们处于相同的生态系统中,被合资企业、股权支付、区块链创新等所包围。这个博客将奖励用户令牌,他们将能够在系统中交换奖励。更高的可见性、关于每篇文章的高级指标、优质文章和投票决策都可以用代币来交换。以下是这款 dApp 的特点:

  • 通过标题和标签查找文章的搜索系统
  • 像聊天一样实时回复评论部分
  • 作者的写作工具
  • 提升文章知名度的推广工具
  • 一个可见性分数,表示每篇文章在 dApp 内容海洋中的可见性
  • 设计工具,为您的每个评论和出版物自定义外观

然后,画出构成整个 dApp 的组件之间的关系图会有所帮助——不同的后端服务、前端和区块链交互。这个想法是你可以直观地看到你的应用程序中什么是重要的。您可以包括您认为与最终 dApp 相关的任何信息,以便提醒您什么是重要的,什么是不重要的。你会发现你自动丢弃了无关紧要的东西。请看下图:

你可以得到更多的技术,但在创意设计阶段这并不重要,你的目标是填补你计划创造的空白。它会在你意识到之前告诉你什么是重要的。然后,您可以创建智能合约中的数据结构类型的方案、用户界面的构建块以及用于交付敏捷交互的服务器的性能特性。

然后,问自己几个设计问题是很重要的,例如:

  • 我们将如何处理用户的突然增加?
  • 如果我们面临 DDoS 之类的攻击,必须部署什么系统来保持应用程序运行?
  • 从用户的角度来看,一个成功的交互是什么样的?
  • 我们认为我们会在哪里面临不确定性,哪些事情还不够清楚?
  • 完成所有核心功能的实际日期是什么时候?
  • 这个想法的一个最小可行产品 ( MVP )是什么样子的?哪些功能是不可或缺的?
  • 我们什么时候能完成 MVP?
  • 我们为临时用户解决了什么问题?
  • 我们提供了哪些他们在别处找不到的解决方案?
  • 我们如何在开始之前就发现或创造对我们想法的需求?
  • 我们能让早期用户帮助我们交付迎合这类人的产品吗?

  • 我们理想中的用户是什么样的:他们做什么,他们的爱好是什么,他们在网上逛到哪里(这样我们就可以把他们带入开发过程,创造出更好的产品)?

  • 我们的目的是什么,我们为什么要这样做?说出为什么背后的三个原因。
  • 我们在哪里可以做得更好、更快、更有效率?

从那里,您可以开始从这些想法创建您的分散式应用程序。一定要创建一个免费且易于使用的谷歌表单,来澄清所有你认为相关的问题。在你的回答中要明确和详细。永远记住专注于解决用户的问题。成功交付高质量的分散式应用程序的关键是要有一个坚实的基础,说明你为什么要这样做,这样你才能在困难时期保持动力——这就是问题如此重要的原因。面对困难的任务,从内心获得动力是一个诀窍。

让我们回顾一下到目前为止你所学的内容。您学习了使用几个 mind 工具构建分散式应用程序的步骤,以暴露您想法的弱点,这样您就可以创建可靠的应用程序,而不会陷入设计不佳的交互中。你明白伟大的问题是清晰思维的核心基础。你想问自己尽可能多的问题,写下来记住你的目的,这样你才能有充分的动力继续下去,完成你的目标。继续阅读下一节,了解更多关于设置开发环境的信息。

安装以太坊和松露

要创建真正强大的去中心化应用程序,我们需要以太坊和松露的本地版本。你不可能自己得到以太坊;您需要使用一个客户端,在本例中是 Geth。Truffle 是一个在你的机器上创建、部署和测试 dApps 的框架,不需要等待外部服务,所有这些都在一个地方完成。

以太坊有不同的实现,最著名的是奇偶校验和 Geth。当安装以太坊时,你实际上做的是得到一个实现其协议的客户端,所以你可以选择你最喜欢的开发系统。

我们继续安装以太坊和松露吧。首先,要在 Mac 上运行以太坊,需要运行以下命令:

brew update && brew upgrade
brew tap ethereum/ethereum
brew install ethereum

这将在几分钟内编译出你的 Mac 所需的所有代码。要在 Windows 上获得 Geth,你必须从他们的官方网站下载二进制文件:https://geth.ethereum.org/downloads/;您也可以获得其他系统的二进制文件,但是如果可能的话,从终端安装 Geth 会更容易也更有趣。然后,只需打开geth.exe文件运行以太坊。

要在 Linux 上安装以太坊,您必须在终端上执行以下代码行:

sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum 
sudo apt-get update && sudo apt-get upgrade
sudo apt-get install ethereum

现在,要得到松露,你需要做一些额外的步骤:

  1. 首先,在 http://nodejs.org 的官方网站上安装 Node.js LTS
  2. 然后,用文件资源管理器打开它,运行安装过程
  3. 完成后,运行node -vnodejs -v;如果它不工作,请验证您已经安装了它
  4. 通过运行npm i -g truffle安装truffle

现在,您只需在一个空文件夹上运行以下命令,就可以将 Truffle 用于您的项目,这将生成任何truffle项目所需的文件结构:

truffle init

设置和配置以太坊和松露

现在我们有了所需的工具,我们将设置基本的文件结构,这样我们就有了一个干净的环境来处理所有我们想要的 dApps。只要需要,您就可以反复使用它,因为它已经设置了所有的依赖项。

首先,让我们创建一个名为dapp的文件夹,它将包含我们所有的文件。然后,用你的终端或命令行,执行truffle init来设置 Truffle,确保你在dapp文件夹中。

在那个文件夹中安装 Truffle 之后,运行npm init来设置 Node.js 的package.json文件,这将允许你安装 npm 插件。它会问你一些关于你的项目的一般信息;简单地填写你喜欢的或者按回车键让它们空着,这是我通常做的,除非我打算把那个项目分发给其他人使用。

您将看到创建了以下文件夹:

  • 你的合同将去向何方。现在,它有一个迁移合同,在您改进代码时更新您的合同。
  • 这里是你定义如何部署你的智能合同,构造器将有什么参数,等等。
  • 你的智能合约和 dApps 的测试将在这里进行。
  • package.json:主 npm 文件,用于从节点注册表安装软件包。
  • truffle-config.js:一个配置文件,定义你将如何连接到区块链,你将使用什么以太坊账户,等等。

安装所需的软件包

我们现在要做的是安装使用 React 和 webpack 所需的基本包。首先,使用以下命令将您的npm版本更新到最新版本:

npm i -g npm@latest

如果您还没有这样做,请转到您的dapp项目文件夹,并安装webpack及以下内容:

npm i -S webpack webpack-cli

Webpack 是一个实用程序,它将所有的 JavaScript 文件合并成一个巨大的、易于管理的 JavaScript 文件,这样就可以优化开发时间。

在 Webpack 之后,安装所有的babel依赖项。Babel 是一个与 webpack 配合使用的实用程序,它可以获取您的 JavaScript 文件并将其转换为最新版本,这样每个浏览器都可以兼容新的 JavaScript 功能,因为不同浏览器之间有很大的差异需要标准化。Babel 就是这么做的,你可以这样安装它:

npm i -S @babel/core @babel/preset-env @babel/preset-react babel-loader

然后,我们需要安装react.js,因为我们将在我们的项目中使用它,如下所示:

npm i -S react react-dom

设置 webpack

我们现在可以生成webpack.config.js文件,在这里我们将指定如何处理我们的 JavaScript 文件,以及组合版本将被部署到哪里。在您的dapp/ folder的根级别创建一个空的webpack.config.js文件,配置如下:

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    }
}

模块导出是在node.js项目中使用的导出对象。它包含了rules数组,在这里您可以指出哪些文件必须通过哪些编译器,在本例中是babel-loader。entry 和 output 属性定义了我们的文件在合并后的生成位置。用一些附加信息来扩展 webpack 配置文件,以定义 HTML 结果文件;这是将 JavaScript 文件捆绑在一起自动生成有效的 HTML 页面所必需的。安装以下装载机:

npm i -S html-webpack-plugin html-loader

像这样更新您的 webpack 配置:

const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },
    plugins: [
        new html({
            template: './src/index.html',
            filename: './index.html'
        })
    ]
}

设置源文件夹

让我们来看看设置源文件夹的以下步骤:

  1. 创建一个src文件夹,所有的开发代码都将存放在这里。现在,您的项目设置应该是这样的:

  1. src/中创建一个名为index.html的新文件,代码如下:
<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>Startup</title>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>
  1. 对象将是我们的 React 项目开始的地方。设置好 HTML、webpackbabel之后,我们可以开始创建将在我们的项目中使用的主react.js文件。在src/ folder中,创建一个名为index.jsx的文件,它将包含我们最初的react.js文件:
import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>The project has been setup.</div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

这里,我们导入了React和 ReactDOM,用于连接 React 和我们的 HTML 文件。然后,我们创建一个Main类,它有一个简单的构造函数和一个render()函数,该函数返回一条消息,确认项目已经正确设置。

  1. 最后,您可以使用以下代码编译这些文件,其中-p表示生产:
webpack -p
  1. 请记住在您的项目文件夹中执行它。编译完文件后,您需要运行一个静态服务器,它会将文件传送到您的浏览器,这样您就可以使用 dApp 了。为此,使用以下命令安装http-server:
npm i -g http-server
  1. 然后,为您的分发文件夹运行它:
http-server dist/
  1. 在浏览器上打开localhost: 8080,实时查看您的 dApp 项目:

恭喜你!您现在有了一个工作启动项目,您可以为您想要创建的其他 dApps 复制它。

通过执行以下步骤,您可以在 GitHub 上发布项目,以便在其他需要的情况下克隆它:

  1. 打开https://github.com,点击 new 创建一个新的存储库。将其命名为dapp,选择 gitignore 节点和一个 MIT 许可证。这是我创建的一个:https://github.com/merlox/dapp
  2. 现在,回到您的终端,如果您的系统上安装了git,请键入git init。这将在你的文件夹中启动一个新的 GitHub 项目。
  3. 然后,当你用自己的凭证提交新文件时,你需要告诉 GitHub 你想更新哪个库;您可以使用以下命令永久完成所有这些操作:
git config remote.origin.url https://<YOUR-USERNAME>:<YOUR-PASSWORD>@github.com/<YOUR-USERNAME>/dapp
  1. 使用以下命令从存储库中提取初始许可证文件:
git pull
  1. git add .添加文件,用git commit -m提交。首先,提交它们并用git push origin master推动它们。然后,您将在新的存储库中看到您的文件。

记住不要推动任何新的变更,因为您希望这个存储库对于使用相同文件结构的未来项目保持原样。

创建 dApps

现在,您已经准备好使用 Truffle、React、Webpack 和 Solidity 创建 dApps。为了尽快获得所需的知识,我们将经历创建一个完全有效的分散式应用程序所需的所有步骤。在这一章中,我们将创建一个音乐推荐社交媒体平台,人们将能够在这个平台上发布他们喜欢的歌曲,以帮助他们的朋友找到有趣的音乐来欣赏,所有这些都存储在智能合同中,而无需集中的服务器。

我们将首先创建智能合同,然后是用户界面,最后,我们将使用web3.js将它们组合在一起。当主界面完成后,我们将测试我们的分散式应用程序,以确保它正常工作。

创建智能合同

在开始创建智能合同之前,让我们先定义一下我们需要它做什么:

  • 我们需要一个数组,以字符串或字节 32 格式存储每个用户的音乐推荐
  • 定义用户信息的结构
  • 每个用户推荐的映射
  • 一组被关注的用户,这样我们就可以看到新的音乐更新

像往常一样,我们首先创建基本的智能contract结构:

pragma solidity 0.5.0;

contract SocialMusic {

}

然后,我们定义将要使用的变量,如下所示:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
}

每个用户结构将保存该用户推荐的所有音乐。现在,我们需要创建函数来添加新的音乐推荐。我们没有删除或修改过去推荐的功能,因为我们希望它成为分享过去和现在音乐品味的永久场所,如下所示:

pragma solidity 0.5.0;

contract SocialMusic {
 struct User {
 bytes32 name;
 uint256 age;
 string state; // A short description of who they are or how they feel
 string[] musicRecommendations;
 address[] following;
 }
 mapping(address => User) public users;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }
}

addSong()函数将歌曲名作为一个字符串,并将该歌曲推送到特定以太坊地址的音乐推荐数组中。_songName的长度必须介于 1 和100字符之间,以避免过大或空的建议。

然后,我们需要功能来创建新用户和跟随他人。如果人们不想设置他们的nameagestate,他们将能够只通过他们的地址发布音乐推荐;它们是匿名的,所以setup函数是可选的:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
    }
}

setup函数必须至少接收用户的_name,其他参数是可选的,并且在设置之前提出的建议将与该用户保持链接。下面是follow函数的样子:

// To follow new users
function follow(address _user) public {
    require(_user != address(0));
    users[msg.sender].following.push(_user);
}

我们只是向一组后续用户推送一个新地址。您可以使用 remix 部署您的合同,以手动测试所有功能是否正常工作。为了用 Truffle 部署它,我们首先需要设置truffle-config.js配置文件,并确保我们的SocialMusic.sol文件在我们项目的contracts/ folder中。正如您在前面的课程中所学的,要为ropsten设置truffle-config.js,我们需要在第 63 行取消对ropsten对象的注释:

    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/${infuraKey}`),
      network_id: 3, // Ropsten's id
      gas: 5500000, // Ropsten has a lower block limit than mainnet
      confirmations: 2, // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
      skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
    },

然后,取消文件开头第 21 行的变量注释:

const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "fj4jll3k.....";

const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();

将您的infuraKey更改为您的个人密钥,您可以在infura.io中创建项目后找到该密钥。如果不知道如何获得infuraKey,回到第三章以太坊资产,执行以下代码:

const infuraKey = "v3/8e12dd4433454738a522d9ea7ffcf2cc";

用您的元掩码助记符创建一个.secret文件,Truffle 将使用它来部署您的第一个以太坊帐户,以部署您的SocialMusic智能合约。如果你正在做一个 git 项目,一定要将.secret添加到你的.gitignore文件中,这样你的账户就不会被泄露给别人看到并窃取你的以太。

在部署您的合同之前,您需要安装钱包提供商,以便truffle能够访问您的帐户:

npm i -S truffle-hdwallet-provider

现在,您需要告诉 Truffle 您想要部署哪些合同。您可以通过打开migrations/1_initial_migrations.js文件并对其进行相应的更改来实现:

const SocialMusic = artifacts.require("./SocialMusic.sol")

module.exports = function(deployer) {
  deployer.deploy(SocialMusic)
}

使用以下内容设置您的秘密助记符后,为ropsten部署您的合同;记得准备足够的ropsten乙醚用于展开:

truffle deploy --network ropsten --reset

如果你以前部署了一个无效的合同,标志将强制 Truffle 部署一个新版本的合同。Truffle 的好处是,在设置好一切之后,您可以快速部署新版本的合同,以便非常高效地进行测试。如果一切运行成功,您将会看到如下内容:

恭喜你!您刚刚部署了您的SocialMusic智能合同。继续阅读,了解如何创建用户界面,我们将使用该界面与智能合同进行交互。

创建用户界面

因为我们已经正确地建立了一个干净的react.js项目,所以我们可以马上开始创建应用程序的用户界面。在继续并集成真正的智能合约代码之前,我们将使用样本数据来检查设计。

打开src/index.js文件,开始设计编码:

import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>
                <h1>Welcome to Decentralized Social Music!</h1>
                <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
                <div className="buttons-container">
                    <button>Setup Account</button>
                    <button>Add Music</button>
                    <button>Follow People</button>
                </div>
                <h3>Latest musical recommendations from people using the dApp</h3>
                <div ref="general-recommendations"></div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

我们在render()函数中编写我们的设计,因为它是所有代码向用户显示的地方。我创建了两个主要部分:一个是h1部分,欢迎人们使用 dApp,用一条短信告诉他们三个开始使用它的按钮;另一个是h3部分,向人们展示由网络上随机的人推荐的最新 10 首音乐:

为了改善应用程序的外观,我们将使用一些基本的 CSS,以便让用户感觉很棒。在src/中创建一个名为index.css的新文件。为了能够在我们的react.js应用程序中使用 CSS,我们需要使用一个理解 CSS 的新加载器。打开您的webpack.config.js文件,将以下部分添加到 rules 块中,就像您对以前的加载器所做的那样:

{
    test: /\.css$/,
    exclude: /node_modules/,
    use: [
        {loader: 'style-loader'},
        {loader: 'css-loader'}
    ]
}

然后,安装css-loaderstyle-loader,如下所示:

npm i -S style-loader css-loader

现在,我们可以在index.css中编写 CSS 代码,如下所示:

body {
    margin: 0;
    font-family: sans-serif;
    text-align: center;
}

button {
    border-radius: 10px;
    padding: 20px;
    color: white;
    border: none;
    background-color: rgb(69, 115, 233);
    cursor: pointer;
}

button:hover {
    opacity: 0.7;
}

.buttons-container button:not(:last-child){
    margin-right: 5px;
}

您应该会看到类似下面的截图:

我们现在需要实现这些特性中的每一个,但在此之前,让我们创建一个名为 Recommendation 的新组件,它将是一个单独的盒子,包含某个用户的个人音乐推荐。下面是它的样子:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

class Main extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div>
                <h1>Welcome to Decentralized Social Music!</h1>
                <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
                <div className="buttons-container">
                    <button>Setup Account</button>
                    <button>Add Music</button>
                    <button>Follow People</button>
                </div>
                <h3>Latest musical recommendations from people using the dApp</h3>
                <div ref="general-recommendations">
                    <Recommendation
                        name="John"
                        address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                        song="Regulate - Nate Dogg"
                    />
                </div>
            </div>
        )
    }
}

class Recommendation extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div className="recommendation">
                <div className="recommendation-name">{this.props.name}</div>
                <div className="recommendation-address">{this.props.address}</div>
                <div className="recommendation-song">{this.props.song}</div>
            </div>
        )
    }
}

ReactDOM.render(<Main />, document.querySelector('#root'))

我们添加了这个新组件,它显示三个 div,包含每个推荐的名称、地址和歌曲。我还在Main组件中添加了一个示例用法,以便您可以看到它是如何工作的。道具只是你从一个组件传递到另一个组件的变量,它们用它们的变量名来标识。让我们用一些 CSS 代码来改进这个东西的外观,如下所示:

.recommendation {
    width: 40%;
    margin: auto;
    background-color: whitesmoke;
    border-radius: 20px;
    margin-bottom: 10px;
    padding: 40px;
}

.recommendation-name, .recommendation-address {
    display: inline-block;
    color: #444444;
    font-style: italic;
}

.recommendation-name {
    margin-right: 10px;
}

.recommendation-address {
    color: rgb(156, 156, 156);
}

.recommendation-song {
    font-weight: bolder;
    font-size: 16pt;
    margin-top: 10px;
}

以下是我们刚刚做出的更改以及更多音乐推荐示例的效果:

就这样——你刚刚创建了你的去中心化SocialMusic平台的 UI。让我们通过集成web3.js使它变得动态,这样我们就可以使用我们的智能合约来允许人们与它进行交互。

将智能合约连接到 web 应用程序

分散式应用程序由智能契约和用户界面组成。现在我们有了这两个元素,我们可以通过使用web3连接前端和后端来组合它们,这是从浏览器与以太坊区块链交互的最强大的工具。

让我们从得到web3.js开始。您可以使用以下命令安装它:

npm i -S web3

然后,将其导入到您的index.js文件中,如下所示:

import React from 'react'
import ReactDOM from 'react-dom'
import web3 from 'web3'
import './index.css'

你真的不需要安装web3,因为如果不是所有用户,大多数用户都会安装元掩码,它会自动将web3.js注入到你的应用程序中。然而,在你的应用程序代码中使用web3是一个很好的实践,因为你可以控制在应用程序中使用哪个版本。

为了将我们的react.js应用程序与我们的SocialMusic契约完全连接起来,我们需要实现契约的每个功能,以便可以从我们设计的用户界面执行它们。我们还想检索特定的信息,比如最近推荐的五首歌曲。有许多方法可以开始实现您的合同,因此,我们将首先让我们的 web 应用程序中的所有三个按钮与智能合同一起正常工作。

设计安装表单

首先,我们有设置帐户按钮。这个应用程序应该向用户显示一个带有几个输入的表单,用来设置他们的姓名、年龄和州,其中只有姓名是必填的。应该有一个取消按钮以及提交按钮。让我们创建一个新的 React 组件,我们称之为Form,它将包括所有这些需求:

class Form extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <form className={this.props.className}>
                <input type="text" ref="form-name" placeholder="Your name" />
                <input type="number" ref="form-age" placeholder="Your age" />
                <textarea ref="form-state" placeholder="Your state, a description about yourself"></textarea>
                <div>
                    <button>Cancel</button>
                    <button>Submit</button> 
                </div>
            </form>
        )
    }
}

我们添加了两个输入,一个是状态的文本区域,两个是取消或提交的按钮。请注意我是如何在 form 元素上添加一个定制的className属性的,这样我们就可以从外部组件动态地设置这个类,否则它将无法工作。我们只想在用户点击 Setup Account 时显示这个表单,所以我们将在我们的三个按钮下面添加表单组件作为隐藏元素,因为这个位置对用户来说更有意义,因为它离鼠标更近。如何隐藏网站中的元素?通过使用将 display 设置为 none 的自定义类。

首先,我们在构造函数中设置新的状态变量,以便在不必要时隐藏表单:

class Main extends React.Component {
    constructor() {
        super()

        this.state = {
            isFormHidden: true
        }
    }
}

然后,我们将Form组件添加到按钮的正下方,该组件带有一个动态类名,该动态类名根据我们希望显示表单的时间而变化:

render() {
  return (
      <div>
          <h1>Welcome to Decentralized Social Music!</h1>
          <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
          <div className="buttons-container">
              <button>Setup Account</button>
              <button>Add Music</button>
              <button>Follow People</button>
          </div>

          <Form className={this.state.isFormHidden ? 'hidden' : ''} />

          <h3>Latest musical recommendations from people using the dApp</h3>
          <div ref="general-recommendations">
              <Recommendation
                  name="John"
                  address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                  song="Regulate - Nate Dogg"
              />
              <Recommendation
                  name="Martha"
                  address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                  song="X - Xzibit"
              />
          </div>
      </div>
  )
}

如您所见,我添加了一个名为isFormHidden的状态元素,它指示表单是否隐藏。然后,我将我们的FormclassName设置为依赖于状态的动态组件,以便它在适当的时候保持隐藏。我们需要使用 React 的状态,因为这是 React 更新显示信息的主要方式。React 是对状态的反应,所以每次它改变时,都会更新整个 web 应用程序。如果我们简单地选择组件并直接更新它的类,React 不会知道发生了什么,而且会变得混乱,因为状态是每个交互式 web 应用程序的基本元素。

然后,创建一个 CSS 类隐藏它,如下所示:

.hidden {
    display: none;
}

看看直播页面上的结果。您不应该看到任何东西,因为您的表单是隐藏的。要显示它,您必须在您的设置帐户按钮上添加一个onClick事件,如下所示:

<button onClick={() => {
    if(this.state.isFormHidden) this.setState({isFormHidden: false})
    else this.setState({isFormHidden: true})
}}>Setup Account</button>

它将读取窗体的状态,以便在单击时隐藏或显示它。你会发现设计一塌糊涂,所以我们必须改进:

为每个输入添加一个新的通用类,用单独的类来区分文本区域,如下所示:

<form className={this.props.className}>
    <input className="form-input" type="text" ref="form-name" placeholder="Your name" />
    <input className="form-input" type="number" ref="form-age" placeholder="Your age" />
    <textarea className="form-input form-textarea" ref="form-state" placeholder="Your state, a description about yourself"></textarea>

    <div>
        <button className="cancel-button">Cancel</button>
        <button>Submit</button>
    </div>
</form>

然后,创建具有所需外观的新 CSS 类,如下所示:

.form-input {
    display: block;
    width: 200px;
    border-radius: 20px;
    padding: 20px;
    border: 1px solid #444444;
    margin: 10px auto;
}

.form-textarea {
    height: 200px;
}

.cancel-button {
    margin-right: 10px;
}

这是造型改变后的样子:

看起来好多了。

实现设置功能

现在,我们必须让它与我们的智能合同进行交互。为此,我们必须在ropsten上创建一个新的已部署契约实例。我们需要地址和ABI接口,你可以在部署SocialMusic合同时松露为你创建的build/contracts/文件夹中快速找到。只需将SocialMusic.json复制到您的src/ folder中,就可以更方便地访问。请记住,如果您决定扩展该文件的功能,您需要用合同的新 ABI 版本替换它。只要重复同样的步骤,你应该很好:

接下来,我们需要一种在 React 应用程序中导入 JSON 文件的方法。幸运的是,如果您使用的是 webpack 2.0 或更新版本(我现在使用的是 4.19),您不需要做任何额外的事情,因为 webpack 默认支持 JSON 文件。在以前的版本中,你必须添加一个新的json-loader来管理这些文件。只需将文件添加到文件的开头,就像这样:

import React from 'react'
import ReactDOM from 'react-dom'
import web3 from 'web3'
import './index.css'
import ABI from './SocialMusic.json'

您可以用您想要的名称导入 JSON 文件;ABI变量的目的是能够读取 JSON 文件的值。然后,用您的智能契约的地址和您的abi接口创建一个变量。记住,如果你丢失了它,你可以随时使用deploy --network ropsten --reset获得一个新地址来部署一个新版本。

class Main extends React.Component {
    constructor() {
        super()

        const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
        const abi = ABI.abi

        this.state = {
            isFormHidden: true
        }
    }
    ...
}

我们希望使用自己的 web3 1.0 版本部署合同,因为 MetaMask 注入的版本已经过时,我们不能依赖不受控制的版本。这就是为什么我们将创建一个新的 web3 实例,如下所示:

import React from 'react'
import ReactDOM from 'react-dom'
import myWEB3 from 'web3'
import './index.css'
import ABI from './SocialMusic.json'

class Main extends React.Component {
    constructor() {
        super()

        window.myWeb3 = new myWEB3(myWEB3.givenProvider)
        const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
        const abi = ABI.abi

        this.state = {
            isFormHidden: true
        }
    }
...

我将web3变量重命名为myWeb3,以避免与 MetaMask 注入的变量混淆。注意myWeb3前面的window关键词;这是用来设置全局变量的,这样你就可以从 dApp 的任何地方访问myWeb3。只要在任何地方都能访问我们定制的 web3,生活就会变得更加轻松。稍后我们将在 async await 中使用 promises。为了能够对这个版本的 webpack/babel 使用 async await,您需要安装babel-polyfill,它负责编译您的异步代码,以便它能够在所有浏览器上正常工作。使用以下内容安装它:

npm i -S babel-polyfill

然后,将其添加到 webpack 配置文件中,如下所示:

require('babel-polyfill')
const html = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: ['babel-polyfill', './src/index.js'],
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
...

现在,我们将使用一些助手函数和setupAccount函数的集成来创建契约的实例。

首先,更新构造函数以在 dApp 加载时执行函数来设置契约实例,如下所示:

constructor() {
    super()

    window.myWeb3 = new myWEB3(myWEB3.givenProvider)
    this.state = {
        isFormHidden: true
    }

    this.setContractInstance()
}

然后,创建正确设置用户帐户和合同所需的函数,如下所示:

async getAccount() {
 return (await myWeb3.eth.getAccounts())[0]
}

async setContractInstance() {
    const contractAddress = '0x0217ED41bC271a712f91477c305957Da44f91068'
    const abi = ABI.abi
    const contractInstance = new myWeb3.eth.Contract(abi, contractAddress, {
        from: await this.getAccount(),
        gasPrice: 2e9
    })
    await this.setState({contractInstance: contractInstance})
}

async setupAccount(name, age, status) {
    await this.state.contractInstance.methods.setup(this.fillBytes32WithSpaces(name), age, status).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})
}

fillBytes32WithSpaces(name) {
    let nameHex = myWeb3.utils.toHex(name)
    for(let i = nameHex.length; i < 66; i++) {
        nameHex = nameHex + '0'
    }
    return nameHex
}

之后,用Form属性更新您的render()函数,告诉 React 当用户点击 setup 按钮时要做什么,当用户点击 Cancel 按钮时要做什么:

render() {
    return (
        <div>
            <h1>Welcome to Decentralized Social Music!</h1>
            <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
            <div className="buttons-container">
                <button onClick={() => {
                    if(this.state.isFormHidden) this.setState({isFormHidden: false})
                    else this.setState({isFormHidden: true})
                }}>Setup Account</button>
                <button>Add Music</button>
                <button>Follow People</button>
            </div>

            <Form
                className={this.state.isFormHidden ? 'hidden' : ''}
                cancel={() => {
                    this.setState({isFormHidden: true})
                }}
                setupAccount={(name, age, status) => {
                    this.setupAccount(name, age, status)
                }}
            />

            <h3>Latest musical recommendations from people using the dApp</h3>
            <div ref="general-recommendations">
                <Recommendation
                    name="John"
                    address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                    song="Regulate - Nate Dogg"
                />
                <Recommendation
                    name="Martha"
                    address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                    song="X - Xzibit"
                />
            </div>
        </div>
    )
}

最后,用新功能更新您的Form组件,以便在用户与输入交互时触发设置功能:

class Form extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <form className={this.props.className}>
                <input className="form-input" type="text" ref="form-name" placeholder="Your name" />
                <input className="form-input" type="number" ref="form-age" placeholder="Your age" />
                <textarea className="form-input form-textarea" ref="form-state" placeholder="Your state, a description about yourself"></textarea>
                <div>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.cancel()
                    }} className="cancel-button">Cancel</button>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.setupAccount(this.refs['form-name'].value, this.refs['form-age'].value, this.refs['form-state'].value)
                    }}>Submit</button>
                </div>
            </form>
        )
    }
}

这里有相当多的变化,所以让我解释一下我做了什么。

首先,我创建了setContractInstance函数,该函数用于用我们的智能契约的地址来设置契约实例,以便我们稍后可以将它用于其他函数。getAccount函数是快速获取当前用户地址的助手。

然后,我创建了setupAccount函数,它接收三个参数,我们希望使用这些参数来设置名为fillBytes32WithSpaces的帮助函数的用户帐户,因为我们需要用这个版本的web3.js填充 bytes32 类型变量中的所有空格,否则它将拒绝交易。这个函数只是为我们部署的智能契约中的setup()函数创建一个事务。

接下来,我为Form组件创建了一些 prop 函数,当用户单击 Cancel 或 Submit 时将执行这些函数。我们希望在用户取消时隐藏表单,所以我简单地将表单的状态设置为 hidden。当用户点击提交时,我们从所有输入中提取数据,并将它们发送给setupAccount函数。请注意我是如何在每个按钮的点击事件中使用event.preventDefault()来避免刷新页面的,因为所有的 HTML 按钮都是提交按钮,理应向服务器发送信息。

请注意,我们在设置用户时使用了.send()函数,这是生成交易和消耗汽油的数据。在里面,我使用了我的以太坊地址,这样它就知道谁应该进行交易:

.send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})

但是您不希望使用相同的地址,因为您的元掩码无法访问它。您可以简单地删除该参数,使函数看起来像这样:

.send()

告诉 React 自动查找用户的地址有时不起作用,因此您可以设置自己的地址。记得解锁你的元掩码并使用 ropsten,然后将你当前的地址粘贴到那里。

一旦更改完成,继续与您的 dApp 进行交互,以验证它确实在向智能合约提交交易。

我们现在要做的是设置添加音乐按钮,以便用户可以创建音乐推荐。首先,通过更新构造函数中的状态对象,用一个新组件创建设计,就像我们之前做的那样:

constructor() {
    super()

    window.myWeb3 = new myWEB3(myWEB3.givenProvider)
    this.state = {
        isFormHidden: true,
        isAddMusicHidden: true
    }

    this.setContractInstance()
}

然后,创建一个新的addMusic()函数,它将把指定的歌曲推送到数组中:

async addMusic(music) {
    await this.state.contractInstance.methods.addSong(music).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})
}

通过将onClick事件监听器添加到 add music 按钮来更新render()函数,这将更新状态以显示 Add Music 表单。然后,添加新的AddMusic组件,就像这样:

render() {
    return (
        <div>
            <h1>Welcome to Decentralized Social Music!</h1>
            <p>Setup your account, start adding musical recommendations for your friends and follow people that may interest you</p>
            <div className="buttons-container">
                <button onClick={() => {
                    if(this.state.isFormHidden) this.setState({isFormHidden: false})
                    else this.setState({isFormHidden: true})
                }}>Setup Account</button>
                <button onClick={() => {
                    if(this.state.isAddMusicHidden) this.setState({isAddMusicHidden: false})
                    else this.setState({isAddMusicHidden: true})
                }}>Add Music</button>
                <button>Follow People</button>
            </div>

            <Form
                className={this.state.isFormHidden ? 'hidden' : ''}
                cancel={() => {
                    this.setState({isFormHidden: true})
                }}
                setupAccount={(name, age, status) => {
                    this.setupAccount(name, age, status)
                }}
            />

            <AddMusic
                className={this.state.isAddMusicHidden ? 'hidden': ''}
                cancel={() => {
                    this.setState({isAddMusicHidden: true})
                }}
                addMusic={music => {
                    this.addMusic(music)
                }}
            />

            <h3>Latest musical recommendations from people using the dApp</h3>
            <div ref="general-recommendations">
                <Recommendation
                    name="John"
                    address="0x5912d3e530201d7B3Ff7e140421F03A7CDB386a3"
                    song="Regulate - Nate Dogg"
                />
                <Recommendation
                    name="Martha"
                    address="0x1034403ad2f8e9da55272CEA16ec1f2cBdae0E5c"
                    song="X - Xzibit"
                />
            </div>
        </div>
    )
}

最后,用class函数定义新的AddMusic组件:

class AddMusic extends React.Component {
    constructor() {
        super()
    }

    render() {
        return(
            <div className={this.props.className}>
                <input type="text" ref="add-music-input" className="form-input" placeholder="Your song recommendation"/>
                <div>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.cancel()
                    }} className="cancel-button">Cancel</button>
                    <button onClick={event => {
                        event.preventDefault()
                        this.props.addMusic(this.refs['add-music-input'].value)
                    }}>Submit</button>
                </div>
            </div>
        )
    }
}

我们遵循了与创建Form组件时相同的步骤。简单地设置渲染 HTML,将AddMusic元素放在Form元素下面,同时保持隐藏,并设置所有的道具函数。然后,创建一个向智能合约添加新歌曲的函数。我们还创建了一个新的状态变量来切换这些按钮的隐藏类。

你可能已经注意到,如果你点击添加歌曲,然后在没有取消的情况下设置帐户,div 会一直打开——我们不希望这样。我们希望在任何给定时间只打开其中的一个部分。我们可以通过一个在打开新组件之前更新隐藏所有组件的状态的函数来实现,如下所示:

hideAllSections() {
    this.setState({
        isFormHidden: true,
        isAddMusicHidden: true
    })
}

然后,在打开该部分之前,向按钮添加函数调用:

<button onClick={() => {
    this.hideAllSections()
    if(this.state.isFormHidden) this.setState({isFormHidden: false})
    else this.setState({isFormHidden: true})
}}>Setup Account</button>
<button onClick={() => {
    this.hideAllSections()
    if(this.state.isAddMusicHidden) this.setState({isAddMusicHidden: false})
    else this.setState({isAddMusicHidden: true})
}}>Add Music</button>

让我们添加最后一个按钮功能,以便能够跟踪人。我们将显示所有已经注册的人的列表,这样用户就可以关注他们希望看到更新的人。为了实现这一点,我们将不得不修改我们的契约,以便我们可以添加一个包含最新新成员的数组,这些新成员将在用户执行setup函数时得到更新,如下面的代码块所示:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
    address[] public userList;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
        userList.push(msg.sender);
    }

    // To follow new users
    function follow(address _user) public {
        require(_user != address(0));
        users[msg.sender].following.push(_user);
    }

    // Returns the array of users
    function getUsersList() public view returns(address[] memory) {
        return userList;
    }
}

重新部署与 Truffle 的合同,如下所示:

truffle deploy --network ropsten --reset

我们不必手动复制新地址并更新 json 文件,而是直接从构建文件夹中获取所有信息,包括地址,如下所示:

import ABI from '../build/contracts/SocialMusic.json'

...
async setContractInstance() {
    const contractAddress = ABI.networks['3'].address
    const abi = ABI.abi
    const contractInstance = new myWeb3.eth.Contract(abi, contractAddress, {
        from: await this.getAccount(),
        gasPrice: 2e9
    })
    await this.setState({contractInstance: contractInstance})
}
...

现在,您不必担心每次都要更新信息,这很好,因为您可以自由地访问不同的外部文件夹,因为 webpack 会将所有需要的信息打包在一起,所以您访问的是src/文件夹之外的文件也没关系。现在,让我们创建所需的功能来获取最新的人员,以便用户可以使用几个新组件来跟踪他们:

class FollowPeopleContainer extends React.Component {
    constructor() {
        super()
    }

    render() {
        let followData = this.props.followUsersData
        // Remove the users that you already follow so that you don't see em
        for(let i = 0; i < followData.length; i++) {
            let indexOfFollowing = followData[i].following.indexOf(this.props.userAddress)
            if(indexOfFollowing != -1) {
                followData = followData.splice(indexOfFollowing, 1)
            }
        }
        return (
            <div className={this.props.className}>
                {followData.map(user => (
                    <FollowPeopleUnit
                        key={user.address}
                        address={user.addres}
                        age={user.age}
                        name={user.name}
                        state={user.state}
                        recommendations={user.recommendations}
                        following={user.following}
                        followUser={() => {
                            this.props.followUser(user.address)
                        }}
                    />
                ))}
            </div>
        )
    }
}

class FollowPeopleUnit extends React.Component {
    constructor() {
        super()
    }

    render() {
        return (
            <div className="follow-people-unit">
                <div className="follow-people-address">{this.props.address}</div>
                <div className="follow-people-name">{myWeb3.utils.toUtf8(this.props.name)}</div>
                <div className="follow-people-age">{this.props.age}</div>
                <div className="follow-people-state">"{this.props.state}"</div>
                <div className="follow-people-recommendation-container">
                    {this.props.recommendations.map((message, index) => (
                        <div key={index} className="follow-people-recommendation">{message}</div>
                    ))}
                </div>
                <button
                    className="follow-button"
                    onClick={() => {
                        this.props.followUser()
                    }}
                >Follow</button>
            </div>
        )
    }
}

FollowPeopleContainer是一个简单的组件,它保存了您可以使用您的帐户关注的所有用户。它将接收来自this.props.followUsersData道具内的Main组件的数据,该组件发送一个包含多达 10 个用户的数组,每个用户最多可以有两个音乐推荐,这样您就可以看到他们是什么类型的人。它还会从阵列中删除已经关注的用户,这样您就不会将他们视为新用户。最后,它生成一个包含所有必需的用户属性的FollowPeopleUnit组件,该组件的功能是将跟踪特定用户所需的信息传输到Main组件。

注意每个FollowPeopleUnit中的key={user.address}属性,因为我们需要能够单独识别它们,这是 React 为避免重复元素而强制执行的。

另一方面,FollowPeopleUnit组件由一组 div 组成,向用户显示所有需要的信息。因为我们在一个名为this.props.recommendations的数组中有两个建议,所以我们必须遍历所有这些建议来单独显示每条消息。当你想用 react 动态生成 HTML 元素时,你必须用圆括号()代替花括号{}来使用数组的.map()函数,因为所有的 HTML 元素都必须在这些类型的括号内。

现在我们有了两个新组件,我们必须定义函数,使它们在我们的Main组件中交互;你可以在 GitHub 上查看完整的代码,这样你就知道在遇到错误时应该把所有东西放在哪里。

首先,我们将地址系统更新为动态的,这样您就不必手动用代码键入您的以太坊地址。例如,假设我们有以下代码:

await this.state.contractInstance.methods.follow(address).send({from: '0x2f6ccd575FA71e2912a31b65F7aFF45C8bf91155'})

相反,我们将使用下面的代码,这对于使我们的 dApp 与许多不同的用户交互是必不可少的。您可以看到我们在setContractInstance()函数中设置了userAddress状态变量:

await this.state.contractInstance.methods.follow(address).send({from: this.state.userAddress})

接下来,我们创建了复杂的getFollowPeopleUsersData()函数,它获取最近的用户地址;如果用户不多的话,最多需要 10 个或者更少。然后,它用我们想要的所有属性创建一个userData对象,并用智能合同状态变量的信息填充它,首先用getUsersMusicRecommendationLength()获得音乐推荐数组的长度,然后用getUsersMusicRecommendation()获得每个单独的音乐推荐。在函数的底部,我们得到了那个特定的人正在关注的用户的数组,以防我们需要访问他们。

如您所见,我们使用了智能合同中的一些新功能。这是因为我们无法在不增加复杂性的情况下实现这一切。以下是我们更新后的智能合同:

pragma solidity 0.5.0;

contract SocialMusic {
    struct User {
        bytes32 name;
        uint256 age;
        string state; // A short description of who they are or how they feel
        string[] musicRecommendations;
        address[] following;
    }
    mapping(address => User) public users;
    address[] public userList;

    // To add a new musical recommendation
    function addSong(string memory _songName) public {
        require(bytes(_songName).length > 0 && bytes(_songName).length <= 100);
        users[msg.sender].musicRecommendations.push(_songName);
    }

    // To setup user information
    function setup(bytes32 _name, uint256 _age, string memory _state) public {
        require(_name.length > 0);
        User memory newUser = User(_name, _age, _state, users[msg.sender].musicRecommendations, users[msg.sender].following);
        users[msg.sender] = newUser;
        userList.push(msg.sender);
    }

    // To follow new users
    function follow(address _user) public {
        require(_user != address(0));
        users[msg.sender].following.push(_user);
    }

    // Returns the array of users
    function getUsersList() public view returns(address[] memory) {
        return userList;
    }

    // Returns the music recommendations
    function getUsersMusicRecommendation(address _user, uint256 _recommendationIndex) public view returns(string memory) {
        return users[_user].musicRecommendations[_recommendationIndex];
    }

    // Returns how many music recommendations that user has
    function getUsersMusicRecommendationLength(address _user) public view returns(uint256) {
        return users[_user].musicRecommendations.length;
    }

    // Returns the addresses of the users _user is following
    function getUsersFollowings(address _user) public view returns(address[] memory) {
        return users[_user].following;
    }
}

有几个函数可以从各自的数组中检索每个特定人员的以下数据和推荐数据。这样做是因为我们不能用公共数组自动获得整个数组;众所周知,公共数组每次只返回整个数组中的一个元素,所以我们需要一个函数来获取全部元素。同样的事情也发生在字符串上——我们必须创建一个函数来单独获取每个字符串,因为我们不能只发送一个字符串数组,因为它们是低级多维byte[][]数组。Solidity 不允许你发送一个byte[][][],它相当于string[],因为它太大太复杂了。

记得在用 Truffle 做了修改后重新部署你的代码。现在,您不必更新智能协定地址,也不必将 ABI 复制到您的源文件夹,因为它设置为直接从构建文件夹中获取已部署的协定数据。

我们的 dApp 看起来还没有达到应有的效果,所以如果你想达到同样的效果,这里有完整的 CSS 代码供你参考:

body {
    margin: 0;
    font-family: sans-serif;
    text-align: center;
}

button {
    border-radius: 10px;
    padding: 20px;
    color: white;
    border: none;
    background-color: rgb(69, 115, 233);
    cursor: pointer;
}

button:hover {
    opacity: 0.7;
}

.hidden {
    display: none;
}

.buttons-container button:not(:last-child){
    margin-right: 5px;
}

.recommendation {
    width: 40%;
    margin: auto;
    background-color: whitesmoke;
    border-radius: 20px;
    margin-bottom: 10px;
    padding: 40px;
}

.follow-people-unit {
    width: 40%;
    background-color: whitesmoke;
    border-radius: 20px;
    margin: 10px auto;
    padding: 20px;
}

.recommendation-name, .recommendation-address, .follow-people-address, .follow-people-name, .follow-people-age {
    display: inline-block;
    color: #444444;
    font-style: italic;
}

.recommendation-name, .follow-people-name {
    margin-right: 10px;
}

.recommendation-address, .follow-people-address {
    color: rgb(156, 156, 156);
}

.recommendation-song, .follow-people-recommendation {
    font-weight: bolder;
    font-size: 16pt;
    margin-top: 10px;
}

.form-input {
    display: block;
    width: 200px;
    border-radius: 20px;
    padding: 20px;
    border: 1px solid #444444;
    margin: 10px auto;
}

.form-textarea {
    height: 200px;
}

.cancel-button {
    margin-right: 10px;
}

.follow-people-state {
    font-style: italic;
    font-weight: bolder;
    color: #444444;
}

.follow-button {
    margin-top: 10px;
}

这里有一个最终结果的截图,你可以用webpack -dhttp-server dist/tolocalhost:8080看到它:

点击前面截图中显示的按钮,与您的新 dApp 互动。Follow People 按钮将花费几秒钟的时间从智能合约中加载数据,因为我们正在运行几个请求以在 JavaScript 中生成我们的自定义对象。

最后,我可以添加功能来显示最新的音乐推荐和取消关注系统,但我会留给您作为练习智能合同 dApp 实现技能的练习。这个想法是在页面底部显示动态生成的推荐组件,而不是我们已经有的静态组件;您只需从我们的智能合同中获取数据,就可以做到这一点。

这个 dApp 远非完美;您可以通过优化数据结构来解决一些速度问题,这样您就可以只检索所需的信息,而不是整个数组。你也可以通过用 truffle 测试代码来修复一些安全问题。最终结果由你决定;你决定什么时候应用程序可以被认为是完成的,因为你可以添加并继续添加使它更好的功能——这就是所有伟大的 dApps 是如何诞生的。

你可以在我的 GitHub 上查看最终代码:https://github.com/merlox/social-music

摘要

在这一章中,你亲眼看到了如何创建一个包含所有细微差别和所需变化的分散式应用程序。您已经经历了整个过程,从用 React、webpack 和 Truffle 设置开发环境开始。您已经学习了如何创建整齐地组织代码的 React 组件,以便轻松管理 dApp 的所有复杂性。

记得把这个应用程序添加到你的 GitHub,作为你完成它的证明,用有价值的项目改善你的简历,这样未来的客户可以亲眼看到你能为他们做什么,并且你已经掌握了构建一个完全去中心化的应用程序的所有步骤。在下一章中,您将了解更多关于使用更高级的技巧来改进您的 dApps,以使它们感觉灵敏并像高质量系统一样运行。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组