效果预览
项目的视频教程部分已经发布到了b站
https://space.bilibili.com/391924926/channel/seriesdetail?sid=2745034
初始化状态
添加
删除
开发环境准备
系统环境
- 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。
1.下载Ganache https://trufflesuite.com/ganache/,选择适合您操作系统的版本和安装文件
2。按照提示进行安装。
3。启动应用
首次打开会看到这样的界面
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>