Dapp应用层开发
Dapp应用层要做什么
Dapp应用层最主要的是实现用户侧的交互逻辑,包括web页面和页面事件响应。同时不同的操作会带来不同的事件,我们还需要针对页面事件去调用后端智能合约方法,存入便签、更新便签、读取便签等。 由于是基于React框架做的实现,我们首先要准备一套Dapp应用层的开发环境。
Dapp应用层开发环境准备
- 首先需要安装nodejs环境,本文采用的是v9.11.2版本,npm版本为v5.6.0。我们可以使用nvm安装,也可以通过系统自带的包管理安装。具体的下载安装方法这里不再赘述,可参考官方文档:Node.js下载安装指导
- 准备好nodejs和npm环境后,我们可以一键创建一个React项目。根据不同的环境,我们有两种命令。
1$ npx create-react-app notewall-dapp
2or
3$ create-react-app notewall-dapp
- 执行完之后,我们可以进入notewall-dapp目录,目录里是一个标准的未装任何依赖的项目目录。这个项目里我们需要安装如下依赖:
1$ cd notewall-dapp
2$ npm install --save react-toastify react-bootstrap axios
其中react-toastify是一个第三方的弹窗通知库;axios是一个http请求库,完成我们对合约网关的请求。
- 试着启动一下
$ npm start
npm会启动这个react项目,并且将http server监听在本地3000端口。浏览器访问http://localhost:3000
可以查看结果。
如果我们看到了如下页面显示,则代表项目环境初始化完成。
为了能让项目运行我们的便签,我们需要更改根目录的App.js如下。
1/**
2 * @file notewall app entry
3 * @author XUPERBAAS Team
4 */
5import React, {Component} from 'react';
6import './App.css';
7import NoteWall from './note/NoteWall.js';
8
9export default class App extends Component {
10 render() {
11 return (
12 <div className='App'>
13 <header className='App-header'>
14 <NoteWall />
15 </header>
16 </div>
17 );
18 }
19}
Dapp应用层开发
如上所述,Dapp应用层主要分为两大部分,一个是页面展示与交互逻辑,另一个是与智能合约的交互。与传统web应用需要后端服务器不同的是,Dapp应用层只需要前端部分,Dapp的后端是区块链节点。我们通过调用以太坊节点上的接口向智能合约发起调用。而这些调用全部需要在前端Javascript中完成。
我们可以规划项目目录,新建两个子目录分别为gateway和note。gateway中主要实现与合约网关的交互逻辑,note目录中主要实现用于前端展示的组件。最终的项目目录如下图所示。
合约网关交互
由于前端页面展示风格千变万化,我们可以先从与合约网关交互这一个相对靠后的逻辑层次实现起来。 与智能合约网关的交互是用的是http请求,我们直接实用一个http库axios进行通信。我们从实际情况考虑,Dapp是将合约的相关操作和结果展示在前端页面,所以我们完成和合约网关的交互,开发对应的前端页面,就可以完成开发Dapp的需求。
结合最初的用例分析,我们需要在三个事件中与智能合约进行交互。
- 当用户打开应用时,需要展示所有的便签
- 当用户新增便签点击上链后,需要将便签内容保存。相对于我们已经实现的智能合约是InsertNote操作。
- 当用户更新便签点击上链后,需要将便签内容的更新保存下来。在合约中是UpdateNote操作。
1、Client构造
我们将这些交互抽象到一个NotewallGatewayClient中,在gateway中新建一个client.js。 新建一个NoteWallGatewayClient类
1import Axios from 'axios';
2export default class NoteWallGatewayClient {
3 /**
4 * @param gatewayAddress {string} 合约网关地址
5 * @param address {string} 合约部署地址
6 * @param senderAddress {string} 发起交易的账户地址
7 * @param senderPrivateKey {string} 发起交易的账户私钥
8 */
9 constructor(gatewayAddress, address, senderAddress, senderPrivateKey) {
10 this.baseUrl = gatewayAddress+"/v2/ethereum/tx/";
11 this.address = address;
12 this.senderPrivateKey = senderPrivateKey;
13 this.sender = senderAddress;
14 this.notes = {};
15 // 合约网关采用basic auth认证,对合约网关的http请求设置一个拦截器,配置相应的header内容。其中account和password是我们之前获取的用户名和密码。
16 Axios.interceptors.request.use((config) => {
17 config.headers={
18 "Authorization":'Basic '+btoa('admin:9581564b-93d9-d5bd-ea97-d639d83ca32c'),
19 "Content-Type":'application/json',
20 }
21 return config;
22 },(error) =>{
23 return Promise.reject(error);
24 });
25 }
26}
通过合约网关,分为交易类和非交易类型,这两种类型请求的参数有所不同,我们将这两种类型的请求封装一下方便后续调用。
1// 交易类型请求
2transaction(invoke, method){
3 let url = this.baseUrl+this.address+"/"+method
4 return Axios.post(url,{
5 from:this.sender,
6 privateKey:this.senderPrivateKey,
7 invokeParameters:invoke
8 })
9}
10// 非交易类型请求
11call(invoke, method){
12 let url = this.baseUrl+this.address+"/"+method
13 return Axios.post(url,{
14 from:this.sender,
15 invokeParameters:invoke
16 })
17}
创建交易类请求后会得到交易Hash,合约网关提供了根据交易Hash查询上链结果查询接口,我们对该接口进行封装。
1replyTx(id){
2 let url = this.baseUrl+id
3 return Axios.get(url)
4}
2、交易类方法调用
这里我们先实现创建note和更新note两个交易类的方法。
1/**
2 * @param title {string} 便签标题
3 * @param content {string} 便签正文
4 * @param callback {function} 回调函数
5 */
6insertNote(title, content, callback) {
7 let insert={
8 "_title": title,
9 "_content": content
10 }
11 this.transaction(insert, "InsertNote").then(resp => {
12 let timerId=setInterval(()=>{
13 this.replyTx(resp.data.result.txId).then(resp =>{
14 if (Object.keys(resp.data.result).length !== 0) {
15 clearInterval(timerId);
16 callback(null, resp.data.result)
17 }
18 })
19 },3000);
20 });
21}
22
23/**
24 * @param id {int} 便签id
25 * @param title {string} 便签标题
26 * @param content {string} 便签正文
27 * @param callback {function} 回调函数
28 */
29updateNote(id, title, content, callback) {
30 let update={
31 "_id": id,
32 "_title": title,
33 "_content": content
34 }
35 this.transaction(update, "UpdateNote").then(resp => {
36 console.log(resp);
37 let timerId=setInterval(()=>{
38 this.replyTx(resp.data.result.txId).then(resp =>{
39 if (Object.keys(resp.data.result).length !== 0) {
40 clearInterval(timerId);
41 callback(null,resp.data.result);
42 }
43 })
44 },3000);
45 });
46}
可以看到,我们进行创建和更新note的代码很简单,只需要将请求的合约函数相关参数组装为object对象,然后向合约网关发起post请求接受交易Id结果即可;此外,为了方便的检测交易结果是否上链,我们设置了一个定时器,每隔3s检查下交易的状态。
3、非交易类方法调用
非交易类方法主要为view类型的合约方法,我们可以使用合约网关提供的接口直接调用相关的合约方法,会得到合约返回结果。
在具体用例中,我们需要粘连合约中GetNoteIds和GetNote方法。从而满足页面渲染时直接展示所有合约的需求。
这里我们定义一个getAllNotes方法:
1/**
2 * callback的参数为notes
3 * notes格式为map[noteId] {
4 * id {string}
5 * title {string}
6 * content {string}
7 * }
8 */
9getAllNotes(callback){
10 let getNoteIds = {
11 }
12 this.call(getNoteIds, "GetNoteIds").then(async resp => {
13 let noteIds = resp.data.result._noteIds;
14 for (const noteId of noteIds) {
15 let id = {
16 "id": noteId
17 }
18 this.call(id, "GetNote").then(resp => {
19 let results=resp.data.result;
20 let note={id:noteId};
21 note["title"]=results.Result0;
22 note["content"]=results.Result1;
23 this.notes[noteId] = note;
24 });
25 }
26 return this.notes;
27 }).then(notes => {
28 callback(notes);
29 });
30}
到此,与合约交互部分的逻辑都已经开发完成了。NoteWallWeb3Client向外暴露三个方法分别是getAllNotes、insertNote、updateNote。
页面开发
开发页面前,我们首先将页面交互元素做一个拆分。主要的交互模块有:
- 登录表单,登录表单需要用户填写合约地址、发起交易的账户信息等。用户只有提交了登录表单才能查看合约。
- 便签,每一个便签样式类似,但内容不同,一个便签对应合约存储中的一个便签实例。
- 便签板,上面挂载所有的便签元素,并控制其他模块是否渲染。
- 编辑器,编辑器用来给用户创建和更新便签时写入内容用。应该允许用户输入标题、内容。
1、导入样式依赖
本项目会依赖到bootstrap样式表,所以在public/index.html
中我们插入bootstrap.css
1<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.0/css/bootstrap.min.css" integrity="sha384-PDle/QlgIONtM1aqA2Qemk5gPOE7wFq8+Em+G/hmo5Iq0CCmYZLv3fVRDJ4MMwEA" crossorigin="anonymous">
2、登录表单
登录表单对应到web3Client的参数,需要用户输入以太坊JSONRPC地址、合约地址、交易账号、交易秘钥四个参数。如图:
我们直接使用react-bootstrap类库构建表单,当用户点击登录时,会将变量反传到父组件。
1/**
2 * @file login form component
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import {
7 Button,
8 Form,
9 Jumbotron
10} from 'react-bootstrap';
11import './LoginForm.css';
12
13export default class LoginForm extends Component {
14 constructor(props) {
15 super(props);
16
17 this.state = {
18 gatewayAddress: '',
19 contractAddress: '',
20 senderAddress: '',
21 senderPrivateKey: ''
22 };
23 }
24
25 // 这里我们只做最简单的字符串长度校验,有兴趣的读者可以引用web3库的addressFormatter做更标准的地址检查
26 validateForm() {
27 return this.state.gatewayAddress.length > 0
28 && this.state.contractAddress.length > 0
29 && this.state.senderAddress.length > 0
30 && this.state.senderPrivateKey.length > 0;
31 }
32
33 // 输入元素内容有变更时即赋值
34 handleChange = event => {
35 this.setState({
36 [event.target.id]: event.target.value
37 });
38 }
39
40 // 这里会调用父组件传入的saveContractInfo方法,通知上层表单提交
41 handleSubmit = event => {
42 this.props.saveContractInfo(this.state.gatewayAddress, this.state.contractAddress,
43 this.state.senderAddress, this.state.senderPrivateKey);
44 }
45
46 render() {
47 return (
48 <div className='LoginForm'>
49 <Jumbotron>
50 <form onSubmit={this.handleSubmit}>
51 <Form.Group controlId='gatewayAddress'>
52 <Form.Label>合约网关地址</Form.Label>
53 <Form.Control
54 autoFocus
55 type='text'
56 value={this.state.gatewayAddress}
57 onChange={this.handleChange}
58 size='sm'
59 />
60 </Form.Group>
61 <Form.Group controlId='contractAddress'>
62 <Form.Label>合约地址</Form.Label>
63 <Form.Control
64 value={this.state.contractAddress}
65 onChange={this.handleChange}
66 type='text'
67 size='sm'
68 />
69 </Form.Group>
70 <Form.Group controlId='senderAddress'>
71 <Form.Label>交易账号</Form.Label>
72 <Form.Control
73 value={this.state.senderAddress}
74 onChange={this.handleChange}
75 type='text'
76 size='sm'
77 />
78 </Form.Group>
79 <Form.Group controlId='senderPrivateKey'>
80 <Form.Label>交易秘钥</Form.Label>
81 <Form.Control
82 value={this.state.senderPrivateKey}
83 onChange={this.handleChange}
84 type='password'
85 size='sm'
86 />
87 </Form.Group>
88 <Form.Group>
89 <Button block
90 size="large"
91 type="submit"
92 variant="primary"
93 disabled={!this.validateForm()}
94 className="mt-5"
95 >登录</Button>
96 </Form.Group>
97 </form>
98 </Jumbotron>
99 </div>
100 );
101 }
102}
登录表单样式LoginForm.css如下。
1@media all and (min-width: 480px) {
2 .LoginForm {
3 padding: 60px 0;
4 }
5
6 .LoginForm form {
7 max-width: 540px;
8 width: 540px;
9 margin: 0 auto;
10 font-size: 15px;
11 text-align: left;
12 color: #111;
13 }
14}
便签
便签模块主要是在样式控制上,这里我们将便签设计成不同颜色区分,会有不同倾斜角的正方形卡片。标题文字加粗。如效果图:
Note元素组件定义:
1/**
2 * @file note component
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import './Note.css';
7
8/**
9 * @props
10 * id {int}
11 * title {string}
12 * content {string}
13 * onClick {function} 被点击时需要调用,上层组件做响应
14 */
15export default class Note extends Component {
16 render() {
17 const {id, title, content} = this.props;
18 return (
19 <div className="note" onClick={() =>
20 this.props.onClick(id, title, content)}>
21 <div className="note-panel">
22 <h2>{this.props.title}</h2>
23 <p>{this.props.content.substring(0, 20)}</p>
24 </div>
25 </div>
26 );
27 }
28}
样式控制代码:
1body {
2 margin: 1em;
3}
4
5.note-wall,
6.note {
7 list-style: none;
8}
9
10.note-wall {
11 overflow: hidden;
12 padding: 3em;
13
14}
15
16.note-panel {
17 display: block;
18 width: 5em;
19 height: 5em;
20 padding: 2px;
21 text-decoration: none;
22 color: #000;
23 background: #ffc;
24 -webkit-box-shadow: 5px 5px 7px rgba(33, 33, 33, .7);
25 -moz-box-shadow: 5px 5px 7px rgba(33, 33, 33, 1);
26 box-shadow: 5px 5px 7px rgba(33, 33, 33, .7);
27 -webkit-transition: -webkit-transform .15s linear;
28 -moz-transition: -moz-transform .15s linear;
29 -o-transition: -o-transform .15s linear;
30}
31
32.note {
33 float: left;
34 margin: 1em;
35}
36
37.note-panel h2 {
38 padding-bottom: 10px;
39 font-size: 20%;
40 font-weight: 700;
41}
42
43.note-panel p {
44 font-family: "STXingkai", "Reenie Beanie", "Microsoft Yahei", arial, sans-serif;
45 font-size: 20%;
46}
47
48.note-panel {
49 -webkit-transform: rotate(-6deg);
50 -moz-transform: rotate(-6deg);
51 -o-transform: rotate(-6deg);
52}
53
54.note-wall div:nth-child(even) .note-panel {
55 position: relative;
56 top: 5px;
57 background: #cfc;
58 -webkit-transform: rotate(4deg);
59 -moz-transform: rotate(4deg);
60 -o-transform: rotate(4deg);
61}
62
63.note-wall div:nth-child(3n) .note-panel {
64 position: relative;
65 top: -5px;
66 background: #ccf;
67 -webkit-transform: rotate(-3deg);
68 -moz-transform: rotate(-3deg);
69 -o-transform: rotate(-3deg);
70}
71
72.note-wall div:nth-child(5n) .note-panel {
73 position: relative;
74 top: -10px;
75 -webkit-transform: rotate(5deg);
76 -moz-transform: rotate(5deg);
77 -o-transform: rotate(5deg);
78}
79
80.note-panel:focus {
81 position: relative;
82 z-index: 5;
83 -webkit-box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
84 -moz-box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
85 box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
86 -webkit-transform: scale(1.25);
87 -moz-transform: scale(1.25);
88 -o-transform: scale(1.25);
89}
1、便签板
便签板组件负责挂载其余组件,并负责通过子组件的事件回调完成子组件的通信。主要有以下核心控制逻辑: 1.当用户未登录时,不显示便签板,只显示登录表单。登录表单提交后,初始化gatewayClient,为后续渲染便签做准备。便签板NoteWall.js代码如下。
1/**
2 * @file notewall class. main component in app.
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import LoginForm from './LoginForm.js';
7import NoteWallGatewayClient from '../gateway/client';
8
9export default class NoteWall extends Component {
10 constructor(props) {
11 super(props);
12 this.state = {
13 login: false
14 };
15 this.saveContractInfo = this.saveContractInfo.bind(this);
16 this.gatewayClient = null;
17 // 控制是否登录
18 this.login = false;
19 }
20
21 // 定义了LoginForm回调方法,初始化gatewayClient
22 // gatewayClient将被用于获取便签和创建、更新便签
23 saveContractInfo(gatewayAddress, contractAddress, senderAddress, senderPrivateKey) {
24 this.gatewayClient = new NoteWallGatewayClient(gatewayAddress, contractAddress, senderAddress, senderPrivateKey);
25 this.setState({login: true});
26 }
27
28 render() {
29 if (!this.state.login) {
30 return (
31 <div className='note-wall'>
32 <LoginForm saveContractInfo={this.saveContractInfo} />
33 </div>
34 );
35 }
36 }
37}
2.当用户登录后,增加渲染便签。全量便签数据通过gatewayClient周期性获取。
1/**
2 * @file notewall class. main component in app.
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import LoginForm from './LoginForm.js';
7import Note from './Note.js';
8import NoteWallGatewayClient from '../gateway/client.js';
9
10export default class NoteWall extends Component {
11 constructor(props) {
12 super(props);
13 this.state = {
14 editorShow: false,
15 login: false,
16 noteList: []
17 };
18
19 this.periodicGetList = this.periodicGetList.bind(this);
20 this.saveContractInfo = this.saveContractInfo.bind(this);
21 this.gatewayClient = null;
22 this.login = false;
23 }
24
25 saveContractInfo(gatewayAddress, contractAddress, senderAddress, senderPrivateKey) {
26 this.gatewayClient = new NoteWallGatewayClient(gatewayAddress, contractAddress, senderAddress, senderPrivateKey);
27 this.setState({login: true});
28 }
29
30 // Fetch the list on first mount
31 componentWillMount() {
32 this.getListFromGateway();
33 this.periodicGetList();
34 }
35
36 // 每三秒获取一次全量note列表
37 periodicGetList() {
38 setTimeout(function () {
39 this.getListFromGateway();
40 this.periodicGetList();
41 }
42 .bind(this),
43 3000
44 );
45 }
46
47 // 获取全量note列表
48 getListFromGateway() {
49 // 未登录时由于没有client,跳过查询
50 if (!this.state.login) {
51 return;
52 }
53 this.gatewayClient.getAllNotes(notes => {
54 this.setState({
55 noteList: notes
56 });
57 });
58 }
59
60 render() {
61 let list = this.state.noteList;
62 // 如果已登录,则渲染便签
63 if (this.state.login) {
64 return (
65 <div className='note-wall'>
66 <ToastContainer className='toast-notification'/>
67 {
68 Object.keys(list).map(noteId => {
69 let note = list[noteId];
70 return (
71 <Note key={note.id} id={note.id} title={note.title}
72 content={note.content} onClick={this.showEditor} />
73 );
74 })
75 }
76 </div>
77 );
78 }
79 return (
80 <div className='note-wall'>
81 <LoginForm saveContractInfo={this.saveContractInfo} />
82 </div>
83 );
84 }
85}
3.增加editor便签编辑器的控制。
便签编辑器在新建便签和更新便签时需要显示。当更新便签时需要知道便签的ID、标题和正文,所以需要当做参数传入。另外只有当用户需要创建和更新时才需要显示编辑器,所以编辑器需要能够响应这些事件,控制是否显示。同时,编辑器负责便签的存入,需要调用合约接口,需要用到gatewayClient。另外,编辑器存入便签后的相关receipt或者错误数据应该能够通知给用户,所以需要传入回调用的通知函数。
目前为止,因为编辑器还没有实现,我们先假定编辑器组件名字为Editor。这样NoteWall.js文件将更新成。
1/**
2 * @file notewall class. main component in app.
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import LoginForm from './LoginForm.js';
7import Note from './Note.js';
8import Editor from './Editor.js';
9import NoteWallGatewayClient from '../gateway/client.js';
10
11export default class NoteWall extends Component {
12 constructor(props) {
13 super(props);
14 this.state = {
15 editorShow: false,
16 login: false,
17 noteList: []
18 };
19 this.showEditor = this.showEditor.bind(this);
20 this.closeEditor = this.closeEditor.bind(this);
21 this.periodicGetList = this.periodicGetList.bind(this);
22 this.saveContractInfo = this.saveContractInfo.bind(this);
23 this.notify = this.notify.bind(this);
24 this.errNotify = this.notify.bind(this);
25 this.gatewayClient = null;
26 this.login = false;
27 }
28
29 // 开关编辑器用
30 showEditor(id, title, content) {
31 this.setState({
32 editorShow: !this.state.editorShow,
33 id: id,
34 title: title,
35 content: content
36 });
37 }
38
39 // 关闭编辑器用,关闭编辑器后将立即获取便签一次
40 closeEditor() {
41 this.setState({
42 editorShow: false
43 }, () => {
44 this.getListFromGateway();
45 });
46 }
47
48 componentWillMount() {
49 this.getListFromGateway();
50 this.periodicGetList();
51 }
52
53 periodicGetList() {
54 setTimeout(function () {
55 this.getListFromGateway();
56 this.periodicGetList();
57 }
58 .bind(this),
59 3000
60 );
61 }
62
63 getListFromGateway() {
64 if (!this.state.login) {
65 return;
66 }
67 this.gatewayClient.getAllNotes(notes => {
68 this.setState({
69 noteList: notes
70 });
71 });
72 }
73
74 saveContractInfo(gatewayAddress, contractAddress, senderAddress, senderPrivateKey) {
75 this.gatewayClient = new NoteWallGatewayClient(gatewayAddress, contractAddress, senderAddress, senderPrivateKey);
76 this.setState({login: true});
77 }
78
79 // 通知函数,调用toast库,通知用户入链信息
80 notify(msg) {
81 toast(msg);
82 }
83
84 // 异常通知函数,调用toast.error方法,通知用户入链异常信息
85 errNotify(msg) {
86 toast.error(msg, {
87 autoClose: 18000
88 });
89 }
90
91 render() {
92 let list = this.state.noteList;
93 if (this.state.login) {
94 return (
95 <div className='note-wall'>
96 <ToastContainer className='toast-notification'/>
97 {
98 Object.keys(list).map(noteId => {
99 let note = list[noteId];
100 return (
101 <Note key={note.id} id={note.id} title={note.title}
102 content={note.content} onClick={this.showEditor} />
103 );
104 })
105 }
106 {/* 参数作用将在Editor组件时看到 */}
107 <Editor key={this.state.id}
108 show={this.state.editorShow}
109 id={this.state.id}
110 title={this.state.title}
111 content={this.state.content}
112 onClick={this.showEditor}
113 closeEditor={this.closeEditor}
114 gatewayClient={this.gatewayClient}
115 notify={this.notify}
116 errNotify={this.errNotify}
117 />
118 </div>
119 );
120 }
121 return (
122 <div className='note-wall'>
123 <LoginForm saveContractInfo={this.saveContractInfo} />
124 </div>
125 );
126 }
127}
4.最后我们在便签板上还需要增加一个“新增便签”的按钮,用户点击后弹出编辑器。所以完整的便签板代码如下:
1/**
2 * @file notewall class. main component in app.
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import LoginForm from './LoginForm.js';
7import Note from './Note.js';
8import Editor from './Editor.js';
9import NoteWallGatewayClient from '../gateway/client.js';
10import {ToastContainer, toast} from 'react-toastify';
11import 'react-toastify/dist/ReactToastify.css';
12
13export default class NoteWall extends Component {
14 constructor(props) {
15 super(props);
16 this.state = {
17 editorShow: false,
18 login: false,
19 noteList: []
20 };
21 this.showEditor = this.showEditor.bind(this);
22 this.closeEditor = this.closeEditor.bind(this);
23 this.periodicGetList = this.periodicGetList.bind(this);
24 this.createNote = this.createNote.bind(this);
25 this.saveContractInfo = this.saveContractInfo.bind(this);
26 this.notify = this.notify.bind(this);
27 this.errNotify = this.notify.bind(this);
28 this.gatewayClient = null;
29 this.login = false;
30 }
31
32 createNote() {
33 if (!this.state.editorShow) {
34 this.showEditor(null, '', '');
35 }
36 }
37
38 showEditor(id, title, content) {
39 this.setState({
40 editorShow: !this.state.editorShow,
41 id: id,
42 title: title,
43 content: content
44 });
45 }
46
47 closeEditor() {
48 this.setState({
49 editorShow: false
50 }, () => {
51 this.getListFromGateway();
52 });
53 }
54
55 componentWillMount() {
56 this.getListFromGateway();
57 this.periodicGetList();
58 }
59
60 periodicGetList() {
61 setTimeout(function () {
62 this.getListFromGateway();
63 this.periodicGetList();
64 }
65 .bind(this),
66 3000
67 );
68 }
69
70 getListFromGateway() {
71 if (!this.state.login) {
72 return;
73 }
74 this.gatewayClient.getAllNotes(notes => {
75 this.setState({
76 noteList: notes
77 });
78 });
79 }
80
81 saveContractInfo(gatewayAddress, contractAddress, senderAddress, senderPrivateKey) {
82 this.gatewayClient = new NoteWallGatewayClient(gatewayAddress, contractAddress, senderAddress, senderPrivateKey);
83 this.setState({login: true});
84 }
85
86 notify(msg) {
87 toast(msg);
88 }
89
90 errNotify(msg) {
91 toast.error(msg, {
92 autoClose: 18000
93 });
94 }
95
96 render() {
97 let list = this.state.noteList;
98 if (this.state.login) {
99 return (
100 <div className='note-wall'>
101 <ToastContainer className='toast-notification'/>
102 <button className='editor-btn header-btn' onClick={this.createNote}>新建便签</button>
103 {
104 Object.keys(list).map(noteId => {
105 let note = list[noteId];
106 return (
107 <Note key={note.id} id={note.id} title={note.title}
108 content={note.content} onClick={this.showEditor} />
109 );
110 })
111 }
112 <Editor key={this.state.id}
113 show={this.state.editorShow}
114 id={this.state.id}
115 title={this.state.title}
116 content={this.state.content}
117 onClick={this.showEditor}
118 closeEditor={this.closeEditor}
119 gatewayClient={this.gatewayClient}
120 notify={this.notify}
121 errNotify={this.errNotify}
122 />
123 </div>
124 );
125 }
126 return (
127 <div className='note-wall'>
128 <LoginForm saveContractInfo={this.saveContractInfo} />
129 </div>
130 );
131 }
132}
便签板完成后,效果图如下:
2、编辑器
编辑器本身需要完成四个功能:
1.如果是已有便签更新,那么需要显示便签标题、便签正文; 2.右上角需要有一个“上链”的按钮,用户更新完成后可以点击这个按钮完成便签存入; 3.当便签上链交易完成后,编辑器退出; 4.当用户放弃编辑时,点击编辑器外即可退出编辑。
编辑Editor.js,代码如下:
1/**
2 * @file editor component
3 * @author BaaS Team
4 */
5import React, {Component} from 'react';
6import './Editor.scss';
7
8const titleRef = React.createRef();
9const contentRef = React.createRef();
10
11export default class Editor extends Component {
12 constructor(props) {
13 super(props);
14 this.buttonClicked = this.buttonClicked.bind(this);
15 this.saveNoteWithGateway = this.saveNoteWithGateway.bind(this);
16 this.clickMargin = this.clickMargin.bind(this);
17 // 上链中,编辑器不应退出,直到上链结束
18 this.marginLock = false;
19 this.state = {
20 btnMessage: '上链'
21 };
22 this.gatewayClient = this.props.gatewayClient;
23 }
24
25 componentDidMount() {
26 titleRef.current.focus();
27 }
28
29 buttonClicked() {
30 if (this.marginLock) {
31 return;
32 }
33 // 修改按钮显示
34 this.setState({
35 btnMessage: '上链中'
36 });
37 this.marginLock = true;
38 this.saveNoteWithGateway();
39 }
40
41 saveNoteWithGateway() {
42 if (this.props.id != null) {
43 this.gatewayClient.updateNote(this.props.id,
44 titleRef.current.innerText,
45 contentRef.current.innerText,
46 (err, receipt) => {
47 if (!err) {
48 this.props.notify(`已上链. \n 区块高度${receipt.blockNumber}`);
49 this.props.closeEditor();
50 }
51 else {
52 this.props.errNotify(`区块链交易遇到问题: ${err}`);
53 }
54 this.setState({
55 btnMessage: '上链'
56 });
57 this.marginLock = false;
58 });
59 }
60 // 新增便签
61 else {
62 this.gatewayClient.insertNote(titleRef.current.innerText,
63 contentRef.current.innerText,
64 (err, receipt) => {
65 if (!err) {
66 this.props.notify(`已上链. \n 区块高度${receipt.blockNumber}`);
67 this.props.closeEditor();
68 }
69 else {
70 this.props.errNotify(`区块链交易遇到问题: ${err}`);
71 }
72 this.setState({
73 btnMessage: '上链'
74 });
75 this.marginLock = false;
76 });
77 }
78 }
79
80 // 如果点击编辑器外部,编辑器退出
81 clickMargin() {
82 if (!this.marginLock) {
83 this.props.onClick(this.props.title, this.props.content);
84 }
85 }
86
87 render() {
88 return (
89 <div className={this.props.show ? 'editor display-block' : 'editor display-none'}
90 onClick={() => this.clickMargin()}>
91 <div id='notebook-paper' onClick={e => {
92 e.stopPropagation();
93 }
94 }>
95 <header>
96 <h1 id='title-text' contentEditable='true' suppressContentEditableWarning={true}
97 ref={titleRef} autoFocus>{this.props.title}</h1>
98 <button className='editor-btn header-btn' onClick={this.buttonClicked}>
99 {this.state.btnMessage}
100 </button>
101 </header>
102 <div id='content'>
103 <div id='content-text' contentEditable='true'
104 suppressContentEditableWarning={true} ref={contentRef}>
105 {this.props.content}
106 </div>
107 </div>
108 </div>
109 </div>
110 );
111 }
112}
Editor的样式如下。
1.editor {
2 position: fixed;
3 top: 0;
4 left: 0;
5 width:100%;
6 height: 100%;
7 background: rgba(0, 0, 0, 0.6);
8 }
9
10 .editor-main {
11 position:fixed;
12 background: white;
13 width: 80%;
14 height: auto;
15 top:50%;
16 left:50%;
17 transform: translate(-50%,-50%);
18 }
19
20 .display-block {
21 display: block;
22 }
23
24 .display-none {
25 display: none;
26 }
27
28 * {
29 -webkit-box-sizing:border-box;
30 -moz-box-sizing:border-box;
31 -ms-box-sizing:border-box;
32 -o-box-sizing:border-box;
33 box-sizing:border-box;
34 }
35
36 body {
37 background: #f1f1f1;
38 font-family:helvetica neue, helvetica, arial, sans-serif;
39 font-weight:200;
40 }
41
42 #title-text, #content-text {
43 &:focus {
44 outline: 0;
45 }
46 }
47
48 #notebook-paper {
49 text-align: left;
50 color: #050000;
51 width:960px;
52 height:500px;
53 background: linear-gradient(to bottom,white 29px,#00b0d7 1px);
54 margin:50px auto;
55 background-size: 100% 30px;
56 position:relative;
57 padding-top:150px;
58 padding-left:160px;
59 padding-right:20px;
60 overflow:auto;
61 border-radius:5px;
62 -webkit-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
63 -moz-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
64 -ms-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
65 -o-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
66 box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
67 &:before {
68 content:'';
69 display:block;
70 position:absolute;
71 z-index:1;
72 top:0;
73 left:140px;
74 height:100%;
75 width:1px;
76 background:#db4034;
77 }
78 header {
79 height:150px;
80 width:100%;
81 background:white;
82 position:absolute;
83 top:0;
84 left:0;
85 h1 {
86 font-size:60px;
87 line-height:60px;
88 padding:127px 20px 0 160px;
89 }
90 }
91 #content {
92 margin-top:67px;
93 font-size:20px;
94 line-height:30px;
95 }
96
97 #hipsum {
98 margin:0 0 30px 0;
99 }
100
101 }
102
103 //Colours
104 $green: #2ecc71;
105 $red: #e74c3c;
106 $blue: #3498db;
107 $yellow: #f1c40f;
108 $purple: #8e44ad;
109 $turquoise: #1abc9c;
110
111 // Basic Button Style
112 .editor-btn {
113 box-sizing: border-box;
114 appearance: none;
115 background-color: transparent;
116 border: 2px solid $red;
117 border-radius: 0.6em;
118 color: $red;
119 cursor: pointer;
120 display: block;
121 align-self: center;
122 font-size: 3px;
123 font-weight: 500;
124 line-height: 1;
125 margin: 20px;
126 padding: 5px 2px;
127 text-decoration: none;
128 text-align: center;
129 text-transform: uppercase;
130 font-family: 'Montserrat', sans-serif;
131 font-weight: 700;
132
133 &:hover {
134 color: #fff;
135 outline: 0;
136 }
137 &:focus {
138 outline: 0;
139 }
140 }
141
142 .header-btn {
143 border-color: $blue;
144 border: 0;
145 border-radius: 0;
146 color: $blue;
147 position: absolute;
148 top: 20px;
149 right: 20px;
150 width: 100px;
151 height: 30px;
152 overflow: hidden;
153 z-index: 1;
154 transition: color 150ms ease-in-out;
155
156 &:after {
157 content: '';
158 position: absolute;
159 display: block;
160 top: 0;
161 left: 50%;
162 transform: translateX(-50%);
163 width: 0;
164 height: 100%;
165 background: $blue;
166 z-index: -1;
167 transition: width 150ms ease-in-out;
168 }
169
170 &:hover {
171 color: #fff;
172 &:after {
173 width: 110%;
174 }
175 }
176 }
实现完成后效果如下 编辑便签
上链完成,编辑器退出,弹出通知条
到此Dapp应用层开发完毕,我们可以在本地手动测试一下创建便签、更新便签、退出编辑等功能。接下来我们准备将Dapp进行部署。