Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QCon 演讲:为什么前端工程师更应该掌握区块链 DApp 开发(下) #101

Open
cssmagic opened this Issue Nov 19, 2018 · 6 comments

Comments

Projects
None yet
3 participants
@cssmagic
Copy link
Owner

cssmagic commented Nov 19, 2018

摘要

魔法哥在 “QCon 全球软件开发大会” 2018 上海站的演讲广受好评。为让这个演讲发挥更大的价值,本文尝试以图文配合的方式还原现场体验。

在实战环节中,我们将和小明一起做出自己的第一款 DApp。实战案例清晰简洁,覆盖完整的开发流程,带你轻松跨入区块链应用开发的大门。


(本文是演讲的下半部分,建议您先读完 上半部分 再阅读本文。)

slide-p42

在做好准备工作之后,小明终于开始动手了!

slide-p43

我们回顾一下传统 Web App 和 DApp 的架构图。

在开发传统 Web 应用时,我们一般会先确定后端接口。同理,在开发 DApp 时,我们通常也会先把合约端的接口准备好。

首先,我们需要了解合约代码的编写模式。

slide-p44

讲到这里,终于可以给大家看前面那段 “被打码” 的代码了。这是一段最小化的星云链智能合约代码。我们来看一看它有哪些要素:

slide-p45

  • CommonJS 模块(必须)

    先看一眼最后一行,前端同学们肯定秒懂。整个合约其实就是一个 CommonJS 模块。

  • 导出一个类(必须)

    这个模块导出的是一个类,这个类正是合约的主体。

  • 构造器方法(可选)

    既然合约主体是一个类,而类本身并不能直接运行,那么这就意味着合约每次被调用时,这个类都会被实例化。类可以有(也可以没有)一个构造器方法,这个方法在每次实例化时执行(这是类的基本行为,大家应该都很熟悉了)。构造器方法并不是必需的,我们可以根据实际需要来编写它。

  • 初始化方法(必须)

    这个类需要有一个 init() 方法,这个方法只会在合约部署成功后被自动执行一次,以后就再也无法被调用了。因此,这个初始化方法通常用来完成整个 “应用” 级别的初始化工作。它是必需的,哪怕你的应用不需要初始化,也需要提供一个空的 init() 方法。

  • 私有方法(可选)

    这个类可以有一些私有方法,私有方法可以在类的内部被调用,但它不会暴露成为合约的公开接口。私有方法的特征是下划线开头,比如上图代码中的 _helper()。我们可以根据实际需要定义私有方法,便于合约逻辑的实现,但它不是必需的。

    这个 “下划线” 的命名约定并不是 JS 语言本身的特性,而是星云链智能合约运行环境的设计。这个设计也很符合前端开发者的命名习惯。

  • 公开接口(必须)

    除去上述特定名称的方法和私有方法以外,剩下的所有方法都会作为合约的公开接口暴露出来,以供客户端调用。比如上图代码中的 method()

只要满足以上条件,就是一个合法的智能合约了。也就是说,你可以按照自己的开发习惯来组织合约代码,只要满足上述条件就可以了。

其次,另一项重要的基础知识是 “合约存储区”。

slide-p46

前面提到过,每个合约都有自己的独立存储区,那么智能合约自然也提供了操作存储区的 API。星云链合约存储区提供了多种操作方式,本文为方便讲解,只介绍其中最基础的用法。

在智能合约的运行环境中,有一个 LocalContractStorage 全局对象,它提供了三个最基本的 API(参见上图)。

前端工程师看到这里应该会会心一笑,因为这跟浏览器里的本地存储 API 几乎一致。我们在浏览器端要实现持久化存储,最常用的就是本地存储(localStorage);而在智能合约中,实现持久化存储也是通过类似的 API 和类似的思路来实现的。

👉 值得一提的是,.set() 方法接受的 value 参数可以是复合结构,比如数组或对象等。也就是说,相对于浏览器端的 localStorage.setItem() API,合约存储区的 .set() 方法更方便,不需要手动处理 value 的序列化和反序列化问题。

接下来,我们就要开始实现合约接口了。

slide-p47

既然智能合约要提供与传统后端功能相当的接口,那不妨先来看盾,传统的后端接口是怎样的。

slide-p48

在小明的网站中,后端至少要提供两个接口,分别对应以下两项功能:

  • 获取预言列表

    /api/getAllItems

    这是一个 GET 接口。不需要参数。返回所有预言组成的数组,每个数组成员都是一个对象,用于记录某条预言的基本数据——内容(content)和发布时间(published_at)。

  • 发布预言

    /api/create

    这显然是一个 POST 接口。在向它提交数据时,需要提供一个 content 参数,以便传入预言内容;并不需要提供发布时间,因为服务端会记录时间。

    在设计这种 “新增” 操作的接口时,我们通常会把发布成功的这一条数据返回,以便客户端立即把新内容渲染出来。

如何在合约端实现这两个接口的功能?别着急,慢慢来。小明先把合约的大致结构写好:

slide-p49

这个合约似乎并不需要在构造器里做什么事儿,留空就行。

然后需要做一些初始化工作。小明需要做的就是在合约存储区里建一个 key,取名 'items',用来保存所有预言。在初始化阶段,显然啥预言也没有,就存一个空数组进去。

slide-p50

随后就要进入重点了,小明开始写两个合约接口。

slide-p51

第一个接口是 “获取预言列表”,即 getAllItems() 方法。

slide-p52

实现这个方法其实非常简单——从存储区里读出 'items' 这个 key 的值,返回就行了。写出的代码也是一目了然。

👉 请留意:这个方法里只有读操作,没有写操作。

第二个接口是 “发布预言”,即 create() 方法。

slide-p53

这个接口稍稍复杂一些,我们一步一步来。

slide-p54

我们先把这条新预言的数据准备好,存到 newItem 变量中备用。

slide-p55

接下来更新存储区:我们先取出所有预言的列表,然后把新预言 push 进去,最后把预言列表写回存储区。

slide-p56

在这个方法的最后,我们把这一条新预言的数据作为函数返回值,以便客户端在拿到调用结果后立即把新预言渲染出来——这与传统后端接口 /api/create 的设计保持一致。

👉 请留意:这个方法需要更新存储区,也就是说,有写操作。

好,两个接口已经全部实现,合约代码就写好了。

slide-p57

合约需要部署到链上才能真正发挥作用。这里请出 “星云 Web 钱包” 来完成合约的部署和测试。

部署合约的操作比较简单,这里就不赘述了。

slide-p58

部署成功之后,还可以测试一下合约行为是否符合预期。(如果发现合约代码有 bug,则需要重新部署。)

slide-p59

准备好合约端之后,小明接下来开始实现客户端。

网页形态的客户端开发大家都很熟悉了,这里不赘述,只着重讲一下 DApp 客户端独有的行为——合约调用。

slide-p60

调用合约有两种情况,或者说,有两种方式:

  • 不需要写数据时

    有些合约接口是不需要向存储区写入数据的,比如仅仅读数据或纯计算。此类调用可以直接调用,并可立即得到结果。

  • 需要写数据时

    有些合约接口是需要向存储区写入数据的,即需要 “上链”。此类接口需要通过发起交易来调用,具体过程在 demo 环节已经展示过了。

在实现这两种类型的合约调用时,需要用到客户端 SDK。它的作用是帮助我们与链交互,我们不用操心区块链网络的具体地址、接口和参数等细节,直接使用 SDK 提供的接口就可以完成调用合约、转账、查询网络状态等操作了。

slide-p61

星云链官方的客户端 SDK 功能完备,但设计风格偏底层,在实际开发中略显繁琐。因此这里推荐一款第三方的 SDK——Nasa.js。

Nasa.js 的安装方式很简单,直接用 npm 就可以。

加载方式也很简单,在页面中引入 nasa.js 文件,随后就可以使用它提供的各种 API 了。

slide-p62

Nasa.js 提供了近 30 个 API,在日常开发中,最常用的是以下三个:

  • Nasa.query()
  • Nasa.call()
  • Nasa.getTxResult()

这三者都与合约调用相关。第一项用于 “不需要写数据” 类型的合约调用,后两项涉及 “需要写数据” 的情况。具体用法会在下面的代码中详细讲解。

小明开始写网页端代码了!他先实现 “展示预言列表” 这个功能。

slide-p63

第一行很简单,就是把合约地址保存好。

接下来,小明需要调用合约的 getAllItems 接口,以获得所有的预言数据,并在页面中渲染出来。这个接口并不需要写数据,因此使用 Nasa.query() API 来调用它。

这个 API 接受三个参数,分别是合约地址、合约接口的函数名、传给函数的参数。

👉 提示:关于第三个参数 “传给函数的参数”,如果合约接口不需要参数,这里就传一个空数组进去;如果合约接口需要参数,就把若干个参数放在一个数组里传进去。由于合约的 getAllItems 接口不需要参数,小明传了空数组进去。

通过 Nasa.query() 发生的合约调用,可以很快得到调用结果,这个过程就好像是在调用一个 Ajax 接口。Nasa.query() 的返回值是一个 Promise,Promise 包裹的值就是合约调用结果——这个设计和常见的 Ajax 库也是十分类似的。

slide-p64

小明在 Promise 的 .then() 回调中拿到合约接口的调用结果。调用结果有一个 execResult 字段,用来保存合约接口函数的返回值。小明通过它可以得到一个包含所有预言数据的数组,然后在网页上就可以渲染出预言列表了。

小明接着实现第二个功能——“发布预言”。

发布页面有一个表单,小明需要给表单的提交事件绑定事件处理函数(代码略),并在这个事件处理函数中实现发布预言的操作(以下代码均写在事件处理函数中)。

slide-p65

前两行是准备工作,把合约地址和已经输入的文字保存好。

接下来,小明需要调用合约的 create 接口,以完成新预言的发布。这个接口需要通过发起交易来完成上链,于是小明使用 Nasa.call() API 来调用它。

这个 API 同样接受三个参数,参数含义与 Nasa.query() 的三个参数完全一致。小明依次传入合适的值。

这个 API 会唤起钱包插件,引导用户完成交易。

slide-p66

这个 API 也返回一个 Promise,但 Promise 包裹的值与 Nasa.query() 不同。因为通过交易来调用合约,无法直接得到调用结果,只有当这一笔交易被矿工处理完成并打包上链,我们才能得到这次合约调用的结果。因此,这个 Promise 只能给我们一个交易流水号,我们随后可以拿着这个流水号向链查询交易的状态和调用结果。

当钱包插件把交易发出后,小明就可以在 .then() 回调中拿到交易流水号(payId)。接下来就轮到 Nasa.getTxResult() 这个 API 出场了。

这个 API 接受一个参数,就是交易流水号;返回一个 Promise,Promise 包裹的值就是本次交易的状态和本次调用的结果。(查询交易结果是一个轮询的过程,不过我们不用操心这些细节,Nasa.js 会自动完成这个过程,我们只需要关心这个 Promise 就可以了。)

slide-p67

.then() 回调中,小明把交易流水号传给 Nasa.getTxResult()。由于这个 API 也返回 Promise,小明可以把它 return 出去,让这个 Promise 链条继续往下走。

当查询发现交易被矿工处理完成之后,可以在下一个 .then() 回调中拿到本次交易的状态和本次调用的结果。同样还是在 execResult 字段中,我们可以获取合约接口函数的返回值。

slide-p68

当然保险起见,我们最好先判断一下交易是不是成功了。如果status 字段的值为 1,则表示交易已被正常处理。

小明在这里拿到合约接口函数 create 的返回值(也就是发布成功的这条新预言的数据),把它渲染并添加到页面中就可以了。

👉 提示:以上代码仅仅演示了最核心的逻辑。对于一个体验良好的 DApp 来说,还需要处理好各种交互反馈和错误提示。

至此,整个 DApp 从合约端到客户端,就全部开发完成了。小明把这两端的功能跑通,网站顺利上线。

slide-p69

👉 这里顺便插一句,对 DApp 来说,甚至可以没有 “上线” 这个环节。因为整个应用的业务逻辑已经以智能合约的形式部署上链了,如果小明只打算自己用的话,客户端代码保存为 HTML 页面,在本地打开照样可以用。

网站看起来还是那个网站,不过它的本质已经升级为基于区块链的 DApp 了。

万事具备,小明静静等待下一届世界杯的到来。

slide-p70

2018 年世界杯,小明又一次正确预测出了冠军得主。

slide-p71

小明终于成功地在女朋友面前秀了一把。

slide-p72

从此他们幸福地生活在一起……(全剧终)

slide-p73

故事到这里就讲完了。不过在听故事的过程中,大家头脑中可能会冒出一些问题。

首先,这个实战案例是不是太简单了?

slide-p74

时间关系,我们无法在短短一次演讲中实现更多的功能。不过,互联网产品都是迭代式开发的嘛,小明确实也打算升级一下这个 DApp。他设想的新功能有:

  • 支持多用户:这么好的应用,只有自己在用太可惜了。小明打算让它支持多用户。

  • 可点赞:有了多用户,就可以加入一些社交元素。比如当你看到一条有意思的预言,可以给它点赞。

  • 可打赏:不止是点赞,你甚至还可以给预言的作者打赏。

先说 “点赞”,这个功能相对比较好实现,合约端新增一个点赞接口,再给预言数据增加一个点赞数的字段,差不多就可以了。

再看 “多用户” 和 “打赏” 功能,乍一看很复杂,但其实在区块链上非常容易实现。

slide-p75

在开发传统 Web App 的时候,如果要识别不同用户,往往需要自己做一套账号系统,或者对接第三方社交账号。而在区块链上就不用这么麻烦,因为每个在链上的用户都有自己独一无二的身份标识——“地址”。DApp 通过地址就可以区分每位用户。

此外,在传统应用中如果要实现打赏功能,就需要接入支付平台,比如银联、支付宝或微信支付等。但这对个人开发者来说是极为困难的。别急,这里又体现出 DApp 的优势了——区块链的老本行就是记账,每条公链通常都有自己的原生货币系统,DApp 直接使用链的支付功能就可以了。

区块链 “原生的账号系统” 和 “原生的支付系统”,堪称 DApp 的两大天生神力!

slide-p76

这样一个 DApp 最终确实被做了出来,叫作 “我是预言帝”。这实际上也是魔法哥自己学习开发 DApp 的第一款作品。

另一个经常被问到的问题是:大家经常提到区块链 “不可篡改”,到底是指什么?小明的女朋友凭什么相信他?…………

……

……


完整文章已收录到 “CSS魔法” 微信公众号,微信扫码即可阅读全文:

weixin-qrcode


相关阅读


© Creative Commons BY-NC-ND 4.0   |   我要订阅   |   我要打赏

@icksky

This comment has been minimized.

Copy link

icksky commented Jan 14, 2019

感谢, 受教了, 有个疑问想请教一下.
DAPP 有办法应用到普通用户身上么? 因为目前看来 一定要安装星云链的扩展程序, 还要有自己的钱包.

@cssmagic

This comment has been minimized.

Copy link
Owner Author

cssmagic commented Jan 15, 2019

DApp 确实有这样的门槛存在。

有一些游戏的做法是在自己的后端帮用户管理钱包、帮用户与链交互。这也是一种解决方案。

@icksky

This comment has been minimized.

Copy link

icksky commented Jan 15, 2019

如果由后端统一处理, 创建钱包的动作也是后端来做么? 所以引导用户氪金之前, 钱包内余额是0, 这之前所有产生的交易所用到的 Gas 都要游戏所有者进行垫付吧?
还是其实只有一个钱包, 所有用户用的都是这个钱包产生交易..... 这样好像用区块链没什么意义了呀.


另外, 关于不可篡改这个优点. 如果小明建立合约时设置了一个 update 方法, 不就可以修改之前的预言了?

刚刚接触这个, 一头雾水 问题比较多 还请前辈赐教

@cssmagic

This comment has been minimized.

Copy link
Owner Author

cssmagic commented Jan 16, 2019

如果由后端统一处理, 创建钱包的动作也是后端来做么?……这样好像用区块链没什么意义了呀.

这确实很考验游戏的设计者,在便捷性和公信力之间找平衡。

关于不可篡改这个优点……

这个在文章末尾专门有讲,可以扫码阅读公众号里的完整版。


你问的都是好问题,欢迎多交流!

@icksky

This comment has been minimized.

Copy link

icksky commented Jan 18, 2019

已关注. 感谢科普.

@hkongm

This comment has been minimized.

Copy link

hkongm commented Jan 21, 2019

再次手动点赞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.