Dapp应用层开发
Dapp应用层要做什么
Dapp应用层最主要的是实现用户侧的交互逻辑,包括web页面和页面事件响应。同时不同的操作会带来不同的事件,我们还需要针对页面事件去调用后端链码方法,存入便签、更新便签、读取便签等。
由于是基于React框架做的实现,我们首先要准备一套Dapp应用层的开发环境。
Dapp应用层开发环境准备
- 首先需要安装nodejs环境,本文采用的是v10.16.0版本,npm版本为v6.9.0。我们可以使用nvm安装,也可以通过系统自带的包管理安装。具体的下载安装方法这里不再赘述,可参考官方文档:Node.js下载安装指导
- 准备好nodejs和npm环境后,我们可以一键创建一个React项目。根据不同的环境,我们有两种命令。
$ npx create-react-app notewall-dapp
or
$ create-react-app notewall-dapp
- 执行完之后,我们可以进入notewall-dapp目录,目录里是一个标准的未装任何依赖的项目目录。
- 试着启动一下
$ npm start
npm会启动这个react项目,并且将http server监听在本地3000端口。浏览器访问http://localhost:3000可以查看结果。
如果我们看到了如下页面显示,则代表项目环境初始化完成,我们可以开始编写应用代码了。
Dapp应用层开发
如上所述,Dapp应用层主要分为两大部分,一个是页面展示与交互逻辑,另一个是与链码的交互。页面展示与交互逻辑由前端和后端部分完成,与链码的交互由后端部分完成。与以太坊不需要后端服务器不同,Fabric与链码交互时,需要msp、证书等信息,需要由后端保存,所以Fabric Dapp多了后端服务器的实现。
我们可以规划项目目录,新建两个子目录分别为client和server。server中主要实现与合约的交互逻辑,client目录中主要实现用于前端展示的组件,为了保持实现语言上的一致性,前后端统一用nodejs实现。
合约交互
由于前端页面展示风格千变万化,我们可以先从后端逻辑实现起来。与链码的交互,我们使用Fabric官方提供的Node SDK。 结合最初的用例分析,我们需要在三个事件中与链码进行交互。
- 当用户打开应用时,需要展示所有的便签
- 当用户新增便签点击上链后,需要将便签内容保存。相对于我们已经实现的链码是InsertNote操作。
- 当用户更新便签点击上链后,需要将便签内容的更新保存下来。在合约中是UpdateNote操作。
准备网络配置
为了实现在外网环境下与Dapp的交互我们在超级链BaaS平台中创建联盟链类型的fabric网络,在网络详情页面可以看到网络的详细配置,在页面右上角可以下载生成的网络证书文件。
fabric网络信息如下:
因为生成Fabric Client需要根据网络信息进行配置,需要提前准备好并使服务启动时能够读取。我们在项目中创建artifacts目录用于存放这些信息,Client配置中主要是网络信息及证书。目录如下:
网络配置test_config.yaml详细内容如下:
name: "config"
version: 1.0.0
client:
organization: org1
logging:
level: debug
credentialStore:
path: artifacts/sdk/msp-store
cryptoStore:
path: artifacts/sdk/msp-work-dir
BCCSP:
security:
enabled: true
default:
provider: "SW"
hashAlgorithm: "SHA2"
softVerify: true
level: 256
tlsCerts:
systemCertPool: false
client:
keyfile:
certfile:
organizations:
org-orderer:
mspid: org-ordererMSP
certificateAuthorities:
- ca.org-orderer
adminPrivateKey:
path: artifacts/crypto-config/org-orderer/users/Admin@org-orderer/msp/keystore/0210362ecc00e5511bd924799dccadc3b015d804c0f0a7633041dfb2471ecc0e_sk
signedCert:
path: artifacts/crypto-config/org-orderer/users/Admin@org-orderer/msp/admincerts/Admin@org-orderer-cert.pem
peers:
org1:
mspid: org1MSP
certificateAuthorities:
- ca.org1
adminPrivateKey:
path: artifacts/crypto-config/org1/users/Admin@org1/msp/keystore/f4be32c6b1332d9b334fc10805e46f0074dacb1ac4123b23ff1daf9bf2041ef9_sk
signedCert:
path: artifacts/crypto-config/org1/users/Admin@org1/msp/admincerts/Admin@org1-cert.pem
peers:
- org1-peer0
certificateAuthorities:
ca.org-orderer:
url: http://org-orderer-rootca:7053
httpOptions:
verify: false
tlsCACerts:
path: artifacts/crypto-config/org-orderer/tlsca/tlsca.org-orderer-cert.pem
ca.org1:
url: http://org1-rootca:7054
httpOptions:
verify: false
tlsCACerts:
path: artifacts/crypto-config/org-orderer/tlsca/tlsca.org-orderer-cert.pem
orderers:
orderer0:
url: grpcs://106.12.60.245:7050
grpcOptions:
ssl-target-name-override: orderer0
allow-insecure: 0
tlsCACerts:
path: artifacts/crypto-config/org-orderer/tlsca/tlsca.org-orderer-cert.pem
peers:
org1-peer0:
url: grpcs://106.12.60.243:7051
grpcOptions:
ssl-target-name-override: org1-peer0
tlsCACerts:
path: artifacts/crypto-config/org1/tlsca/tlsca.org1-cert.pem
channels:
nml-channel:
orderers:
- orderer0
peers:
org1-peer0:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
policies:
queryChannelConfig:
minResponses: 1
maxTargets: 1
retryOpts:
attempts: 5
initialBackoff: 500ms
maxBackoff: 5s
backoffFactor: 2.0
后端服务启动
服务端文件的目录结构为:
服务启动时会默认展示主页,具体代码如下:
/**
* @file index js file
* @author BaaS Team
*/
var express = require('express');
var router = express.Router();
// GET home page
router.get('/', function (req, res, next) {
res.render('index', {
title: 'Express'
});
});
module.exports = router;
除此之外,后端服务启动时会构建NoteWallFabricClient对象,并定义路由,具体代码如下:
/**
* @file /api handler
* @author BaaS Team
*/
const express = require('express');
const NoteWallWeb3Client = require('./ethereum/web3/client.js');
const NoteWallFabricClient = require('./fabric/chaincode/client');
const router = express.Router();
let mode = process.env.BLOCKCHAIN_TYPE.toUpperCase();
let blockchainClient;
if (mode === 'ETHEREUM') {
let httpProvider = process.env.HTTP_PROVIDER;
if (!httpProvider) {
console.error('HTTP_PROVIDER not defined');
process.exit(1);
}
let contractAddress = process.env.CONTRACT_ADDRESS;
if (!contractAddress) {
console.error('CONTRACT_ADDRESS not defined');
process.exit(1);
}
let senderAddress = process.env.SENDER_ADDRESS;
if (!senderAddress) {
console.error('SENDER_ADDRESS not defined');
process.exit(1);
}
let senderPrivateKey = process.env.SENDER_PRIVATE_KEY;
if (!senderPrivateKey) {
console.error('SENDER_PRIVATE_KEY not defined');
process.exit(1);
}
blockchainClient = new NoteWallWeb3Client(httpProvider, contractAddress, senderAddress, senderPrivateKey);
}
else if (mode === 'FABRIC') {
let networkConfigPath = process.env.FABRIC_NETWORK_CONFIG_PATH;
if (!networkConfigPath) {
console.error('FABRIC_NETWORK_CONFIG_PATH not defined');
process.exit(1);
}
let channelName = process.env.FABRIC_CHANNEL_NAME;
if (!channelName) {
console.error('FABRIC_CHANNEL_NAME not defined');
process.exit(1);
}
let chaincodeName = process.env.FABRIC_CHAINCODE_NAME;
if (!chaincodeName) {
console.error('FABRIC_CHAINCODE_NAME not defined');
process.exit(1);
}
blockchainClient = new NoteWallFabricClient(networkConfigPath, channelName, chaincodeName);
}
else {
console.error('BLOCKCHAIN_TYPE not valid. ETHEREUM or FABRIC');
}
router.route('/note').post((req, res, next) => {
let id = req.body.id;
let title = req.body.title;
let content = req.body.content;
console.log(`${id} ${title} ${content}`);
// update
if (id) {
blockchainClient.updateNote(id, title, content, (err, receipt) => {
if (!err) {
res.json({status: 0, message: 'ok'});
}
else {
res.json({status: 1, message: err});
}
});
}
// insert
else {
blockchainClient.insertNote(title, content, (err, receipt) => {
if (!err) {
res.json({status: 0, message: 'ok'});
}
else {
res.json({status: 1, message: err});
}
});
}
});
router.get('/notes', (req, res, next) => {
blockchainClient.getAllNotes(notes => {
res.json({status: 0, message: notes});
});
});
module.exports = router;
Client构造
我们将这些交互抽象到一个NoteWallFabricClient中,在routes目录中创建fabric/chaincode子目录,并在chaincode目录下创建client.js文件。 新建一个NoteWallFabricClient类
class NoteWallFabricClient {
constructor(networkConfigPath, channelName, chaincodeName) {
let client = fabricClient.loadFromConfig(networkConfigPath);
client.initCredentialStores();
let channel = client.getChannel(channelName);
this.client = client;
this.channel = channel;
this.chaincodeName = chaincodeName;
}
}
module.exports = NoteWallFabricClient;
构造函数通过Fabric网络配置文件构建了一个Client,实际使用时需要根据用户自身网络信息配置该文件。然后从client对象中获取channel对象,并将chaincode名称保存。
交易类方法调用
这里我们先实现创建note交易的方法。
/**
* @param title {string} 便签标题
* @param content {string} 便签正文
* @param callback {function} 回调函数
*/
async insertNote(title, content, callback) {
// note id由当前时间戳生成,用户可以强化此处id生成方法
let id = + new Date();
// 用于标识是否发向所有peer的交易提案成功
let allGood = true;
let errorMessage = null;
try {
let txId = this.client.newTransactionID(true);
// 构建交易请求
let request = {
chaincodeId: this.chaincodeName,
fcn: 'insert',
args: [id.toString(), title, content],
txId: txId
};
// 发送交易提案,也即背书
let results = await this.channel.sendTransactionProposal(request, true);
let proposalResponses = results[0];
let proposal = results[1];
// 处理所有peer的返回
for (const i in proposalResponses) {
if (proposalResponses[i] instanceof Error) {
allGood = false;
errorMessage = util.format('invoke chaincode proposal resulted in an error :: %s',
proposalResponses[i].toString());
console.log(errorMessage);
} else if (proposalResponses[i].response && proposalResponses[i].response.status === 200) {
console.info('invoke chaincode proposal was good');
} else {
allGood = false;
errorMessage = util.format('invoke chaincode proposal failed for an unknown reason %j',
proposalResponses[i]);
console.log(errorMessage);
}
}
// 如果返回都成功,将交易提交给orderer
if (allGood) {
const promises = [];
let eventHubs = await this.channel.getChannelEventHubsForOrg();
eventHubs.forEach(eh => {
console.log('invokeEventPromise - setting up event:' + eh.getName());
let invokeEventPromise = new Promise((resolve, reject) => {
let eventTimeout = setTimeout(() => {
let message = 'REQUEST_TIMEOUT:' + eh.getPeerAddr();
console.log(message);
eh.disconnect();
}, 3000);
eh.registerTxEvent(txId.getTransactionID(), (tx, code, blockNum) => {
console.log('The chaincode invoke chaincode transaction has been committed on peer %s',
eh.getPeerAddr());
console.log('Transaction %s has status of %s in blocl %s', tx, code, blockNum);
clearTimeout(eventTimeout);
if (code !== 'VALID') {
let message = util.format('The invoke chaincode transaction was invalid, code:%s',
code);
console.log(message);
reject(new Error(message));
} else {
let message = 'The invoke chaincode transaction was valid.';
console.log(message);
resolve(message);
}
}, err => {
clearTimeout(eventTimeout);
console.log('err:' + err);
reject(err);
},
// the default for 'unregister' is true for transaction listeners
// so no real need to set here, however for 'disconnect'
// the default is false as most event hubs are long running
// in this use case we are using it only once
{unregister: true, disconnect: true}
);
eh.connect();
});
promises.push(invokeEventPromise);
});
const ordererRequest = {
txId: txId,
proposalResponses: proposalResponses,
proposal: proposal
};
const sendPromise = this.channel.sendTransaction(ordererRequest);
promises.push(sendPromise);
let results = await Promise.all(promises);
let response = results.pop();
if (response.status === 'SUCCESS') {
console.log('Successfully sent transaction to the orderer.');
} else {
errorMessage = util.format('Failed to order the transaction. Error code: %s', response.status);
console.log(errorMessage);
}
// 监控event hubs结果
for (let i in results) {
let eventHubResult = results[i];
let eventHub = eventHubs[i];
console.log('Event results for event hub :%s', eventHub.getPeerAddr());
if (typeof eventHubResult === 'string') {
console.log(eventHubResult);
} else {
if (!errorMessage) {
errorMessage = eventHubResult.toString();
}
console.log(eventHubResult.toString());
}
}
}
} catch (error) {
console.error('Failed to invoke due to error: ' + error.stack ? error.stack : error);
errorMessage = error.toString();
}
return callback(errorMessage, null);
}
更新note的方法和新建note的方法基本一致,唯一的区别就是将链码函数从insert改为update。
非交易类方法调用
非交易类方法主要为view类型的合约方法,可以直接查询而不用生成区块。具体代码如下:
/**
* callback的参数为notes
* notes格式为map[noteId] {
* id {int}
* title {string}
* content {string}
* }
*/
async getAllNotes(callback) {
let request = {
chaincodeId: this.chaincodeName,
fcn: 'queryAll',
args: ['']
};
let queryResponses = await this.channel.queryByChaincode(request, true);
console.log('Query has completed, checking results:' + (queryResponses));
return callback(JSON.parse(queryResponses.toString('utf8')));
}
到此,与合约交互部分的逻辑都已经开发完成了。NoteWallFabricClient向外暴露三个方法分别是getAllNotes、insertNote、updateNote。
页面开发
开发页面前,我们首先将页面交互元素做一个拆分。主要的交互模块有
- 便签,每一个便签样式类似,但内容不同,一个便签对应合约存储中的一个便签实例;
- 便签板,上面挂载所有的便签元素,并控制其他模块是否渲染;
- 编辑器,编辑器用来给用户创建和更新便签时写入内容用。应该允许用户输入标题、内容;
客户端文件目录结构为:
主页
主页比较简单,主要内容是定义了主渲染区”root“,代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React App</title>
<link rel="shortcut icon" href="public/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1,
shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
主页默认渲染便签板,代码如下:
/**
* @file client index
* @author BaaS Team
*/
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
index样式控制表:
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
index中渲染了APP对象,渲染代码如下:
/**
* @file app component
* @author BCE BaaS Team
*/
import React, {Component} from 'react';
import './App.css';
import NoteWall from './NoteWall';
export default class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<NoteWall />
</header>
</div>
);
}
}
APP样式控制表:
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
animation: App-logo-spin infinite 20s linear;
}
.App-header {
display: flex;
flex-direction: column;
min-height: 100vh;
font-size: calc(10px + 2vmin);
color: #fff;
background-color: #282c34;
align-items: center;
justify-content: center;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
便签
便签模块主要是在样式控制上,这里我们将便签设计成不同颜色区分,会有不同倾斜角的正方形卡片。标题文字加粗。如效果图:
Note元素组件定义
/**
* @file note component
* @author BaaS Team
*/
import React, {Component} from 'react';
import './Note.css';
export default class Note extends Component {
render() {
const {id, title, content} = this.props;
return (
<div className="note" onClick={() =>
this.props.onClick(id, title, content)}>
<div className="note-panel">
<h2>{this.props.title}</h2>
<p>{this.props.content}</p>
</div>
</div>
);
}
}
样式控制代码:
* {
margin: 0;
padding: 0;
}
body {
margin: 1em;
font-family: arial, sans-serif;
font-size: 100%;
color: #fff;
background: #666;
}
h2,
p {
font-size: 100%;
font-weight: 400;
}
.note-wall,
.note {
list-style: none;
}
.note-wall {
overflow: hidden;
padding: 3em;
}
.note-panel {
display: block;
width: 5em;
height: 5em;
padding: 2px;
text-decoration: none;
color: #000;
background: #ffc;
-webkit-box-shadow: 5px 5px 7px rgba(33, 33, 33, .7);
-moz-box-shadow: 5px 5px 7px rgba(33, 33, 33, 1);
box-shadow: 5px 5px 7px rgba(33, 33, 33, .7);
-webkit-transition: -webkit-transform .15s linear;
-moz-transition: -moz-transform .15s linear;
-o-transition: -o-transform .15s linear;
}
.note {
float: left;
margin: 1em;
}
.note-panel h2 {
padding-bottom: 10px;
font-size: 20%;
font-weight: 700;
}
.note-panel p {
font-family: "Reenie Beanie", arial, sans-serif;
font-size: 20%;
}
.note-panel {
-webkit-transform: rotate(-6deg);
-moz-transform: rotate(-6deg);
-o-transform: rotate(-6deg);
}
.note-wall div:nth-child(even) .note-panel {
position: relative;
top: 5px;
background: #cfc;
-webkit-transform: rotate(4deg);
-moz-transform: rotate(4deg);
-o-transform: rotate(4deg);
}
.note-wall div:nth-child(3n) .note-panel {
position: relative;
top: -5px;
background: #ccf;
-webkit-transform: rotate(-3deg);
-moz-transform: rotate(-3deg);
-o-transform: rotate(-3deg);
}
.note-wall div:nth-child(5n) .note-panel {
position: relative;
top: -10px;
-webkit-transform: rotate(5deg);
-moz-transform: rotate(5deg);
-o-transform: rotate(5deg);
}
.note-panel:focus {
position: relative;
z-index: 5;
-webkit-box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
-moz-box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
box-shadow: 10px 10px 7px rgba(0, 0, 0, .7);
-webkit-transform: scale(1.25);
-moz-transform: scale(1.25);
-o-transform: scale(1.25);
}
便签板
便签板组件负责挂载其余组件,并负责通过子组件的事件回调完成子组件的通信。主要有以下核心控制逻辑:
/**
* @file notewall class. main component in app.
* @author BaaS Team
*/
import React, {Component} from 'react';
import Note from './note/Note.js';
import Editor from './note/Editor.js';
export default class NoteWall extends Component {
constructor(props) {
super(props);
this.state = {
editorShow: false,
noteList: []
};
this.showEditor = this.showEditor.bind(this);
this.closeEditor = this.closeEditor.bind(this);
this.periodicGetList = this.periodicGetList.bind(this);
}
showEditor(id, title, content) {
this.setState({
editorShow: !this.state.editorShow,
id: id,
title: title,
content: content
});
}
closeEditor() {
this.setState({
editorShow: false
}, () => {
this.getList();
});
}
// Fetch the list on first mount
componentWillMount() {
this.getList();
this.periodicGetList();
}
periodicGetList() {
setTimeout(
function () {
this.getList();
this.periodicGetList();
}
.bind(this),
3000
);
}
// Retrieves the list of items from the Express app
getList() {
fetch('/api/notes')
.then(res => res.json())
.then(res => this.setState({noteList: res.message}));
}
render() {
let list = this.state.noteList;
return (
<div className="note-wall">
<button className="btn header-btn" onClick={e => {
this.showEditor(null, '', '');
}
}>新建</button>
{
Object.keys(list).map(noteId => {
let note = list[noteId];
return (
<Note key={note.id} id={note.id} title={note.title}
content={note.content} onClick={this.showEditor} />
);
})
}
<Editor show={this.state.editorShow} id={this.state.id} title={this.state.title}
content={this.state.content} onClick={this.showEditor} closeEditor={this.closeEditor} />
</div>
);
}
}
便签板完成后,效果图如下:
编辑器
编辑器本身需要完成四个功能:
- 如果是已有便签更新,那么需要显示便签标题、便签正文;
- 右上角需要有一个“上链”的按钮,用户更新完成后可以点击这个按钮完成便签存入;
- 当便签上链交易完成后,编辑器退出;
- 当用户放弃编辑时,点击编辑器外即可退出编辑。
编辑Editor.js,代码如下
/**
* @file editor component
* @author BaaS Team
*/
import React, {Component} from 'react';
import './Editor.scss';
const titleRef = React.createRef();
const contentRef = React.createRef();
export default class Editor extends Component {
constructor(props) {
super(props);
this.saveNote = this.saveNote.bind(this);
}
saveNote() {
fetch('/api/note', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.props.id,
title: titleRef.current.innerText,
content: contentRef.current.innerText
})
})
.then(res => {
res.json();
})
.then(list => this.setState({
noteList: list
}, () => this.props.closeEditor()));
}
render() {
return (
<div className={this.props.show ? 'editor display-block' : 'editor display-none'}
onClick={() => this.props.onClick(this.props.title, this.props.content)}>
<div id="notebook-paper" onClick={e => {
e.stopPropagation();
}
}>
<header>
<h1 id="title-text" contentEditable="true" suppressContentEditableWarning={true}
ref={titleRef}>{this.props.title}</h1>
<button className="btn header-btn" onClick={this.saveNote}>保存</button>
</header>
<div id="content">
<div id="content-text" contentEditable="true"
suppressContentEditableWarning={true} ref={contentRef}>
{this.props.content}
</div>
</div>
</div>
</div>
);
}
}
Editor的样式表:
.editor {
position: fixed;
top: 0;
left: 0;
width:100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}
.editor-main {
position:fixed;
background: white;
width: 80%;
height: auto;
top:50%;
left:50%;
transform: translate(-50%,-50%);
}
.display-block {
display: block;
}
.display-none {
display: none;
}
* {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
-ms-box-sizing:border-box;
-o-box-sizing:border-box;
box-sizing:border-box;
}
body {
background: #f1f1f1;
font-family:helvetica neue, helvetica, arial, sans-serif;
font-weight:200;
}
#notebook-paper {
text-align: left;
color: #050000;
width:960px;
height:500px;
background: linear-gradient(to bottom,white 29px,#00b0d7 1px);
margin:50px auto;
background-size: 100% 30px;
position:relative;
padding-top:150px;
padding-left:160px;
padding-right:20px;
overflow:hidden;
border-radius:5px;
-webkit-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
-moz-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
-ms-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
-o-box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
box-shadow:3px 3px 3px rgba(0,0,0,.2),0px 0px 6px rgba(0,0,0,.2);
&:before {
content:'';
display:block;
position:absolute;
z-index:1;
top:0;
left:140px;
height:100%;
width:1px;
background:#db4034;
}
header {
height:150px;
width:100%;
background:white;
position:absolute;
top:0;
left:0;
h1 {
font-size:60px;
line-height:60px;
padding:127px 20px 0 160px;
}
}
#content {
margin-top:67px;
font-size:20px;
line-height:30px;
}
#hipsum {
margin:0 0 30px 0;
}
}
//Colours
$green: #2ecc71;
$red: #e74c3c;
$blue: #3498db;
$yellow: #f1c40f;
$purple: #8e44ad;
$turquoise: #1abc9c;
// Basic Button Style
.btn {
box-sizing: border-box;
appearance: none;
background-color: transparent;
border: 2px solid $red;
border-radius: 0.6em;
color: $red;
cursor: pointer;
display: block;
align-self: center;
font-size: 3px;
font-weight: 500;
line-height: 1;
margin: 20px;
padding: 5px 2px;
text-decoration: none;
text-align: center;
text-transform: uppercase;
font-family: 'Montserrat', sans-serif;
font-weight: 700;
&:hover,
&:focus {
color: #fff;
outline: 0;
}
}
.header-btn {
border-color: $purple;
// border: 0;
border-radius: 0;
color: $purple;
position: absolute;
top: 20px;
right: 20px;
width: 50px;
height: 30px;
overflow: hidden;
z-index: 1;
transition: color 150ms ease-in-out;
&:after {
content: '';
position: absolute;
display: block;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 100%;
background: $purple;
z-index: -1;
transition: width 150ms ease-in-out;
}
&:hover {
color: #fff;
&:after {
width: 110%;
}
}
}
实现完成后效果如下
编辑便签
到此Dapp应用层开发完毕,我们可以在本地手动测试一下创建便签、更新便签、退出编辑等功能。接下来我们准备将Dapp进行部署。