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

效果预览

项目的视频教程部分已经发布到了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节点
  • 创建工作区后,屏幕将显示有关服务器的一些详细信息,并列出一些帐户。每个账户自带100ETH 。这种模式启动,数据只保存在内存中,关闭重新打开数据会重新初始化。另请注意,您可以从顶部的搜索框中搜索区块编号或交易哈希。
  • 点击 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>

猜你喜欢

转载自blog.csdn.net/u010359479/article/details/128231178