DApp实战:开发属于你的第一个DApp - 我的日记本

时间:2022-12-06 10:55:35

效果预览

项目的视频教程部分已经发布到了b站

DApp实战:开发属于你的第一个DApp - 我的日记本

​https://space.bilibili.com/391924926/channel/seriesdetail?sid=2745034​

初始化状态

DApp实战:开发属于你的第一个DApp - 我的日记本

添加

DApp实战:开发属于你的第一个DApp - 我的日记本

DApp实战:开发属于你的第一个DApp - 我的日记本

DApp实战:开发属于你的第一个DApp - 我的日记本

删除

DApp实战:开发属于你的第一个DApp - 我的日记本

开发环境准备

系统环境

  • Remix
  • Ganache
  • nodejs最新版
  • metamask

开发框架

  • vue-cli脚手架
  • web3.js
  • element-ui

vue-cli脚手架创建工程

vue create my-node
Vue CLI v5.0.1


New version available 5.0.1 5.0.8
Run yarn global add @vue/cli to update!



? Please pick a preset: Default ([Vue 2] babel, eslint)


Vue CLI v5.0.1
Creating project in /Users/sleep/Desktop/my-node.
Initializing git repository...
Installing CLI plugins. This might take a while...

yarn install v1.22.17
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...

success Saved lockfile.
info To upgrade, run the following command:
$ curl --compressed -o- -L https://yarnpkg.com/install.sh | bash
Done in 14.18s.
Invoking generators...
Installing additional dependencies...

yarn install v1.22.17
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
Done in 4.74s.
Running completion hooks...

Generating README.md...

Successfully created project my-node.
Get started with the following commands:

$ cd my-node
$ yarn serve

安装依赖库

npm 安装 elementUI,web3js

npm i element-ui web3 -S

Solidity编写智能合约

注意点:

删除数组的练习, 我们发现数组元素不会真正删除 只会置为0值

1,2,3,5 => 1,2,0,5

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract Array {
uint256[] public arr=[1,2,3,4];


constructor() {
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
arr[3] = 5;
}
function addArr(uint _x) public {
arr.push(_x);
}

function deleteArr(uint256 i) public {
delete arr[i];
}


function getArr() public view returns (uint256[] memory) {
return arr;
}
}

数组无法直接将某个元素删除,只能从后向前逐个删除。基于这个特性,我需要通过把把元素从右往左移动从而删除元素 保留元素的顺序

// [1, 2, 3] -- remove(1) --> [1, 3, 3] --> [1, 3]
// [1, 2, 3, 4, 5, 6] -- remove(2) --> [1, 2, 4, 5, 6, 6] --> [1, 2, 4, 5, 6]
// [1, 2, 3, 4, 5, 6] -- remove(0) --> [2, 3, 4, 5, 6, 6] --> [2, 3, 4, 5, 6]
function remove(uint _index) public {
require(_index < arr.length, "index out of bound");
for (uint i = _index; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr.pop();//交换后再把最后一个元素移除
}

我们也可以把最后一个元素copy 到被移动元素的位置从而达到移除元素。无序。

function remove(uint index) public {
arr[index] = arr[arr.length - 1];
arr.pop();//删除最后一个元素
}

最终合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Note {
// 定义日记本数据结构
struct node {
string name;
string content;
string date;
}
//定义每个用户自己的日记本列表存储对象
mapping(address => node[]) public userNodeList;
//添加成功的通知
event addSuccess(address);
//删除成功的通知
event deleteSuccess(address);
//获取当前用户的日记列表
function getUserNodeList() external view returns (node[] memory) {
return userNodeList[msg.sender];
}
//添加一条日记
function addNode(
string memory _name,
string memory _content,
string memory _date
) external {
userNodeList[msg.sender].push(
node({name: _name, content: _content, date: _date})
);
emit addSuccess(msg.sender);
}
//删除一条日记
function deleteNode(uint256 _x) external {
require(userNodeList[msg.sender].length > _x, "out of index");
for (uint256 i = _x; i < userNodeList[msg.sender].length - 1; i++) {
userNodeList[msg.sender][i] = userNodeList[msg.sender][i + 1];
}
userNodeList[msg.sender].pop();
emit deleteSuccess(msg.sender);
}
}

编写前端界面样式

在 main.js 中写入以下内容:

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false

//先导入elementUI库
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

new Vue({
render: h => h(App),
}).$mount('#app')

把前端的html结构搭建出来

<template>
<div id="app">
<el-button type="primary" size="mini" @click="addNode">添加</el-button>
<el-table
:data="tableData"
style="width: 100%; ">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="标题"
width="180">
</el-table-column>
<el-table-column
prop="content"
label="内容">
<template v-slot="scope">
<el-button @click="getContent(scope.row.content)">查看</el-button>
</template>
</el-table-column>
<el-table-column>
<template v-slot="scope">
<el-button @click="deleteNode(scope.$index)" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>

<el-dialog :visible.sync="open">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="标题">
<el-input type="text" v-model="form.name"/>
</el-form-item>
<el-form-item label="内容">
<el-input type="textarea" v-model="form.name"/>
</el-form-item>
<el-form-item label="时间">
<el-date-picker v-model="form.date" type="datetime" placeholder="请输入时间"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
<el-button type="primary" @click="open = false"> </el-button>
</div>
</el-dialog>

<el-dialog :visible.sync="contentOpen">
<div>{{ form.content }}</div>
</el-dialog>
</div>
</template>

<script>

export default {
name: 'App',
components: {},
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
content: '上海市普陀区金沙江路 1518 弄'
}],
form: {
name: "",
content: "",
date: "",
},
open: false,
contentOpen: false
}
},
methods: {
addNode() {
this.open = true
},
async deleteNode(index) {
console.log(index)
try {
let res = await this.$confirm("确定要删除吗?");
if (!res) {
this.$message.error("删除失败")
}
//todo 调用删除合约方法
} catch (e) {
console.log(e)
}
},
getContent(content) {
this.contentOpen = true
this.form.content = content
}
}
}
</script>

准备区块链环境

Ganache

官网:​​https://trufflesuite.com/docs/ganache/​

Ganache 是一个用与本地开发的区块链,用于在Ethereum区块链上开发去中心化的应用程序。Ganache 模拟了Ethereum网络,你可以在发布到生产环境之前看到你的 DApp 将如何执行。

Ganache 有两种形式:UI 和 CLI。

DApp实战:开发属于你的第一个DApp - 我的日记本

1.下载Ganache ​​https://trufflesuite.com/ganache/​​,选择适合您操作系统的版本和安装文件

2。按照提示进行安装。

3。启动应用

首次打开会看到这样的界面

DApp实战:开发属于你的第一个DApp - 我的日记本

4.启动节点

  • 点击quickstart按钮启动一个ETH节点


  • 创建工作区后,屏幕将显示有关服务器的一些详细信息,并列出一些帐户。每个账户被给予 100 以太币。在所有帐户中自动拥有以太币可以让您专注于开发您的应用程序。这种模式启动,数据只保存在内存中,关闭重新打开数据会重新初始化。
  • 有六个可用页面:
  • ​accounts​​ 显示生成的帐户及其余额。这是默认视图。
  • ​Blocks​​显示区块链上开采的每个区块,以及使用的 gas 和交易。
  • ​transactions​​列出了针对区块链运行的所有交易。
  • ​Contracts​​列出了您工作区的 Truffle 项目中包含的合约。
  • ​events​​列出了自该工作区创建以来触发的所有事件。Ganache 将尝试解码由您的 Truffle 项目中的合约触发的事件。
  • ​logs​​显示服务器的日志,这对调试很有用。

另请注意,您可以从顶部的搜索框中搜索区块编号或交易哈希。


  • 点击 New workspace 创建一个新的工作区
  • 需要预先配置参数
  • 定义工作区的名称和描述
  • 定义服务相关参数配置
  • 可以配置每个地址默认分配的ETH数量和账户总数等信息
  • 配置好以后点击 ​​save workspace​​完成最终配置。数据会保存到本地磁盘中,下次再启动应用还会重新加载数据。

web3.js调用合约

初始化web3js

import Web3 from 'web3';

const web3 = new Web3(window.ethereum)
console.log(web3.version)

实例化合约

const contract = new web3.eth.Contract([abi], "合约地址");

调用合约方法

contract.methods.方法名(参数)
//读取类的用 call
.call({from: account})
//写入类的用 send
.send({from: account})

合约事件监听

myContract.events.event({
filter: {},
fromBlock: 'latest'
})
.on("connected", (subscriptionId) => {
console.log("connected", subscriptionId);
})
.on('data', event => {
console.log('data',event);
//可以在这里写逻辑,比如刷新列表
this.getUserNodeList(event.returnValues[0])
})
.on('changed', event => {
// 从本地数据库中删除事件
console.log('changed', event)
})
.on('error', (error, receipt) => {
// 如果交易被网络拒绝并带有交易收据,第二个参数将是交易收据。
console.log(error, receipt)
});

最终代码

<template>
<div id="app">
<div v-if="!view"> {{ tips }}</div>
<div v-else>
<el-button type="primary" size="mini" @click="addNode">添加</el-button>
<el-table
:data="tableData"
style="width: 100%; ">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="标题"
width="180">
</el-table-column>
<el-table-column
prop="content"
label="内容">
<template v-slot="scope">
<el-button @click="getContent(scope.row.content)">查看</el-button>
</template>
</el-table-column>
<el-table-column>
<template v-slot="scope">
<el-button @click="deleteNode(scope.$index)" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>

<el-dialog :visible.sync="open">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="标题">
<el-input type="text" v-model="form.name"/>
</el-form-item>
<el-form-item label="内容">
<el-input type="textarea" v-model="form.content"/>
</el-form-item>
<el-form-item label="时间">
<el-date-picker v-model="form.date" type="datetime" placeholder="请输入时间"/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="open = false"> </el-button>
<el-button type="primary" @click="saveNode"> </el-button>
</div>
</el-dialog>

<el-dialog :visible.sync="contentOpen">
<div>{{ form.content }}</div>
</el-dialog>
</div>
</div>
</template>

<script>
import Web3 from "web3";

export default {
name: 'App',
components: {},
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
content: '上海市普陀区金沙江路 1518 弄'
}],
form: {
name: "",
content: "",
date: "",
},
open: false,
contentOpen: false,
view: false,
tips: "",
web3: null,
contract: null,
//部署合约后填写正确的合约地址
contractAddress: "0x........",
abi: [
//....从json获取
],
accounts: []
}
},
async mounted() {
this.view = this.checkWallet()
this.checkNetwork(0)
await this.eventChange('connect')
await this.eventChange('accountsChanged')
await this.eventChange('chainChanged')
this.accounts = await this.enableAccounts()
console.log(this.accounts[0])
this.web3 = await this.initWeb3js();
this.contract = new this.web3.eth.Contract(this.abi, this.contractAddress);
this.watchEvents(this.contract.events.addSuccess);
this.watchEvents(this.contract.events.deleteSuccess);
console.log(this.contract.methods)
await this.getUserNodeList(this.accounts[0])
},
methods: {
addNode() {
this.open = true
},
async deleteNode(index) {
console.log(index)
try {
let res = await this.$confirm("确定要删除吗?");
if (!res) {
this.$message.error("删除失败")
}
//todo 调用删除合约方法
try {
let res = await this.contract.methods.deleteNode(index).send({from: this.accounts[0]})
console.log(res)
await this.getUserNodeList(this.accounts[0])
} catch (e) {
this.$message.error(e)
}
} catch (e) {
console.log(e)
}
},
getContent(content) {
this.contentOpen = true
this.form.content = content
},

/*web3js 相关方法*/
async checkWallet() {
if (typeof window.ethereum === 'undefined') {
this.tips = "请安装钱包插件";
return false;
}
return true;
},
checkNetwork(networkId) {
if (!window.ethereum.isConnected()) {
console.log('connect error!', networkId);
this.view = false;
this.tips = "请配置钱包链接地址";
return false;
}
this.view = true;
return true
},
async eventChange(event) {
try {
window.ethereum.on(event, async (data) => {
console.log(data)
switch (event) {
case "connect":
console.log(data)
this.checkNetwork(data)
break;
case "networkChanged":
this.checkNetwork(data)
break;
case 'accountsChanged':
console.log(data)
//这里注意一定要重新复制account
this.accounts = data;
await this.getUserNodeList(data[0]);
break
}
})

} catch (e) {
this.$message.error(e)
}
},
initWeb3js() {
return new Web3(window.ethereum)
},
async enableAccounts() {
try {
return await window.ethereum.request({method: 'eth_requestAccounts'});
} catch (e) {
this.$message.error(e)
}
},
watchEvents(event) {
event({
filter: {},
fromBlock: 'latest'
})
.on("connected", (subscriptionId) => {
//当订阅成功连接时触发一次。返回订阅 id。
console.log("connected", subscriptionId);
})
.on('data', event => {
console.log('data',event);
//可以在这里写逻辑,比如刷新列表
this.getUserNodeList(event.returnValues[0])
})
.on('changed', event => {
//当事件从区块链上移除时触发。
// 从本地数据库中删除事件
console.log('changed', event)
})
.on('error', (error, receipt) => {
//当订阅中出现错误时触发。
// 如果交易被网络拒绝并带有交易收据,第二个参数将是交易收据。
console.log(error, receipt)
});
},
/*合约函数*/
async getUserNodeList(account) {
this.tableData = await this.contract.methods
.getUserNodeList()
.call({from: account})
},
async saveNode() {
let {name, content, date} = this.form
console.log(this.form)
try {
let res = await this.contract.methods.addNode(name, content, date.toString()).send({from: this.accounts[0]})
console.log(res)
this.open = false
this.form = {}
await this.getUserNodeList(this.accounts[0])
} catch (e) {
this.$message.error(e)
}
},
}
}
</script>