news 2026/9/16 5:08:53

DApp开发实战:从智能合约到前端集成,宠物商店项目全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DApp开发实战:从智能合约到前端集成,宠物商店项目全解析

很多人刚接触DApp开发时,问我最多的问题往往是“我到底该先学合约还是先学前端”“为什么我照着教程写了合约却调不通接口”。说实话,这个问题我当年也纠结了很久。DApp的开发路线和传统Web应用差别不小,它既要求你理解区块链底层的交易、签名、Gas机制,又要求你具备一定的前端工程能力,能把钱包接入、合约调用、状态同步这些事情串起来。这篇文章我打算从一个完整项目的角度切入,带你从概念认知一路走到实战开发,以经典的宠物商店DApp为案例,手把手拆解每一个关键环节,适合Solidity零基础但有一定编程经验的同学,也适合已经写过简单合约、想在工程层面补齐短板的人。

1. DApp开发的核心概念与技术选型

1.1 从传统应用架构到去中心化应用架构

传统Web应用的用户注册、数据存储、逻辑执行,都集中在中心化服务器上。服务端改一行逻辑,所有用户立刻受到影响;数据库里删掉一条记录,用户端就再也查不到。这种架构高效,但信任完全寄托在运营方身上。DApp想解决的就是信任问题:把关键业务逻辑放到链上执行,把数据状态放在链上存储,让代码即规则、记录即凭证。

DApp的全称是Decentralized Application,但它并不是说所有东西都要去中心化。实际工程里,通常只有核心资产与关键逻辑上链,比如转账、铸造、交易撮合这类涉及账户状态变更的操作;而一些高频、低价值的操作,比如读取列表、展示图片,依然会用中心化服务器或IPFS来承接。这样做的好处很直观:链上逻辑越少,Gas成本越低,响应速度越快,用户体验也越好。

一个典型的DApp分为三层:智能合约层(区块链上运行的业务逻辑)、前端交互层(用户实际操作的页面)、数据索引层(用于高效查询和展示链上数据)。理解这个分层结构非常重要,因为后续所有的开发、调试、优化,本质上都是在围绕这三层做文章。

1.2 为什么宠物商店是公认的最佳入门项目

市面上讲DApp开发的教程很多,但宠物商店(Pet Shop)这个案例几乎出现在所有主流框架的官方文档里,被一代代开发者反复用来练手。原因其实很现实:它的业务逻辑足够简单,却完整覆盖了DApp开发的全部核心流程。

你可以把宠物商店想象成一家开在区块链上的宠物领养店。店主把一批宠物信息发布到链上,用户浏览宠物列表,选中心仪的宠物后,通过钱包发起一笔带状态的交易,完成领养。整个流程不依赖任何后台服务,所有领养记录都公开可查且不可篡改。

这个场景虽然看着不起眼,但它涉及了DApp开发需要面对的几乎所有关键问题:合约怎么设计数据结构、状态变量怎么存、前端怎么读取合约数据、钱包怎么发交易、交易确认期间用户界面怎么处理、合约逻辑写错之后怎么升级修复。这些问题在宠物商店项目里都有非常清晰的解法和代码实现,把这一套东西搞明白了,再看NFT市场、去中心化交易所这类复杂的项目,思路就会清晰很多。

1.3 技术栈选型:Solidity、Hardhat、ethers.js

DApp开发的技术栈这些年已经比较稳定了,核心就三块:合约语言用Solidity,开发框架可以选Hardhat或Foundry,前端交互用ethers.js或web3.js。我给新人的建议是:Solidity + Hardhat + ethers.js这套组合,资料最多、坑最少,遇到问题最容易找到答案。

Solidity是当前区块链生态里使用最广泛的合约语言,语法上接近JavaScript,有面向对象的特征,支持继承、库、自定义修饰器等。它和普通编程语言最大的区别在于Gas意识——每一行代码执行都要消耗Gas,所以写合约时不仅要考虑逻辑正确,还要考虑执行成本。

Hardhat是目前最主流的Solidity开发框架,自带本地以太坊节点、合约编译部署脚本、调试控制台和测试环境。相比老牌的Truffle,Hardhat在调试体验上强很多,支持console.log打印合约内部变量,这个功能在排查复杂逻辑时简直是救命稻草。

ethers.js则是连接前端和链的桥梁,它负责构造交易、签名、发送请求、监听事件。选它而不是web3.js,主要因为API设计更现代,体积更小,TypeScript支持也更好。后面第五部分我会详细展开合约调用和事件监听的具体写法。

2. 环境准备与工程化开发流程

2.1 开发环境三步走:Node.js、钱包、测试币

搭建DApp开发环境是新手最容易卡住的地方,很多人在合约代码还没写一行之前,就被环境问题劝退了。实际上整个环境只需要三步:装Node.js、装浏览器钱包插件、领测试币。

首先Node.js版本建议16以上,最好用LTS版本。Hardhat和ethers.js对Node版本有要求,版本太老会报一些莫名其妙的依赖错误。安装完成后在终端验证一下node -vnpm -v,确保命令可用。

然后是浏览器钱包,最常用的是MetaMask。这里有个关键认知要建立:钱包不只是用来存资产的,它实际上是DApp的“身份系统”和“签名器”。用户在页面上点击“领养”按钮,前端会把交易信息交给钱包签名,签名后的交易广播到区块链网络。整个过程里,私钥始终存储在钱包本地,DApp网站自己碰不到用户的私钥,这也是DApp和传统网站登录体系最大的区别。

最后是测试币。在真实网络上部署合约需要支付Gas费,Gas费需要用主网的ETH支付。新手阶段当然不能拿真金白银去练手,所以要用测试网络和测试币。最常见的做法是把MetaMask切换到Sepolia测试网络,然后从水龙头网站领取免费的测试ETH。注意测试币没有任何实际价值,它的作用只是让开发者在模拟真实环境的情况下不发愁手续费。

2.2 用Hardhat初始化项目并配置本地网络

环境准备好之后,就可以开始搭建项目骨架。Hardhat提供了一套非常规整的项目结构,初始化命令很简单:

mkdir pet-shop-tutorial cd pet-shop-tutorial npm init -y npm install --save-dev hardhat npx hardhat init

执行npx hardhat init后,选择“Create a JavaScript project”,Hardhat会自动生成标准目录结构。其中最重要的几个文件:

  • contracts/:存放Solidity合约源码,默认自带一个Lock.sol示例合约
  • scripts/:存放部署脚本,用于把合约部署到链上
  • test/:存放测试文件,Hardhat内置了测试运行器
  • hardhat.config.js:Hardhat配置文件,网络、编译器版本、Gas设置都在这里调整

初始化完成后,先打开hardhat.config.js,把Solidity编译器版本和网络配置补充完整。我建议的模板配置是这样的:

require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.20", networks: { hardhat: { chainId: 1337 }, sepolia: { url: "https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY", accounts: ["YOUR_PRIVATE_KEY"] } } };

这里有两个关键细节。hardhat网络是Hardhat内置的本地开发网络,每次启动会自动创建一批带测试ETH的账号,专门用来本地调试;chainId设置成1337是为了和MetaMask默认的本地网络保持一致。sepolia是公共测试网络,部署脚本会真正把合约广播到测试链上,URL来自节点服务商(比如Alchemy或Infura)提供的API地址,accounts数组填写的是部署账号的私钥,这个私钥一定不要提交到代码仓库。

2.3 本地节点、部署脚本与自动化测试流程

合约是跑在链上的程序,但这里的“链”可以是本地开发链。Hardhat自带一个内存级以太坊节点,启动命令是:

npx hardhat node

启动后终端会显示20个测试账号及对应的私钥,每个账号里有10000个测试ETH。这个本地节点最大的意义在于:合约部署和交易确认都在毫秒级完成,不需要等待出块,特别适合开发阶段快速调试。

部署脚本写在scripts/目录下,用ethers.js获取合约工厂并发送部署交易:

const hre = require("hardhat"); async function main() { const Adoption = await hre.ethers.getContractFactory("Adoption"); const adoption = await Adoption.deploy(); await adoption.waitForDeployment(); console.log("Adoption contract deployed to:", adoption.target); } main().catch((error) => { console.error(error); process.exitCode = 1; });

注意到一个细微但重要的差异:新版本ethers.js中部署后的合约地址是通过adoption.target获取的,而不是旧版的adoption.address。很多教程还在用旧API,如果你照着老教程敲代码发现返回undefined,多半就是这个原因。

至于自动化测试,Hardhat推荐用Mocha + Chai的组合。测试的价值不只是验证代码正确性,更重要的是让你敢重构。合约一旦部署就无法修改,如果逻辑有漏洞,损失是实打实的,所以测试在DApp开发里不是一个可选项,而是必选项。

3. 宠物商店合约设计与实现

3.1 需求解析与数据结构设计

宠物商店的业务核心一句话就能说清:用户可以从商店领养一只宠物,每只宠物只能被领养一次。但把这句话翻译成合约代码,需要想清楚几个问题:宠物信息存在哪里,怎么标识一只宠物,领养关系怎么记录,合约怎么防止同一只宠物被重复领养。

合约的数据结构我设计成两个部分。第一部分是pets数组,存放所有宠物信息;第二部分是adopters映射,记录每个宠物被谁领养。Solidity里数组适合按索引顺序遍历,映射适合通过键快速查询。宠物数量有限且需要全部展示给用户,所以用数组;领养关系需要频繁按宠物ID查询领养人地址,所以用映射。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Adoption { struct Pet { string name; string breed; uint256 age; bool isAdopted; } Pet[] public pets; mapping(uint256 => address) public adopters; event PetAdopted(uint256 indexed petId, address indexed adopter); constructor() { pets.push(Pet("Max", "Golden Retriever", 2, false)); pets.push(Pet("Bella", "French Bulldog", 1, false)); pets.push(Pet("Charlie", "Beagle", 3, false)); } }

构造函数里预置了三条宠物信息,保证合约部署后前端页面有数据可展示。PetAdopted事件是合约向外发出的“通知”,前端通过监听这个事件,可以第一时间感知领养交易的结果。事件字段用indexed标记,是为了让以太坊日志系统能按该字段快速过滤,后面讲前端时你会看到这是查询历史的利器。

3.2 核心函数实现与Gas优化细节

领养功能的核心函数是adopt。它接收一个宠物ID参数,做两件关键的事:校验宠物是否已被领养,更新领养状态。校验逻辑用来保证同一只宠物不会被重复领养,这是整个合约最重要的不变量。

function adopt(uint256 petId) external returns (bool) { require(petId >= 0 && petId < pets.length, "Invalid pet ID"); require(pets[petId].isAdopted == false, "Pet already adopted"); pets[petId].isAdopted = true; adopters[petId] = msg.sender; emit PetAdopted(petId, msg.sender); return true; }

require是Solidity的条件校验语句,当条件不满足时,交易会回滚,所有状态变更都不生效,Gas费用也不会完全退还。msg.sender是交易发起者的地址,在这里就是调用这个函数的用户钱包地址。把msg.sender记录到adopters映射中,就完成了“谁领养了哪只宠物”的关系登记。

还需要提供两个查询函数。一个按ID查询领养人地址,一个返回所有宠物列表:

function getAdopter(uint256 petId) external view returns (address) { return adopters[petId]; } function getPets() external view returns (Pet[] memory) { return pets; }

注意这两个函数都加了view修饰符,表示它们只读取状态而不修改状态。view函数调用不会产生Gas费用,因为节点本地就能返回结果,不需要广播交易。这是一个非常实用的优化点:前端首屏加载时读取数据,只要走view函数,用户完全不需要支付手续费。

3.3 合约安全审计:重入攻击与权限控制

很多初学者觉得宠物商店业务简单,不需要考虑安全问题。这个想法比较危险,因为合约安全问题往往不取决于你想做什么,而取决于攻击者能找到什么漏洞。即使是最简单的领养合约,也有几个设计细节值得养成习惯。

第一个是重入攻击。经典的攻击模式是:合约在转出ETH之后才更新内部状态,攻击者利用回调函数在状态更新前再次调用提取函数,反复盗取资金。虽然宠物商店合约本身不涉及ETH转出,但养成“先更新状态再转账”的习惯非常重要。更严谨的做法是用OpenZeppelin提供的ReentrancyGuard,在关键函数上加nonReentrant修饰器。

第二个是权限控制。目前adopt函数任何人都能调用,这在宠物商店的业务场景中没问题,但如果某个你只想让特定合约调用的函数没有权限保护,就可能在主网上被人恶意调用,产生意料之外的后果。Solidity里最常用的权限模式是Ownable——合约有一个owner变量,构造函数里设为部署者地址,关键函数用onlyOwner修饰器限制调用者。

第三个是使用安全的编译器版本和库。Solidity 0.8.x系列内置了算术溢出检查,可以防范整数溢出攻击,所以新项目建议直接用0.8.x,不要为了兼容老代码而刻意降到0.7甚至更老的版本。涉及ERC20转账、NFT铸造这些标准操作时,优先使用OpenZeppelin的Contratos Library,不要自己造轮子。

4. 前端开发与钱包集成

4.1 脚手架工程结构与前端技术栈梳理

合约开发完成后,要把它跑在浏览器里变成用户能操作的产品,需要起一个前端应用。宠物商店项目我用Vite + React来搭建前端,因为Vite的开发服务器启动快、热更新体验好,相比老式Webpack配置简单太多;React则提供了直观的组件化开发方式,状态管理和UI渲染的社区方案都成熟。

前端项目结构按功能模块划分:

pet-shop-frontend/ ├── src/ │ ├── components/ # 展示组件,比如PetCard、AdoptButton │ ├── pages/ # 页面级组件 │ ├── hooks/ # 自定义hooks,比如useMetaMask、useContract │ ├── utils/ # 工具函数,比如合约地址配置 │ └── App.tsx ├── .env.local # 环境变量,存放合约地址 └── package.json

在React里集成区块链交互,推荐使用@tanstack/react-query管理异步状态,用ethers.js执行链上调用。这一套组合的好处是:把合约读取、交易发送、交易确认这些异步流程用hooks统一封装起来,UI组件只负责渲染状态,这样后续业务扩展时不会导致组件代码膨胀。

4.2 从钱包连接到合约读取的完整实现

钱包连接是整个DApp前端的第一步。用户只有连接钱包后,前端才能拿到用户地址、发起签名交易。现在的标准做法是用以太坊的window.ethereum接口,这是MetaMask等钱包注入浏览器的全局对象。

import { BrowserProvider } from "ethers"; async function connectWallet() { if (!window.ethereum) { alert("请安装MetaMask钱包插件"); return; } const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); return { account: accounts[0], provider, signer }; }

eth_requestAccounts这一步会弹出MetaMask的授权窗口,用户点击确认后,前端才能获得钱包地址。拿到地址后,后续所有需要签名的操作都用signer对象发送。这里有个体验优化点:页面加载时先检查window.ethereum是否存在,不存在就提示用户安装,这样用户可以第一时间发现问题,而不是在点击某个功能按钮时才看到报错。

合约读取的封装代码:

import { Contract } from "ethers"; const contractAddress = import.meta.env.VITE_CONTRACT_ADDRESS; const abi = [...] // 从hardhat编译产物中拷贝 async function getPets(provider) { const contract = new Contract(contractAddress, abi, provider); const pets = await contract.getPets(); return pets.map((pet, index) => ({ id: index, name: pet.name, breed: pet.breed, age: Number(pet.age), isAdopted: pet.isAdopted })); }

这里提一个实践小技巧:合约地址和ABI在每次部署合约后都会变化,所以我把它们放进.env.local环境变量和独立ABI文件中,前端代码里只引用变量名,这样重新部署合约后只需要改环境变量,不用动业务代码。

4.3 发送交易与监听确认状态

用户点击“领养”按钮后,前端需要构造一笔交易并通过钱包签名后发送上链。这是DApp交互中最重要的用户感知环节——交易发送出去不代表马上完成,还需要等待区块确认。如果前端不做任何状态提示,用户看着按钮没有反馈,会觉得应用卡死了。

完整的领养流程代码:

async function adoptPet(petId, signer) { const contract = new Contract(contractAddress, abi, signer); const tx = await contract.adopt(petId); // 此时交易已经被广播,但尚未确认 setPending(true); const receipt = await tx.wait(); // 这里说明交易已经上链并获得确认 setPending(false); console.log("Transaction hash:", receipt.hash); }

这一步有两个很容易踩的坑。第一,contract.adopt(petId)返回的是一个交易响应对象,此时交易还未打包,需要调用tx.wait()等待确认。很多新手以为拿到响应对象就是成功了,结果页面刷新后发现状态没变,就是漏了这一步。第二,交易确认时间取决于当前网络拥堵情况和Gas价格设置。MetaMask默认的Gas如果给低了,交易可能长时间处于pending状态。前端针对这个场景,要做一个“交易已提交,等待确认”的过渡UI,并定时检查交易状态。

为了进一步提升体验,还可以用contract.on("PetAdopted", handler)监听合约事件。当领养成功时,合约会抛出PetAdopted事件,前端监听到这个事件后,自动刷新宠物列表。

contract.on("PetAdopted", (petId, adopter, event) => { console.log(`Pet ${petId} adopted by ${adopter}`); queryClient.invalidateQueries(["pets"]); });

5. 常见问题与调试实战

5.1 典型报错信息速查表

DApp开发报错信息五花八门,但大多数高频报错背后的原因其实非常集中。我整理了一份速查表,基本覆盖了新手到中级开发者最常见的几种错误场景。

报错信息可能原因解决办法
Nonce too high/low本地缓存交易序号与链上不一致在MetaMask设置中清除活动账号数据
Transaction underpricedGas价格设置过低手动提高Gas价格或切换网络重试
Insufficient funds账号余额不足以支付Gas费领测试币或降低Gas Limit
execution reverted合约require条件不满足查看合约日志定位具体require位置
Cannot read properties of undefined (reading 'target')使用的是旧版ethers.js的API改用contract.target获取合约地址
Invalid address合约地址传错或格式不对检查环境变量和部署输出地址

第1条的问题遇到过很多次:本地开发网络重启后,MetaMask里缓存的nonce还是旧值,导致发送的每一笔交易都失败。这种情况最常见于用本地Hardhat节点开发调试时,改node scripts/deploy.js脚本重新部署了合约,结果前端忘了切换网络。

5.2 合约调试技巧:console.log与事件日志定位

调试合约逻辑比调试普通代码麻烦很多,因为合约运行在链上,你不能直接打断点。Hardhat的console.log功能是排查合约内部状态的利器。在合约文件里引入hardhat/console.sol,然后就可以像写JavaScript一样打印变量值:

import "hardhat/console.sol"; function adopt(uint256 petId) external { console.log("adopt called by %s, petId %s", msg.sender, petId); ... }

注意console.log只在测试和本地开发时可见,不会上链,所以不需要担心生产环境的数据泄露。在编写测试时,你可以模拟用户的调用行为,观察合约内部的状态变化,这种调试方式比靠猜来定位问题高效得多。

前端侧的调试重点是事件日志。每一笔交易都有一条交易收据(receipt),里面包含logs数组,记录了交易触发的事件。用ethers.js解析收据里的日志,可以精确还原交易执行过程中发生了什么。结合事件日志和浏览器中的交易详情页(如Sepolia网络可以在区块链浏览器中查看交易),基本能定位95%以上的开发问题。

5.3 测试网络与主网环境衔接的一些经验

开发完成之后面临的关键决策是:把合约部署到哪里。测试网络和主网络是两套完全不同的环境,不要因为在Sepolia测试网上跑通了就觉得万事大吉。有几个坑要提前规避。

第一,测试网络和主网的区别不只是“测试币”和“真钱”的区别,网络环境和节点服务也不相同。Sepolia测试网比较流畅,但交易速度、区块时间都可能和主网不同,这意味着你需要在主网环境下把关键流程再走一遍。第二,测试网络上的合约地址和主网不同,前端代码里的合约地址和环境变量都要同步更新。第三,主网的Gas费用较测试网络高很多,部署一份合约动辄几十美元,所以合约代码的Gas优化在主网上线前要认真做一遍。

如果项目涉及线上资产,强烈建议先做一次“模拟主网”的验证:包括第三方安全审计、权限测试、应急响应方案。如果你的合约有管理账号私钥,私钥的保存方式直接决定了合约的安全性。冷存储、多签钱包、硬件钱包都是可靠的方案。

写在最后

从概念模型到代码落地,从本地调试到测试网上线,宠物商店这个看似简单的项目实际上承载了DApp开发链路里几乎所有核心知识点。我见过很多初学者在做完这个项目后立刻去啃DeFi或NFT的源码,发现理解速度快很多,这就是基本功的复利效应。

如果想在宠物商店的基础上继续进阶,有几个方向可以参考:给宠物加上图片和链下元数据,接入IPFS去中心化存储;增加管理员的宠物上架和撤销功能,引入权限控制;把领养改成拍卖或随机分配模式,涉及随机数生成和更复杂的业务状态机;还可以在测试网部署自己的ERC20代币作为领养积分。每一个扩展方向都会逼着你学习新的知识模块。

最后分享一条我个人在建第一个DApp时踩过的坑:不要花太多时间纠结框架选型或代码风格。工具只是手段,关键是要理解一条交易从钱包签名到上链确认的完整生命周期,以及合约状态变更在前端怎么反映。把这两个核心逻辑吃透,你会发现所有DApp的交互本质上都是同一套模式。带着这个认知去写代码,事半功倍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 5:08:43

CIFAR10上MAE自监督预训练:掩码策略、PyTorch实现与调参实践

简介&#xff1a;基于CIFAR-10的MAE&#xff08;掩码自编码器&#xff09;实现包&#xff0c;适合深度学习研究者与有一定基础的学员复现何恺明团队的MAE自监督预训练流程。资源完整重现了论文关键设定&#xff1a;先用MAE对ViT进行无标签预训练&#xff0c;再分别微调分类器与…

作者头像 李华
网站建设 2026/9/16 5:06:56

Unity+普通摄像头实现隔空手势UI交互与事件分析实战

前阵子我接到一个线下展厅的互动大屏需求&#xff1a;用户站在屏幕前&#xff0c;隔空比划几下&#xff0c;就能完成按钮点击、页面翻页&#xff0c;同时后台还要记录每个人在哪些区域停留、点了哪个按钮、交互时长是多少。甲方一开始想上Kinect或者TOF深度相机&#xff0c;一算…

作者头像 李华
网站建设 2026/9/16 5:06:24

Win8兼容性避坑指南:效果呈现故障根因与解决路径

接手过演示项目的人应该都懂&#xff1a;所有环境和代码在开发机上跑得飞起&#xff0c;一到客户那台“指定环境”就各种打脸。我前段时间就碰上这么一档子事&#xff0c;项目要求把一个桌面端的演示工具跑到一台装着Win8操作系统的老机器上&#xff0c;效果得和Win7/10上一致。…

作者头像 李华
网站建设 2026/9/16 5:03:32

系统提示词泄露攻防实录:原理拆解与分层防御指南

做AI应用的人&#xff0c;最近多少都刷到过那种"一句话套出AI系统提示词"的帖子。有人把各家产品的系统提示词&#xff08;system prompts&#xff09;整理成合集&#xff0c;挂在公开仓库里&#xff0c;围观和搬运的人都不少。我在实际项目里也被套出过几次&#xf…

作者头像 李华
网站建设 2026/9/16 5:03:28

glibc升级失败导致系统无法开机?完整救援恢复步骤与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 5:02:42

2026年ComfyUI零基础实操指南:从安装到首个工作流生成

1. 这不是“又一个ComfyUI教程”&#xff0c;而是一份能让你真正跑通第一个工作流的实操手记我带过三十多个从零开始学ComfyUI的学员&#xff0c;其中超过八成卡在“安装完就卡死”“下载了整合包却打不开节点”“照着视频拖了十个节点&#xff0c;运行报错说Missing Model”这…

作者头像 李华