业无高卑志当坚,男儿有求安得闲?这篇文章主要讲述从全栈开发者迈向Web3弄潮儿相关的知识,希望能为你提供帮助。
?作者丨John Vester
?编译丨陈峻
?策划丨孙淑娟
?本文从 Web1~3 的基本特征讲起,通过一个选举示例,向您展示了全栈开发者将如何迈向 Web3 的技术实践过程。?
?近年来大热的美剧《创业公司(StartUp)》虚构了一种被称为 GenCoin 的数字货币,可用于各种创新式的金融交易场景中。而在我看来,它可以被理解为一种具有 Web3 核心属性的区块链分布式设计产品。如果您对 Web3 还不甚了解的话,让我们先回顾一下 Web 的三个主要时代:?
在 Web2 时代,Web 服务主要集中和被控制在诸如:谷歌、苹果和亚马逊等少数技术提供商的手里。而作为 Web2 的替代方案(https://consensys.net/blog/blockchain-explained/what-is-web3-here-are-some-ways-to-explain-it-to-a-friend/),Web3 创建了一个无需准入的数据存储方式。其中不存在任何个人或公司控制或拥有着数据,而且数据的真实性也得到了充分保证。这些数据会被存储在区块链网络中的公共分类账本(public ledger)上。因此,不再是由一个实体拥有数据,而是由多个节点(即:运行着区块链的计算机)存储着数据,并就数据是否有效达成了共识。
从比特币(https://bitcoin.org/en/)到以太坊等协议的应用,Web3 以此类数据存储协议为基础,开启了各种全新的用例。例如:
当然,上述应用的关键在于,数字货币的所有权(如:DAO 会员资格、或音乐版权等)都被掌控在用户的手中。在世界上任何地方,只要有互联网连接,任何人都可以自由地交易、销售和构建这些物品,而完全脱离了某个公司或政府的规则控制。对于这样的 Web3 理想主义,我在此不做评判,只是单纯从开发者的角度和您探讨,一个全栈开发者将如何具备 Web3 的技术能力。
从全栈说起?
源于 2015 年的“全栈开发者”一词是指:一个软件工程师可以为任何级别的软件技术栈做出贡献。例如,面对某个与服务层相关的功能性缺陷,刚刚完成了客户端相关任务的同一开发者,可以无缝“接单”,去高效地抓 bug。您可以通过链接 --https://dzone.com/articles/do-not-publishfull-stack-development-truly-possibl,了解更多有关全栈开发的概念。
Web3 基础?
为了深入研究 Web3,我依次创建了一个智能合约,以及一个 Dapp 与之进行交互。其中,
Web3 技术栈?
目前,针对 Web3 的成熟技术栈组合,通常包括以下组件:
以太坊 Dapp 的需求?
假设有一个居委会即将举办定期选举,附近的居民将对一系列的决议进行投票。那么,我们就可以将该选举构建成为一个以太坊 Dapp。由于数据被存储在公开的区块链上,而不是单个公司的私有服务器上,因此任何人都可以通过与智能合约的交互,以无需许可的方式,检索投票结果。据此,投票结果就不存在被篡改或伪造的情况,进而避免了争议的发生。
创建智能合约?
首先,我们需要利用前文提到的:Infura、NPM、Truffle 框架、Ganache、以及 Solidity 等 Web3 技术栈组件,来创建一个能与应用协同的智能合约。其创建的流程如下图所示:
我们可以根据该流程,去招募以太坊的开发者,具体内容请参见链接 --https://consensys.net/developers/onboarding-step-2/。
使用 React 创建 Dapp?
有了智能合约,Web3 工程师便可以使用 NPM、MetaMask、HTML/CSS/javaScript/React、以及 Web3.js 等 Web3 技术栈组件,构建居委会选举的应用。在本例中,我们将采用 React(https://reactjs.org/) 框架和如下流程:
首个以太坊 Dapp?
【从全栈开发者迈向Web3弄潮儿】我会通过 Infura 的注册页面(https://infura.io/register)创建一个免费帐户,并创建一个名为 jvc-homeowners-ballot 的项目:
下图中有关该项目的细节,我会在下文中详细讨论:
Truffle 入门在本地主机上,我创建了一个名为 jvc-homeowners-ballot 的文件夹,并使用 CLI 命令 --truffle init,来初始化 Truffle。初始化完成后的目录结构为:
├── contracts
│└── Migrations.sol
├── migrations
│└── 1_initial_migration.js
├── test
└── truffle-config.js
接着,我用如下命令为基于 Truffle 的钱包 provider,添加了对应的依赖项:
npm install --save @truffle/hdwallet-provider
为了创建本地开发网络,我通过命令 ganache 启动 Ganache CLI。
根据 CLI 的如下响应信息,我们可以看到 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)
,
现在,我们可以在新的终端中通过命令 --truffle console,来启动 Truffle 控制台,能显示如下提示:
truffle(development)>
我们可以在控制台中,通过命令 --const HDWalletProvider = require(@truffle/hdwallet-provider);
来创建钱包。当然,它可能会导致未定义的响应。
接下来,我需要通过 Mnemonic Code Converter(https://iancoleman.io/bip39/)网站,生成一个 12 字的助记词(12-word mnemonic phrase,类似私钥),并将其通过如下命令,更新到 Truffle 控制台处:
const mnemonic = 12 words here;
const wallet = new HDWalletProvider(mnemonic, "http://localhost:8545");
上述两条命令虽然也会导致未定义的响应,但是钱包的控制台最终会显示如下运行结果:
truffle(development)>
wallet
HDWalletProvider
walletHdpath: "m/44/60/0/0/",
wallets:
...
,
addresses: [
0xa54b012b406c01dd99a6b18ef8b55a15681449af,
0x6d507a70924ea3393ae1667fa88801650b9964ad,
0x1237e0a8522a17e29044cde69b7b10b112544b0b,
0x80b4adb18698cd47257be881684fff1e14836b4b,
0x09867536371e43317081bed18203df4ca5f0490d,
0x89f1eeb95b7a659d4748621c8bdbabc33ac47bbb,
0x54ceb6f0d722dcb33152c953d5758a08045f254d,
0x25d2a8716792b98bf9cce5781b712f00cf33227e,
0x37b6364fb97028830bfeb0cb8d2b14e95e2efa05,
0xe9f56031cb6208ddefcd3cdd5a1a41f7f3400af5
],
...
添加以太坊资金进行测试?
现在我们需要为 Dapp 获取一些测试资金,并使用 Ropsten Ethereum Faucet(https://faucet.ropsten.be/)将资金添加到现有的、由 ConsenSys(https://consensys.net/)创建的 MetaMask(https://metamask.io/index.html)钱包中。当然,为了降低意外情况所导致的真实资金损失的风险,您可以在 MetaMask 中创建多个帐户,其中至少有一个帐户可专用于开发和测试。请记住:永远不要与任何人分享您的助记词,也不要在任何地方上传您的私钥!
如下图所示,为了添加测试资金,我需要输入自己的帐户地址:
如下图所示,通过 Ropsten Etherscan 站点,我们可以验证交易是否能够成功完成:
最终准备步骤?
请使用如下命令将 dotenv 依赖项添加到该项目中:
npm install --save dotenv
接着,请在项目的根目录下创建一个.env 的新文件,并在其中包含如下两行:
INFURA_API_KEY=INSERT YOUR API KEY HERE (no quotations)
MNEMONIC="12 words here"
其中,INFURA_API_KEY 是在创建 jvc-homeowners-ballot 项目时给定的项目 ID。注意:请确保.env 文件被包含在.gitignore 文件中,以避免其他有权访问该存储库的人,擅自使用此机密信息。
最后一项准备步骤是更新 truffle-config.js 文件。我们首先需要在文件的顶部添加如下三行:
JavaScript
require("dotenv").config();
const HDWalletProvider = require("@truffle/hdwallet-provider");
接着,我们利用 dotenv 将如下网络信息,添加至上述依赖项:
JavaScript
ropsten:
provider: () =>
new HDWalletProvider(
process.env.MNEMONIC,
`https://ropsten.infura.io/v3/$process.env.INFURA_API_KEY`
),
network_id: 3, // Ropstens 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 )
,
设置智能合约?
准备好了 Infura、Truffle、以及测试资金后,让我们开始设置智能合约。针对前面的居委会选举示例,我们将使用位于本项目 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;
正如上面的代码所示,该合同将非常简单,参选居民只需选择是或否即可。其对应的 contracts 文件夹结构如下图所示:
.
├── JvcHomeownersBallot.sol
└── Migrations.sol
有了合约,我们就需要建立部署合约的方法。下面让我们转移到 migrations 文件夹,将如下内容添加到该文件夹下的 2_deploy_contracts.js 文件中:
JavaScript
const JvcHomeownersBallot = artifacts.require("JvcHomeownersBallot.sol");
module.exports = function(deployer)
deployer.deploy(JvcHomeownersBallot);
;
然后,我们可以使用如下命令执行合约的迁移:
truffle migrate --network ropsten
迁移的响应结果为:
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: 5Seconds: 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: 0Seconds: 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: 0Seconds: 0
- Saving migration to chain.
- Blocks: 0Seconds: 0
- Saving migration to chain.
至此,我们已将 JvcHomeownersBallot 智能合约部署到了 Ropsten 网络中。我们可以进一步使用如下 URL,来验证智能合约,并在“Deploying JvcHomeownersBallot”日志中提供合约的地址:
https://ropsten.etherscan.io/
或是:
https://ropsten.etherscan.io/address/0xdeCef6474c95E5ef3EFD313f617Ccb126236910e
使用 React 创建 Dapp?
在上述提到的 jvc-homeowners-ballot 文件夹的同级目录,我将创建一个名为 jvc-homeowners-ballot-client 的目录,通过调用 React CLI 和如下命令,来创建同名的 React 应用:
npx create-react-app jvc-homeowners-ballot-client
接着,我通过如下命令,将 Web3 的依赖项安装到 React 应用中:
cd jvc-homeowners-ballot-client
npm installWeb3
核心的 React 应用一旦就绪,我们就需要建立合约应用的二进制接口(application binary interface,ABI),以便 Dapp 与以太坊生态系统上的各种合约进行通信。
根据 JvcHomeownerBallot.sol 智能合约文件的内容,我们在 build/contracts 文件夹下打开 JvcHomeownersBallet.json 文件,并使用 abi.js 文件的 jvcHomeOwnersBallot 常量的“abi”属性值。具体内容如下:
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
];
该文件应当被放置在 React 应用目录 src 的新建子文件夹 abi 内。
下面,我们根据如下配置,从头开始更新 Apps.js:
JavaScript
import React,useStatefrom "react";
importjvcHomeownersBallotfrom "./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 开发?
在开始标准的 React 开发之前,让我们先来看看完整的 App.js 文件 (如下所示):
JavaScript
import React,useStatefrom "react";
importjvcHomeownersBallotfrom "./abi/abi";
importWeb3from "web3";
import Nav from "./components/Nav.js";
import "./App.css";
importmakeStylesfrom "@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 () =>
setWaiting(true);
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = (await storageContract.methods.voteYes().estimateGas()) * 1.5;
const post = await storageContract.methods.voteYes().send(
from: account,
gas,
);
setVoteSubmitted(post.from);
setWaiting(false);
;
const voteNo = async () =>
setWaiting(true);
const accounts = await window.ethereum.enable();
const account = accounts[0];
const gas = (await storageContract.methods.voteNo().estimateGas() * 1.5);
const post = await storageContract.methods.voteNo().send(
from: account,
gas,
);
setVoteSubmitted(post.from);
setWaiting(false);
;
return (
<
div className=classes.root>
<
Nav />
<
div className="main">
<
div className="card">
<
Typography variant="h3" gutterBottom>
JVC Homeowners Ballot
<
/Typography>
<
Typography gutterBottom>
How do you wish to vote?
<
/Typography>
<
span className="buttonSpan">
<
Button
id="yesButton"
className="button"
variant="contained"
color="primary"
type="button"
onClick=voteYes>
Vote Yes<
/Button>
<
div className="divider"/>
<
Button
id="noButton"
className="button"
color="secondary"
variant="contained"
type="button"
onClick=voteNo>
Vote No<
/Button>
<
div className="divider"/>
<
/span>
waiting &
&
(
<
div>
<
CircularProgress />
<
Typography gutterBottom>
Submitting Vote ... please wait
<
/Typography>
<
/div>
)
!waiting &
&
voteSubmitted &
&
(
<
Typography gutterBottom>
Vote Submitted: voteSubmitted
<
/Typography>
)
<
span className="buttonSpan">
<
Button
id="getVotesButton"
className="button"
color="default"
variant="contained"
type="button"
onClick=getVotes>
Get Votes<
/Button>
<
/span>
(yesVotes >
0 || noVotes >
0) &
&
(
<
div>
<
Typography variant="h5" gutterBottom>
Current Results
<
/Typography>
<
Grid container spacing=1>
<
Grid item xs=6>
<
div className="resultsAnswer resultsHeader">
Vote<
/div>
<
/Grid>
<
Grid item xs=6>
<
div className="resultsValue resultsHeader">
# of Votes<
/div>
<
/Grid>
<
Grid item xs=6>
<
div className="resultsAnswer">
Yes<
/div>
<
/Grid>
<
Grid item xs=6>
<
div className="resultsValue">
yesVotes<
/div>
<
/Grid>
<
Grid item xs=6>
<
div className="resultsAnswer">
No<
/div>
<
/Grid>
<
Grid item xs=6>
<
div className="resultsValue">
noVotes<
/div>
<
/Grid>
<
/Grid>
<
/div>
)
<
/div>
<
/div>
<
/div>
);
export default App;
运行 Dapp?
我们可以使用 Yarn CLI 的如下命令,来启动基于 React 的 Dapp:
yarn start
在完成编译和验证之后,您会看到如下应用界面:
它拥有三个选项:
小结?
综上所述,一旦建立了智能合约,从客户端的角度来看,我们将能够沿用 Web2 的如下方面到 Web3 上:
而 Web3 的独特之处主要体现在:
可见,从全栈开发者迈向 Web3 的学习曲线并不陡峭,而且我们可以寻求各种工具、框架和库的帮助。如果您对上述项目所涉及到的源代码感兴趣的话,可以通过如下链接,访问到它在 GitLab 上两个存储库:
??
推荐阅读
- 详解HTTP 与 HTTPS 的不同之处
- 高危!!Kubernetes 新型容器逃逸漏洞预警
- Hyperledger Fabric 2.x 自定义智能合约
- Mongodb 数据库的简单使用
- Linux之ping命令
- 为什么在数据驱动的路上,AB 实验值得信赖()
- #yyds干货盘点# Kubernetes 搞定网络原来可以如此简单((25))
- (准备工作)
- 2小时速学大数据编程语言 Scala 秘籍