创建基于区块链的社交媒体平台
掌握以太坊开发从大量的理论和技术开始,但是,在某些时候,你必须采取跳跃,以便开始将你最近获得的知识应用到构建你的投资组合的现实世界场景中。这就是为什么我们要创建一个基于区块链的社交媒体平台,因为它是区块链技术的最佳使用案例之一,因为我们为人们提供信任。不幸的是,许多集中式社交媒体公司正在滥用这种信任,窃取用户隐私并将其货币化。Twitter 或脸书等社交媒体平台之所以出名,是因为它们让人们能够在一个利用互联网功能的界面上与许多人保持联系。
本章将带你经历创建一个动态的社交媒体平台的挑战,这个平台完全依赖于区块链,没有集中的服务器。你将理解如何用 React 创建一个漂亮的用户界面。然后,您将探索如何更好地组织信息,以便允许人们使用智能合约找到他们想要的内容。最后,你将使用 web3 把一切联系在一起,你将能够使用你的社交媒体平台。
在本章中,我们将讨论以下主题:
- 了解分散的社交媒体
- 创建用户界面
- 构建智能合同
- 完成 dApp
了解分散的社交媒体
当谈到基于以太坊的社交媒体 dApps 时,我们帮助人们解决了许多当前集中式公司尚未有效解决的问题。我们可以在以下方面提供帮助:
- 在分散式区块链上保护用户隐私
- 由于关于区块链的信息是永久性的,因此不允许来自外部中央实体的审查,从而保证完全的自由
- 一个不变的固定存储系统,在内容创建几十年后仍可访问
然而,当我们考虑构建一个分散的社交媒体平台时,我们忽略了以下几个对现代应用程序至关重要的重要方面:
- 速度:用户将无法像正常的集中式应用程序那样快速地使用 dApp,因为他们依赖于一个庞大、缓慢的互联计算机网络。
- 存储限制:以太坊的空间是有限的,所以每个字节都很昂贵,这导致你可以在区块链上存储的东西有很大的限制,所以我们必须找到克服这些自然限制的方法,同时尽可能地保持分散。
- 汽油费用:普通的集中式应用程序不必为其系统上的每个操作支付汽油费用,因为它们知道所有这些费用都是在集中式服务器中支付的。在区块链,每笔交易的成本都可能很高。我们将通过使用 testnets 来解决这个问题,在创建最终应用程序之前,gas 没有任何价值。
另一个大问题是,我们不能在区块链上存储图像和视频;如果我们希望保持主系统的分散性,我们将不得不依赖分散存储解决方案,如 IPFS;然而,这不是强制性的。
最初的概念
我们的目标是创建一个有效的社交媒体平台,克服或完全避免区块链的局限性。为了简化我们 dApp 的复杂性,我们将构建一个类似于 Twitter 的应用程序,在这个意义上,用户只能共享文本消息,而不能共享图像或视频。
因为我们是开发人员,我们将为程序员、设计师、开发人员和各种技术相关领域创建一个 Twitter,在这里人们可以在一个有共同兴趣的社区中感到受欢迎。我们希望它具有以下功能:
- 共享文本字符串的能力仅受智能合同的能力限制
- 向每个内容添加标签的能力
- 一个功能,能够查看标签,人们已经包括在他们的内容点击标签
- 订阅标签的功能
我们不希望人们跟随他人,我们只是给他们跟随标签的能力,这样他们就可以专注于内容而不是信息。让我们开始致力于用户界面,它将成为我们的社交媒体 dApp,让技术爱好者通过标签关注内容,而不是特定的用户。
创建用户界面
这个特定项目的用户界面将围绕内容和标签,因为标签是用户发现新趋势内容的方式。用户将能够订阅特定的标签,以便在他们的 feed 上接收这些主题的内容。
像往常一样,我们开始用松露建立一个新项目。按照以下步骤设置您的项目:
- 克隆启动存储库(https://github.com/merlox/dapp),其中包含在 React dApp 上工作的初始配置:
git clone https://github.com/merlox/dapp
- 将存储库重命名为
social-media-dapp
以组织内容:
mv dapp/ social-media-dapp/
- 通过转到 GitHub 创建一个新的空 GitHub 存储库(无需许可证或
.gitignore
,因为它们已经包含在您的项目中),并使用以下命令更新 pull/push URL:
git config remote.origin.url https://<YOUR-USERNAME>:<YOUR-PASSWORD>@github.com/<YOUR-USERNAME>/social-media-dapp
- 推送第一次提交。用
npm i
安装依赖项,用webpack -wd
运行webpack
。 - 通过使用
http-server dist/
运行一个静态服务器来打开您的应用程序,并转到http://localhost: 8080
来查看是否一切都设置正确。
现在你可以开始创建你的用户界面了。你已经知道如何去做了,那么为什么不在看到我的之前先去创造你自己的呢?此时,你会对自己的能力感到惊讶,所以我鼓励你尝试构建自己的系统。我们的想法是通过指导您完成各个步骤来共同构建这个 dApp,直到您拥有一个高质量的 dApp,您可以用它来构建您的简历或为 ICO 进一步开发,或者作为改善人类的开源软件。
配置 webpack 样式
最后,您必须拥有两个部分:一个是最受欢迎的标签,来自我们智能合同中的映射,另一个是您可以阅读每个特定标签的更多信息,同时能够发布内容。您可能希望设置样式加载器,以便能够在您的 dApp 上使用 css,这在您刚刚克隆的默认 dApp 上没有设置。为此,请在停止 webpack 后安装以下依赖项:
npm i -S style-loader css-loader
既然您已经安装了能够在项目中使用 CSS 文件所需的库,那么您可以通过在loaders
块中为css
文件添加一个新的加载器来更新您的 webpack 配置文件。请注意,我们同时使用了两个加载器—style-loader
先加载。否则,它不起作用:
{
test: /\.css$/,
exclude: /node_modules/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
设置初始结构
打开index.js
文件,开始创建你的用户界面。首先,我从构造函数开始,创建一些我们稍后会用到的必要变量:
- 为任何 React 项目设置所需的导入,加上
css
文件,由于样式和 css 加载器,我们现在可以导入该文件:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
- 用一些虚拟数据设置构造函数,看看我们用智能契约中的变量填充它后,最终的应用程序会是什么样子:
class Main extends React.Component {
constructor() {
super()
this.state = {
content: [{
author: '0x211824098yf7320417812j1002341342342341234',
message: 'This is a test',
hashtags: ['test', 'dapp', 'blockchain'],
time: new Date().toLocaleDateString(),
}, {
author: '0x211824098yf7320417812j1002341342342341234',
message: 'This is another test',
hashtags: ['sample', 'dapp', 'Ethereum'],
time: new Date().toLocaleDateString(),
}],
topHashtags: ['dapp', 'Ethereum', 'blockchain', 'technology', 'design'],
followedHashtags: ['electronics', 'design', 'robots', 'futurology', 'manufacturing'],
displaySubscribe: false,
displaySubscribeId: '',
}
}
- 用
ReactDOM
渲染创建render()
函数:
render() {
return (
<div className="main-container">
</div>
)
}
}
ReactDOM.render(<Main />, document.querySelector('#root'))
正如您所看到的,我们的应用程序的状态包含了带有以太坊地址的content
对象,作为该片段的作者、消息、标签和时间。我们以后可能会改变这一点,但目前这已经足够好了。我们还添加了两个数组,其中包含这个特定用户的 top hashtags 和 followed tags。这些显示 subscribe 变量是一个必要的恶魔,当用户每次悬停在一个标签上时显示一个 subscribe 按钮,这样他们就可以选择订阅来提高 dApp 的交互性。
呈现标签
我们现在可以创建包含所有逻辑的 render 函数,但是要注意:这有点复杂,因为我们要显示状态中的所有数组,所以要有耐心,看代码块来理解它。请遵循以下步骤:
- 创建一个新的函数来生成 hashtag 的 HTML,因为我们希望向按钮添加变量逻辑,以确保
hashtag
文本对显示订阅或取消订阅按钮的用户做出反应。请记住,我们希望用户能够关注标签;这就是为什么我们需要订阅和取消订阅按钮:
generateHashtags(hashtag, index) {
let timeout
return (
<span onMouseEnter={() => {
clearTimeout(timeout)
this.setState({
displaySubscribe: true,
displaySubscribeId: `subscribe-${hashtag}-${index}`,
})
}} onMouseLeave={() => {
timeout = setTimeout(() => {
this.setState({
displaySubscribe: false,
displaySubscribeId: '',
})
}, 2e3)
}}>
<a className="hashtag" href="#">#{hashtag}</a>
<span className="spacer"></span>
<button ref={`subscribe-${hashtag}-${index}`} className={this.state.displaySubscribe && this.state.displaySubscribeId == `subscribe-${hashtag}-${index}` ? '' : 'hidden'} type="button">Subscribe</button>
<span className="spacer"></span>
</span>
)
}
- 更新
render()
函数来生成内容和 hashtag 块,因为我们需要一种简单的方法来创建要显示的内容;所有逻辑将在render()
功能中执行:
render() {
let contentBlock = this.state.content.map((element, index) => (
<div key={index} className="content">
<div className="content-address">{element.author}</div>
<div className="content-message">{element.message}</div>
<div className="content-hashtags">{element.hashtags.map((hashtag, i) => (
<span key={i}>
{this.generateHashtags(hashtag, index)}
</span>
))}
</div>
<div className="content-time">{element.time}</div>
</div>
))
- 添加 hashtag 块,其唯一的工作是创建将显示给用户的 JSX 对象,使用我们刚刚使用的
generateHashtags()
函数:
let hashtagBlock = this.state.topHashtags.map((hashtag, index) => (
<div key={index}>
{this.generateHashtags(hashtag, index)}
</div>
))
let followedHashtags = this.state.followedHashtags.map((hashtag, index) => (
<div key={index}>
{this.generateHashtags(hashtag, index)}
</div>
))
- 在
render()
函数的末尾,添加带有我们刚刚设置的块变量的return
块:
return (
<div className="main-container">
<div className="hashtag-block">
<h3>Top hashtags</h3>
<div className="hashtag-container">{hashtagBlock}</div>
<h3>Followed hashtags</h3>
<div className="hashtag-container">{followedHashtags}</div>
</div>
<div className="content-block">
<div className="input-container">
<textarea placeholder="Publish content..."></textarea>
<input type="text" placeholder="Hashtags separated by commas..."/>
<button type="button">Publish</button>
</div>
<div className="content-container">
{contentBlock}
</div>
</div>
</div>
)
}
我们添加了一个名为generateHashtags
的函数,因为我们必须添加相同的逻辑来在许多地方显示 subscribe 按钮,所以有必要创建一个函数,在需要的时候精确地完成这个任务,而不需要复制这些长代码块。然后,在render()
函数中,您可以看到我们使用该函数在许多将使用 hashtag 的地方生成 hashtag 逻辑。在返回之前,我们有三个变量,它们只是用我们的状态数据动态地生成 JSX 分量。最后,render()
函数很好地显示了这些块。
改善外观
我还导入了index.css
文件,该文件包含以最佳方式显示我们的应用程序的网格组件,具有易于维护的清晰结构:
- 将常规样式添加到应用程序的主要组件中,如主体和按钮,使它们看起来更好:
body {
margin: 0;
background-color: whitesmoke;
font-family: sans-serif;
}
button {
background-color: rgb(201, 47, 47);
color: white;
border-radius: 15px;
border: none;
cursor: pointer;
}
button:hover {
background-color: rgb(131, 0, 0);
}
- 添加常规隐藏和间隔样式以隐藏元素并创建动态间隔:
.hidden {
display: none;
}
.spacer {
margin-right: 5px;
}
- 添加容器的样式,将它们放置在所有主流浏览器都接受的网格系统中:
.main-container {
display: grid;
grid-template-columns: 30% 70%;
margin: auto;
width: 50%;
grid-column-gap: 10px;
}
.input-container {
margin-bottom: 10px;
padding: 30px;
display: grid;
grid-template-columns: 80% 1fr;
grid-template-rows: 70% 30%;
grid-gap: 10px;
}
- 设置输入和文本区域的格式,以创建更好看且易于使用的设计:
.input-container textarea {
padding: 10px;
border-radius: 10px;
font-size: 11pt;
font-family: sans-serif;
border: 1px solid grey;
grid-column: 1 / 3;
}
.input-container input {
padding: 10px;
border-radius: 10px;
font-size: 11pt;
font-family: sans-serif;
border: 1px solid grey;
}
- 为内容块的所有元素提供一个好看的设计,类似于 Twitter 中的一条推文:
.content {
background-color: white;
border: 1px solid grey;
margin-bottom: 10px;
padding: 30px;
box-shadow: 4px 4px 0px 0 #cecece;
}
.content-address {
color: grey;
margin-bottom: 5px;
}
.content-message {
font-size: 16pt;
margin-bottom: 5px;
}
.content-hashtags {
margin-bottom: 5px;
}
.content-time {
color: grey;
font-size: 12pt;
}
- 格式化这些标签,将它们放置在正确的位置,同时增加它们的大小:
.hashtag-block {
text-align: center;
}
.hashtag-container {
line-height: 30px;
}
.hashtag {
font-size: 15pt;
}
- 如果您想获得相同外观,可以复制并粘贴 css。这是 dApp 现在的样子:
- 你可以在https://github.com/merlox/social-media-dapp/tree/master/src的 GitHub 上查看完整的代码。
我试图模仿一种简单的卡通设计,使其更有趣,同时保持一个清晰的界面,人们可以轻松阅读而不会混淆。请注意您创建的用户界面,因为它们是每个 dApp 的主要组件。看起来很专业的 dApp 会吸引更多的注意力。更多的关注通常会转化为更多的收入,因为你能够在正确的时间将人们的注意力引导到正确的地方。
构建智能合同
我们将要构建的智能契约将通过存储所有消息、标签和用户来充当我们的分散式应用程序的后端。在我们的应用程序中,我们希望保持用户匿名;这就是为什么它们被表示为地址而不是用户名——以将人们的注意力引向正在谈论的内容,而不是谁在传递信息。
正如你已经知道的,我们将创建一个没有图片或视频的以标签为中心的社交媒体平台。这就是为什么我们所有的数据都将存储在映射和数组的组合中。
规划设计流程
在直接进入代码之前,我希望您了解我们将遵循的过程,以优化整个过程,避免混乱,并通过清楚地了解需要做什么来避免错误,从而节省时间。这个过程看起来是这样的:
- 创建一个智能合约文件,并在评论中写下对合约目的的描述,例如这些功能将如何工作以及谁将使用它。尽可能简洁,因为这将有助于您和维护人员理解它的全部内容。
- 开始创建变量和函数签名,也就是没有体的函数,只有名字和参数。使用 NatSpec 格式记录每个函数,以获得更多说明。
- 开始独立实现每个功能,直到全部完成。如果需要,可以添加更多。
- 通过将合同复制粘贴到 remix 或任何其他 IDE 来手动测试合同,以快速发现问题,并在虚拟 EVM 中运行所有功能,在虚拟中,您不必支付任何费用或等待确认。理想情况下,您应该编写 Truffle 测试来验证一切都在工作,但有时为了节省时间可以跳过它。
这是一个流程图,你可以记住它:
这种类型的过程是我遵循的最大限度地提高我的生产力,而不是疯狂的规格。如果你立即开始对一个解决方案进行编码,你就有陷入困境的风险,你不得不重新构建整个代码库,同时在这个过程中产生不必要的错误。这就是计划如此重要的原因。此外,确切地知道该做什么,什么时候做,会让你的生活容易得多。
现在,我们可以通过描述智能合约背后的想法来开始创建我们的智能合约。在您的contracts/
文件夹中创建一个名为SocialMusic.sol
的文件,并在文件顶部的注释中描述该合同的最终版本。在查看我自己的解决方案之前尝试自己做,因为唯一的学习方法是自己练习:
// This is a social media smart contract that allows people to publish strings of text in short formats with a focus on hashtags so that they can follow, read and be in touch with the latest content regarding those hashtags. There will be a mapping of the top hashtags. A struct for each piece of content with the date, author, content and array of hashtags. We want to avoid focusing on specific users that's why user accounts will be anonymous where addresses will the be the only identifiers.
pragma solidity ^0.5.5;
contract SocialMedia {}
不管你有没有意识到,通过写这个描述,你已经理清了你的思路。现在您可以开始创建函数和变量了。假设您已经有了一个用户界面,您会希望将该界面分成几个块,并创建提供这些块中显示的数据的函数;例如,看一下您的应用程序的以下块:
你可以明显地看到一些随机的标签。在查看界面时,您必须问自己,我需要在智能合约中实现什么才能实现这一点?嗯,这似乎是显而易见的,但往往并不那么容易。在这种情况下,您必须创建一个函数来检索 top hashtags。该函数将从排序后的数组或映射中获取数据,并将其发送给用户,可能是一个参数,用于确定在任何时候要检索多少个 top hashtags,以便您可以试验不同的数量。要创建这个函数,你必须实现某种排序机制,可能是一个视图或纯粹的函数,它不消耗气体来处理。另一方面,如何确定这些标签的顺序呢?可能是一个评分系统,根据用途增加每个标签的值。
你看,从我们整个应用程序的一个小而明显的片段中,你意识到你需要以下内容:
- 带有需要排序的顶部标记的数组或映射。
- 一个检索这些 hashtags 的函数,带有一个可选参数来确定有多少个,以便您可以对它们进行试验。
- 考虑到区块链的局限性,该函数可以对现有的标签进行排序。它必须是一个纯粹的或查看功能,以避免过多的气体成本。
- 一个给每个标签打分的系统,这样我们就可以根据它们的受欢迎程度来排序。
您必须对应用程序的每个组件进行相同的分析过程。不管它看起来有多明显,试着在你的头脑中描述这些部分,这样你就能预先想象出需要什么,以及什么是可能的,从而为你自己节省数小时的挫折和错误代码。
建立数据结构
完成所需的规划后,通过执行以下步骤,可以随意编写所有所需部分的功能签名:
- 首先定义稍后将在结构和事件中使用的变量:
struct Content {
uint256 id;
address author;
uint256 date;
string content;
bytes32[] hashtags;
}
event ContentAdded(uint256 indexed id, address indexed author, uint256 indexed date, string content, bytes32[] hashtags);
- 添加映射、数组和剩余的状态变量:
mapping(address => bytes32[]) public subscribedHashtags;
mapping(bytes32 => uint256) public hashtagScore; // The number of times this hashtag has been used, used to sort the top hashtags
mapping(bytes32 => Content[]) public contentByHashtag;
mapping(uint256 => Content) public contentById;
mapping(bytes32 => bool) public doesHashtagExist;
mapping(address => bool) public doesUserExist;
address[] public users;
Content[] public contents;
bytes32[] public hashtags;
uint256 public latestContentId;
- 定义函数签名:
function addContent(string memory _content, bytes32[] memory _hashtags) public {}
function subscribeToHashtag(bytes32 _hashtag) public {}
function unsubscribeToHashtag(bytes32 _hashtag) public {}
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {}
function getFollowedHashtags() public view returns(bytes32[] memory) {}
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {}
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {}
function sortHashtagsByScore() public view returns(bytes32[] memory) {}
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {}
你对我们一会儿想出的函数和变量的数量感到惊讶吗?在那个过程中,你可能没有考虑到像checkExistingSubscription
或getContentIdsByHashtag
这样的函数。说实话,写合同之前我并不知道需要那些功能;只是在创建了整个代码之后,它们才变得必要。如果在创建代码之前没有想出所有需要的变量和函数,也没关系。随着你的发展,它们会在适当的时候出现。你不必事先编写所有的函数和计划每一个函数和变量;那太疯狂了。所以要有耐心,要知道,在实现了最初的功能之后,您可能需要添加一些额外的功能来获得想要的功能。
记录未来的功能
这些函数还不够清楚,所以为什么不写关于它们的 NatSpec 文档呢?这是一个乏味的过程,但你会为此感谢自己,因为它会提醒你在编码时正在做什么。以下是我的版本和附带的文档:
- 从添加内容、订阅和取消订阅功能开始:
/// @notice To add new content to the social media dApp. If no hashtags are sent, the content is added to the #general hashtag list.
/// @param _content The string of content
/// @param _hashtags The hashtags used for that piece of content
function addContent(string memory _content, bytes32[] memory _hashtags) public {}
/// @notice To subscribe to a hashtag if you didn't do so already
/// @param _hashtag The hashtag name
function subscribeToHashtag(bytes32 _hashtag) public {}
/// @notice To unsubscribe to a hashtag if you are subscribed otherwise it won't do nothing
/// @param _hashtag The hashtag name
function unsubscribeToHashtag(bytes32 _hashtag) public {}
- getter 函数用于顶部和后面的标签。我们需要这些函数将它们显示在用户界面的侧边栏上:
/// @notice To get the top hashtags
/// @param _amount How many top hashtags to get in order, for instance the top 20 hashtags
/// @return bytes32[] Returns the names of the hashtags
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {}
/// @notice To get the followed hashtag names for this msg.sender
/// @return bytes32[] The hashtags followed by this user
function getFollowedHashtags() public view returns(bytes32[] memory) {}
- getter 通过 ID 发挥作用。我们需要它们返回分解成单个部分的结构变量:
/// @notice To get the contents for a particular hashtag. It returns the ids because we can't return arrays of strings and we can't return structs so the user has to manually make a new request for each piece of content using the function below.
/// @param _hashtag The hashtag from which get content
/// @param _amount The quantity of contents to get for instance, 50 pieces of content for that hashtag
/// @return uint256[] Returns the ids of the contents so that you can get each piece independently with a new request since you can't return arrays of strings
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {}
/// @notice Returns the data for a particular content id
/// @param _id The id of the content
/// @return Returns the id, author, date, content and hashtags for that piece of content
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {}
- helper 函数对 hashtags 进行排序,并检查现有的订阅。当用户订阅通过排序来更新整个标签的分数时,将使用这些标签,具体取决于分数:
/// @notice Sorts the hashtags given their hashtag score
/// @return bytes32[] Returns the sorted array of hashtags
function sortHashtagsByScore() public view returns(bytes32[] memory) {}
/// @notice To check if the use is already subscribed to a hashtag
/// @return bool If you are subscribed to that hashtag or not
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {}
NatSpec 文档用基本描述、参数和返回值来描述您的所有函数,以供其他编码人员查看,以便他们可以维护您的代码。它们还可以帮助您理解当代码库增长时会发生什么。
接下来,我们必须逐一实现所有功能,直到全部完成。这是最耗时的过程,因为考虑到坚固性的限制,有些部分比其他部分更硬。做这件事的时候尽量保持乐观。如果你设置一个一两个小时的计时器,在完成之前你不会分心,你会比你预期的更早完成。这是最大化生产力的著名的番茄工作法,我建议你用它在更少的时间里完成更多的事情。
实现添加内容功能
添加内容功能是我们正在构建的 dApp 中最复杂的,因为我们需要完成以下任务:
- 检查用户给出的内容是否有效
- 向正确的状态变量添加新内容
- 增加包含在内容片段中的标签的分数
- 将内容动态存储在一个
general
标签中,人们可以使用它来查找随机内容,而无需排序 - 如果用户是新客户,则将他们添加到用户阵列中
因为我们必须实现许多功能,所以功能不可避免地会很复杂。这就是为什么花时间把它做好是很重要的,因为我们可以很容易地制造气阱,消耗所有可用的气体。在看到我的解决方案之前,请通过执行以下步骤,在您的计算机上尽可能好地实施它们:
- 添加
require()
检查以确保内容有效:
/// @notice To add new content to the social media dApp. If no hashtags are sent, the content is added to the #general hashtag list.
/// @param _content The string of content
/// @param _hashtags The hashtags used for that piece of content
function addContent(string memory _content, bytes32[] memory _hashtags) public {
require(bytes(_content).length > 0, 'The content cannot be empty');
Content memory newContent = Content(latestContentId, msg.sender, now, _content, _hashtags);
// If the user didn't specify any hashtags add the content to the #general hashtag
- 根据用户是否添加了标签,我们将执行相应的功能来排序和增加这些标签的值:
if(_hashtags.length == 0) {
contentByHashtag['general'].push(newContent);
hashtagScore['general']++;
if(!doesHashtagExist['general']) {
hashtags.push('general');
doesHashtagExist['general'] = true;
}
} else {
for(uint256 i = 0; i < _hashtags.length; i++) {
contentByHashtag[_hashtags[i]].push(newContent);
hashtagScore[_hashtags[i]]++;
if(!doesHashtagExist[_hashtags[i]]) {
hashtags.push(_hashtags[i]);
doesHashtagExist[_hashtags[i]] = true;
}
}
}
- 使用前面描述的函数按分数对数组进行排序,我们在发出正确事件的同时创建用户:
hashtags = sortHashtagsByScore();
contentById[latestContentId] = newContent;
contents.push(newContent);
if(!doesUserExist[msg.sender]) {
users.push(msg.sender);
doesUserExist[msg.sender] = true;
}
emit ContentAdded(latestContentId, msg.sender, now, _content, _hashtags);
latestContentId++;
}
下面是我在这个函数中一步一步所做的工作:
- 我检查了包含消息的
_content
变量是否为空,方法是将它转换为字节并检查它的长度。这是检查字符串是否为空的方法之一,因为你不能得到字符串类型的长度。 - 我用所需的参数创建了
Content
struct 实例,并填充了使用该 struct 的映射,这样我们以后就可以找到这段内容了。 - 用户可以自由地不指定任何标签,在这种情况下,内容将被添加到
#general
标签中,以便以某种方式组织它,供那些希望从应用程序中获得一般信息的人使用。请记住,我们主要通过标签进行交互,所以将每条消息组织成一条是必要的。 - 如果用户指定了几个标签,我们会将内容添加到所有标签中,同时还会创建人们可以关注的新标签。目前,我们对人们可以使用多少标签没有任何限制,因为我们正在试验应用程序将如何工作。如果我们决定设置这样的限制,我们可以稍后再关注这些细节。
- 将用户添加到用户数组中,并发出
ContentAdded
事件来通知其他人新的内容。
创建促销引擎
我们需要一种方法,通过创建一个增加标签价值的评分系统来告诉用户哪些账户表现最好。这就是为什么我们创建了hashtagScore
映射,作为对正在使用的标签流行程度的测量。推广引擎仅仅是一种根据流行程度对标签进行评级的方式。因此,当有人订阅该标签或为该标签添加新内容时,该标签的得分会增加。有人退订会减少。这些都是不可见的,所以用户只能看到顶部的标签。
让我们继续订阅功能,让人们能够关注他们感兴趣的特定主题。要实现推广引擎,我们只需更新 subscribe 和 unsubscribe 函数中使用的特定 hashtag 的分数。还是那句话,在看到解决方案之前尝试自己去实现,在学习和获得经验的同时磨砺自己的技能。下面是 subscribe 函数,它为特定用户增加了所选 hashtag 的分数:
/// @notice To subscribe to a hashtag if you didn't do so already
/// @param _hashtag The hashtag name
function subscribeToHashtag(bytes32 _hashtag) public {
if(!checkExistingSubscription(_hashtag)) {
subscribedHashtags[msg.sender].push(_hashtag);
hashtagScore[_hashtag]++;
hashtags = sortHashtagsByScore();
}
}
然后我们有了取消订阅功能,它减少了 hashtag 值,因为它变得不那么相关了:
/// @notice To unsubscribe to a hashtag if you are subscribed otherwise it won't do nothing
/// @param _hashtag The hashtag name
function unsubscribeToHashtag(bytes32 _hashtag) public {
if(checkExistingSubscription(_hashtag)) {
for(uint256 i = 0; i < subscribedHashtags[msg.sender].length; i++) {
if(subscribedHashtags[msg.sender][i] == _hashtag) {
delete subscribedHashtags[msg.sender][i];
hashtagScore[_hashtag]--;
hashtags = sortHashtagsByScore();
break;
}
}
}
}
subcribeToHashtag
函数简单地检查用户是否已经订阅了新的主题以添加到他们的兴趣列表中,同时也对标签进行排序,因为特定主题的分数已经增加了。在我们的智能合约中,标签是根据使用来评价的。订阅它们的人越多,为特定标签创建的内容越多,它的排名就越高。
unsubscribeToHashtag
函数遍历特定用户的所有标签,并从列表中删除选中的标签。这个循环不应该引起任何气体问题,因为我们不期望人们关注成千上万的话题。无论如何,正确的做法是限制可订阅标签的数量,以避免 gas 错误。我将把那件事留给你。最后,我们降低标签的分数,并根据变化对所有标签进行排序。
实现 getter 函数
接下来,让我们看看将用于向用户显示数据的 getter 函数。这些功能不消耗任何汽油,因为它们从下载和同步的区块链中读取数据,而这些数据总是可用的,不依赖于互联网连接。让我们来看看以下步骤:
- 创建
getTopHashtags()
函数,该函数以 bytes32 格式向用户返回一个姓名列表,这样用户就可以看到哪些人正在进行趋势分析。这是新内容的主要发现系统:
/// @notice To get the top hashtags
/// @param _amount How many top hashtags to get in order, for instance the top 20 hashtags
/// @return bytes32[] Returns the names of the hashtags
function getTopHashtags(uint256 _amount) public view returns(bytes32[] memory) {
bytes32[] memory result;
if(hashtags.length < _amount) {
result = new bytes32[](hashtags.length);
for(uint256 i = 0; i < hashtags.length; i++) {
result[i] = hashtags[i];
}
} else {
result = new bytes32[](_amount);
for(uint256 i = 0; i < _amount; i++) {
result[i] = hashtags[i];
}
}
return result;
}
- 添加函数来获取后面的 hashtags,这非常简单,因为它使用
subscribedHashtags[]
映射返回指定的列表:
/// @notice To get the followed hashtag names for this msg.sender
/// @return bytes32[] The hashtags followed by this user
function getFollowedHashtags() public view returns(bytes32[] memory) {
return subscribedHashtags[msg.sender];
}
- 实现
getContentIdsByHashtag()
功能。这将负责返回一个 id 数组,其中包含用户可能订阅的特定 hashtag 的所有内容:
/// @notice To get the contents for a particular hashtag. It returns the ids because we can't return arrays of strings and we can't return structs so the user has to manually make a new request for each piece of content using the function below.
/// @param _hashtag The hashtag from which get content
/// @param _amount The quantity of contents to get for instance, 50 pieces of content for that hashtag
/// @return uint256[] Returns the ids of the contents so that you can get each piece independently with a new request since you can't return arrays of strings
function getContentIdsByHashtag(bytes32 _hashtag, uint256 _amount) public view returns(uint256[] memory) {
uint256[] memory ids = new uint256[](_amount);
for(uint256 i = 0; i < _amount; i++) {
ids[i] = contentByHashtag[_hashtag][i].id;
}
return ids;
}
- 添加简单的
getContentById()
函数,需要将 id 的结构分解成可消化的变量,因为我们还不能返回结构:
/// @notice Returns the data for a particular content id
/// @param _id The id of the content
/// @return Returns the id, author, date, content and hashtags for that piece of content
function getContentById(uint256 _id) public view returns(uint256, address, uint256, string memory, bytes32[] memory) {
Content memory c = contentById[_id];
return (c.id, c.author, c.date, c.content, c.hashtags);
}
前面的函数非常简单。getContentIdsByHashtag
函数有点棘手,因为我们通常不需要它,但是,因为 Solidity 不允许我们返回结构数组或字符串数组,所以我们必须获得 id,以便稍后我们可以用getContentById
函数独立地获得每一段内容,它确实成功地返回了每个变量。
这是我们实现一切所需的最后两个助手功能:
sortHashtagsByScore()
函数用于返回一个标签列表,按每个标签的流行度排序,因为我们正在读取每个标签的值:
/// @notice Sorts the hashtags given their hashtag score
/// @return bytes32[] Returns the sorted array of hashtags
function sortHashtagsByScore() public view returns(bytes32[] memory) {
bytes32[] memory _hashtags = hashtags;
bytes32[] memory sortedHashtags = new bytes32[](hashtags.length);
uint256 lastId = 0;
for(uint256 i = 0; i < _hashtags.length; i++) {
for(uint j = i+1; j < _hashtags.length; j++) {
// If it's a buy order, sort from lowest to highest since we want the lowest prices first
if(hashtagScore[_hashtags[i]] < hashtagScore[_hashtags[j]]) {
bytes32 temporaryhashtag = _hashtags[i];
_hashtags[i] = _hashtags[j];
_hashtags[j] = temporaryhashtag;
}
}
sortedHashtags[lastId] = _hashtags[i];
lastId++;
}
return sortedHashtags;
}
checkExistingSubscription()
函数返回用户是否订阅的布尔值:
/// @notice To check if the use is already subscribed to a hashtag
/// @return bool If you are subscribed to that hashtag or not
function checkExistingSubscription(bytes32 _hashtag) public view returns(bool) {
for(uint256 i = 0; i < subscribedHashtags[msg.sender].length; i++) {
if(subscribedHashtags[msg.sender][i] == _hashtag) return true;
}
return false;
}
排序函数很难读懂,因为它看起来很复杂。然而,这只是几个for
循环,一个正常的,一个反向的,在另一个循环中,不断地将分数较高的标签移动到顶部,直到最好的标签位于我们的sortedHashtags
数组的第一个位置。这将被用来取代过去的,无序状态的hashtags
阵列。
checkExistingSubscription
函数遍历您订阅的所有标签,如果列表中有标签,则返回true
。这是订阅功能保持数组整洁而不重复订阅所必需的。
更新后的完整代码可以在 https://github.com/merlox/social-media-dapp 的 GitHub 上看到。
现在剩下的是测试所有这些功能是否工作。继续将代码粘贴到 Remix 或任何其他 IDE 中,以便它指出必须修复的错误。然后把契约部署到 JavaScript VM 上,没有任何成本,一个一个的运行那些功能。请注意,您必须将bytes32
变量转换为十六进制,如果您安装了元掩码,您可以使用浏览器开发工具中的web3.toHex()
函数来完成此操作。
理想情况下,你可以用 Truffle 编写测试来自动检查新变化引起的错误。我让你来决定。
合同已经准备好运行,下一步是在您的 dApp 中实现它,以便信息来自我们刚刚创建的分散后端。在下一节中,我们来看看这是如何做到的。
完成 dApp
您的 React.js web 应用程序看起来很棒,剩下的就是将智能合约连接到您的应用程序中的函数,以便它们在保持分散性的同时相互通信,因为任何人都可以在他们想要的地方自由使用 React 应用程序,而不依赖于集中式服务器。
将智能合约与 web 应用程序连接的第一步是安装 web3.js,因为它是以太坊和 web 浏览器之间的桥梁,尽管您可能不需要它,因为我们已经有了 MetaMask。在任何情况下,重要的是让它选择一个稳定的版本,不会因为我们的 dApp 而改变。继续在您的项目文件夹上运行npm i -S web3
。
设置智能合同实例
当在 React 应用程序中实现智能契约时,必须做的第一件事是契约实例,这样我们就可以在整个分布式应用程序中从该契约调用方法。我们将使用由 Truffle 提供的已编译的契约及其地址。让我们执行以下步骤:
- 将 web3 导入您的项目:
import Web3Js from 'web3'
你认为我为什么把变量命名为Web3Js
而不仅仅是Web3
?因为 MetaMask 注入了自己版本的 web3,准确的说是命名为Web3
,所以我们在开发的时候,可能会用到注入的 web3 版本,而不是我们有兴趣导入的版本。使用稍微不同的名称很重要,以避免干扰元掩码注入的 web3。
- 使用当前的提供者全局设置 web3,这样您就可以在整个应用程序中使用它,而不必担心范围问题。
- 创建一个名为
setup()
的函数,包含元掩码设置逻辑。该函数将在页面加载时在构造函数中执行:
class Main extends React.Component {
constructor() {
// Previous code omitted for simplicity
this.setup()
}
async setup() {
window.web3js = new Web3Js(ethereum)
try {
await ethereum.enable();
} catch (error) {
alert('You must approve this dApp to interact with it, reload it to approve it')
}
}
}
我们创建了一个新的 setup 函数,因为我们不能在构造函数上使用 await,因为它不是一个异步函数。在其中,我们创建了一个不叫web3
(小写)的全局web3js
变量,因为 MetaMask 已经使用了那个变量名,我们冒着使用错误版本的风险。正如你所看到的,这个例子中的提供者叫做ethereum
,一个来自 MetaMask 的全局变量,包含了我们开始使用 web3 所需的所有内容;这是一种初始化 web3 实例的新方法,它与旧的 dApps 兼容,因为 MetaMask 团队在安全性方面做了一些改变。然后我们等待enable()
函数获得用户的许可来注入 web3,因为我们不想在没有用户同意的情况下暴露用户密钥。如果用户不允许,我们会显示一个错误,让他们知道我们需要他们授予这个 dApp 的权限,以便正常工作。
- 设置智能合同实例。因为我们已经安装了 Truffle,所以我们可以编译我们的智能契约来生成包含 ABI 的 JSON 文件,这是使用应用程序所必需的。然后,我们可以将合同部署到
ropsten
:
truffle compile
truffle deploy --network ropsten --reset
您可能会收到以下消息:
"Unknown network "ropsten". See your Truffle configuration file for available networks."
- 这意味着您没有通过
ropsten
网络正确设置 Truffle 配置文件。用npm i -S truffle-hdwallet-provider
安装钱包提供者。然后用下面的代码修改truffle-config.js
:
const HDWalletProvider = require('truffle-hdwallet-provider')
const infuraKey = "https://ropsten.infura.io/v3/8e12dd4433454738a522d9ea7ffcf2cc"
const fs = require('fs')
const mnemonic = fs.readFileSync(".secret").toString().trim()
module.exports = {
networks: {
ropsten: {
provider: () => new HDWalletProvider(mnemonic, 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 )
}
}
}
- 告诉 Truffle 使用以下代码在您的
migrations/
文件夹中创建一个2_deploy_contract.js
文件名来部署您的合同:
const SocialMedia = artifacts.require("./SocialMedia.sol")
module.exports = function(deployer) {
deployer.deploy(SocialMedia);
}
- 如您所见,我们只有最少的配置参数,所以请保持整洁。在您的项目文件夹中创建一个
.secret
文件,粘贴您的以太坊种子短语,如果您担心公开您的种子,您可以通过重置元掩码或在另一个浏览器中安装它来获得。Truffle 将使用种子短语来部署合同,因此请确保您的第一个帐户中有足够的ropsten
乙醚。然后再跑truffle deploy --network ropsten --reset
。 - 用以下代码更新您的
setup
函数,以创建一个合同实例:
async setup() {
window.web3js = new Web3Js(ethereum)
try {
await ethereum.enable();
} catch (error) {
alert('You must approve this dApp to interact with it, reload it to approve it')
}
const user = (await web3js.eth.getAccounts())[0]
const contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
from: user
})
await this.setState({contract, user})
}
我们已经在应用程序的状态中设置了用户帐户,以便在需要时可以轻松访问。
分散您的数据
为了全面实现智能合约,我们必须查看网站的每个部分,用智能合约中的数据更新其内容。让我们从左上角到右下角。按照这个顺序,我们首先要使用getTopHashtags()
函数去中心化顶部 hashtags 部分:
async setup() {
window.web3js = new Web3Js(ethereum)
try {
await ethereum.enable();
} catch (error) {
alert('You must approve this dApp to interact with it, reload it to approve it')
}
const user = (await web3js.eth.getAccounts())[0]
window.contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
from: user
})
await this.setState({contract, user})
}
你还必须更新你的render()
函数,因为你刚刚部署了你的智能合约。我们将从另一个名为getContent()
的函数中获取内容:
render() {
return (
<div className="main-container">
<div className="hashtag-block">
<h3>Top hashtags</h3>
<div className="hashtag-container">{this.state.topHashtagBlock}</div>
<h3>Followed hashtags</h3>
<div className="hashtag-container">{this.state.followedHashtagsBlock}</div>
</div>
<div className="content-block">
<div className="input-container">
<textarea ref="content" placeholder="Publish content..."></textarea>
<input ref="hashtags" type="text" placeholder="Hashtags separated by commas without the # sign..."/>
<button onClick={() => {
this.publishContent(this.refs.content.value, this.refs.hashtags.value)
}} type="button">Publish</button>
</div>
<div className="content-container">
{this.state.contentsBlock}
</div>
</div>
</div>
)
}
修改后看起来是这样的:
让我们更新 get content 函数,根据用户是否有任何活动订阅来生成数据:
- 为了获得用户将会看到的所有内容,我们需要获得
latestContentId
,这是当时有多少内容可用的数字,以防用户还没有订阅任何标签:
async getContent() {
const latestContentId = await this.state.contract.methods.latestContentId().call()
const amount = 10
const amountPerHashtag = 3
let contents = []
let counter = amount
- 如果用户通过遍历所有 id 来跟随标签,则获取内容片段:
// If we have subscriptions, get content for those subscriptions 3 pieces per hashtag
if(this.state.followedHashtags.length > 0) {
for(let i = 0; i < this.state.followedHashtags.length; i++) {
// Get 3 contents per hashtag
let contentIds = await this.state.contract.methods.getContentIdsByHashtag(this.bytes32(this.state.followedHashtags[i]), 3).call()
let counterTwo = amountPerHashtag
if(contentIds < amountPerHashtag) counterTwo = contentIds
for(let a = counterTwo - 1; a >= 0; a--) {
let content = await this.state.contract.methods.getContentById(i).call()
content = {
id: content[0],
author: content[1],
time: new Date(parseInt(content[2] + '000')).toLocaleDateString(),
message: content[3],
hashtags: content[4],
}
content.message = web3js.utils.toUtf8(content.message)
content.hashtags = content.hashtags.map(hashtag => web3js.utils.toUtf8(hashtag))
contents.push(content)
}
}
}
- 如果用户还没有订阅任何 hashtags,更新
counter
变量进行反向循环,这样我们就可以先获得最新的部分:
// If we don't have enough content yet, show whats in there
if(latestContentId < amount) counter = latestContentId
for(let i = counter - 1; i >= 0; i--) {
let content = await this.state.contract.methods.getContentById(i).call()
content = {
id: content[0],
author: content[1],
time: new Date(parseInt(content[2] + '000')).toLocaleDateString(),
message: content[3],
hashtags: content[4],
}
content.message = web3js.utils.toUtf8(content.message)
content.hashtags = content.hashtags.map(hashtag => web3js.utils.toUtf8(hashtag))
contents.push(content)
}
- 生成
contentsBlock
,它包含创建一条内容的所有元素,类似于一条推文或一篇脸书帖子:
let contentsBlock = await Promise.all(contents.map(async (element, index) => (
<div key={index} className="content">
<div className="content-address">{element.author}</div>
<div className="content-message">{element.message}</div>
<div className="content-hashtags">{element.hashtags.map((hashtag, i) => (
<span key={i}>
<Hashtag
hashtag={hashtag}
contract={this.state.contract}
subscribe={hashtag => this.subscribe(hashtag)}
unsubscribe={hashtag => this.unsubscribe(hashtag)}
/>
</span>
))}
</div>
<div className="content-time">{element.time}</div>
</div>
)))
this.setState({contentsBlock})
}
这个getContent()
函数检查用户是否有任何活动的订阅,以便它可以为每个 hashtag 检索最多三段内容。它还将获得上传到 dApp 的 10 篇最新文章。它非常大,因为它根据智能契约上可用的 hashtags 的数量生成数据。如果你关注 100 个标签,你会看到 300 条新内容,因为我们在 feed 中每个标签有 3 篇文章。我们还添加了 10 个随机内容,这些内容将从智能契约中的数组contents
中获取。
创建 hashtag 组件
每个 hashtag 都是一个小机器,包含大量逻辑来检测用户是否订阅。这看起来很简单,但是请记住,我们需要获得每个用户的每个 hashtag 的状态,这意味着我们必须执行大量的请求,这会降低 dApp 的性能。创建函数时要干净,这样它们才能顺利运行。
我们使用了一个名为 hashtag 的新组件,它是一个 HTML 对象,返回一个交互式 Hashtag 文本,可以点击订阅或取消订阅。这是创建此类功能以降低复杂性的最干净的方式:
- 创建带有几个状态变量的构造函数,根据用户的行为显示或隐藏标签:
class Hashtag extends React.Component {
constructor(props) {
super()
this.state = {
displaySubscribe: false,
displayUnsubscribe: false,
checkSubscription: false,
isSubscribed: false,
}
}
- 创建
bytes32()
和checkExistingSubscription()
函数来检查当前用户是否已经关注了这个特定的标签:
componentDidMount() {
this.checkExistingSubscription()
}
bytes32(name) {
let nameHex = web3js.utils.toHex(name)
for(let i = nameHex.length; i < 66; i++) {
nameHex = nameHex + '0'
}
return nameHex
}
async checkExistingSubscription() {
const isSubscribed = await this.props.contract.methods.checkExistingSubscription(this.bytes32(this.props.hashtag)).call()
this.setState({isSubscribed})
}
render()
函数非常大,所以我们将它分成两个主要部分:检测用户是否订阅的功能和显示正确按钮的功能:
render() {
return (
<span onMouseEnter={async () => {
if(this.state.checkSubscription) await this.checkExistingSubscription()
if(!this.state.isSubscribed) {
this.setState({
displaySubscribe: true,
displayUnsubscribe: false,
})
} else {
this.setState({
displaySubscribe: false,
displayUnsubscribe: true,
})
}
}} onMouseLeave={() => {
this.setState({
displaySubscribe: false,
displayUnsubscribe: false,
})
}}>
- 实现当用户悬停在标签上时显示的订阅或取消订阅按钮:
<a className="hashtag" href="#">#{this.props.hashtag}</a>
<span className="spacer"></span>
<button onClick={() => {
this.props.subscribe(this.props.hashtag)
this.setState({checkSubscription: true})
}} className={this.state.displaySubscribe ? '' : 'hidden'} type="button">Subscribe</button>
<button onClick={() => {
this.props.unsubscribe(this.props.hashtag)
this.setState({checkSubscription: true})
}} className={this.state.displayUnsubscribe ? '' : 'hidden'} type="button">Unsubscribe</button>
<span className="spacer"></span>
</span>
)
}
}
render()
函数显示标签,当鼠标悬停时,标签显示订阅或取消订阅按钮。checkExistingSubscription()
函数获取特定 hashtag 订阅的状态,以便为希望取消订阅的用户显示正确类型的按钮。
创建标签 getter
我们现在可以创建一个函数,在页面加载时从智能契约中获取顶部标签和后面的标签。我们将通过检索 followed 和 top 标签来实现。这些将通过循环显示给用户,直到界面充满数据。
尝试自己实现它,完成后会看到以下结果:
- 定义创建结果标签 JSX 所需的变量:
async getHashtags() {
let topHashtagBlock
let followedHashtagsBlock
const amount = 10
const topHashtags = (await contract.methods.getTopHashtags(amount).call()).map(element => web3js.utils.toUtf8(element))
const followedHashtags = (await this.state.contract.methods.getFollowedHashtags().call()).map(element => web3js.utils.toUtf8(element))
- 开始遍历标签块,直到我们填充了顶部标签列表:
if(topHashtags.length == 0) {
topHashtagBlock = 'There are no hashtags yet, come back later!'
} else {
topHashtagBlock = topHashtags.map((hashtag, index) => (
<div key={index}>
<Hashtag
hashtag={hashtag}
contract={this.state.contract}
subscribe={hashtag => this.subscribe(hashtag)}
unsubscribe={hashtag => this.unsubscribe(hashtag)}
/>
</div>
))
}
- 如果用户没有关注任何标签,我们将显示一条消息。如果是,我们将遍历所有跟随的 Hashtag 来生成带有所需数据的 hash tag 组件。用我们刚刚创建的新块更新状态,以在
render()
函数中显示它们:
if(followedHashtags.length == 0) {
followedHashtagsBlock = "You're not following any hashtags yet"
} else {
followedHashtagsBlock = followedHashtags.map((hashtag, index) => (
<div key={index}>
<Hashtag
hashtag={hashtag}
contract={this.state.contract}
subscribe={hashtag => this.subscribe(hashtag)}
unsubscribe={hashtag => this.unsubscribe(hashtag)}
/>
</div>
))
}
this.setState({topHashtagBlock, followedHashtagsBlock, followedHashtags})
}
创建发布功能
发布新的内容是一项简单的任务,它要求我们验证所有的输入都包含有效的文本字符串。由于我们将 hashtags 存储在 bytes32 变量中,因此我们需要正确格式化用户引入的 hashtags,以便智能合约能够安全地处理它们。
让我们使用发布功能,这样我们就可以通过执行以下步骤开始生成内容:
- 如果您还没有创建
bytes32()
函数,请创建它,因为我们很快就会用到它:
bytes32(name) {
let nameHex = web3js.utils.toHex(name)
for(let i = nameHex.length; i < 66; i++)
{
nameHex = nameHex + '0'
}
return nameHex
}
- 添加
publishContent()
函数来处理带有标签的消息。hashtags 将以字符串格式给出,其中包含逗号分隔的字符串列表,没有散列符号(#
)。确保合同的标签正确分离和格式化:
async publishContent(message, hashtags) {
if(message.length == 0) alert('You must write a message')
hashtags = hashtags.trim().replace(/#*/g, '').replace(/,+/g, ',').split(',').map(element => this.bytes32(element.trim()))
message = this.bytes32(message)
try {
await this.state.contract.methods.addContent(message, hashtags).send({
from: this.state.user,
gas: 8e6
})
} catch (e) {console.log('Error', e)}
await this.getHashtags()
await this.getContent()
}
下面是对我们刚刚添加的两个函数的解释:
bytes32()
:该函数用于将普通字符串转换为十六进制,以保证可靠性,因为新的更新迫使 web3 用户在处理bytes
类型的变量时将数据转换为十六进制。-
publishContent()
:这个函数看起来有点混乱,因为我们使用 regex 将用户输入的 hashtag 转换为每个 hashtag 的一个有效的明文字符串数组。它做的事情包括删除空格、删除重复的逗号和标签符号,然后将字符串分解成一个有效的数组,可以在我们的智能契约中使用。 -
记得更新您的
setup()
函数,以便它在加载时获得最新内容:
async setup() {
window.web3js = new Web3Js(ethereum)
try {
await ethereum.enable();
} catch (error) {
alert('You must approve this dApp to interact with it, reload it to approve it')
}
const user = (await web3js.eth.getAccounts())[0]
window.contract = new web3js.eth.Contract(ABI.abi, ABI.networks['3'].address, {
from: user
})
await this.setState({contract, user})
await this.getHashtags()
await this.getContent()
}
- 是时候专注于创建订阅功能了。它们将在用户点击订阅或取消订阅时执行,具体取决于当前状态。尝试自己实现它们,完成后回来将你的解决方案与我的进行比较。请记住,这是关于尝试和失败,直到代码变得足够好。这是我的解决方案:
async subscribe(hashtag) {
try {
await this.state.contract.methods.subscribeToHashtag(this.bytes32(hashtag)).send({from: this.state.user})
} catch(e) { console.log(e) }
await this.getHashtags()
await this.getContent()
}
async unsubscribe(hashtag) {
try {
await this.state.contract.methods.unsubscribeToHashtag(this.bytes32(hashtag)).send({from: this.state.user})
} catch(e) { console.log(e) }
await this.getHashtags()
await this.getContent()
}
这两个函数都很简单。当用户按下标签名称旁边的按钮时,它们运行相应的订阅或取消订阅功能。注意我们如何使用 try catch 来避免在调用契约时出现故障时破坏整个应用程序;这也是因为有时它有一个奇怪的失败系统,它会无缘无故地停止执行。当你觉得需要的时候,只需添加 try catch 块。
你可以在 GitHub 的https://github.com/merlox/social-media-dapp找到更新版本,里面有完整的实现代码供你参考。差不多就是这样!现在,你的区块链发展简历中有了一个新项目,你可以向雇主展示,或者建立一个更好的分散式社交媒体平台来筹集资金。
摘要
当谈到为用户创建一个完全去中心化的社交媒体平台来自由发布内容时,大概就是这样了。在本章中,您了解了在区块链上创建这种类型的应用程序相对于在集中式系统上创建它的好处。然后你用 Truffle 和 React 从头开始创建了用户界面。之后,您开发了智能合约,并将其连接到 dApp,使其具有交互性。总的来说,您获得了大量的经验,您可以在此基础上创建一个具有有趣特性的不同类型的社交媒体平台,比如关注用户和添加用于与不同 API 交互的 oracles。
在下一章中,我们将探索区块链分散式电子商务市场背后的构建过程,在这里您将为自己的企业创建一个功能齐全的商店。