从全栈开发者迈向Web3弄潮儿

Posted WOT技术大会

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从全栈开发者迈向Web3弄潮儿相关的知识,希望对你有一定的参考价值。

从全栈开发者迈向Web3弄潮儿_智能合约

作者丨John Vester

编译丨陈峻

策划丨孙淑娟

本文从 Web1~3 的基本特征讲起,通过一个选举示例,向您展示了全栈开发者将如何迈向 Web3 的技术实践过程。

近年来大热的美剧《创业公司(StartUp)》虚构了一种被称为 GenCoin 的数字货币,可用于各种创新式的金融交易场景中。而在我看来,它可以被理解为一种具有 Web3 核心属性的区块链分布式设计产品。如果您对 Web3 还不甚了解的话,让我们先回顾一下 Web 的三个主要时代:

从全栈开发者迈向Web3弄潮儿_ide_02

在 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 技术栈组件,来创建一个能与应用协同的智能合约。其创建的流程如下图所示:

从全栈开发者迈向Web3弄潮儿_5e_03

我们可以根据该流程,去招募以太坊的开发者,具体内容请参见链接 --https://consensys.net/developers/onboarding-step-2/。

使用 React 创建 Dapp

有了智能合约,Web3 工程师便可以使用 NPM、MetaMask、HTML/CSS/JavaScript/React、以及 Web3.js 等 Web3 技术栈组件,构建居委会选举的应用。在本例中,我们将采用 React(https://reactjs.org/) 框架和如下流程:

从全栈开发者迈向Web3弄潮儿_5e_04

首个以太坊 Dapp

我会通过 Infura 的注册页面(https://infura.io/register)创建一个免费帐户,并创建一个名为 jvc-homeowners-ballot 的项目:

从全栈开发者迈向Web3弄潮儿_智能合约_05

下图中有关该项目的细节,我会在下文中详细讨论:

从全栈开发者迈向Web3弄潮儿_ide_06

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 中创建多个帐户,其中至少有一个帐户可专用于开发和测试。请记住:永远不要与任何人分享您的助记词,也不要在任何地方上传您的私钥!

如下图所示,为了添加测试资金,我需要输入自己的帐户地址:

从全栈开发者迈向Web3弄潮儿_ide_07

如下图所示,通过 Ropsten Etherscan 站点,我们可以验证交易是否能够成功完成:

从全栈开发者迈向Web3弄潮儿_智能合约_08

最终准备步骤

请使用如下命令将 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: 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 网络中。我们可以进一步使用如下 URL,来验证智能合约,并在“Deploying JvcHomeownersBallot”日志中提供合约的地址:

https://ropsten.etherscan.io/

或是:

https://ropsten.etherscan.io/address/0xdeCef6474c95E5ef3EFD313f617Ccb126236910e

从全栈开发者迈向Web3弄潮儿_智能合约_09

使用 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, 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 开发

在开始标准的 React 开发之前,让我们先来看看完整的 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 () =>
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

在完成编译和验证之后,您会看到如下应用界面:

从全栈开发者迈向Web3弄潮儿_5e_10

它拥有三个选项:

  • VOTE YES - 提交赞成票
  • VOTE NO - 提交反对票
  • GET VOTES – 在 Dapp 的下部显示投赞成与反对票的总数

小结

综上所述,一旦建立了智能合约,从客户端的角度来看,我们将能够沿用 Web2 的如下方面到 Web3 上:

  • 目前在 Web2 项目中常用的 JavaScript 客户端框架,可以被继续使用。
  • NPM 可以被用来包含依赖项,以促进 Web3 的开发。
  • 类似于 Web2 应用程序与传统数据存储的交互方式,Web3 的 Truffle 和 MetaMask 库也允许应用程序与数据进行交互。
  • 现有的业务规则和 UI/UX 设计,将继续满足产品所有者对于 Web3 特性和功能上的要求。

而 Web3 的独特之处主要体现在:

  • 在区块链上构建的 Dapps,都采用同一个事实源向各个信息消费者的请求,提供可靠的数据。
  • 我们不再需要知道“谁”参与了交易、或查询存储在区块链智能合约中的信息(哪怕是别的 Dapp 去访问存储的数据)。由于结果总是固定不变的,因此 Dapp 只需关注应用程序的业务规则。正如上述居委会选举的简单示例那样,无论选票被查询多少次,即便是有另一个 Dapp,其结果总是完全相同的。
  • 由于其分布式的特性,因此控制权被返回给了消费者,而非停留在少数人的手中。

可见,从全栈开发者迈向 Web3 的学习曲线并不陡峭,而且我们可以寻求各种工具、框架和库的帮助。如果您对上述项目所涉及到的源代码感兴趣的话,可以通过如下链接,访问到它在 GitLab 上两个存储库:

以上是关于从全栈开发者迈向Web3弄潮儿的主要内容,如果未能解决你的问题,请参考以下文章

信创产业已成现象级新风口,快来加入争做“弄潮儿”

Server SAN:云计算时代的弄潮儿

新人新视角 | 经历丰富的弄潮儿

新人新视角 | 经历丰富的弄潮儿

争做IT新时代的弄潮儿

学习新特性,带你做C++学习的弄潮儿!!!