使用react开发dapp应用实战-宠物认领

阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么

除此之外,你最好还了解一些react知识。

本文通过实例教大家来开发去中心化应用,应用效果如图:

 

从本文,你可以学习到:

  • 搭建智能合约开发环境
  • 创建Truffle项目
  • 编写智能合约
  • 编译和部署智能合约到区块链
  • 如何通过Web3和智能合约交互
  • MetaMask 的使用

 

项目背景

Pet有一个宠物店,有16只宠物,他想开发一个去中心化应用,让大家来领养宠物。

在truffle box中,已经提供了pet-shop的网站部分的代码和react框架,我们只需要编写合约及交互部分。

环境搭建

  1. 安装Node
  2. 安装 Truffle :npm install -g truffle
  3. 安装Ganache

创建项目

  1. 建立项目目录并进入

mkdir pet-shop-react

cd pet-shop-react

  1. 使用truffle unbox 创建项目

> truffle unbox react-box

Downloading...

Unpacking...

Setting up...

Unbox successful. Sweet!

 

Commands:

 

Compile: truffle compile

Migrate: truffle migrate

Test contracts: truffle test

Test dapp: npm test

Run dev server: npm run start

Build for production: npm run build

 

 

也可以使用truffle init 来创建一个全新的项目。

项目目录结构

contracts/ 智能合约的文件夹,所有的智能合约文件都放置在这里,里面包含一个重要的合约Migrations.sol(稍后再讲)

migrations/ 用来处理部署(迁移)智能合约 ,迁移是一个额外特别的合约用来保存合约的变化。

test/ 智能合约测试用例文件夹

truffle.js/ 配置文件

src/ 前端业务文件夹

build/ 合约编译后存放文件夹

 

 

编写智能合约

智能合约承担着分布式应用的后台逻辑和存储。智能合约使用solidity编写,可阅读

solidity系列文章

在contracts目录下,添加合约文件Adoption.sol

 

pragma solidity ^0.4.17;

 

contract Adoption {

address[16] public adopters; // 保存领养者的地址

// 领养宠物

function adopt(uint petId) public returns (uint) {

require(petId >= 0 && petId <= 15); // 确保id在数组长度内

 

adopters[petId] = msg.sender; // 保存调用这地址

return petId;

}

// 返回领养者

function getAdopters() public view returns (address[16]) {

return adopters;

}

}

 

智能合约

Truffle集成了一个开发者控制台,可用来生成一个开发链用来测试和部署智能合约。

编译

Solidity是编译型语言,需要把可读的Solidity代码编译为EVM字节码才能运行。

> truffle compile

 

Compiling ./contracts/Adoption.sol...

Compiling ./contracts/Migrations.sol...

Compiling ./contracts/SimpleStorage.sol...

Writing artifacts to ./build/contracts

 

部署

编译之后,就可以部署到区块链上。

在migrations文件夹下已经有一个1_initial_migration.js部署脚本,用来部署Migrations.sol合约。

Migrations.sol 用来确保不会部署相同的合约。

现在我们来创建一个自己的部署脚本3_deploy_contracts.js

var Adoption = artifacts.require("./Adoption.sol");

module.exports = function(deployer) {

deployer.deploy(Adoption);

};

 

启动Ganache:

 

修改truffle.js 文件:

module.exports = {

// See <http://truffleframework.com/docs/advanced/configuration>

networks: {

development: {

host: "localhost",

port: 7545,

network_id: "*" // Match any network id

}

}

};

 

部署

truffle migrate

 

Using network 'development'.

 

Running migration: 1_initial_migration.js

Replacing Migrations...

... 0xb7a9652d5fd73a7df1703c874e4fc510f5b714fb61203ada3931babfd7f3cb25

Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0

Saving successful migration to network...

... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956

Saving artifacts...

Running migration: 2_deploy_contracts.js

Replacing SimpleStorage...

... 0x989a1bf7b17292d05bc5fbc5a70d27d2b002ba2d0265d366f836f619763a733e

SimpleStorage: 0x345ca3e014aaf5dca488057592ee47305d9b3e10

Saving successful migration to network...

... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0

Saving artifacts...

Running migration: 3_deploy_contracts.js

Deploying Adoption...

... 0xda3a44984cadaf6481a4b462c495b6dbdc3ab77592de062ac3d286c24e6fd1ec

Adoption: 0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f

Saving successful migration to network...

... 0x55b13865c62964c3b211dd22c1106101855e698e00dbf6d21aad05d6707cf626

Saving artifacts...

到上面为止服务器端就开发完成。

 

React编写页面

在src目录下,创建components和images文件夹

cd src

mkdir components

mkdir images

 

在util编写公共方法

getTransactionReceiptMined.js:

module.exports = function getTransactionReceiptMined(txHash, interval) {

const self = this;

const transactionReceiptAsync = function(resolve, reject) {

self.getTransactionReceipt(txHash, (error, receipt) => {

if (error) {

reject(error);

} else if (receipt == null) {

setTimeout(

() => transactionReceiptAsync(resolve, reject),

interval ? interval : 500);

} else {

resolve(receipt);

}

});

};

 

if (Array.isArray(txHash)) {

return Promise.all(txHash.map(

oneTxHash => self.getTransactionReceiptMined(oneTxHash, interval)));

} else if (typeof txHash === "string") {

return new Promise(transactionReceiptAsync);

} else {

throw new Error("Invalid Type: " + txHash);

}

};

 

isAdopted.js:

export default function isAdopted(adopter) {

return adopter !== '0x0000000000000000000000000000000000000000';

}

 

将官方的pet shop中image下图片拷贝到image目录

 

创建dogImageMap.js文件:

import boxer from './images/boxer.jpeg';

import frenchBulldog from './images/french-bulldog.jpeg';

import goldenRetriever from './images/golden-retriever.jpeg';

import scottishTerrier from './images/scottish-terrier.jpeg';

 

 

export default function dogImageMap(path) {

switch(path) {

case 'images/boxer.jpeg': {

return boxer;

}

 

case 'images/french-bulldog.jpeg': {

return frenchBulldog;

}

 

case 'images/golden-retriever.jpeg': {

return goldenRetriever;

}

 

default: {

return scottishTerrier;

}

}

}

 

在compents文件下创建Pet.js

import React, { Component } from 'react';

import PropTypes from 'prop-types';

import dogImageMap from '../dogImageMap';

import isAdopted from '../utils/isAdopted';

 

class Pet extends Component {

constructor(props) {

super(props);

 

this.state = {

pending: false,

}

}

 

render() {

const {

pet,

adopter,

index,

} = this.props;

 

return (

<div className="panel-body">

<img alt="140x140" data-src="holder.js/140x140" className="img-rounded img-center" style={{ width: '100%' }} src={dogImageMap(pet.picture)} data-holder-rendered="true" />

<strong>Breed</strong>: <span className="pet-breed">{pet.breed}</span><br/>

<strong>Age</strong>: <span className="pet-age">{pet.age}</span><br/>

<strong>Location</strong>: <span className="pet-location">{pet.location}</span><br/><br/>

<button

className="btn btn-default btn-adopt"

type="button"

disabled={isAdopted(adopter)}

onClick={() => this.props.adoptPet(index)}

>

{

isAdopted(adopter) ? 'Success' : 'Adopt'

}

</button>

<br />

<strong>Owner: {(adopter)}</strong>

</div>

);

}

}

 

Pet.propTypes = {

pet: PropTypes.object,

adopters: PropTypes.string,

index: PropTypes.number.isRequired,

adoptPet: PropTypes.func.isRequired,

}

 

export default Pet;

 

创建pets.js:

export const petsArray = [

{

"id": 0,

"name": "Frieda",

"picture": "images/scottish-terrier.jpeg",

"age": 3,

"breed": "Scottish Terrier",

"location": "Lisco, Alabama"

},

{

"id": 1,

"name": "Gina",

"picture": "images/scottish-terrier.jpeg",

"age": 3,

"breed": "Scottish Terrier",

"location": "Tooleville, West Virginia"

},

{

"id": 2,

"name": "Collins",

"picture": "images/french-bulldog.jpeg",

"age": 2,

"breed": "French Bulldog",

"location": "Freeburn, Idaho"

},

{

"id": 3,

"name": "Melissa",

"picture": "images/boxer.jpeg",

"age": 2,

"breed": "Boxer",

"location": "Camas, Pennsylvania"

},

{

"id": 4,

"name": "Jeanine",

"picture": "images/french-bulldog.jpeg",

"age": 2,

"breed": "French Bulldog",

"location": "Gerber, South Dakota"

},

{

"id": 5,

"name": "Elvia",

"picture": "images/french-bulldog.jpeg",

"age": 3,

"breed": "French Bulldog",

"location": "Innsbrook, Illinois"

},

{

"id": 6,

"name": "Latisha",

"picture": "images/golden-retriever.jpeg",

"age": 3,

"breed": "Golden Retriever",

"location": "Soudan, Louisiana"

},

{

"id": 7,

"name": "Coleman",

"picture": "images/golden-retriever.jpeg",

"age": 3,

"breed": "Golden Retriever",

"location": "Jacksonwald, Palau"

},

{

"id": 8,

"name": "Nichole",

"picture": "images/french-bulldog.jpeg",

"age": 2,

"breed": "French Bulldog",

"location": "Honolulu, Hawaii"

},

{

"id": 9,

"name": "Fran",

"picture": "images/boxer.jpeg",

"age": 3,

"breed": "Boxer",

"location": "Matheny, Utah"

},

{

"id": 10,

"name": "Leonor",

"picture": "images/boxer.jpeg",

"age": 2,

"breed": "Boxer",

"location": "Tyhee, Indiana"

},

{

"id": 11,

"name": "Dean",

"picture": "images/scottish-terrier.jpeg",

"age": 3,

"breed": "Scottish Terrier",

"location": "Windsor, Montana"

},

{

"id": 12,

"name": "Stevenson",

"picture": "images/french-bulldog.jpeg",

"age": 3,

"breed": "French Bulldog",

"location": "Kingstowne, Nevada"

},

{

"id": 13,

"name": "Kristina",

"picture": "images/golden-retriever.jpeg",

"age": 4,

"breed": "Golden Retriever",

"location": "Sultana, Massachusetts"

},

{

"id": 14,

"name": "Ethel",

"picture": "images/golden-retriever.jpeg",

"age": 2,

"breed": "Golden Retriever",

"location": "Broadlands, Oregon"

},

{

"id": 15,

"name": "Terry",

"picture": "images/golden-retriever.jpeg",

"age": 2,

"breed": "Golden Retriever",

"location": "Dawn, Wisconsin"

}

]

 

修改app.js文件:

import React from 'react'

import _ from 'lodash';

import AdoptionContract from '../build/contracts/Adoption.json'

import { petsArray } from './pets.js';

import getWeb3 from './utils/getWeb3'

import Pet from './components/Pet';

import './css/oswald.css'

import './css/open-sans.css'

import './css/pure-min.css'

import './App.css'

 

class App extends React.Component {

constructor(props) {

super(props)

 

this.state = {

web3: {},

adoptionContract: null,

pets: [],

adopters: [],

blockNumber: null,

}

 

this.adoptPet = this.adoptPet.bind(this);

this.getBlockNumber = this.getBlockNumber.bind(this);

}

 

componentWillMount() {

getWeb3

.then(results => {

this.setState({

web3: results.web3

}, () => {

this.getBlockNumber()

this.instantiateContract()

this.fetchPets();

});

})

.catch(() => {

console.log('Error finding web3.')

})

}

 

getBlockNumber() {

this.state.web3.eth.getBlockNumber((error, block) => {

this.setState({ blockNumber: block });

});

}

 

fetchPets() {

this.setState({ pets: petsArray });

}

 

adoptPet(item) {

const { adoptionContract } = this.state;

const newAdopters = this.state.adopters;

 

this.state.web3.eth.getAccounts((error, accounts) => {

adoptionContract.deployed().then((instance) => {

return instance.adopt(item, { from: accounts[0]})

.then((result) => {

this.state.web3.eth.getTransactionReceiptMined(result.tx)

.then((newResult) => {

newAdopters[item] = accounts[0];

this.setState({

blockNumber: newResult.blockNumber,

adopters: newAdopters

})

});

})

.catch((err) => {

console.log(err.message);

})

});

});

}

 

instantiateContract() {

const contract = require('truffle-contract')

const adoption = contract(AdoptionContract);

adoption.setProvider(this.state.web3.currentProvider)

 

adoption.deployed().then((instance) => {

return instance.getAdopters.call().then((result) => {

this.setState({

adopters: result,

adoptionContract: adoption

});

});

});

}

 

render() {

const {

pets,

adopters

} = this.state

 

return (

<div className="App">

<div className="container">

<div className="row">

<div className="col-xs-12 col-sm-8 col-sm-push-2">

<h1 className="text-center">Pete's Pet Shop</h1>

<hr/>

<br/>

</div>

</div>

 

<div id="petsRow" className="row">

{

_.map(pets, (pet, index) => {

return (

<div className="col-sm-6 col-md-4 col-lg-3" key={index}>

<div className="panel panel-default panel-pet">

<div className="panel-heading">

<h3 className="panel-title">{pet.name}</h3>

</div>

</div>

<Pet

pet={pet}

adopter={adopters[index]}

index={index}

adoptPet={this.adoptPet}

/>

</div>

)

})

}

</div>

</div>

</div>

);

}

}

 

export default App

 

 

修改utils下getWeb3.js文件到url路径到7454端口。

import Web3 from 'web3'

import getTransactionReceiptMined from './getTransactionReceiptMined';

let getWeb3 = new Promise(function(resolve, reject) {

// Wait for loading completion to avoid race conditions with web3 injection timing.

window.addEventListener('load', function() {

var results

var web3 = window.web3

 

// Checking if Web3 has been injected by the browser (Mist/MetaMask)

if (typeof web3 !== 'undefined') {

// Use Mist/MetaMask's provider.

web3 = new Web3(web3.currentProvider)

web3.eth.getTransactionReceiptMined = getTransactionReceiptMined;

results = {

web3: web3

}

 

console.log('Injected web3 detected.');

 

resolve(results)

} else {

// Fallback to localhost if no web3 injection. We've configured this to

// use the development console's port by default.

var provider = new Web3.providers.HttpProvider('http://127.0.0.1:7545')

 

web3 = new Web3(provider)

 

results = {

web3: web3

}

 

console.log('No web3 instance injected, using Local web3.');

 

resolve(results)

}

})

})

 

export default getWeb3

 

由于copy官方例子,使用了bootstrap.mini.css

将css放入到public文件夹中,并修改index.html,加上下面这句话:

<link href="%PUBLIC_URL%/bootstrap.min.css" rel="stylesheet">

 

运行:

yarn run start

 

在浏览器中运行

安装 MetaMask

MetaMask 是一款插件形式的以太坊轻客户端,开发过程中使用MetaMask和我们的dapp进行交互是个很好的选择,通过此链接安装,安装完成后,浏览器工具条会显示一个小狐狸图标。

配置钱包

在接受隐私说明后,会出现页面如下:

metamask-initial.png

这里我们通过还原一个Ganache为我们创建好的钱包,作为我们的开发测试钱包。点击页面的Import Existing DEN,输入Ganache显示的助记词。

candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

然后自己想要的密码,点击OK。

如图:

metamask-seed.png

连接开发区块链网络

默认连接的是以太坊主网(左上角显示),选择Custom RPC,添加一个网络:http://127.0.0.1:7545,点返回后,显示如下:

metamask-account1.png

 

这是左上角显示为Private Network,账号是Ganache中默认的第一个账号。

注意 在交易时候 请注意保持metamask保持活跃状态。

 

 

点击领取。

 

 

成功:

 

 

感谢Tiny熊和kkomaz,pet shop 相关文章。https://blog.csdn.net/yuanfangyuan_block/article/details/79412830

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值