跳转至

各种 dApps 集成

这一章是关于用新技术改进你现有的 dApps 和智能合约,使它们更快、更好、更有效。有趣的是,大多数 dApps 都可以通过几个小技巧来改进。您将发现 dApp 开发的新方面,包括创建您自己的 oracles 和后端来处理智能合约。首先,您将从提高您的 React 技能开始,然后我们将转到后端,以便您学习如何为需要大量资源才能正常工作的混合 dApps 创建更好的集中式后端。之后,我们将回到前端,学习如何使用 web3.js 构建更强大的 dapp。为了涵盖与 dapp 相关的所有领域,您将使用最近获得的关于服务器的知识来构建 Oracle,这是处理 Oracle 时要考虑的主要组件。最后,为了完成改进,您将学习如何改进您的开发工作流,以产生在时间和资源方面最有效的代码。

在本章中,我们将讨论以下主题:

  • 更好地应对应用
  • 使用 NGINX 的可伸缩 Node.js 后端
  • 更好的 web3.js 应用程序
  • 构建您自己的神谕
  • 改进您的开发工作流程

更好地应对应用

您熟悉创建 React 应用程序所需的工作流。然而,新型 dApps 的许多方面更难控制。这包括智能合约连接、在 Solidity 中为您的功能处理数据,以及创建可扩展的组件。

正确组织组件

当您的应用程序开始增长时,您希望确保您的代码库足够干净以支持新的改进,而不必在以后重写整个系统。为此,首先要将组件分离到不同的文件中,这样就可以保持内容有序。

例如,看看这个名为index.js的文件:

import React from 'react'
import ReactDOM from 'react-dom'

class Main extends React.Component { ... }

class ArtContainer extends React.Component { ... }

class ArtPiece extends React.Component { ... }

class Form extends React.Component { ... }

class ButtonContainer extends React.Component { ... }

ReactDOM.render(<Main />, document.querySelector('#root')

您可以看到有五个组件都在一个由数百行代码组成的大文件中。对于只有几个组件的较小项目来说,这是可以接受的,但是当您开始处理较大的应用程序时,您必须将组件放在不同的文件中。为此,请用完全相同的名称为每个组件创建一个文件。这里有一个例子:

// ArtPiece.js

import React from 'react'
import ReactDOM from 'react-dom'

class ArtPiece extends React.Component { ... }

export default ArtPiece

注意,您必须使用export default关键字导出您的组件,这样您就可以明确地得到那个组件。然后,您的src文件夹将看起来像这样:

src/
    Main.js
    ArtContainer.js
    ArtPiece.js
    Form.js
    ButtonContainer.js

现在,在您的Main.js组件中,您必须导入您将使用的所有组件。不然就不行了。这种重构可以很容易地在任何类型的项目中完成,因为它只是将组件分离成文件;但是,请确保将它们导入和导出到正确的位置。

动态生成组件

提高 React dApps 的另一个技巧是动态生成组件。您可能遇到过这样的情况,因为您有某种数组,所以您必须生成几个具有不同属性的子组件。这看起来很简单,但是很不直观,因为 React 只理解它的虚拟 HTML 中的某种类型的对象。

假设您从智能合约中获得了以下包含一些动物的不同属性的对象数组:

const myAnimals = [
    {
        name: 'Example',
        type: 'tiger',
        age: 10
    }, {
        name: 'Doge',
        type: 'dog',
        age: 12
    }, {
        name: 'Miaw',
        type: 'cat',
        age: 3
    }
]

您希望为每个对象生成一个Animal组件。你不能简单地把它们全部循环起来,然后创建组件;您必须使用带有普通括号的.map()函数,而不是花括号,因为 React 组件非常挑剔。下面是它的样子:

  1. 首先,使用要在数组中显示的元素设置构造函数,如下所示:
import React from 'react'
import ReactDOM from 'react-dom'

class AnimalContainer extends React.Component {
    constructor () {
        super()
        this.state = {
            myAnimals: [
                {
                    name: 'Example',
                    type: 'tiger',
                    age: 10
                }, {
                    name: 'Doge',
                    type: 'dog',
                    age: 12
                }, {
                    name: 'Miaw',
                    type: 'cat',
                    age: 3
                }
            ]
        }
    }
}

ReactDOM.render(<AnimalContainer />, document.querySelector('#root'))
  1. 然后,设置渲染函数,用map()函数查看所有元素,尽管您可以使用普通的for()循环来生成 JSX 组件的数组。请注意,我们返回的是普通()括号内的每个元素,而不是花{}括号,因为 JSX 要求返回动态 HTML 元素:
render () {
    return (
        <div>
            {this.state.myAnimals.map(element => (
                <Animal
                    name={element.name}
                    type={element.type}
                    age={element.age}
                />
            ))}
        </div>
    )
}
  1. 最后,创建Animal组件,以便它显示在您的 dApp 上:
class Animal extends React.Component {
    constructor () {
        super()
    }

    render () {
        return (
            <div>
                <div>Name: {this.props.name}</div>
                <div>Type: {this.props.name}</div>
                <div>Age: {this.props.name}</div>
            </div>
        )
    }
}

如您所见,AnimalContainer组件正在使用.map()函数动态生成Animal。这就是如何将 JavaScript 对象转换成 React 组件。请注意,我们在渲染函数中生成组件,并且.map()函数块在普通括号中,而不是在大括号中:

.map(element => ())

更快地启动项目

React 项目的另一个问题是,您必须从头开始安装依赖项,建立一个webpack文件,并确保一切正常工作。这是乏味的,花费了太多宝贵的时间。为了解决这个问题,有了create-react-app库,尽管它添加了许多不必要的包,最终可能会导致问题,使升级变得更加困难,因为它是基于一个封闭的系统。

最好使用初创公司 React 项目的最简化版本。这就是为什么我创建了开源dapp项目,它包含了 react dApp 项目的最小、最简版本,可以让你马上开始。您可以从我的 GitHub 获得最新版本,代码如下:

$ git clone https://github.com/merlox/dapp

然后用npm i安装所有的依赖项,运行webpack watch让你的文件在用webpack -d -w修改时保持捆绑,并在dist/文件夹中运行你选择的静态服务器。例如,你可以选择http-server dist/

dapp项目正在为您完成以下任务,以便您可以立即开始您的新 dApp 工作:

  • 安装所有的reactwebpackbabeltruffle依赖项。刚刚好的数量,因为它甚至不包括.css加载器,所以你可以轻松地管理你的包。如果你想使用它,你仍然需要在全球范围内安装 Truffle。
  • 为你建立一个webpack.config.js文件,在/src/index.js有一个入口,输出到/dist/,用加载器加载所有的.js.html文件。
  • 设置最简单的 HTML 和 JavaScript 索引文件。

因此,每当您必须开始一个新项目时,您可以简单地克隆dapp存储库来更快地开始。

使用 NGINX 的可伸缩 Node.js 后端

Node.js 是创建命令行应用程序、服务器、实时后端和各种用于开发 web 应用程序的工具时最强大的工具之一。它的美妙之处在于 Node.js 是服务器上的 JavaScript,它可以很好地与您的 React 前端结合,实现无处不在的 JavaScript。尽管它是集中式的,但你会在分散式项目中使用它很多次,在这些项目中你无法摆脱以太坊区块链的限制。你看,Solidity 和 Vyper 是严重受限的:除了基本的基于函数的代码,你做不了多少。总有一天,您将不得不为高级应用程序使用集中式后端,比如那些需要仪表板的应用程序。

至少在分散托管和存储解决方案得到显著改善之前,我们将不得不通过集中后端来完成智能合约无法轻松完成的特定任务。

另一方面,NGINX (发音为引擎 X )是一个 web 服务器,可以用作反向代理和负载平衡器等。与 Node.js 结合使用,这是一个了不起的工具,因为它加快了后端调用的速度,并极大地提高了可伸缩性。简而言之,NGINX 是 Node.js 在高级项目中最好的朋友,这些项目需要为大量用户提供最佳性能。这并不意味着它不能用于简单的 Node.js 应用程序,一点也不:NGINX 对于小型应用程序来说也非常出色,可以帮助您轻松控制端口和理解域名。你会学到所有你需要正确使用它的大 dApps。

我们将从学习如何使用 NGINX 后端创建 Node.js 应用程序开始,然后我们将它连接到一个真实的域名,最终部署一个具有负载平衡和其他改进的可扩展 NGINX 后端。

创建 Node.js 服务器

你可以在任何你想要的地方创建 Node.js 应用程序,但是总有一天你必须将这个应用程序转移到一个真正的主机服务上,比如亚马逊网络服务 EC2 ( AWS EC2 )或者 DigitalOcean。两者都是很好的选择,所以我们将探索如何部署到数字海洋。

无论如何,我们将首先在本地创建 Node.js 服务器,然后将其转移到托管解决方案。假设我们有下面的场景:你刚刚用 React 创建了一个 dApp,它工作得非常完美,效率非常高,所以你希望其他人能够免费使用这个应用程序。您可以将它部署到一个静态托管站点,比如 GitHub 页面或 HostGator 提供的页面,但是您希望扩展应用程序的功能,并拥有一个只允许某些用户访问的数据库和管理员页面。这就是你需要一个定制服务器和一个虚拟私人服务器的地方,虚拟私人服务器基本上是一台远程计算机,在这里你可以做任何你想做的事情来创建定制服务器,通常使用 Linux 操作系统。

为了实现这一切,您必须从创建一个 Node.js 服务器开始,它为您提供静态文件,而不是使用像http-server这样的工具。让我们首先为我们在前几章中创建的社交音乐应用程序创建一个静态服务器。继续在您的项目目录中创建一个server/public/文件夹,并将基本代码移动到public文件夹中:

我们移动了所有的文件,除了那些与节点相关的文件,比如package.json和那些与 GitHub 相关的文件,比如LICENSE,这样我们可以在一个单独的位置组织我们的服务器文件。

首先在server/中创建一个名为server.js的文件作为主文件,其中包含我们将用来设置服务器的主要必需库:

const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const path = require('path')
const port = 9000
const distFolder = path.join(__dirname, '../public', 'dist')

然后,配置将负责在外部用户请求时传递正确文件的服务器侦听器:

app.use(distFolder, express.static(distFolder))
app.use(bodyParser.json())

app.use((req, res, next) => {
   console.log(`${req.method} Request to ${req.originalUrl}`)
   next()
})
app.get('*/bundle.js', (req, res) => {
   res.sendFile(path.join(distFolder, 'bundle.js'))
})
app.get('*', (req, res) => {
   res.sendFile(path.join(distFolder, 'index.html'))
})

app.listen(port, '0.0.0.0', (req, res) => {
    console.log(`Server listening on localhost:${port}`)
})

我们首先做的是导入expressbody-parser。Express 是一个使用 Node.js 创建 web 服务器的框架,而 body-parser 正在处理我们所有的 JSON 请求,以便能够理解这些类型的消息,因为默认情况下,Node.js 不理解 JavaScript 请求json对象。然后,我创建了几个get请求处理程序,当从dist文件夹请求时,发送bundle.js文件和index.htmlapp.use()是一个中间件,这意味着它接收所有请求,进行一些处理,并让其他请求块完成它们的工作。在这种情况下,我们使用中间件来记录关于每个请求的信息,这样我们就可以在发生任何错误的情况下调试服务器。

使用以下内容安装所需的服务器依赖项:

npm i -S body-parser express

现在,您可以运行服务器了:

node server/server.js

上述命令的问题在于,每当出现错误请求或对服务器文件进行更改时,您都必须重新启动服务器。对于开发来说,最好使用nodemon实用程序,它可以自动刷新服务器。使用以下代码安装它:

npm i -g nodemon

然后,再次运行服务器:

nodemon server/server.js

为了使开发更容易,在您的package.json文件中创建一个新的脚本来更快地运行该命令:

{
  "name": "dapp",
  "version": "1.0.0",
  "description": "",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
 "scripts": {
 "server": "nodemon server/server.js"
 },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.2",
    "babel-polyfill": "^6.26.0",
    "body-parser": "^1.18.3",
    "css-loader": "^2.1.0",
    "express": "^4.16.4",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "react": "^16.8.1",
    "react-dom": "^16.8.1",
    "style-loader": "^0.23.1",
    "truffle-hdwallet-provider": "^1.0.3",
    "web3": "^1.0.0-beta.46",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.2.3"
  }
}

然后,您将能够运行npm run server来启动服务器。

获得托管解决方案

现在我们已经运行了静态服务器,我们可以将它部署在一个托管解决方案上,使我们的 dApp 可以从外部访问。在此之前,将您的项目添加到 GitHub,并进行所有最新的更改,以便稍后在另一台计算机上使用。前往https://digitalocean.com并创建一个链接为https://m.do.co/c/db9317c010bb的账户,它将为你提供价值 100 美元的 60 天免费服务,当你在服务中增加 25 美元时,还会额外增加 25 美元。这足够运行一个基本的 VPS 服务器至少 3 个月了。您必须添加您的信用卡/借记卡或添加 5 美元的 PayPal 美元才能开始使用它。以我为例,我用贝宝支付 5 美元。

转到水滴部分,点击创建水滴:

然后,您必须选择在服务器中安装什么发行版;这种分布被称为液滴。您可以选择一键安装 Node.js,但是我认为当您没有用户界面时,知道如何从头开始安装 Node.js 是非常重要的。因此,选择 Ubuntu 18.04 作为每月 5 美元的操作系统:

选择离您居住地最近的数据中心,以获得最佳性能。我生活在西班牙,所以我会选择德国或者英国的服务器。对你来说,可能不一样:

保持其余选项不变,然后按 Create 创建它。你会看到它是如何实时创建的。点击您的 droplet 并复制 IPv4 地址,您将需要该地址来连接到该服务器:

在 VPS 主机上设置服务器

如果你用的是 Windows,从官方页面下载 PuTTY 来连接外部服务器,这里:https://www.putty.org。安装后打开它:

将您的 IP 地址粘贴到主机名输入中,并通过单击 Open 连接到它。它会警告您连接到未知的服务器;点击“是”即可。然后,它会要求你登录;键入root作为你的默认用户名:每个主机提供商都不一样。

如果您使用的是 Mac,您可以简单地使用以下命令,而不是使用 PuTTY:

ssh root@<your-ip>

虽然 root 是 DigitalOcean 提供的默认用户,但是注意可能每个托管解决方案都不一样,所以要查看他们网站上提供的信息。

然后,它会问你的密码,你可以通过电子邮件获得;由于 DigitalOcean 已经向您发送了您的登录凭据,因此作为一种安全措施,您将看不到它。您可以通过右键单击来粘贴它,因为这就是 PuTTY 的工作方式。

之后,它会立即要求您重新键入当前密码,然后将您的 Unix 密码更改为新密码,因为您不能依赖自动生成的密码:

您现在应该可以访问您的服务器了。如您所见,除了命令行工具,您没有用户界面。您不希望以 root 用户的身份执行所有任务,因为这有安全风险,因为任何操作都可以不受限制地访问整个系统。使用以下代码创建一个新用户:

useradd -m <your-user-name>

下面举个例子:useradd -m merunas-m标志将创建一个/home/merunas用户文件夹。然后,使用su merunas切换到该用户或您创建的任何用户。su命令的意思是“替代用户”。

您还必须使用passwd命令设置密码,否则您将无法在会话开始时登录。例如,您可以使用这个命令:passwd merunas。下次您将希望以该用户身份登录,以避免作为根用户的潜在安全风险。然后,您会希望将 shell 改为 Bash,而不是 sh,以便在按下 ab 时获得自动完成功能,以及其他帮助您编写命令的实用程序。使用以下命令执行此操作:

chsh <your-user> -s /bin/bash

然后,将您的用户添加到sudo组,以便能够作为root用户运行命令,而无需更改用户。您必须以root用户的身份运行:

usermod -aG sudo <your-user>

这里举个例子:usermod -aG sudo merunas

我们现在要做的是从头开始安装 Node.js 和 NGINX。Node.js 的过程有点复杂,因为他们在不断改进他们的软件,所以很难设置,但完全可行。转到https://nodejs.org/en/download/current/,右键点击按钮复制源代码的链接地址:

回到 PuTTY 会话,运行带有源代码链接的wget命令,下载节点二进制文件,以便安装它:

wget https://nodejs.org/dist/v11.10.0/node-v11.10.0.tar.gz

tar提取它,如下命令行所示:

tar -xf node-v11.10.0.tar.gz

通过运行cd node-v11.10.0导航到当前目录。要从该文件夹安装 Node.js,您需要一些依赖项,它们可以安装在名为build-common的包中:

sudo apt install build-common

然后,运行./configuresudo make命令来运行安装。make命令生成所需的配置,但这需要几分钟,所以请耐心等待。以前你还得跑sudo ./install.sh,现在已经没必要了;您仍然可以得到您漂亮的node可执行文件。只需将其复制到二进制文件位置,就可以在全局范围内使用它:

sudo cp node /bin

现在,您可以删除安装文件夹和下载的文件。或者,您可以使用sudo apt install nodejs来安装 Node.js,但这是一个过时的版本,不像官方二进制文件那样维护。现在已经安装了 Node.js,git 可以从 GitHub 克隆您的社交音乐项目,或者使用 mine 的以下命令:

git clone https://github.com/merlox/social-music

sudo apt install npm在外部安装npm,这样就可以安装包了。您必须从其他来源获取它,因为 Node.js 不包含它。npm 的好处在于,你可以轻松地用sudo npm i -g npm立即将其更新到最新版本,所以用 Node.js 从哪里获得哪个版本并不重要,你不能不经过漫长的过程就简单地将其更新到最新版本。

现在,您可以运行npm install来安装来自social-music项目的依赖项。检查您的package.json文件是否包含您之前创建的npm run server命令。否则,用vim或任何其他文本编辑器重新添加,如nano:

"scripts": {
    "server": "node server/server.js"
}

当您使用npm run server命令时,您会看到您的服务器运行正常;问题是你不应该使用nodemon,因为它是为开发而设计的,没有考虑不同环境中可能出现的问题。

因此,您有一个非常适合生产中的 Node.js 项目的实用程序。它叫做pm2,它会让你的服务器保持运行,即使在某个时候发生了致命的错误。这个工具很棒,因为您可以监控您的服务器并运行不同服务的各种实例。使用以下命令全局安装它:

sudo npm i -g pm2

非常好用。您可以只使用pm2 start server/server.js来守护一个服务,这意味着无论它出于什么原因停止运行,都要重新启动它。要停止它,使用运行服务列表中的pm2 delete server

恭喜你!您的服务器上运行着一个 Node.js 应用程序。现在,要让它对全世界开放,你得把它暴露在端口80上,这个端口是所有网站都使用的公共端口。你可以通过修改你的server.js文件或者使用所谓的前端服务器来实现,前端服务器接收所有的请求并把它们重定向到正确的位置。在我们的例子中,这将是 NGINX。但在此之前,我们需要一个可访问的域,使我们的 IP 管理更容易。

获取域名

你需要一个域名来帮助人们用一个容易记住的名字访问你的网站,而不是在他们的浏览器上写一个很长的 IP 号码。该域名将与您的托管解决方案有一些变化。要获得域名,请前往godaddy.com并搜索您想要的域名:

选择最适合您业务的领域:

如果您没有帐户,请点击“添加到购物车”按钮并创建一个帐户来购买。我总是使用贝宝,因为它更容易管理。几分钟后,您的域将出现在仪表板中:

现在,您可以转到 DNS 管理设置,将您的域指向您的托管服务器,以便可以从该名称访问它:

单击您的 A 记录旁边的铅笔图标,将指针更改为您的 IP 地址,如下所示:

通过这一更改,您现在可以使用域名而不是 IP 连接到您的服务器,例如在 Mac 中,如下所示:

ssh root@socialmusic.online

它会像以前一样工作。您还可以在端口 80 上启动 Node.js 服务器,这样您就可以使用该域访问网站。然而,Node.js 在与域对话时受到限制,因此我们必须使用更高级的解决方案。

设置 NGINX

既然您的域已经设置好了,是时候将 NGINX 配置为一个前端服务器来连接您的域和 Node.js 实例了。NGINX 将为您处理所有请求,以便您可以专注于改进 Node.js 应用程序。

像以前一样连接到您的服务器,并使用以下命令安装nginx:

sudo apt install nginx

之后,您必须编辑 NGINX 的配置文件,这些文件位于/etc/nginx/sites-enabled/default。只需用vim编辑您的默认文件:

sudo vim /etc/nginx/sites-enabled/default

然后,添加以下代码,以便能够使用 Node.js 服务器的域:

upstream nodejs {
  server socialmusic.online:9000;
}

server {
  listen 80;
  server_name socialmusic.online;

  gzip on;
  gzip_comp_level 6;
  gzip_vary on;
  gzip_min_length 1000;
  gzip_proxied any;
  gzip_types text/plain text/html text/css application/json text/JavaScript;
  gzip_buffers 16 8k;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_pass http://nodejs;
  }

  location ~ ^/(img/|img/|JavaScript/|js/|css/|stylesheets/|static/) {
    root /home/merunas/social-music/public;
    access_log off;
    expires max;
  }
}

首先,我们定义了一个upstream块。这就是我们告诉 NGINX 我们正在运行的node.js服务器在正确端口的位置。这对保护端口 80 很重要,因为它是大多数请求执行的地方。

然后,我们创建了一个server块。这些类型的块用于在内部定义的端口设置一些配置。listen 80;语句告诉 NGINX 在服务器块内部处理端口 80 的请求。然后,我们添加了一些gzip压缩来加快加载速度,并添加了一个位置块来将所有请求传递给upstream nodejs。另一个位置块用于提供静态文件,以防您有图像之类的东西,因为这是传递静态内容的一种更快的方式。注意,root /home/merunas/social-music/public;根位置是我们的静态文件所在的位置。

请记住为您的域更改socialmusic.online。现在,您可以使用以下命令行运行 NGINX:

sudo service nginx restart

这将重新启动服务,以保持其在后端运行。您的网站现在可以通过您的域名从任何浏览器访问。为了完成部署,我们将添加 SSL。 SSL 是一种加密算法,用于保护访问您 dApp 的用户的通信安全。这很常见,任何严肃的项目都必须添加。

添加 SSL 安全性

为了安装 SSL,我们将使用来自 Let's Encrypt 的免费证书,这是一个非营利组织,其目标是为每个人提供免费的 SSL 证书来保护互联网。以下是步骤:

  1. 安装以下库:
sudo apt install software-properties-common certbot python-certbot-nginx
  1. 运行certbot应用程序来添加 NGINX 服务器:
sudo certbot --nginx
  1. 提供您的电子邮件地址,接受服务条款,并选择 1 作为您的域名:

  1. 它将询问您是否要将所有请求重定向到 443 安全 HTTPS 端口。只需选择第二个选项就可以了:

应该就是这样!现在,您已经为所有请求启用了 HTTPS,您的域名将自动重定向到 HTTPS。这可以手动完成,但是这种方式要简单得多,因此在处理这种复杂的身份验证系统时,您可以避免无数令人头疼的问题。

现在,您已经有了一个运行 HTTPS 的 NGINX 服务器,它使用 Node.js 集中式后端为您的分散式应用程序运行,您可以根据自己的意愿扩展它的高级功能,这些功能在简单的智能合约中是无法实现的。两全其美。

更好的 web3.js 应用程序

web3.js 是与 web 应用程序中的智能合约通信的最常用的实用工具,用于将它们转换成分散的应用程序。它能够管理无止境的交易,一旦设置好就会自动工作。

问题来自于很多 web3.js 应用没有优化,至少没有尽可能的好。因为我们正在处理智能合同,不可避免地代码会很快变得混乱,使得中长期的维护更加困难。这就是为什么从一开始就学习系统以创建更好的 web 3 . js dapp 是重要的,以学习在与智能合约交互时将使你成为更好的程序员的提示和技巧。

你将使用 web3 与许多 dApps 一起工作,所以为什么不学习做事情的最佳方法,以在创建更高质量代码的同时,从长远来看避免你的麻烦呢?以下是制作更好的 web 3 . js dapp 的一些提示和技巧。

设置固定的 web3.js 版本

如果您过去使用过 MetaMask,您会注意到它将 web3.js 注入到您访问的每个页面中,因为它要求 web3.js 能够与智能合约进行交互。这很好:这是一种预期的行为,但它通常会导致古老的 web3.js 版本,主要是 0.20 版本,在 web3.js 1.0 问世后的几年里,一直在使用并且仍然在使用。他们不想强迫用户更新到最新版本,因为这将破坏许多已经依赖于 MetaMask 的 web 3 . js dapp;这是一个巨大的潜在问题。

这就是为什么您必须为您的项目设置一个固定的 web3.js 版本,这样您就不会依赖于 MetaMask 或任何其他以太坊客户端强制您使用的版本。必须提供某种保证,保证你的 dApp 在未来能够继续工作。

为此,请看一下这段代码:

import NewWeb3 from 'web3'

window.addEventListener('load', () => {
    window.web3Instance = new NewWeb3(NewWeb3.givenProvider)
})

我们在这个例子中使用的是 web3.js 1.0。接下来,我们导入NewWeb3类,这只是一个不同的名称,以区别于 MetaMask 提供的Web3,从而建立一个新的web3对象,使用我们的特定版本的web3与区块链进行通信。它被称为web3Instance而不是普通的web3,因为我们想使用不同的名称来避免使用 MetaMask 提供的名称。你看,我们无法知道 MetaMask 什么时候会注入自己版本的web3,所以我们,一定要用一个不同的名字来保证我们的版本设置好了。然后,我们使用window对象设置一个全局web3Instance变量,这样就可以从应用程序中的任何地方访问它,并且我们在页面加载后通过监听事件'load'来实现。

在一个项目中尝试一下,您会看到web3Instance是您在导入中定义的版本。请注意,.givenProvider正在从 MetaMask 获取注入的 web3.js 数据,以设置新的 web3.js 版本。请确保在您未来的所有项目中使用这个技巧,以保证您的 dApp 适用于未来和过去的 web3.js 版本,因为 MetaMask 一直在以不可靠的方式改变它自己的系统。

创建助手功能

助手函数是那些帮助您轻松管理更复杂函数的函数。它们本质上是一些函数,旨在通过一些公共逻辑来帮助其他函数,这样您就不必一遍又一遍地重复您的代码。

这些都是重要的函数,因为它们将极大地提高代码的可维护性。您将能够在更少的代码行中看到发生了什么,并且能够更快地升级您的代码。

例如,在 web3.js 1.0 中,契约必须为每个智能契约调用和事务使用一大行代码:

await this.state.contractInstance.methods.functionName(parameters).send({from: this.state.userAddress})

这是描述性的,但比必要的要长一点。让我们用一个辅助函数来简化它:

async function send(functionName, parameters) {
    await this.state.contractInstance.methods[functionName](parameters).send({from: this.state.userAddress})
}

正如您所看到的,我们已经将 call 中的一个方法转换成了括号版本,因为这样您就可以动态地为对象生成带有唯一参数的函数名。过去,我记得使用以下快捷方式快速选择元素,而不必反复键入相同的结构:

function q(element) {
    return document.querySelector(element)
}

用这样一个简单的帮助函数,我已经用相同的逻辑将一个 22 字符的函数转换成了一个 1 字符的函数。乍一看,这似乎很荒谬,但是当你不得不在一个项目中使用它 100 次时,你会意识到你极大地减少了代码的大小,并且使它更容易阅读。您实际上节省了 2200 行代码。这就是效率和最小的变化!

许诺你的功能

现代 JavaScript 使用 promises 来干净地处理事务,因为它让您可以选择使用相同的函数同步或异步运行代码,而不是使用回调函数,在回调函数中,您必须堆叠代码层来控制事务流。

这就是为什么所有的回调函数都必须转换成承诺,如果它们还没有的话。这对于最新版本的 web3 来说不是问题,但是对于 web3.js 0.20 和许多其他必须使用回调的库来说,最好只是将它们转换成更简单代码的承诺。

有一个名为bluebird的库可以通过将一个对象中的所有函数转换成承诺来帮助你。使用以下内容安装它:

npm i -S bluebird

使用以下代码将其导入 React 项目:

import * as Promise from 'bluebird'

使用以下函数将您的对象方法转换成Async:

web3Instance = Promise.promisifyAll(web3Instance)

然后,你可以使用Async函数代替回调函数,就像这样:

web3Instance.eth.getAccountsAsync()

// Instead of 
web3Instance.eth.getAccounts()

这只是一个例子:这个想法是,您将关键字Async添加到您的回调函数中,以使用 promisified 版本,而不必做任何其他事情。

用 web3.js 监听事件

事件对于管理分散应用程序的流程至关重要,因为您可以实时更新智能合约中发生的变化,并据此采取行动。您甚至可以创建 Node.js 应用程序来通知您关键的更改。

例如,假设您运行一个银行智能合约,并且当智能合约中的资金达到临界 10 ETH 下限时,会激活一个事件:

contract Bank {
    event CriticalLow(uint256 contractBalance);
    ...
}

您希望在发生这种变化时得到通知,所以您在一个node.js实例上设置了一个简单的 web3.js dApp,当发生这种变化时,它会向您发送一封电子邮件:

// Node.js

function sendCriticalEmail() {
    // Sends an email when something critical happens to fix it ASAP
}

function listenToCriticalLow() {
    // Listen to critical events on real-time
}

这可能是一个监控系统,您自己设置了这个系统来管理数百万用户使用的 dApp,以便它尽可能长时间地保持运行。你可以争辩说在这样的场景中监听事件是必不可少的,那么你是如何做到的呢?这是基本结构:

function listenToCriticalLow() {
    const subscription = web3.eth.subscribe('CriticalLow', {
        address: <your-contract-address>
    }, (err, result) => {
        if(!err) sendCriticalEmail()
    })
}

当事件生成时,您的web3.eth.subscription函数将执行回调。这就是你在 web3 中收听事件的基本方式。现在,您知道如何在 dApp 的工作流程中使用它们进行关键操作。

构建您自己的神谕

Oracles 是外部应用程序,帮助您的智能合约从外部世界接收信息,以执行 Solidity 或 Vyper 内部可能无法执行的一些功能。它们的工作方式很简单:您创建一个中央服务器,在需要时调用智能合约的特定功能。

它们用于生成随机数,提供实时价格数据,以及显示来自网站的信息。如你所知,智能合约不能产生随机数,因为在区块链中不能有任何不确定性来避免意外情况。

在本节中,您将学习如何创建一个 Oracle 来为区块链上的游戏生成一个 1 到 100 之间的随机数。已经有 oracles 在做这些工作了,也就是 Oraclize,已经用了很长时间的 Solidity。

构建随机生成的 Oracle

神谕是一种智能契约从外部世界获取信息的方式。它们是集中式服务器、外部区块链和 API 之间的桥梁,智能合约在以太坊上运行。从本质上来说,它们是一种服务,从普通智能合约无法到达的地方为您提供重要信息,它们通过为您的合约设置一个侦听 web3 事件的集中式服务器来工作。

首先,创建一个名为oracle的新项目,运行truffle init以便能够编译契约,用npm init -y设置 npm,并创建一个生成事件和处理Oracle.sol的智能契约:

pragma solidity 0.5.4;

contract Oracle {
    event GenerateRandom(uint256 sequence, uint256 timestamp);
    event ShowRandomNumber(uint256 sequence, uint256 number);
    uint256 public sequence = 0;

    function generateRandom() public {
        emit GenerateRandom(sequence, now);
        sequence += 1;
    }

    function __callback(uint256 _sequence, uint256 generatedNumber) public {
        emit ShowRandomNumber(_sequence, generatedNumber);
    }
}

这很基本:想法是当用户通过调用generateRandom()函数请求时,用随机生成的数字执行__callback()函数。我们将设置一个事件监听器,它将在正确的时间用正确的序列标识符给用户提供随机数。

记得更新migrations文件夹中的1_initial_migrations.js文件,告诉 Truffle 部署正确的合同:

var Oracle = artifacts.require("./Oracle.sol")

module.exports = function(deployer) {
    deployer.deploy(Oracle)
}

然后,通过在truffle-config.js中设置正确的配置,将其部署到ropsten。你已经知道怎么做了,因为我们在前几章中学习了如何在 Truffle 的配置文件中为 Ropsten 设置 Infura:

truffle deploy --network ropsten --reset

现在,我们可以创建 Node.js 应用程序,该应用程序侦听由我们的智能合约生成的事件,并使用下面的代码从一个oracle.js文件内部开始,使用随机生成的数字类型发出正确的请求:

const Web3 = require('web3')
const fs = require('fs')
const path = require('path')
const infura = 'wss://ropsten.infura.io/ws/v3/f7b2c280f3f440728c2b5458b41c663d'
let contractAddress
let contractInstance
let web3
let privateKey
let myAddress

我们已经导入了web3fspath作为库来与我们的契约交互。然后,我们定义了一个 websockets Infura URL,用它来连接 Ropsten,以便部署和与契约交互。使用wss而不是http很重要,因为这是接收事件的唯一方式。最后,我们添加了一些稍后需要的全局变量。

我们生成不带元掩码的事务的方法是创建自定义事务对象并用我们的私钥签名,我们可以基于位于.secret文件中的助记符使用以下函数生成该对象:

// To generate the private key and address needed to sign transactions
function generateAddressesFromSeed(seed) {
    let bip39 = require("bip39");
    let hdkey = require('ethereumjs-wallet/hdkey');
    let hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(seed));
    let wallet_hdpath = "m/44'/60'/0'/0/0";
    let wallet = hdwallet.derivePath(wallet_hdpath).getWallet();
    let address = '0x' + wallet.getAddress().toString("hex");
    let myPrivateKey = wallet.getPrivateKey().toString("hex");
    myAddress = address
    privateKey = '0x' + myPrivateKey
}

这相当复杂,尽管我们只需专注于安装和导入bip39ethereumjs-wallet库来生成用于签署交易的privateKey。我们可以安装以下依赖项:

npm i -S bip39 ethereumjs-wallet web3

然后,我们可以创建一个start函数来建立所需的契约,并开始监听正确的触发事件来调用__callback()函数:

// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')

    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })
}

首先,我们使用前面的generateAddressesFromSeed()函数读取 12 个单词的助记密码来生成我们的私钥和地址。然后,我们用WebsocketProvider为 Ropsten Infura URL 设置了一个新的 web3 实例,因为我们不能用HttpProvider监听事件。之后,我们通过从由 Truffle 生成的 JSON 文件中读取 ABI 数据来设置contractInstance,其中包括已部署契约的地址。

最后,我们使用contractInstance.events.GenerateRandom()函数为GenerateRandom事件设置了一个订阅,它将使用相应的序列调用callback()函数。让我们看看回调函数是什么样子的。记住,这个函数将运行我们的智能契约的__callback()来为用户提供一个随机生成的数字,因为我们不能直接生成具有可靠性的随机数:

// To generate random numbers between 1 and 100 and execute the __callback function from the smart contract
function callback(sequence) {
    const generatedNumber = Math.floor(Math.random() * 100 + 1)

    const encodedCallback = contractInstance.methods.__callback(sequence, generatedNumber).encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedCallback,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Callback transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}

该函数接收序列参数,将值映射到正确的 ID,以便用户可以识别哪个事件是适合他们的。首先,我们使用Math.random()生成一个 1 到 100 之间的随机数,通过一些计算使其适应我们想要的范围。然后,我们生成一个名为tx的事务对象,其中包含我们带有sequencegeneratedNumber的函数数据,以及一些基本参数,如gasfrom地址。最后,我们将该交易发送到我们的Oracle智能契约,首先用privateKey签名,然后用web3.eth.sendSignedTransaction发送。等矿工确认了,我们就看到console.log"Callback transaction confirmed!",或者万一出了问题就报错。

差不多就是这样!我们可以在底部添加start()函数初始化,开始监听事件。以下是完整的代码:

  1. 导入所需的库,并在文件开头设置将在项目中使用的变量:
const Web3 = require('web3')
const fs = require('fs')
const path = require('path')
const infura = 'wss://ropsten.infura.io/ws/v3/f7b2c280f3f440728c2b5458b41c663d'
let contractAddress
let contractInstance
let web3
let privateKey
let myAddress
  1. 创建generateAddressesFromSeed()函数,它为您提供对包含在给定种子中的帐户的访问:
// To generate the private key and address needed to sign transactions
function generateAddressesFromSeed(seed) {
    let bip39 = require("bip39");
    let hdkey = require('ethereumjs-wallet/hdkey');
    let hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(seed));
    let wallet_hdpath = "m/44'/60'/0'/0/0";
    let wallet = hdwallet.derivePath(wallet_hdpath).getWallet();
    let address = '0x' + wallet.getAddress().toString("hex");
    let myPrivateKey = wallet.getPrivateKey().toString("hex");
    myAddress = address
    privateKey = '0x' + myPrivateKey
}
  1. 创建start函数来设置 web3 监听器:

// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')

    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })
}
  1. 最后,创建回调函数,该函数执行智能契约中的__callback()函数。函数名以两个下划线开头,以避免调用现有函数,因为它是 oracle 专用的特殊函数:
// To generate random numbers between 1 and 100 and execute the __callback function from the smart contract
function callback(sequence) {
    const generatedNumber = Math.floor(Math.random() * 100 + 1)

    const encodedCallback = contractInstance.methods.__callback(sequence, generatedNumber).encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedCallback,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Callback transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}
  1. 记住,加载完所有内容后,通过运行文件末尾的start函数来启动 oracle:
start()
  1. 或者,我们可以添加一个函数来执行智能契约中的generateRandom()函数,以验证我们确实在接收另一个订阅的事件,如下所示:
// To send a transaction to run the generateRandom function
function generateRandom() {
    const encodedGenerateRandom = contractInstance.methods.generateRandom().encodeABI()
    const tx = {
        from: myAddress,
        gas: 6e6,
        gasPrice: 5,
        to: contractAddress,
        data: encodedGenerateRandom,
        chainId: 3
    }

    web3.eth.accounts.signTransaction(tx, privateKey).then(signed => {
        console.log('Generating transaction...')
        web3.eth.sendSignedTransaction(signed.rawTransaction)
            .on('receipt', result => {
                console.log('Generate random transaction confirmed!')
            })
            .catch(error => console.log(error))
    })
}
  1. 然后,更新start函数来监听我们使用generateRandom()函数创建的新事件:
// Setup web3 and start listening to events
function start() {
    const mnemonic = fs.readFileSync(".secret").toString().trim()
    generateAddressesFromSeed(mnemonic)

    // Note that we use the WebsocketProvider because the previous HttpProvider is outdated and doesn't allow subscriptions
    web3 = new Web3(new Web3.providers.WebsocketProvider(infura))
    const ABI = JSON.parse(fs.readFileSync(path.join(__dirname, 'build', 'contracts', 'Oracle.json')))
    contractAddress = ABI.networks['3'].address
    contractInstance = new web3.eth.Contract(ABI.abi, contractAddress)

    console.log('Listening to events...')
    // Listen to the generate random event for executing the __callback() function
    const subscription = contractInstance.events.GenerateRandom()
    subscription.on('data', newEvent => {
        callback(newEvent.returnValues.sequence)
    })

    // Listen to the ShowRandomNumber() event that gets emitted after the callback
 const subscription2 = contractInstance.events.ShowRandomNumber()
 subscription2.on('data', newEvent => {
 console.log('Received random number! Sequence:', newEvent.returnValues.sequence, 'Randomly generated number:', newEvent.returnValues.number)
 })
}

这样,您将能够看到契约实际上是如何从 Node.js oracle 接收您随机生成的数字,以确认它工作正常。您可以通过部署自己的 oracles 来尝试一下,这些 oracles 为智能合约提供外部数据,而这些数据是它们自己无法使用这种基于回调的机制和唯一标识符获得的。此外,您可以添加一些外部证据来验证数据来自正确的 oracle,尽管这超出了本指南的范围,因为它太过广泛,无法描述。

像往常一样,如果你想看到最新的变化并尝试工作版本,你可以在我的 GitHub(https://github.com/merlox/oracle)上获得完整的更新代码。如果您想了解我是如何设置的,请看一下 Truffle 配置文件。

改进您的开发工作流程

当谈到创建智能契约和分散应用程序时,一个常见的问题是,我们必须以尽可能最有效的方式工作,以创建最高质量的代码,这样我们就不会花费不必要的时间来修复一开始就不应该存在的问题。

以我个人的经验来看,我创建的最好的应用程序都是从事先详尽的计划中诞生的。这可能感觉没有必要,但是你做得越多,你就越意识到通过一个清晰的计划来描述你的想法的每一个元素,你可以节省多少时间。

你有没有在项目中经常遇到问题,比如 bug 或者困惑?那很可能是因为你没有做足够的规划。在这一节中,您将学习如何规划您的应用程序来建立易于理解的项目,以便您可以更有效地进行开发。

假设你想把自己的技能付诸实践,用真实的项目去了解更多以太坊技术。因此,您决定开发一个相对复杂的 dApp。你首先得到这个想法,然后你根据你认为它应该如何工作来详细说明你的应用程序的组件,你马上开始编码来快速完成它。

对于大多数项目来说,这是一种非常常见的方法,因为我们不想在项目上浪费时间——我们希望快速完成,所以我们立即开发代码。这对于小项目来说没问题,但是对于大项目来说,我们必须遵循如下准则:

  1. 详细描述你的想法:最重要的功能,客户的感受,以及它是关于什么的。
  2. 将它分成更小的部分:前端、后端和智能契约(如果有的话)。然后用你能理解的方式描述这些元素。
  3. 通过写下将被添加到这三个部分中的每一个的功能来更深入地研究。这些将是您的应用程序的主要功能。将它们写在一个没有主体的空文件中:只有带有参数和返回值的函数。
  4. 通过使用 NatSpec 文档描述它们在技术层面上应该做什么来记录这些函数,以便清楚地解释每个参数和返回值。
  5. 开始研究更小的独立函数。这些函数可以是返回一些变量的 getter 函数,或者是用于计算值的简单函数。
  6. 继续更复杂的功能,直到完成所有的功能。当你这样做的时候,写空的测试来检查这些功能的每一个方面。
  7. 通过从您之前设置的单元测试中编写单元测试来纠正项目,并添加一些更多的单元测试,重点关注如果不检查它们可能导致的潜在问题。

你的规划可能会有所不同:你刚刚读到的只是一个简单的指南,是我在试图理解一个成功项目背后的过程后想出来的。为了让您能够以更直观的方式了解它,下面是开发过程的一个示例:

当你创建每个功能时,留下描述下一步需要做什么的笔记,这样当你回来时,你就有了一个清晰简单的目标。例如,这是我最近在做的一个函数:

constructor(address _identityRegistryAddress, address _tokenAddress) public {
    require(_identityRegistryAddress != address(0), 'The identity registry address is required');
    require(_tokenAddress != address(0), 'You must setup the token rinkeby address');
    hydroToken = HydroTokenTestnetInterface(_tokenAddress);
    identityRegistry = IdentityRegistryInterface(_identityRegistryAddress);
    // TODO Uncomment this when the contract is completed
    /* oraclize_setProof(proofType_Ledger); */
}

著名的 atom.io 代码编辑器中已经安装了一个名为language-todo的扩展,它突出显示了这些类型的TODO注释,这样你就可以很容易地看到它们。您也可以使用搜索功能在整个项目中搜索这些内容。

此外,还有另一个扩展,允许你在一个面板中管理这些提醒。以下是软件包名称,这样您就可以根据需要安装它:

以下是在创建成功项目时改进工作流程的一些附加建议:

  • 在每个文件的顶部,创建一个需要在特定文件中完成的事情的列表,这样你就知道它什么时候完成了。
  • 使用已经制作好的工具来部署您的契约,并编写测试来有效地验证功能。例如,松露、加纳切和 Remix 是测试的必需品,同时提高你的效率。
  • 给每件事设定一个时间限制;尽可能精确,因为项目往往会用尽你给它们的时间。严格保持你的思想集中。
  • 确定在不同的时限内可以做什么。例如,在 1 周内,你可以用两个核心特性创建你的想法的基本版本,在 1 天内,你可以完成 100 个功能中的 5 个,而在 1 个月内,你可以完成基本代码。这个想法是想象什么是现实的估计,有足够的时间实现你的想法。写下需要在 1 天、1 周和 1 个月内完成的事情。
  • 把自己放在你觉得舒服的地方。通常,当你身体感觉良好,思想放松时,伟大的想法就会出现。例如,一个淋浴,适当的温度和持续不断的水流可以放松你的整个身体,这是检验你的假设和探索新想法的最佳场所之一。
  • 永远记住为你的项目创建一个 Git 存储库,即使你认为你不会去做它,因为你经常需要一段代码来做你几年前做过的特定的事情,现在你需要为一个新的项目记住。把你的代码放在 GitHub 上也有助于看到你作为开发者的进步,并建立一个稳固的在线形象。

想出好主意可以有自己的一章。创造力的问题在于,只有当你打破常规时,你才能获得它,因为你不能指望你的大脑在同样的日常经历基础上创造出新的联想。去新的地方旅行,探索奇怪的爱好,对与你所熟悉的完全不同的不同主题真正感兴趣,即使它们一开始看起来很无聊。

摘要

你刚刚完成了这本书最重要的一章,因为我们谈到了优化和效率,这两样东西对你从事的每个项目都是必不可少的。我们从构建更好的 React 应用程序开始,在这里您学习了使用这个强大的框架优化创建前端应用程序的方式,以及正确构建组件的有趣技巧。

然后,您学习了使用 NGINX 创建集中式 Node.js 应用程序,您可以将其用于智能合同还不够的混合项目,包括从想法到代码到在具有 HTTPS 证书的 VPS 服务器上部署的所有步骤。在那之后,您探索了几个 web3.js 改进,以创建更强大的前端,包括事件订阅、助手函数和可以更好控制的承诺。

当谈到创建有能力的智能合约时,您已经经历了最有趣的主题之一:Oracle,因为它们为智能合约提供了对特定应用程序来说不可或缺的有价值的外部信息。最后,您发现了 14 个技巧来改进您对创建项目的思考方式,以便您能够在交付更高质量的代码时变得熟练。

在下一章中,您将从零开始构建一个非常有趣的分散式交易所的主题。这是一个令人兴奋的机会,你会喜欢抓住它的!


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组