本文描述了在dotNet核心中使用像以太坊这样的区块链平台的过程。目标受众是其他想要从以太坊开始的dotNet开发者。需要了解区块链。在本文中,我们构建了一个完整的示例,允许你与自定义编写的智能合约进行交互。

第一代区块链的可以被视为仅比特币而没有智能合约。尽管如此,第二代区块链的表现明显给人更有希望。随着比特币以外的更多区块链平台,变得更加成熟,区块链有了更多可能性。以太坊区块链更像是一个使用加密货币的智能合约的分布式分类账。以太坊的重点更多地放在智能合约部分,然后是加密货币。以太币(以太坊的加密货币)的目的是为执行采矿合约或执行合约的交易提供报酬。

智能合约是为以太坊虚拟机编写的一段代码。这可以用Solidity编写并编译为字节代码。此字节代码放在分类帐中并变为不可变但仍可以与之交互,并且可以更改状态。正如以太坊文档所说:“从实用的角度来看,EVM可以被认为是一个包含数百万个对象的大型分散计算机,称为”帐户“,它们能够维护内部数据库,执行代码并相互通信。“从开发人员的角度来看,你可以将Solidity视为类似Javascript的语言,这有点受限。由于Solidity代码在区块链中运行,因此有充分的理由限制它。像随机数这样简单的东西也是一个挑战。也无法通过Http调用获取数据,因为所有事实需要在系统中。你仍然可以调用合约并输入数据来改变状态,因此外部影响是可行的。

首先安装Mist浏览器和Geth。Mist浏览器是一个GUI,可用作Ether的钱包。Geth是代码连接到的程序接口,Geth连接到以太坊的区块链。对于本文,我们将使用testnet。这样我们就可以免费开采一些以太币。启动Mist后,从菜单中选择使用测试网。创建一个帐户并挖掘一些以太币(菜单项目开发并开始挖掘)。

过了一段时间,你会有一些以太币。这在交易时很方便。即使发布合约或执行合约也要花费成本。现在让我们关闭钱包,否则你无法打开一个新的geth过程。所以在控制台中启动已安装的Geth:

“\Program Files\Geth\geth” --testnet --rpcapi eth,web3,personal --rpc

上图是我们命令的结果。我们看到它正在接收当前的区块链缓存,并且它的http端点正在localhost:8545上进行侦听。这很重要,因为我们需要Mist浏览器和其他应用程序使用IPC或RPC访问它。由于在Windows上只支持IPC实现,我们不能在dotNetCore中使用它。我们在解决方案中使用web3 RPC

现在你可以再次打开钱包。只是不能开始挖掘,因为有独立的Geth正在运行。

现在是时候开始开发,打开Visual Studio并创建一个新项目了。请注意,我们的Github提供了该代码。创建“ASP.NET核心Web应用程序”,然后选择“Web.API模板”。我们将创建一个服务,其中包含一些与区块链交互的方法,并向区块链发布合约。这个存钱合约将存储我们的代币余额。合约开采后我们可以调用合约方法。没什么高大上的,也不是一个完整的应用程序,但很高兴看到我们能做什么。我们选择使用Azure Table存储来保持系统的持久性,它快速且便宜。

首先将这些依赖项添加到Project.json中:

    "Nethereum.Web3": "2.0.0-rc1","Portable.BouncyCastle": "1.8.1.1","WindowsAzure.Storage": "8.1.1"

保存并查看正在恢复的软件包。前两个是以太坊相关,最后一个用于表存储。Nethereum.Web3是通过RPC json访问本地Geth进程的完整类库。BouncyCastle是Nethereum所需的加密库。

首先,我们需要一个模型来捕获我们的以太坊合约状态。以太坊没有任何选择让合约退出区块链,主要是出于安全/不可变的原因。一旦合约被放入区块链,就无法更改,也无法检索到Solidity代码。这就是我们需要将这些信息存储在我们的系统中的原因。在模型文件夹中创建一个名为EthereumContractInfo的文件,该文件派生自Azure Storage类TableEntity

using Microsoft.WindowsAzure.Storage.Table;namespace EthereumStart.Models
{public class EthereumContractInfo : TableEntity{public string Abi { get; set; }public string Bytecode { get; set; }public string TransactionHash { get; set; }public string ContractAddress { get; set; }public EthereumContractInfo(){}public EthereumContractInfo(string name, string abi, string bytecode, string transactionHash){PartitionKey = "contract";RowKey = name;Abi = abi;Bytecode = bytecode;TransactionHash = transactionHash;}}
}

现在创建一个名为Services的文件夹并创建文件IEthereumService接口,这样我们就可以将它用于依赖注入:

using System.Threading.Tasks;
using EthereumStart.Models;
using Nethereum.Contracts;namespace EthereumStart.Services
{public interface IEthereumService{string AccountAddress { get; set; }Task<bool> SaveContractToTableStorage(EthereumContractInfo contract);Task<EthereumContractInfo> GetContractFromTableStorage(string name);Task<decimal> GetBalance(string address);Task<bool> ReleaseContract(string name, string abi, string byteCode, int gas);Task<string> TryGetContractAddress(string name);Task<Contract> GetContract(string name);}
}

所有方法都应该返回一个任务,因为我们希望使实现使用异步。我们的想法是,我们将发布合约,尝试获取它的地址,然后在该地址上调用它的方法。现在我们创建文件BasicEthereumService来实现接口。

using Microsoft.Extensions.Options;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Table;
using Nethereum.Web3;
using System;
using System.Threading.Tasks;
using EthereumStart.Models;
using Nethereum.Contracts;namespace EthereumStart.Services
{public class BasicEthereumService : IEthereumService{private Nethereum.Web3.Web3 _web3;private string _accountAddress;private string _password;private string _storageKey;private string _storageAccount;public string AccountAddress{get{return _accountAddress;}set{_accountAddress = value;}}public BasicEthereumService(IOptions<EthereumSettings> config){_web3 = new Web3("http://localhost:8545");_accountAddress = config.Value.EhtereumAccount;_password = config.Value.EhtereumPassword;_storageAccount = config.Value.StorageAccount;_storageKey = config.Value.StorageKey;}public async Task<bool> SaveContractToTableStorage(EthereumContractInfo contract){StorageCredentials credentials = new StorageCredentials(_storageAccount, _storageKey);CloudStorageAccount account = new CloudStorageAccount(credentials, true);var client = account.CreateCloudTableClient();var tableRef = client.GetTableReference("ethtransactions");await tableRef.CreateIfNotExistsAsync();TableOperation ops = TableOperation.InsertOrMerge(contract);await tableRef.ExecuteAsync(ops);return true;}public async Task<EthereumContractInfo> GetContractFromTableStorage(string name){StorageCredentials credentials = new StorageCredentials(_storageAccount, _storageKey);CloudStorageAccount account = new CloudStorageAccount(credentials, true);var client = account.CreateCloudTableClient();var tableRef = client.GetTableReference("ethtransactions");await tableRef.CreateIfNotExistsAsync();TableOperation ops = TableOperation.Retrieve<EthereumContractInfo>("contract", name);var tableResult = await tableRef.ExecuteAsync(ops);if (tableResult.HttpStatusCode == 200)return (EthereumContractInfo)tableResult.Result;elsereturn null;}public async Task<decimal> GetBalance(string address){var balance = await _web3.Eth.GetBalance.SendRequestAsync(address);return _web3.Convert.FromWei(balance.Value, 18);}public async Task<bool> ReleaseContract(string name, string abi, string byteCode, int gas){// check contractNamevar existing = await this.GetContractFromTableStorage(name);if (existing != null) throw new Exception($"Contract {name} is present in storage");try{var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);if (resultUnlocking){var transactionHash = await _web3.Eth.DeployContract.SendRequestAsync(abi, byteCode, _accountAddress, new Nethereum.Hex.HexTypes.HexBigInteger(gas), 2);EthereumContractInfo eci = new EthereumContractInfo(name, abi, byteCode, transactionHash);return await SaveContractToTableStorage(eci);}}catch (Exception exc){return false;}return false;}public async Task<string> TryGetContractAddress(string name){// check contractNamevar existing = await this.GetContractFromTableStorage(name);if (existing == null) throw new Exception($"Contract {name} does not exist in storage");if (!String.IsNullOrEmpty(existing.ContractAddress))return existing.ContractAddress;else{var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);if (resultUnlocking){var receipt = await _web3.Eth.Transactions.GetTransactionReceipt.SendRequestAsync(existing.TransactionHash);if (receipt != null){existing.ContractAddress = receipt.ContractAddress;await SaveContractToTableStorage(existing);return existing.ContractAddress;}}}return null;}public async Task<Contract> GetContract(string name){var existing = await this.GetContractFromTableStorage(name);if (existing == null) throw new Exception($"Contract {name} does not exist in storage");if (existing.ContractAddress == null) throw new Exception($"Contract address for {name} is empty. Please call TryGetContractAddress until it returns the address");var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);if (resultUnlocking){return _web3.Eth.GetContract(existing.Abi, existing.ContractAddress);}return null;}}
}

这是很多代码。我将跳过SaveLoad -ContractFromTableStorage,因为这些只是简单的Azure表交互。

在构造函数中,我们看到与Geth进程的连接,我们连接到端口8545,因此它可以进行RPC json通信。

第一个方法实现的是getBalance。由于一切都围绕金钱,所以检查地址的以太币的余额是很重要的,比如你的账户,钱包甚至合约。在此示例中,所有以太坊交互都通过对象web3完成。在我们在Wei中取得余额之后,这就像是人民币的分数,然后是1018因子而不是102。我们可以使用convert.FromWEi将其转换回以太币。

第二个方法实现的是ReleaseContract。它首先检查我们是否尚未发布合约并将其保留在存储中。如果没有,我们可以开始解锁帐户120秒。当我们想要部署合约或其他东西时,需要解锁。之后,我们可以调用deploy方法并获取交易哈希。这是必要的,因为现在合约将被开采。将挖掘视为区块链的同行所做的过程,以便合约被接受到区块链中。当12个同行已经这样做时,合约地址被退回。这个挖掘过程需要花钱(又名Gas),并且会从你输入的_accountAddress中扣除。这个数量在Wei中,我们在控制器中指定它,它将调用EthereumService。每份合约都有不同的汽油价格。编译合约时可以使用此值。我们可以在方法SendRequestAsync中指定合约构造函数参数。在我们的情况下,我们指定2,因为合约发布时我们的余额应为2个以太币。

如上所述,必须挖掘部署才能获得合约地址。我们需要这个地址来调用它上面的方法。在我们的TryGetContractAddress中,我们检查我们的合约是否已经在我们的表存储中有一个地址,如果没有,我们会询问以太坊区块链。如果GetTransactionReceipt返回有效地址,我们可以保留它。

我们服务的最后一个方法是GetContract,这只是对以太坊合约的引用。如你所见,合约必须存在于表存储中才能获得合约地址。我们将在下一部分之后讨论调用合约。

所以现在我们从dotNet离开下,转到solidity程序语言。首先让我们看看我们的测试合法性;

pragma solidity ^0.4.6;
contract CoinsContract  {uint public balance;function CoinsContract(uint initial) {balance = initial;}function addCoins(uint add) returns (uint b) {b = balance + add;return b;}function subtractCoins(uint add) returns (uint b) {b = balance - add;return b;}
}

它只是一个基于其构造函数值的piggybank从该余额开始。然后我们可以调用加法和减法来修改我们的代币余额。我知道这是非常基本的但是一开始总是好的,对吗?合约发布后,我们可以从dotNet代码中调用addCointssubtractCoints方法。那你为什么要这样做呢?它只会花费我们以太?好的好处是,每次调用方法都会被添加到分配分类帐中,因此可以在https://testnet.etherscan.io/查看。

为了发布这个合约,我们需要将它编译为字节代码。我们使用Remix网站这个基于网络的基本编辑器可以编译和测试你的合约。编译完成后,我们可以获得字节代码(请不要忘记前面的0x)和接口,也称为ABI。在签订合约时需要提供这两个部件。ABI代表应用程序二进制接口,就像Web服务的WSDL一样。

回到Visual Studio,在我们发布合约并开始调用方法之前,我们只需再做四个步骤。

首先,我们创建名为EthereumSettings的设置文件:

namespace EthereumStart.Model
{public class EthereumSettings{public EthereumSettings(){}public string EhtereumAccount { get; set; }public string EhtereumPassword { get; set; }public string StorageKey { get; set; }public string StorageAccount { get; set; }}
}

其次,我们将这些设置添加到appsettings.json:

 "ehtereumAccount": "x","ehtereumPassword": "y","storageKey": "w","storageAccount": "v"

当然,不是使用这些值,而是使用你自己的以太坊帐户和密码以及Azure存储帐户和密钥。

第三,我们在我们的startup.cs中添加了ConfigureServices方法中的代码:

 services.Configure<EthereumSettings>(Configuration);services.AddScoped<IEthereumService, BasicEthereumService>();

对于我们的最后一步,添加一个名为EthereumTestController的控制器,内容应该是:

using EthereumStart.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;namespace EthereumStart.Controllers
{[Route("api/[controller]")]public class EthereumTestController : Controller{private IEthereumService service;private const string abi = @"[{""constant"":false,""inputs"":[{""name"":""add"",""type"":""uint256""}],""name"":""addCoins"",""outputs"":[{""name"":""b"",""type"":""uint256""}],""payable"":false,""type"":""function""},{""constant"":false,""inputs"":[{""name"":""add"",""type"":""uint256""}],""name"":""subtractCoins"",""outputs"":[{""name"":""b"",""type"":""uint256""}],""payable"":false,""type"":""function""},{""constant"":true,""inputs"":[],""name"":""balance"",""outputs"":[{""name"":"""",""type"":""uint256""}],""payable"":false,""type"":""function""},{""inputs"":[{""name"":""initial"",""type"":""uint256""}],""payable"":false,""type"":""constructor""}]";private const string byteCode = "0x6060604052341561000c57fe5b604051602080610185833981016040528080519060200190919050505b806000819055505b505b610143806100426000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630173e3f41461005157806349fb396614610085578063b69ef8a8146100b9575bfe5b341561005957fe5b61006f60048080359060200190919050506100df565b6040518082815260200191505060405180910390f35b341561008d57fe5b6100a360048080359060200190919050506100f8565b6040518082815260200191505060405180910390f35b34156100c157fe5b6100c9610111565b6040518082815260200191505060405180910390f35b600081600054019050806000819055508090505b919050565b600081600054039050806000819055508090505b919050565b600054815600a165627a7a723058200085d6d7778b3c30ba2e3bf4af4c4811451f7367109c1a9b44916d876cb67c5c0029";private const int gas = 4700000;public EthereumTestController(IEthereumService ethereumService){service = ethereumService;}[HttpGet][Route("getBalance/{walletAddress}")]public async Task<decimal> GetBalance([FromRoute]string walletAddress){return await service.GetBalance(walletAddress);}[HttpGet][Route("releaseContract/{name}")]public async Task<bool> ReleaseContract([FromRoute] string name){return await service.ReleaseContract(name, abi, byteCode, gas);}[HttpGet][Route("checkContract/{name}")]public async Task<bool> CheckContract([FromRoute] string name){return await service.TryGetContractAddress(name) != null;}[HttpGet][Route("exeContract/{name}/{contractMethod}/{value}")]public async Task<string> ExecuteContract([FromRoute] string name, [FromRoute] string contractMethod, [FromRoute] int value){string contractAddress = await service.TryGetContractAddress(name);var contract = await service.GetContract(name);if (contract == null) throw new System.Exception("Contact not present in storage");var method = contract.GetFunction(contractMethod);try{// var result = await method.CallAsync<int>(value);var result = await method.SendTransactionAsync(service.AccountAddress, value);return result.ToString();}catch (Exception ex){return "error";}}[HttpGet][Route("checkValue/{name}/{functionName}")]public async Task<int> CheckValue([FromRoute] string name, [FromRoute] string functionName){string contractAddress = await service.TryGetContractAddress(name);var contract = await service.GetContract(name);if (contract == null) throw new System.Exception("Contact not present in storage");var function = contract.GetFunction(functionName);var result = await function.CallAsync<int>();return result;}}
}

它看起来很多代码,但它是一些方法。首先,我们有合约的ABI和二进制代码,第二个是我们加载服务的构造函数。然后我们可以调用4个http调用(请自己添加localhost +端口)

  • /api/EthereumTest/getBalance/0xfC1857DD580B41c03D7 e086dD23e7cB e1f0Edd17,这将检查钱包,并应返回5 Ehter。
  • /api/EthereumTest/releaseContract/coins,这将释放合约将结果保存到Azure存储。
  • /api/EthereumTest/checkContract/coins,这将检查合约地址是否可用。如果为true,则存在合约地址,我们可以调用它。这可能需要一些时间(有时2分钟,但有时20秒)。
  • /api/EthereumTest/exeContract/coins/addCoins/123,实际调用合约和方法addCoins的值为123。一旦调用它,就会给出一个交易结果。可以使用CallAsync但是它会在你的本地以太坊VM中调用,因此这不会导致交易。因为它是一个交易,所以返回交易地址。我们也可以在Etherscan网站上看到我们的合约。Etherscan显示了以太坊的主要和测试网络的所有交易。有了这个,你就可以证明你做了一笔交易。这是我们的一个交易可以查看。
  • /api/EthereumTest/checkValue/coins/balance,当我们的ExeContract中的交易被挖掘(验证)时,我们也可以查看我们的乘法结果。合约中包含一个公共变量lastResult。可以调用此方法来获取当前状态。在与123签订合约后,余额为125。
  • /api/EthereumTest/exeContract/coins/subtractCoins/5,现在我们减去5个以太币,再次检查余额,它应该是120。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。

汇智网原创翻译,转载请标明出处。这里是原文

.NetCore使用以太坊区块链简介相关推荐

  1. 2020 中国开源年会暨阿帕奇中国路演-以太坊区块链黑客松:构造下一个数字世界...

    点击上方"开源社"关注我们 | 编辑:黄欣宜 | 设计:王福政 | 责编:王玥敏 9月11日 -10月 25日 // 线上黑客松  今年,新冠状病毒突袭全球,似乎给全世界按下了&q ...

  2. 【区块链】以太坊区块链技术初探

    2019独角兽企业重金招聘Python工程师标准>>> [国内首家]以太坊区块链实战教学:http://edu.csdn.net/course/detail/6455 LinApex ...

  3. 以太坊区块链同步_以太坊69:如何在10分钟内建立完全同步的区块链节点

    以太坊区块链同步 by Lukas Lukac 卢卡斯·卢卡奇(Lukas Lukac) Ethereu M 69:如何在10分钟内建立完全同步的区块链节点 (Ethereum 69: how to ...

  4. 以太坊区块链_以太坊区块链搭建与使用(一)-私有链

    步骤 一.下载go语言,并配置环境变量 //以太坊源代码依赖的编译与运行环境 二.通过git clone以太坊源码(go-ethereum),并编译 一.go安装 step1:下载 官方(一般打不开) ...

  5. 如何搭建socks5和ss节点_以太坊区块链搭建与使用(三)-联盟链

    首先对以下概念说明下: 一.以太坊大家都知道比特币使用的技术是区块链技术,比特币也是区块链技术的代表. 即比特币=区块链1.0随着区块链技术的发展以太坊也诞生了,也就是我们说的 区块链2.0.为什么说 ...

  6. 如何用web3.js在以太坊区块链上保存数据?

    2019独角兽企业重金招聘Python工程师标准>>> 虽然有北大博士讲,95%的区块链项目都没有前途,但我们知道区块链还是有它的优势,比如数据的不可篡改性对于版权保护有相当大的意义 ...

  7. solidity payable_以太坊区块链搭建与使用(五)-智能合约Solidity

    一.智能合约Solidity开发工具 1.remix-ide http://remix.ethereum.org/ 在线版本,也可以去github下载安装到本地.开发.编译.发布.执行.测试 2.re ...

  8. 美联社将在以太坊区块链记录NCAA锦标赛篮球比分和赛程表

    美联社与Everipedia合作,将在以太坊区块链上发布疯狂三月(NCAA锦标赛)的篮球比分和赛程表.首场比赛将于美东时间今天下午6点开始. 文章链接:https://www.tuoluocaijin ...

  9. 用以太坊区块链保证Asp.Net Core的API安全(上)

    去中心化应用程序(DApp)的常见设计不仅依赖于以太坊区块链,还依赖于API层.在这种情况下,DApp通过用户的以太坊帐户与智能合约进行交互,并通过交换用户凭据而发布的JWT token与API层进行 ...

最新文章

  1. golang1.16新特性速览
  2. POJ 1470 Closest Common Ancestors (最近公共祖先LCA 的离线算法Tarjan)
  3. 正则表达式,VI,SED及shell编程2010-12-05
  4. SpringBoot 使用【p6spy-spring-boot-starter】集成 p6spy 监控数据库(配置方法举例)
  5. python判断字符串是纯数字_python判断字符串是否纯数字的方法
  6. jzoj6307-安排【归并排序】
  7. nodejs cluster_NodeJS 极简教程 lt;1gt; NodeJS 特点 amp; 使用场景
  8. locust+httprunner+Jmeter QQ群微信群问题记录
  9. JavaScript文档对象模型DOM节点操作之兄弟节点(4)
  10. 使用elastic job 不分片任务加载失败的可能原因
  11. 《Java》Java Applet运行原理
  12. java数字时钟界面_Java数字时钟实现代码
  13. 网络拓扑图是用什么软件画的?
  14. 工具一栏,就是小锤子右边的一排都没亮
  15. 导出excel 并且处理长数字,处理科学计数法,以文本形式存储的数字
  16. java 通用权限管理_通用权限管理设计篇(一)
  17. linux vim粘贴快捷键设置,vim粘贴模式快捷方式
  18. 程序员的编辑器—VIM(怀念Unix下的岁月)
  19. 【bzoj4806~bzoj4808】炮车马后——象棋四连击
  20. mysql 存储过程 compile_存储过程 | iMySQL | 老叶茶馆

热门文章

  1. Iproute2简介
  2. office for mac的自动恢复文件的路径
  3. 牛客网论坛最具争议的Java面试成神笔记,GitHub已下载量已过百万
  4. FreeNAS加入AD域实现账号同步
  5. 移动WiFi大比拼 谁会成为独角兽
  6. 【MATLAB自带的svmtrain和libsvm中的svmtrain函数的区别】
  7. java制作水效果_JAVA图像处理系列(十)——艺术效果:水纹
  8. 面试题:进程间通信方式,线程间通信方式
  9. WEB FRONT-END NOTE DAY02
  10. 摸鱼前端的自检(六)你不知道的web性能优化