摘要
魔法哥在 “QCon 全球软件开发大会” 2018 上海站的演讲广受好评。在实战环节中,我们将做出自己的第一款 DApp。实战案例清晰简洁,覆盖完整的开发流程,带你轻松跨入区块链应用开发的大门。
(本文是演讲的下半部分,建议您先读完 上半部分 再阅读本文。)
在做好准备工作之后,小明终于开始动手了!
我们回顾一下传统 Web App 和 DApp 的架构图。
在开发传统 Web 应用时,我们一般会先确定后端接口。同理,在开发 DApp 时,我们通常也会先把合约端的接口准备好。
首先,我们需要了解合约代码的编写模式。
讲到这里,终于可以给大家看前面那段 “被打码” 的代码了。这是一段最小化的星云链智能合约代码。我们来看一看它有哪些要素:
CommonJS 模块(必须)
先看一眼最后一行,前端同学们肯定秒懂。整个合约其实就是一个 CommonJS 模块。
导出一个类(必须)
这个模块导出的是一个类,这个类正是合约的主体。
构造器方法(可选)
既然合约主体是一个类,而类本身并不能直接运行,那么这就意味着合约每次被调用时,这个类都会被实例化。类可以有(也可以没有)一个构造器方法,这个方法在每次实例化时执行(这是类的基本行为,大家应该都很熟悉了)。构造器方法并不是必需的,我们可以根据实际需要来编写它。
初始化方法(必须)
这个类需要有一个 init()
方法,这个方法只会在合约部署成功后被自动执行一次,以后就再也无法被调用了。因此,这个初始化方法通常用来完成整个 “应用” 级别的初始化工作。它是必需的,哪怕你的应用不需要初始化,也需要提供一个空的 init()
方法。
私有方法(可选)
这个类可以有一些私有方法,私有方法可以在类的内部被调用,但它不会暴露成为合约的公开接口。私有方法的特征是下划线开头,比如上图代码中的 _helper()
。我们可以根据实际需要定义私有方法,便于合约逻辑的实现,但它不是必需的。
这个 “下划线” 的命名约定并不是 JS 语言本身的特性,而是星云链智能合约运行环境的设计。这个设计也很符合前端开发者的命名习惯。
公开接口(必须)
除去上述特定名称的方法和私有方法以外,剩下的所有方法都会作为合约的公开接口暴露出来,以供客户端调用。比如上图代码中的 method()
。
只要满足以上条件,就是一个合法的智能合约了。也就是说,你可以按照自己的开发习惯来组织合约代码,只要满足上述条件就可以了。
其次,另一项重要的基础知识是 “合约存储区”。
前面提到过,每个合约都有自己的独立存储区,那么智能合约自然也提供了操作存储区的 API。星云链合约存储区提供了多种操作方式,本文为方便讲解,只介绍其中最基础的用法。
在智能合约的运行环境中,有一个 LocalContractStorage
全局对象,它提供了三个最基本的 API(参见上图)。
前端工程师看到这里应该会会心一笑,因为这跟浏览器里的本地存储 API 几乎一致。我们在浏览器端要实现持久化存储,最常用的就是本地存储(localStorage);而在智能合约中,实现持久化存储也是通过类似的 API 和类似的思路来实现的。
👉 值得一提的是,
.set()
方法接受的value
参数可以是复合结构,比如数组或对象等。也就是说,相对于浏览器端的localStorage.setItem()
API,合约存储区的.set()
方法更方便,不需要手动处理value
的序列化和反序列化问题。
接下来,我们就要开始实现合约接口了。
既然智能合约要提供与传统后端功能相当的接口,那不妨先来看盾,传统的后端接口是怎样的。
在小明的网站中,后端至少要提供两个接口,分别对应以下两项功能:
获取预言列表
/api/getAllItems
这是一个 GET 接口。不需要参数。返回所有预言组成的数组,每个数组成员都是一个对象,用于记录某条预言的基本数据——内容(content
)和发布时间(published_at
)。
发布预言
/api/create
这显然是一个 POST 接口。在向它提交数据时,需要提供一个 content
参数,以便传入预言内容;并不需要提供发布时间,因为服务端会记录时间。
在设计这种 “新增” 操作的接口时,我们通常会把发布成功的这一条数据返回,以便客户端立即把新内容渲染出来。
如何在合约端实现这两个接口的功能?别着急,慢慢来。小明先把合约的大致结构写好:
这个合约似乎并不需要在构造器里做什么事儿,留空就行。
然后需要做一些初始化工作。小明需要做的就是在合约存储区里建一个 key,取名 'items'
,用来保存所有预言。在初始化阶段,显然啥预言也没有,就存一个空数组进去。
随后就要进入重点了,小明开始写两个合约接口。
第一个接口是 “获取预言列表”,即 getAllItems()
方法。
实现这个方法其实非常简单——从存储区里读出 'items'
这个 key 的值,返回就行了。写出的代码也是一目了然。
👉 请留意:这个方法里只有读操作,没有写操作。
第二个接口是 “发布预言”,即 create()
方法。
这个接口稍稍复杂一些,我们一步一步来。
我们先把这条新预言的数据准备好,存到 newItem
变量中备用。
接下来更新存储区:我们先取出所有预言的列表,然后把新预言 push 进去,最后把预言列表写回存储区。
在这个方法的最后,我们把这一条新预言的数据作为函数返回值,以便客户端在拿到调用结果后立即把新预言渲染出来——这与传统后端接口 /api/create
的设计保持一致。
👉 请留意:这个方法需要更新存储区,也就是说,有写操作。
好,两个接口已经全部实现,合约代码就写好了。
合约需要部署到链上才能真正发挥作用。这里请出 “星云 Web 钱包” 来完成合约的部署和测试。
部署合约的操作比较简单,这里就不赘述了。
部署成功之后,还可以测试一下合约行为是否符合预期。(如果发现合约代码有 bug,则需要重新部署。)
准备好合约端之后,小明接下来开始实现客户端。
网页形态的客户端开发大家都很熟悉了,这里不赘述,只着重讲一下 DApp 客户端独有的行为——合约调用。
调用合约有两种情况,或者说,有两种方式:
不需要写数据时
有些合约接口是不需要向存储区写入数据的,比如仅仅读数据或纯计算。此类调用可以直接调用,并可立即得到结果。
需要写数据时
有些合约接口是需要向存储区写入数据的,即需要 “上链”。此类接口需要通过发起交易来调用,具体过程在 demo 环节已经展示过了。
在实现这两种类型的合约调用时,需要用到客户端 SDK。它的作用是帮助我们与链交互,我们不用操心区块链网络的具体地址、接口和参数等细节,直接使用 SDK 提供的接口就可以完成调用合约、转账、查询网络状态等操作了。
星云链官方的客户端 SDK 功能完备,但设计风格偏底层,在实际开发中略显繁琐。因此这里推荐一款第三方的 SDK——Nasa.js。
Nasa.js 的安装方式很简单,直接用 npm 就可以。
加载方式也很简单,在页面中引入 nasa.js
文件,随后就可以使用它提供的各种 API 了。
Nasa.js 提供了近 30 个 API,在日常开发中,最常用的是以下三个:
Nasa.query()
Nasa.call()
Nasa.getTxResult()
这三者都与合约调用相关。第一项用于 “不需要写数据” 类型的合约调用,后两项涉及 “需要写数据” 的情况。具体用法会在下面的代码中详细讲解。
小明开始写网页端代码了!他先实现 “展示预言列表” 这个功能。
第一行很简单,就是把合约地址保存好。
接下来,小明需要调用合约的 getAllItems
接口,以获得所有的预言数据,并在页面中渲染出来。这个接口并不需要写数据,因此使用 Nasa.query()
API 来调用它。
这个 API 接受三个参数,分别是合约地址、合约接口的函数名、传给函数的参数。
👉 提示:关于第三个参数 “传给函数的参数”,如果合约接口不需要参数,这里就传一个空数组进去;如果合约接口需要参数,就把若干个参数放在一个数组里传进去。由于合约的
getAllItems
接口不需要参数,小明传了空数组进去。
通过 Nasa.query()
发生的合约调用,可以很快得到调用结果,这个过程就好像是在调用一个 Ajax 接口。Nasa.query()
的返回值是一个 Promise,Promise 包裹的值就是合约调用结果——这个设计和常见的 Ajax 库也是十分类似的。
小明在 Promise 的 .then()
回调中拿到合约接口的调用结果。调用结果有一个 execResult
字段,用来保存合约接口函数的返回值。小明通过它可以得到一个包含所有预言数据的数组,然后在网页上就可以渲染出预言列表了。
小明接着实现第二个功能——“发布预言”。
发布页面有一个表单,小明需要给表单的提交事件绑定事件处理函数(代码略),并在这个事件处理函数中实现发布预言的操作(以下代码均写在事件处理函数中)。
前两行是准备工作,把合约地址和已经输入的文字保存好。
接下来,小明需要调用合约的 create
接口,以完成新预言的发布。这个接口需要通过发起交易来完成上链,于是小明使用 Nasa.call()
API 来调用它。
这个 API 同样接受三个参数,参数含义与 Nasa.query()
的三个参数完全一致。小明依次传入合适的值。
这个 API 会唤起钱包插件,引导用户完成交易。
这个 API 也返回一个 Promise,但 Promise 包裹的值与 Nasa.query()
不同。因为通过交易来调用合约,无法直接得到调用结果,只有当这一笔交易被矿工处理完成并打包上链,我们才能得到这次合约调用的结果。因此,这个 Promise 只能给我们一个交易流水号,我们随后可以拿着这个流水号向链查询交易的状态和调用结果。
当钱包插件把交易发出后,小明就可以在 .then()
回调中拿到交易流水号(payId
)。接下来就轮到 Nasa.getTxResult()
这个 API 出场了。
这个 API 接受一个参数,就是交易流水号;返回一个 Promise,Promise 包裹的值就是本次交易的状态和本次调用的结果。(查询交易结果是一个轮询的过程,不过我们不用操心这些细节,Nasa.js 会自动完成这个过程,我们只需要关心这个 Promise 就可以了。)
在 .then()
回调中,小明把交易流水号传给 Nasa.getTxResult()
。由于这个 API 也返回 Promise,小明可以把它 return 出去,让这个 Promise 链条继续往下走。
当查询发现交易被矿工处理完成之后,可以在下一个 .then()
回调中拿到本次交易的状态和本次调用的结果。同样还是在 execResult
字段中,我们可以获取合约接口函数的返回值。
当然保险起见,我们最好先判断一下交易是不是成功了。如果status
字段的值为 1
,则表示交易已被正常处理。
小明在这里拿到合约接口函数 create
的返回值(也就是发布成功的这条新预言的数据),把它渲染并添加到页面中就可以了。
👉 提示:以上代码仅仅演示了最核心的逻辑。对于一个体验良好的 DApp 来说,还需要处理好各种交互反馈和错误提示。
至此,整个 DApp 从合约端到客户端,就全部开发完成了。小明把这两端的功能跑通,网站顺利上线。
👉 这里顺便插一句,对 DApp 来说,甚至可以没有 “上线” 这个环节。因为整个应用的业务逻辑已经以智能合约的形式部署上链了,如果小明只打算自己用的话,客户端代码保存为 HTML 页面,在本地打开照样可以用。
网站看起来还是那个网站,不过它的本质已经升级为基于区块链的 DApp 了。
万事具备,小明静静等待下一届世界杯的到来。
2018 年世界杯,小明又一次正确预测出了冠军得主。
小明终于成功地在女朋友面前秀了一把。
从此他们幸福地生活在一起……(全剧终)
故事到这里就讲完了。不过在听故事的过程中,大家头脑中可能会冒出一些问题。
首先,这个实战案例是不是太简单了?
时间关系,我们无法在短短一次演讲中实现更多的功能。不过,互联网产品都是迭代式开发的嘛,小明确实也打算升级一下这个 DApp。他设想的新功能有:
支持多用户:这么好的应用,只有自己在用太可惜了。小明打算让它支持多用户。
可点赞:有了多用户,就可以加入一些社交元素。比如当你看到一条有意思的预言,可以给它点赞。
可打赏:不止是点赞,你甚至还可以给预言的作者打赏。
先说 “点赞”,这个功能相对比较好实现,合约端新增一个点赞接口,再给预言数据增加一个点赞数的字段,差不多就可以了。
再看 “多用户” 和 “打赏” 功能,乍一看很复杂,但其实在区块链上非常容易实现。
在开发传统 Web App 的时候,如果要识别不同用户,往往需要自己做一套账号系统,或者对接第三方社交账号。而在区块链上就不用这么麻烦,因为每个在链上的用户都有自己独一无二的身份标识——“地址”。DApp 通过地址就可以区分每位用户。
此外,在传统应用中如果要实现打赏功能,就需要接入支付平台,比如银联、支付宝或微信支付等。但这对个人开发者来说是极为困难的。别急,这里又体现出 DApp 的优势了——区块链的老本行就是记账,每条公链通常都有自己的原生货币系统,DApp 直接使用链的支付功能就可以了。
区块链 “原生的账号系统” 和 “原生的支付系统”,堪称 DApp 的两大天生神力!
这样一个 DApp 最终确实被做了出来,叫作 “我是预言帝”。这实际上也是魔法哥自己学习开发 DApp 的第一款作品。
另一个经常被问到的问题是:大家经常提到区块链 “不可篡改”,到底是指什么?小明的女朋友凭什么相信他?
在看完上面的实战案例之后,大家很可能会产生一个疑惑:我可以通过调用合约来更新合约存储区,这算不算 “篡改” 呢?
简单解释一下,区块链的 “不可篡改” 特性,主要有以下两个层面来构成:
不可撤消
区块链的精髓之一在于链式的数据结构,链式结构保证了新区块一旦追加到链上,就再也无法修改或撤消——这是因为链上的所有区块环环相扣,如果有节点试图修改其中一个区块,后续的区块就无法接上,这个修改会立刻被其它节点发现并判为非法。
因此,一笔交易一旦上链,就成为历史的一部分,永久地铭刻在那里。
公开透明
对于公链来说,链上的所有数据都是公开可查的。各条公链通常都会提供一种叫 “区块浏览器” 的工具,通过它可以方便地查到所有区块的内容、每笔交易的详情、任意地址的行为记录,甚至还有智能合约的源代码。
这意味着区块链上的每一次操作都是公开透明的,智能合约的运行逻辑和数据记录也都是公开透明的,没有暗箱操作的可能性。
说到这里,我们再来回顾一下 “修改合约存储区” 的过程:调用合约来更新存储区,本质上是一次交易,需要打包上链才能生效。因此,“修改合约存储区” 这个操作并不是直接修改已经在链上的历史记录,而是通过新记录在老记录上打补丁,我们在任一时刻得到的数据其实是所有修改记录叠加的结果。
因此,对于 DApp 来说,“不可篡改” 并不是说我们无法修改合约的存储区,而是说任何人都无法 “偷偷摸摸地修改”,因为每一次的修改记录都会明明白白地记录在链上。
讲完这个原理,我们可以把故事的结尾再补充一下。如果小明的女朋友想验证这个 DApp 上展示的预言是否真实可靠,她还需要做这两件事情:
Review 合约代码:确认合约的逻辑是否正常。
核对合约的调用记录:确认预言是怎么提交到合约的。
第三个问题,开发者特别关心 DApp 的应用场景。目前已经明朗的应用场景有以下一些:
存证:利用区块链的不可篡改特性来实现存证的需求。比如小明的预言 DApp 就可以归为这一类。
数字资产:加密猫很好地向大众普及了 “数字资产” 的概念,区块链和智能合约可以对数字资产进行确权。
游戏:游戏中虚拟货币和道具的概念与区块链天然契合,目前最火的 DApp 也几乎都是从这个领域里涌现出来的。
实际上区块链应用还处在早期,更多的落地场景还有待我们去探索和发现。
第四个问题,有兴趣尝试 DApp 开发的同学肯定希望获取演讲中提到的各种工具和资源。
由于 DApp 开发需要掌握和学习的资料非常多,魔法哥特别整理了一个资料库,并已在 GitHub 开源:
https://github.com/NasaTeam/Awesome-Nebulas
文中提到的第三方 SDK Nasa.js 实际上也是由魔法哥组队开发的,同样在 GitHub 开源,欢迎你一起来打磨它:
https://github.com/NasaTeam/Nasa.js
最后,说几句题外话吧,聊一聊我接触区块链这小半年来的感悟。
由于某些原因,区块链技术和区块链行业一直被广泛误解。一方面它被夸大为万能的银弹,一方面它的基础设施还极为简陋,这令很多人敬而远之,甚至 “路转黑”。区块链虽然已经诞生了近十年,但在应用层面仍然非常早期,它还有极大的不确定性,还有无数的未知等待我们去探索。
就像这张图,一片荒芜。探索者们独孤前行,然而他们相信光明的未来。
有人说,“区块链是风口、是潮流”。而我想说的是,“我们不跟随潮流,我们推动潮流——因为我们是开发者”。区块链是什么、能做什么,将由我们这些开发者来定义。期待有更多的开发者加入到这个新世界,我在这里等你!
今天的分享就到这里,谢谢!
QCon 北京 2019 现已全新起航,除了一些常规的专题设置外,新增了用户增长、智慧零售等当下热点话题,在技术深度和广度上不断延展。目前大会 7 折报名中,立减 2640 元。识别二维码了解 QCon 十周年的精心策划。