新聞中心
近年來大熱的美劇《創(chuàng)業(yè)公司(StartUp)》虛構(gòu)了一種被稱為 GenCoin 的數(shù)字貨幣,可用于各種創(chuàng)新式的金融交易場景中。而在我看來,它可以被理解為一種具有 Web3 核心屬性的區(qū)塊鏈分布式設(shè)計產(chǎn)品。如果您對 Web3 還不甚了解的話,讓我們先回顧一下 Web 的三個主要時代:

- Web1 - 靜態(tài)網(wǎng)頁(1991 年–2004 年),請參見 --https://en.wikipedia.org/wiki/Web_2.0#Web_1.0
- Web2 - Web 作為一個平臺(自 2004 年起),請參見 --https://en.wikipedia.org/wiki/Web_2.0#Web_2.0
- Web3- 去中心化的設(shè)計,包含了區(qū)塊鏈技術(shù)(自 2009 年起,且近年來發(fā)展勢頭迅猛、前景廣闊),請參見 --https://en.wikipedia.org/wiki/Web3
在 Web2 時代,Web 服務(wù)主要集中和被控制在諸如:谷歌、蘋果和亞馬遜等少數(shù)技術(shù)提供商的手里。而作為 Web2 的替代方案(https://consensys.net/blog/blockchain-explained/what-is-web3-here-are-some-ways-to-explain-it-to-a-friend/),Web3 創(chuàng)建了一個無需準入的數(shù)據(jù)存儲方式。其中不存在任何個人或公司控制或擁有著數(shù)據(jù),而且數(shù)據(jù)的真實性也得到了充分保證。這些數(shù)據(jù)會被存儲在區(qū)塊鏈網(wǎng)絡(luò)中的公共分類賬本(public ledger)上。因此,不再是由一個實體擁有數(shù)據(jù),而是由多個節(jié)點(即:運行著區(qū)塊鏈的計算機)存儲著數(shù)據(jù),并就數(shù)據(jù)是否有效達成了共識。
從比特幣(https://bitcoin.org/en/)到以太坊等協(xié)議的應(yīng)用,Web3 以此類數(shù)據(jù)存儲協(xié)議為基礎(chǔ),開啟了各種全新的用例。例如:
- 由用戶而非公司控制著個人身份
- 未經(jīng)許可的金融系統(tǒng)(如:比特幣)可開展貸款、貨幣、投資等的數(shù)字化貨幣業(yè)務(wù)
- 由 NFT(https://en.wikipedia.org/wiki/Non-fungible_token)去證明諸如音樂(https://royal.io/)、藝術(shù)(https://www.artblocks.io/)等數(shù)字項目的數(shù)字所有權(quán)
- 通過去中心化的自治組織(decentralized autonomous organizations,DAO)臨時組建具有相同目的的團體,例如:Constitution DAO(https://www.constitutiondao.com/)和 social DAO(https://www.fwb.help/)
- 通過邊玩邊賺(Play-to-earn,p2e)的游戲,用戶可以在玩游戲的同時,用來謀生(例如 Axie Infinity,https://axieinfinity.com/)
當然,上述應(yīng)用的關(guān)鍵在于,數(shù)字貨幣的所有權(quán)(如:DAO 會員資格、或音樂版權(quán)等)都被掌控在用戶的手中。在世界上任何地方,只要有互聯(lián)網(wǎng)連接,任何人都可以自由地交易、銷售和構(gòu)建這些物品,而完全脫離了某個公司或政府的規(guī)則控制。對于這樣的 Web3 理想主義,我在此不做評判,只是單純從開發(fā)者的角度和您探討,一個全棧開發(fā)者將如何具備 Web3 的技術(shù)能力。
從全棧說起
源于 2015 年的“全棧開發(fā)者”一詞是指:一個軟件工程師可以為任何級別的軟件技術(shù)棧做出貢獻。例如,面對某個與服務(wù)層相關(guān)的功能性缺陷,剛剛完成了客戶端相關(guān)任務(wù)的同一開發(fā)者,可以無縫“接單”,去高效地抓 bug。您可以通過鏈接 --https://dzone.com/articles/do-not-publishfull-stack-development-truly-possibl,了解更多有關(guān)全棧開發(fā)的概念。
Web3 基礎(chǔ)
為了深入研究 Web3,我依次創(chuàng)建了一個智能合約,以及一個 Dapp 與之進行交互。其中,
- 智能合約(https://www.coinbase.com/learn/crypto-basics/what-is-a-smart-contract)是部署在區(qū)塊鏈上的一段代碼(我下面會以以太坊(https://ethereum.org/en/what-is-ethereum/)為例)。該合約一旦被部署到區(qū)塊鏈上,就不可改變、也無需許可(permissionless),但是任何人都可以檢索到它。
- Dapp(decentralized application,去中心化應(yīng)用)是我們通過 UI(通常來自網(wǎng)頁或應(yīng)用)與智能合約交互的方式。Dapp 會在后端利用智能合約的開放性,采用諸如 IPFS(InterPlanetary File Storage,星際文件存儲)的方式,實現(xiàn)文件的分散存儲,且不會出現(xiàn)停機。畢竟 DDoS 攻擊無法攻擊負責存儲的每個節(jié)點。當然,在考慮部署之前,我們需要針對其安全性,開展全面測試,并處置好代碼中的潛在缺陷與漏洞。
Web3 技術(shù)棧
目前,針對 Web3 的成熟技術(shù)棧組合,通常包括以下組件:
- NPM - 備受 Web2 開發(fā)人員歡迎的節(jié)點包管理器,請參見 --https://nodejs.org/en/
- Truffle 框架 - 專注于 Web3 的開發(fā)工具,請參見 --https://www.trufflesuite.com/
- Ganache – 可以在本地主機上啟動私有區(qū)塊鏈,請參見 --https://www.trufflesuite.com/ganache
- MetaMask - 以太坊的區(qū)塊鏈用戶界面與網(wǎng)關(guān),屬于開源且去中心化的區(qū)塊鏈類型,請參見 --https://metamask.io/
- Solidity – 智能合約編程語言,請參見 --https://solidity.readthedocs.io/en/v0.7.1/
- HTML/CSS/JavaScript - 客戶端的層面,請參見 --https://www.w3.org/standards/webdesign/htmlcss
- Web3.js – 通過以太坊網(wǎng)絡(luò)交互的以太坊 API 庫,請參見 --https://web3js.readthedocs.io/en/v1.3.0/
- Infura - 授予以太坊網(wǎng)絡(luò)訪問權(quán)限的以太坊 API 服務(wù),請參見 --https://infura.io/
以太坊 Dapp 的需求
假設(shè)有一個居委會即將舉辦定期選舉,附近的居民將對一系列的決議進行投票。那么,我們就可以將該選舉構(gòu)建成為一個以太坊 Dapp。由于數(shù)據(jù)被存儲在公開的區(qū)塊鏈上,而不是單個公司的私有服務(wù)器上,因此任何人都可以通過與智能合約的交互,以無需許可的方式,檢索投票結(jié)果。據(jù)此,投票結(jié)果就不存在被篡改或偽造的情況,進而避免了爭議的發(fā)生。
創(chuàng)建智能合約
首先,我們需要利用前文提到的:Infura、NPM、Truffle 框架、Ganache、以及 Solidity 等 Web3 技術(shù)棧組件,來創(chuàng)建一個能與應(yīng)用協(xié)同的智能合約。其創(chuàng)建的流程如下圖所示:
我們可以根據(jù)該流程,去招募以太坊的開發(fā)者,具體內(nèi)容請參見鏈接 --https://consensys.net/developers/onboarding-step-2/。
使用 React 創(chuàng)建 Dapp
有了智能合約,Web3 工程師便可以使用 NPM、MetaMask、HTML/CSS/JavaScript/React、以及 Web3.js 等 Web3 技術(shù)棧組件,構(gòu)建居委會選舉的應(yīng)用。在本例中,我們將采用 React(https://reactjs.org/) 框架和如下流程:
首個以太坊 Dapp
我會通過 Infura 的注冊頁面(https://infura.io/register)創(chuàng)建一個免費帳戶,并創(chuàng)建一個名為 jvc-homeowners-ballot 的項目:
下圖中有關(guān)該項目的細節(jié),我會在下文中詳細討論:
Truffle 入門在本地主機上,我創(chuàng)建了一個名為 jvc-homeowners-ballot 的文件夾,并使用 CLI 命令 --truffle init,來初始化 Truffle。初始化完成后的目錄結(jié)構(gòu)為:
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── test
└── truffle-config.js
接著,我用如下命令為基于 Truffle 的錢包 provider,添加了對應(yīng)的依賴項:
npm install --save @truffle/hdwallet-provider
為了創(chuàng)建本地開發(fā)網(wǎng)絡(luò),我通過命令 ganache 啟動 Ganache CLI。
根據(jù) CLI 的如下響應(yīng)信息,我們可以看到 Ganache 已在本地主機的 8545 端口上運行:
ganache v7.0.1 (@ganache/cli: 0.1.2, @ganache/core: 0.1.2)
Starting RPC server
Available Accounts
==================
(0) 0x2B475e4fd7F600fF1eBC7B9457a5b58469b9EDDb (1000 ETH)
(1) 0x5D4BB40f6fAc40371eF1C9B90E78F82F6df33977 (1000 ETH)
(2) 0xFaab2689Dbf8b7354DaA7A4239bF7dE2D97e3A22 (1000 ETH)
(3) 0x8940fcaa55D5580Ac82b790F08500741326836e0 (1000 ETH)
(4) 0x4c7a1b7EB717F98Fb0c430eB763c3BB9212F49ad (1000 ETH)
(5) 0x22dFCd5df8d4B19a42cB14E87219fea7bcA7C92D (1000 ETH)
(6) 0x56882f79ecBc2D68947C6936D4571f547890D07c (1000 ETH)
(7) 0xD257AFd8958c6616bf1e61f99B2c65dfd9fEE95A (1000 ETH)
(8) 0x4Bb2EE0866578465E3a2d3eCCC41Ea2313372B20 (1000 ETH)
(9) 0xdf267AeFeAfE4b7053ca10c3d661a8CB24E98236 (1000 ETH)
Private Keys
==================
(0) 0x5d58d27b0f294e3222bbd99a3a1f07a441ea4873de6c3a2b7c40b73186eb616d
(1) 0xb9e52d6cfb2c074fa6a6578b946e3d00ea2a332bb356d0b3198ccf909a97fdc8
(2) 0xc52292ce17633fe2724771e81b3b4015374d2a2ea478891dab74f2028184edeb
(3) 0xbc7b0b4581592e48ffb4f6420228fd6b3f954ac8cfef778c2a81188415274275
(4) 0xc63310ccdd9b8c2da6d80c886bef4077359bb97e435fb4fe83fcbec529a536fc
(5) 0x90bc16b1520b66a02835530020e43048198195239ac9880b940d7b2a48b0b32c
(6) 0x4fb227297dafb879e148d44cf4872611819412cdd1620ad028ec7c189a53e973
(7) 0xf0d4dbe2f9970991ccc94a137cfa7cf284c09d0838db0ce25e76c9ab9f4316d9
(8) 0x495fbc6a16ade5647d82c6ad12821667f95d8b3c376dc290ef86c0d926f50fea
(9) 0x434f5618a3343c5e3b0b4dbeaf3f41c62777d91c3314b83f74e194be6c09416b
HD Wallet
==================
Mnemonic: immense salmon nominee toy jungle main lion universe seminar output oppose hungry
Base HD Path: m/44'/60'/0'/0/{account_index}
Default Gas Price
==================
2000000000
BlockGas Limit
==================
30000000
Call Gas Limit
==================
50000000
Chain Id
==================
1337
RPC Listening on 127.0.0.1:8545
項目文件夾中的 truffle-config.js 文件,會被激活并更新如下代碼行:
JSON
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
現(xiàn)在,我們可以在新的終端中通過命令 --truffle console,來啟動 Truffle 控制臺,能顯示如下提示:
truffle(development)>
我們可以在控制臺中,通過命令 --const HDWalletProvider = require('@truffle/hdwallet-provider'); 來創(chuàng)建錢包。當然,它可能會導(dǎo)致未定義的響應(yīng)。
接下來,我需要通過 Mnemonic Code Converter(https://iancoleman.io/bip39/)網(wǎng)站,生成一個 12 字的助記詞(12-word mnemonic phrase,類似私鑰),并將其通過如下命令,更新到 Truffle 控制臺處:
const mnemonic = '12 words here';
const wallet = new HDWalletProvider(mnemonic, "http://localhost:8545");
上述兩條命令雖然也會導(dǎo)致未定義的響應(yīng),但是錢包的控制臺最終會顯示如下運行結(jié)果:
truffle(development)> wallet
HDWalletProvider {
walletHdpath: "m/44'/60'/0'/0/",
wallets: {
...
},
addresses: [
'0xa54b012b406c01dd99a6b18ef8b55a15681449af',
'0x6d507a70924ea3393ae1667fa88801650b9964ad',
'0x1237e0a8522a17e29044cde69b7b10b112544b0b',
'0x80b4adb18698cd47257be881684fff1e14836b4b',
'0x09867536371e43317081bed18203df4ca5f0490d',
'0x89f1eeb95b7a659d4748621c8bdbabc33ac47bbb',
'0x54ceb6f0d722dcb33152c953d5758a08045f254d',
'0x25d2a8716792b98bf9cce5781b712f00cf33227e',
'0x37b6364fb97028830bfeb0cb8d2b14e95e2efa05',
'0xe9f56031cb6208ddefcd3cdd5a1a41f7f3400af5'
],
...
添加以太坊資金進行測試
現(xiàn)在我們需要為 Dapp 獲取一些測試資金,并使用 Ropsten Ethereum Faucet(https://faucet.ropsten.be/)將資金添加到現(xiàn)有的、由ConsenSys(https://consensys.net/)創(chuàng)建的MetaMask(https://metamask.io/index.html)錢包中。當然,為了降低意外情況所導(dǎo)致的真實資金損失的風(fēng)險,您可以在 MetaMask 中創(chuàng)建多個帳戶,其中至少有一個帳戶可專用于開發(fā)和測試。請記住:永遠不要與任何人分享您的助記詞,也不要在任何地方上傳您的私鑰!
如下圖所示,為了添加測試資金,我需要輸入自己的帳戶地址:
如下圖所示,通過 Ropsten Etherscan 站點,我們可以驗證交易是否能夠成功完成:
最終準備步驟
請使用如下命令將 dotenv 依賴項添加到該項目中:
npm install --save dotenv
接著,請在項目的根目錄下創(chuàng)建一個.env 的新文件,并在其中包含如下兩行:
INFURA_API_KEY=INSERT YOUR API KEY HERE (no quotations)
MNEMONIC="12 words here"
其中,INFURA_API_KEY 是在創(chuàng)建 jvc-homeowners-ballot 項目時給定的項目 ID。注意:請確保.env 文件被包含在.gitignore 文件中,以避免其他有權(quán)訪問該存儲庫的人,擅自使用此機密信息。
最后一項準備步驟是更新 truffle-config.js 文件。我們首先需要在文件的頂部添加如下三行:
JavaScript
require("dotenv").config();
const HDWalletProvider = require("@truffle/hdwallet-provider");
接著,我們利用 dotenv 將如下網(wǎng)絡(luò)信息,添加至上述依賴項:
JavaScript
ropsten: {
provider: () =>
new HDWalletProvider(
process.env.MNEMONIC,
`https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`
),
network_id: 3, // Ropsten's id
gas: 5500000, // Ropsten has a lower block limit than mainnet
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
設(shè)置智能合約
準備好了 Infura、Truffle、以及測試資金后,讓我們開始設(shè)置智能合約。針對前面的居委會選舉示例,我們將使用位于本項目 contracts 文件夾中的 JvcHomeownerBallot.sol 合約:
JavaScript
// SPDX-License-Identifier: UNLICENSED (it is common practice to include an open source license or declare it unlicensed)
pragma solidity ^0.8.7; // tells the compiler which version to use
contract Homeowners {
// store the addresses of voters on the blockchain in these 2 arrays
address[] votedYes;
address[] votedNo;
function voteYes() public {
votedYes.push(msg.sender);
}
function voteNo() public {
votedNo.push(msg.sender);
}
function getYesVotes() public view returns (uint) {
return votedYes.length;
}
function getNoVotes() public view returns (uint) {
return votedNo.length;
}
}
正如上面的代碼所示,該合同將非常簡單,參選居民只需選擇是或否即可。其對應(yīng)的 contracts 文件夾結(jié)構(gòu)如下圖所示:
.
├── JvcHomeownersBallot.sol
└── Migrations.sol
有了合約,我們就需要建立部署合約的方法。下面讓我們轉(zhuǎn)移到 migrations 文件夾,將如下內(nèi)容添加到該文件夾下的 2_deploy_contracts.js 文件中:
JavaScript
const JvcHomeownersBallot = artifacts.require("JvcHomeownersBallot.sol");
module.exports = function(deployer) {
deployer.deploy(JvcHomeownersBallot);
};
然后,我們可以使用如下命令執(zhí)行合約的遷移:
truffle migrate --network ropsten
遷移的響應(yīng)結(jié)果為:
Compiling your contracts...
===========================
> Compiling ./contracts/JvcHomeownersBallot.sol
> Artifacts written to /Users/john.vester/projects/jvc/consensys/jvc-homeowners-ballot/build/contracts
> Compiled successfully using:
- solc: 0.8.11+commit.d7f03943.Emscripten.clang
Network up to date.
truffle(development)> truffle migrate --network ropsten
Compiling your contracts...
===========================
> Compiling ./contracts/JvcHomeownersBallot.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /Users/john.vester/projects/jvc/consensys/jvc-homeowners-ballot/build/contracts
> Compiled successfully using:
- solc: 0.8.11+commit.d7f03943.Emscripten.clang
Starting migrations...
======================
> Network name: 'ropsten'
> Network id: 3
> Block gas limit: 8000000 (0x7a1200)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0x5f227f26a31a3667a689be2d7fa6121a21153eb219873f6fc9aecede221b3b82
> Blocks: 5 Seconds: 168
> contract address: 0x9e6008B354ba4b9f91ce7b8D95DBC6130324024f
> block number: 11879583
> block timestamp: 1643257600
> account: 0xa54b012B406C01dd99A6B18eF8b55A15681449Af
> balance: 1.573649230299520359
> gas used: 250142 (0x3d11e)
> gas price: 2.506517682 gwei
> value sent: 0 ETH
> total cost: 0.000626985346010844 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 11879584)
> confirmation number: 2 (block: 11879585)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.000626985346010844 ETH
2_deploy_contracts.js
=====================
Deploying 'JvcHomeownersBallot'
-------------------------------
> transaction hash: 0x1bf86b0eddf625366f65a996e633db589cfcef1a4d6a4d6c92a5c1f4e63c767f
> Blocks: 0 Seconds: 16
> contract address: 0xdeCef6474c95E5ef3EFD313f617Ccb126236910e
> block number: 11879590
> block timestamp: 1643257803
> account: 0xa54b012B406C01dd99A6B18eF8b55A15681449Af
> balance: 1.573133154908720216
> gas used: 159895 (0x27097)
> gas price: 2.507502486 gwei
> value sent: 0 ETH
> total cost: 0.00040093710999897 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 11879591)
> confirmation number: 2 (block: 11879592)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00040093710999897 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.001027922456009814 ETH
- Blocks: 0 Seconds: 0
- Saving migration to chain.
- Blocks: 0 Seconds: 0
- Saving migration to chain.
至此,我們已將 JvcHomeownersBallot 智能合約部署到了 Ropsten 網(wǎng)絡(luò)中。我們可以進一步使用如下 URL,來驗證智能合約,并在“Deploying JvcHomeownersBallot”日志中提供合約的地址:
https://ropsten.etherscan.io/
或是:
https://ropsten.etherscan.io/address/0xdeCef6474c95E5ef3EFD313f617Ccb126236910e
使用 React 創(chuàng)建 Dapp
在上述提到的 jvc-homeowners-ballot 文件夾的同級目錄,我將創(chuàng)建一個名為 jvc-homeowners-ballot-client 的目錄,通過調(diào)用 React CLI 和如下命令,來創(chuàng)建同名的 React 應(yīng)用:
npx create-react-app jvc-homeowners-ballot-client
接著,我通過如下命令,將 Web3 的依賴項安裝到 React 應(yīng)用中:
cd jvc-homeowners-ballot-client
npm installWeb3
核心的 React 應(yīng)用一旦就緒,我們就需要建立合約應(yīng)用的二進制接口(application binary interface,ABI),以便 Dapp 與以太坊生態(tài)系統(tǒng)上的各種合約進行通信。
根據(jù) JvcHomeownerBallot.sol 智能合約文件的內(nèi)容,我們在 build/contracts 文件夾下打開 JvcHomeownersBallet.json 文件,并使用 abi.js 文件的 jvcHomeOwnersBallot 常量的“abi”屬性值。具體內(nèi)容如下:
JavaScript
export const jvcHomeownersBallot = [
{
"inputs": [],
"name": "voteYes",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "voteNo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getYesVotes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "getNoVotes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
];
該文件應(yīng)當被放置在 React 應(yīng)用目錄 src 的新建子文件夾 abi 內(nèi)。
下面,我們根據(jù)如下配置,從頭開始更新 Apps.js:
JavaScript
import React, { useState } from "react";
import { jvcHomeownersBallot } from "./abi/abi";
importWeb3from "web3";
import "./App.css";
constWeb3= newWeb3(Web3.givenProvider);
const contractAddress = "0xdeCef6474c95E5ef3EFD313f617Ccb126236910e";
const storageContract = newWeb3.eth.Contract(jvcHomeownersBallot, contractAddress);
我們可以通過多種方式找到上面提到的 contactAddress。除了我在此使用的 truffle 的 migrate CLI 命令之外,您還可以使用 Etherscan 站點(https://ropsten.etherscan.io/)。
標準的 React 開發(fā)
在開始標準的 React 開發(fā)之前,讓我們先來看看完整的 App.js 文件 (如下所示):
JavaScript
import React, { useState } from "react";
import { jvcHomeownersBallot } from "./abi/abi";
importWeb3from "web3";
import Nav from "./components/Nav.js";
import "./App.css";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import {CircularProgress, Grid, Typography} from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
}));
constWeb3= newWeb3(Web3.givenProvider);
const contractAddress = "0xdeCef6474c95E5ef3EFD313f617Ccb126236910e";
const storageContract = newWeb3.eth.Contract(jvcHomeownersBallot, contractAddress);
function App() {
const classes = useStyles();
const [voteSubmitted, setVoteSubmitted] = useState("");
const [yesVotes, setYesVotes] = useState(0);
const [noVotes, setNoVotes] = useState(0);
const [waiting, setWaiting] = useState(false);
const getVotes = async () => {
const postYes = await storageContract.methods.getYesVotes().call();
setYesVotes(postYes);
const postNo = await storageContract.methods.getNoVotes().call();
setNoVotes(postNo);
};
const voteYes = async 文章標題:從全棧開發(fā)者邁向Web3弄潮兒
分享地址:http://www.5511xx.com/article/coccodd.html


咨詢
建站咨詢
