发布文章

手把手教你从零搭建一款属于自己的 dApp 实战项目(Vue3+TS)

作者
  • avatar
    作者
    Jack Chen @懒人码农

前言

去中心化应用(dApp)是构建在区块链技术上的应用程序,具有去中心化、透明、安全的特性。dApp 开发需要掌握区块链技术、智能合约编写、前端和后端开发等多个领域。

如果不懂区块链,不会智能合约开发、也不会前后端。别担心,看此文就对了,按照本教程步骤操作可以快速搭建属于你的 dApp 项目。

学习路线图

Web3研习社

效果截图

Web3研习社

Web3研习社

项目结构

image.png

技术栈

  • Vue3:是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。
  • Typescript:是由微软进行开发和维护的一种开源的编程语言。TypeScript 是 JavaScript 的严格语法超集,提供了可选的静态类型检查。
  • Ethers.js v6:是一个强大的 JavaScript 库,用于与以太坊区块链进行交互。 它提供了丰富的 API,使得开发者能够轻松地发送交易、读取数据、与智能合约交互等。
  • Hardhat:是一个用于开发以太坊智能合约和 dApp 的开发框架和工具套件。提供了一套功能强大的工具,用于编译、部署、测试和调试智能合约,并与以太坊测试网络进行交互。
  • Ganache:是一个以太坊的个人开发环境,你可以在上面部署合约、开发程序和进行测试。它有桌面版本和命令行工具版本,同时提供对 windows 、Mac 和 Linux 的支持。
  • Solidity:是一种高级的、面向合约的编程语言,用于在以太坊虚拟机 (EVM) 上构建智能合约。它受到 C++、Python 和 JavaScript 的影响,并与这些语言共享语法和编程概念。
  • Metamask(小狐狸):是用于与以太坊区块链进行交互的软件加密货币钱包。它可以通过浏览器扩展程序或移动应用程序让用户访问其以太坊钱包,与去中心化应用进行交互。
  • Element Plus:是一个基于 Vue 3 的高质量 UI 组件库。它包含了丰富的组件和扩展功能,例如表格、表单、按钮、导航、通知等,让开发者能够快速构建高质量的 Web 应用。
  • Remix IDE:是一款可以在线快速编写、调试和部署合约代码的编辑器,完全在浏览器环境中运行,方便易用,非常适合智能合约开发初学者使用。

功能模块

  • 钱包登录
  • 查询余额
  • 我要提款
  • 发布消息
  • 消息列表

准备工作

环境搭建

构建项目

yarn create vite vue3-dapp-demo

Web3研习社

cd vue3-dapp-demo
yarn
yarn dev

Web3研习社

Web3研习社

初始化合约

在根目录新建文件夹my_contact,作为合约项目文件夹。安装hardhat依赖需进入此文件夹后运行如下命令:

cd my_contact
yarn add -D hardhat

Web3研习社

安装成功后,接下来创建简单的hardhat项目,在之前创建好的my_contact文件夹中运行如下命令:

npx hardhat init

让我们创建 JavaScript 或 TypeScript 项目,并完成这些步骤来编译、测试和部署示例合约。我们建议使用 TypeScript,但如果您不熟悉它,只需选择 JavaScript。

Web3研习社

Web3研习社

初始化hardhat项目完成后,再运行npx hardhat & npx hardhat help命令,会看到相关帮助信息如下图所示:

Web3研习社

  • contracts:存放智能合约文件的目录
  • ignition:存放部署智能合约文件的目录
  • test:存放单元测试智能合约文件的目录
  • hardhat.config.ts:Hardhat 配置文件,查看详细配置信息请移步到此:https://hardhat.org/hardhat-runner/docs/config
# 编译合约
npx hardhat compile

Web3研习社

# 测试合约
npx hardhat test

Web3研习社

# 部署合约
npx hardhat ignition deploy ./ignition/modules/Lock.ts

Web3研习社

# 强制编译合约
npx hardhat compile --force

# 清除编译缓存
npx hardhat clean

将 Metamask 或 Dapp 连接到 Hardhat 网络

本地网络部署

# 启动本地节点
npx hardhat node

Web3研习社

打开 Metamask 手动添加本地网络,如下图所示:

Web3研习社

Web3研习社

本地网络添加成功后,根据现有的20个测试账户,复制某个账户私钥,打开 Metamask 选择添加账户按钮,导入私钥添加新账户,如下图所示:

Web3研习社

Web3研习社

Web3研习社

Web3研习社

Web3研习社

# 部署本地网络
npx hardhat ignition deploy ./ignition/modules/Lock.ts --network localhost

Web3研习社

Web3研习社

测试网部署(推荐)

下载安装 Ganache

Web3研习社

如需详细了解Ganache工具的安装和使用,请移步到此:https://54web3.cc/blog/induction-tutorial/ganache-installation-use

修改配置文件hardhat.config.ts

Web3研习社

Web3研习社

代码实现

编写合约

// 创建合约文件:contract/TodoContract.sol

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.24;

// 合约名:TodoContract
contract TodoContract {
    struct Todo {
        uint256 id;
        address author;
        string message;
        uint256 timestamp;
    }

    // 事件:NewTodo,用于记录新创建的待办事项
    event NewTodo(
        uint256 todoID,
        address indexed from,
        string message,
        uint256 timestamp
    );

    // 事件:SendMoneyToContract,用于记录向合约发送以太币的事件
    event SendMoneyToContract(
        uint256 todoID,
        address receiver,
        string message,
        uint256 timestamp
    );

    // 状态变量:todoID,用于记录待办事项的ID
    // 状态变量:todoList,用于存储待办事项列表
    // 状态变量:owner,用于记录合约的拥有者
    uint256 public todoID;
    Todo[] public todoList;
    address payable public owner;

    // 构造函数:初始化合约时设置合约的拥有者
    constructor() payable {
        owner = payable(msg.sender);
    }

    // 修饰符:onlyOwner,用于限制只有合约的拥有者才能调用该函数
    modifier onlyOwner() {
        require(
            msg.sender == owner,
            "\u5fc5\u987b\u662f\u5408\u7ea6\u6240\u6709\u8005"
        );
        _;
    }

    // 函数:withdraw,用于提取合约中的以太币
    function withdraw() public payable onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }

    // 函数:getBalance,用于查询合约中的以太币余额
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // 函数:getTodoList,用于获取待办事项列表
    function getTodoList() public view returns (Todo[] memory) {
        return todoList;
    }

    // 函数:published,用于发布新的待办事项
    function published(string memory _message) public payable {
        todoID += 1;
        Todo memory item = Todo(todoID, msg.sender, _message, block.timestamp);
        todoList.push(item);
        emit NewTodo(todoID, msg.sender, _message, block.timestamp);

        uint256 payAmount = 0.1 ether;
        require(msg.value >= payAmount, "\u4f59\u989d\u4e0d\u8db3");
        (bool success, ) = payable(address(this)).call{value: payAmount}("");
        require(success, "\u5411\u5408\u7ea6\u6c47\u6b3e\u5931\u8d25");
        emit SendMoneyToContract(todoID, msg.sender, _message, block.timestamp);
    }

    // 函数:receive,用于接收以太币
    receive() external payable {}
}

编译合约

npx hardhat compile

通过编译合约文件,我们会得到 Bytecode(字节码)和 ABI( Application Binary Interface)通过 ABI 可与已经部署的合约进行交互。

Web3研习社

创建部署文件

// ignition/modules/TodoContract.ts

import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'

const TodoContractModule = buildModule('TodoContractModule', (m) => {
  const todoContract = m.contract('TodoContract')

  return { todoContract }
})

export default TodoContractModule

部署合约命令

npx hardhat ignition deploy ./ignition/modules/TodoContract.ts --network ganache

在线测试合约

在线打开Remix IDE 以太坊智能合约开发工具,创建合约文件TodoContract.sol,将写好的合约代码复制粘贴进来,保存后会自动编译。

Web3研习社

Web3研习社

Web3研习社

前端开发

钱包登录

// 如需获取完整代码,文末附 github 仓库下载地址
const connectWallet = async () => {
  try {
    if (!window.ethereum) {
      ElNotification({
        title: '提示',
        message: '请先安装浏览器插件 Metamask',
        type: 'warning',
      })
      return
    }

    const provider = new ethers.BrowserProvider(window.ethereum) // 获取提供者
    const signer = await provider.getSigner() // 获取钱包签名
    account.value = await signer.getAddress() // 获取钱包地址
    const network = await provider.getNetwork()
    chainId.value = network.chainId.toString()
    // 保存相关信息到本地
    localStorage.setItem('chainId', chainId.value)
    localStorage.setItem('account', account.value)
  } catch (error) {
    console.log('连接钱包失败:', error)
  }
}
// 监听钱包账户变化
window.ethereum.on("accountsChanged", function (accounts: string[]) {
  account.value = accounts[0]
  localStorage.setItem("account", account.value)
})

查询余额

// 导入 ABI 接口文件
import contractABI from '../artifacts/contracts/TodoContract.sol/TodoContract.json'

// 获取合约账户余额
const getBalance = async () => {
  if (!window.ethereum) {
    ElNotification({
      title: "提示",
      message: "请先安装浏览器插件 Metamask",
      type: "warning",
    });
    return
  }

  try {
    const provider = new ethers.BrowserProvider(window.ethereum)
    const signer = await provider.getSigner()
    account.value = await signer.getAddress()
    const todoContract = new ethers.Contract(contractAddr, contractABI.abi, provider)
    const count = await todoContract.getBalance()
    balance.value = ethers.formatEther(count)
    console.log('获取余额', balance.value)
  } catch (error: any) {
    console.error('获取余额失败:', error)
    ElMessage.error(error.reason || error.data?.message || error.message)
  }
}

我要提款

const getWithdraw = async () => {
  if (!window.ethereum) {
    ElNotification({
      title: "提示",
      message: "请先安装浏览器插件 Metamask",
      type: "warning",
    });
    return
  }

  try {
    const provider = new ethers.BrowserProvider(window.ethereum)
    const signer = await provider.getSigner()
    const todoContract = new ethers.Contract(contractAddr, contractABI.abi, signer)
    await todoContract.withdraw()
  } catch (error: any) {
    console.error('获取提款失败:', error)
  }
}

发布消息

const publishMsg = async () => {
  if (!window.ethereum) {
    ElNotification({
      title: "提示",
      message: "请先安装浏览器插件 Metamask",
      type: "warning",
    });
    return
  }

  try {
    const provider = new ethers.BrowserProvider(window.ethereum)
    const signer = await provider.getSigner()
    const todoContract = new ethers.Contract(contractAddr, contractABI.abi, signer)
    todoContract.on("SendMoneyToContract", async (id, receiver, message, timestamp) => {
      localStorage.setItem("todoCount", id)
      console.log("%d %s", id, receiver)

      const todoList = localStorage.getItem("todoList")
      let list: Todo[] = []
      if (todoList) {
        list = JSON.parse(todoList)
      }
      const todo = new Todo(id, receiver, message, timestamp)
      list.push(todo)
      localStorage.setItem("todoList", JSON.stringify(list))
    })

    const params = { value: ethers.parseEther('0.1') }
    const tx = await todoContract.published(msg.value, params)
    await tx.wait()
    ElMessage.success("发布消息成功")
    setTimeout(() => {
      location.reload()
    }, 2000)
  } catch (error: any) {
    console.error('发布消息失败:', error)
  }
}

消息列表

// 获取消息列表
const getTodoList = async () => {
  if (!window.ethereum) {
    ElNotification({
      title: "提示",
      message: "请先安装浏览器插件 Metamask",
      type: "warning",
    });
    return
  }

  try {
    const provider = new ethers.BrowserProvider(window.ethereum)
    const signer = await provider.getSigner()
    const todoContract = new ethers.Contract(contractAddr, contractABI.abi, signer)
    const list = await todoContract.getTodoList()
    if (list.length > 0) {
      list.map((item: Todo) => {
        const id = item.id
        const time = item.timestamp
        const todo = new Todo(id, item.author, item.message, time)
        todoList.value.push(todo)
      })
    }

    const count = list.length
    localStorage.setItem("todoCount", count)
    todoCount.value = list.length
  } catch (error) {
    console.error('获取消息列表失败:', error)
  }
}

结语

dApp 开发是一项综合性的工作,需要掌握多个领域的知识。通过学习不同平台和工具的文档、参与实际项目,以及不断改进和总结经验,你可以逐步成为一名优秀的 dApp 开发者。后续会输出一系列完整的 dApp 实战项目开发练习教程,涉及到技术栈有 React、Next.js、Wagmi、Vue3、Nuxt.js 等等。如果此文对看官们有一丢丢帮助,请点个赞👍或分享支持一下。

github 仓库:https://github.com/jackchen0120/web3-dapp-all-example

参考资料