React-Native-connect-server

Initial React Native

1
$ npx react-native init imapp && cd imapp

Run on ios

1
$ npx react-native-run-ios

Install SocketCluster Client

1
$ yarn add socketcluster-client

Initial Socket

App.js

1
2
3
4
5
6
7
8
import SocketClusterClient from 'socketcluster-client';

let socket = SocketClusterClient.create({
hostname: 'localhost',
port: 8000
});

socket.transmit('foo', 123);

Server 可以收到 data

基本上已經可以確認接通了 SocketCluster Server 與 Client

之後就可以準備其他事情

參考資料

socketcluster-Client

Java-Spring-Boot-variable

Spring Boot

之前在 Java 已經處理了 route, method 的問題

接下來要處理使用 API 溝通時要傳遞參數的時候要如何實作?

  • Path Variable
  • Query String Variable
  • Request Body Variable

Path Variable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.demo.springbootdemo.web;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/books/{id}")
public Object getOne(@PathVariable long id) {
Map<String, Object> book = new HashMap<>();
book.put("id", id);
book.put("name", "new book");
book.put("number", "123jkda");
return book;
}
}

Java-Spring-Boot-Initial

Initial project And Hello world

download IntelliJ IDEA

建立一個新的 spring demo

Image

安裝好之後可以建立一個新的 Controller

src -> main -> java -> {packagename} -> web -> HelloController.java

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.springbootdemo.web;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class HelloController {
@RequestMapping(value="/say", method = RequestMethod.GET)
public String helloGET() {
return "Hello World GET";
}
}

這樣就可以直接新增了一個 http://loclahost:8081/api/say 的 api

@RestController 代表這是一個 Restful API class

@RequestMapping("route") 來處理 url route 問題

  • value: route
  • method: method [GET, POST, PUT, PATCH, DELETE]

SocketCluster-Consumer

Chennel

之前有聊到 Chennel

但是當訊息量越來越大的時候

可以有一些機制來做傳遞與管理

在前端 Subscribe Channel

1
2
3
4
5
6
(async () => {
let channel = socket.subscribe("foo");
for await (let data of channel) {
console.log("forawait -> data", data);
}
})();

在多個前端可以 subscribe 同一個 channel

代表各個前端可以互相溝通

Consumers

SocketCluster 有多個函式可以針對 Channel 做控制

  • socket.listener
  • socket.receiver
  • socket.procedure
  • socket.channel

上述的 function 都會回傳 async iterables

代表可以透過這個方式來取得 data

1
2
3
4
5
6
7
8
9
10
(async () => {
for await (let { socket } of agServer.listener("connection")) {

(async () => {
for await (let data of socket.receiver('foo')) {
console.log("forawait -> data", data)
}
})();
}
})();

這個可以建立很多個不同的 並行 loop 在同一個 stream 上

但是有可能會需要更加有彈性的作法

或是需要有一些緩衝區域

再慢慢消耗的數據

WritableConsumableStream repo

可以參考這個做法

WritableConsumableStream

for-await-of loop 可以利用 ConsumableStream class

ConsumableStream class Example

可以自定義 socket consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const connectionConsumerA = agServer.listener('connection').createConsumer();
const connectionConsumerB = agServer.listener('connection').createConsumer();

(async () => {
for await (let {socket} of connectionConsumerA) {
console.log(`Consumer ${connectionConsumerA.id} handled connection: ${socket.id}`);
}
})();

(async () => {
for await (let {socket} of connectionConsumerB) {
console.log(`Consumer ${connectionConsumerB.id} handled connection: ${socket.id}`);
}
})();

setTimeout(() => {
// Kill only connectionConsumerA.
connectionConsumerA.kill();
}, 1000);

上述範例會建立兩個 stream

當一個 socket 連上也會同時連上兩個 consumer

兩個的 socket.id 也會是一致的

而在一秒後會把 connectionConsumerA 的 socket 關閉

所以一秒後只會有 connectionConsumerB 可以連上

這樣可以更加彈性的控制 socket 的連線

可以在執行之前做一些事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(async () => {
for await (let {socket} of agServer.listener('connection')) {

(async () => {
console.log('doSomethingWhichTakesAFewSeconds', socket.id)

for await (let data of socket.receiver('foo')) {
console.log("forawait -> data", data)
// ...
}
})();

}
})();

在每個連線之前都可以執行一段程式碼

共用邏輯可以放置在這邊

特殊情境

backend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sleep = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log('doSomethingWhichTakesAFewSeconds');
resolve();
}, 1000)
});
}
(async () => {
for await (let {socket} of agServer.listener('connection')) {

(async () => {
await sleep();

for await (let data of socket.receiver('foo')) {
console.log("forawait -> data", data)
}
})();

}
})();

frondend

1
2
3
4
5
let socket = socketClusterClient.create();

for await (let event of socket.listener('connect')) {
socket.transmit('foo', 123);
}

上述程式碼執行的時候

Backend 會因為 await sleep(); 非同步問題

socket.receiver('foo') 在非同步之後

會無法執行到 console.log("forawait -> data", data)

所有的情境都會造成訊息的丟失

所以需要做一些調整

調整後

如果只是調整順序的話並不能解決問題

Backend

Bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(async () => {
for await (let {socket} of agServer.listener('connection')) {

(async () => {
// This will not work because the iterator is not yet created at this point.
let fooStream = socket.receiver('foo');

// If any messages arrive during this time, they will be ignored!
await doSomethingWhichTakesAFewSeconds();

// The iterator gets created (and starts buffering) here!
for await (let data of fooStream) {
// ...
}
})();

}
})();

Backend

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(async () => {
for await (let {socket} of agServer.listener('connection')) {

(async () => {
// This will create a consumable which will start buffering messages immediately.
let fooStreamConsumable = socket.receiver('foo').createConsumer();

await sleep();

// This loop will start from the beginning of the buffer.
for await (let data of fooStreamConsumable) {
console.log("forawait -> data", data)
}
})();

}
})();

下一個章節再來繼續處理一下 API 相關問題

參考資料

Consumer

SocketCluster Cluster initial.md

Basic Install

Install

nvm

1
2
3
4
5
6
7
8
9
$ nvm install 14.9.0
$ nvm use 14.9.0
$ npm install -g socketcluster@16.0.1
$ socketcluster --help
$ socketcluster create helloworld
$ cd helloworld
$ yarn start // yarn start
$ socketcluster run // run on docker
$ curl http://localhost:8000/health-check // will get OK

也可以試著 使用瀏覽器打開 http://localhost:8000/index.html

預設會有一個簡單的頁面連線 websocket

初始化之後會有一個基礎的骨架來處理 http request 與 socket connections

HTTP Request Handler

1
2
3
4
5
;(async () => {
for await (let requestData of httpServer.listener('request')) {
expressApp.apply(null, requestData)
}
})()

http 的部分不需要再增加程式碼

可以直接使用

Socket connections

1
2
3
4
5
6
;(async () => {
for await (let { socket } of agServer.listener('connection')) {
console.log('forawait -> socket.id', socket.id)
// Handle socket connection.
}
})()

Listen custom Event in socket

Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SocketCluster/WebSocket connection handling loop.
;(async () => {
for await (let { socket } of agServer.listener('connection')) {
console.log(
'forawait -> socket.id',
socket.id
)(
// Handle socket connection.
async () => {
// Set up a loop to handle remote transmitted events.
for await (let data of socket.receiver('helloworld')) {
try {
console.warn('forawait -> helloworld -> data', data)
} catch (error) {
console.log('forawait -> error.message', error.message)
}
}
}
)()
}
})()

可以再加上 try catch 來處理 Error

Client
1
2
3
setTimeout(() => {
socket.transmit('helloworld', 123)
}, 1000)

也可以透過 procedure 回傳一些資訊

Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;(async () => {
// Set up a loop to handle and respond to RPCs.
for await (let request of socket.procedure('customProc')) {
try {
if (request.data && request.data.bad) {
let badCustomError = new Error('Server failed to execute the procedure')
badCustomError.name = 'BadCustomError'
throw badCustomError
}
request.end('Success')
} catch (error) {
console.log('forawait -> customProc -> error.message', error.message)
request.error(error)
}
}
})()

也可以利用 request.error(error) 來處理回傳錯誤

Client 也可以利用 try catch 來接收到錯誤

Subscribe and Publish

另外也可以建立 channel 來處理 subscribe 與 publish

Client 中加上

1
2
3
4
5
6
(async () => {
let channel = socket.subscribe('foo');
for await (let data of channel) {
// ... Handle channel data.
}
})();

就可以在開始聽取 foo 的頻道

接受該頻道的訊息

不只是 Server 的訊息會接收

其他 Client 也可以透過這些頻道彼此溝通

Client

不需要 acknowledgment 的訊息

1
socket.transmitPublish('foo', 'This is some data');

需要 acknowledgment 的訊息

1
await socket.invokePublish('foo', 'This is some more data');

Server

不需要 acknowledgment 的訊息

1
agServer.exchange.transmitPublish('foo', 'This is some data');

需要 acknowledgment 的訊息

1
await agServer.exchange.invokePublish('foo', 'This is some more data');

不管是 Client 或是 Server 如果是需要 acknowledmgment 的訊息
需要配合 Consumers 的配合

參考資料

Basic Usage

SocketCluster-Authorization

Authorization

在 SocketCluster 預設使用 JWT 處理驗證問題

在AGServer 之中有一個參數 authKey 是一個字串,提供 JWT 的 token 建立與驗證使用

Client 也可以使用 socket.authenticate

因為可能一個服務或多個服務 同時會有 HTTP 與 Websocket

所以會希望同一個 token 可以在各服務內使用

做使用者的驗證

建立 JWT Token

HTTP

最基本的使用方式是透過 Express 來建立 token

然後再將此 token 送到客戶短提供使用

客戶端獲得這個 token 的之後必須要加到 socketcluster.authToken

這是 SocketCluster 的預設 JWT localStorage token

建立新連線的時候或是重新連線時 SocketCluster 會自動在 localStorage 取得 JWT

Server

新增一個 Express 的 route

因為是 demo

所以先用 GET 來測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

expressApp.get('/login', async (req, res) => {
const myTokenData = {
username: 'bob',
language: 'English',
company: 'Google',
groups: ['engineering', 'science', 'mathematics']
};

let signedTokenString = await agServer.auth.signToken(myTokenData, agServer.signatureKey);

res.status(200).json({
token: signedTokenString
});
});

之後在瀏覽器測試可以取得 token

1
2
3
{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvYiIsImxhbmd1YWdlIjoiRW5nbGlzaCIsImNvbXBhbnkiOiJHb29nbGUiLCJncm91cHMiOlsiZW5naW5lZXJpbmciLCJzY2llbmNlIiwibWF0aGVtYXRpY3MiXSwiaWF0IjoxNTk5NTczMzA1fQ.TBwhqJlhVlpEwCcqsv9-JT5Vx7Z32D4YpCUebEDZSHQ"
}

Websocket

利用 WebSocket 建立 token 的範例

Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(async () => {
for await (let request of socket.procedure('login')) {
try {
console.log("forawait -> request.data", request.data)
//chgeck use done



socket.setAuthToken({username: request.data.username});
request.end();
return;
} catch(error) {
console.log("forawait -> error", error)
let loginError = new Error(`Could not find a ${request.data.username} user`);
console.log("forawait -> loginError", loginError)
loginError.name = 'LoginError';
request.error(loginError);

return;
}
}
})();
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(async () => {

try {
// Invoke a custom 'login' procedure (RPC) on our server socket
// then wait for the socket to be authenticated.
const [, authResult] = await Promise.all([
socket.invoke("login", credentials),
socket.listener("authenticate").once(),
]);
console.log("authResult", JSON.stringify(authResult))
} catch (error) {
console.log("error", error)
// showLoginError(err);
return;
}
})();

Client 可以拿到的 Resonse 是

1
2
3
4
5
6
7
8
{
"signedAuthToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWNlMTIzIiwiaWF0IjoxNTk5NTc2MDkyLCJleHAiOjE1OTk2NjI0OTJ9.fccJ4zBdCqpoXrHW-NWxEK9r5ykMYyA0aokQRZitUmw",
"authToken": {
"username": "alice123",
"iat": 1599576092,
"exp": 1599662492
}
}

驗證 JWT Token

在 SocketCluster 中不論是 HTTPWebSocket 的驗證方式都是一樣的

但是在這之前要先了解 SocketClsuter 的 Middleware 的使用方式

Middleware

SocketCluster 中可以註冊 Middleware

支援的類別總共四種

  • agServer.MIDDLEWARE_HANDSHAKE
  • agServer.MIDDLEWARE_INBOUND_RAW
  • agServer.MIDDLEWARE_INBOUND
    • from client -> server
  • agServer.MIDDLEWARE_OUTBOUND
    • from server -> client

註冊 Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
agServer.setMiddleware(agServer.MIDDLEWARE_INBOUND, async (middlewareStream) => {
for await (let action of middlewareStream) {
console.log("forawait -> action.type", action.type)
if (action.type === action.TRANSMIT) {
if (!action.data) {
let error = new Error(
'Transmit action must have a data object'
);
error.name = 'InvalidActionError';
action.block(error);
continue;
}
} else if (action.type === action.INVOKE) {
if (!action.data) {
let error = new Error(
'Invoke action must have a data object'
);
error.name = 'InvalidActionError';
action.block(error);
continue;
}
// token 的物件
console.log("forawait -> action.data", action.data)
}
action.allow();
}
});

利用註冊 IN_BOUINDOUT_BOUND 的註冊 middleware

來達成驗證 JWT token

參考資料

Authorization

React Native-ExportComponentUI

Setup

1
2
3
react-native init CounterApp
cd CounterApp
react-native run-ios

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity,
} from 'react-native';


class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 })
}

render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={[styles.wrapper, styles.border]}
onPress={this.increment}
>
<Text style={styles.button}>
{this.state.count}
</Text>
</TouchableOpacity>
</View>
);
}

}
const styles = StyleSheet.create({
container: {
flex: 1, alignItems: "stretch"
},
wrapper: {
flex: 1, alignItems: "center", justifyContent: "center"
},
border: {
borderColor: "#eee", borderBottomWidth: 1
},
button: {
fontSize: 50, color: "orange"
}
});

export default App;

How to expose a Swift UI Component to JS

建立一個 Swift ViewManager

  • File → New → File… (or CMD+N)
  • Select Swift File
  • Name your file CounterViewManager
  • In the Group dropdown, make sure to select the group CounterApp, not the project itself.

!select CounterApp

Configure the Objective-C Bridging Header

1
After you create the Swift file, you should be prompted to choose if you want to configure an Objective-C Bridging Header. Select “Create Bridging Header”.

configure Objective-C Bridging Header

如果您還沒有標題,請立即添加其中兩個標題

1
2
3
4
// CounterApp-Bridging-Header.h

#import "React/RCTBridgeModule.h"
#import "React/RCTViewManager.h"

CounterViewManager.swift

1
2
3
4
5
6
7
8
9
@objc(CounterViewManager)
class CounterViewManager: RCTViewManager {
override func view() -> UIView! {
let label = UILabel()
label.text = "Swift Counter"
label.textAlignment = .center
return label
}
}

新增一個 Obj-C 檔案

  • File → New → File… (or CMD+N)
  • Select Objective-C File
  • Name your file CounterViewManager

CounterViewManager.m

1
2
3
4
5

#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE(CounterViewManager, RCTViewManager)
@end

Access your Component from JS

現在你可以使用 requireNativeComponent 來使用

建立 swiftUI

CounterViewManager.swift

1
2
3
4
5
6
7
8
9
10
@objc(CounterViewManager)
class CounterViewManager: RCTViewManager {
override static func requiresMainQueueSetup() -> Bool {
return true
}

override func view() -> UIView! {
return CounterView()
}
}

CounterView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
class CounterView: UIView {
@objc var count = 0 {
didSet {
button.setTitle(String(describing: count), for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(button)
increment()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var button: UIButton = {
let b = UIButton.init(type: UIButton.ButtonType.system)
b.titleLabel?.font = UIFont.systemFont(ofSize: 50)
b.autoresizingMask = [.flexibleWidth, .flexibleHeight]
b.addTarget(
self,
action: #selector(increment),
for: .touchUpInside
)
return b
}()
@objc func increment() {
count += 1
}
}

How to send props to a Swift Component

可以透過 RCT_EXPORT_VIEW_PROPERTY 來 export props

在這個範例中 將 count export

CounterViewManager.m

1
2
3
4
5
#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE(CounterViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(count, NSNumber)
@end

Important: you have to use Obj-C types for variables exposed to React Native

CounterView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
class CounterView: UIView {
@objc var count: NSNumber = 0 {
didSet {
button.setTitle(String(describing: count), for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(button)
increment()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var button: UIButton = {
let b = UIButton.init(type: UIButton.ButtonType.system)
b.titleLabel?.font = UIFont.systemFont(ofSize: 50)
b.autoresizingMask = [.flexibleWidth, .flexibleHeight]
b.addTarget(
self,
action: #selector(increment),
for: .touchUpInside
)
return b
}()
@objc func increment() {
count = count.intValue + 1 as NSNumber
}
}

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

import React from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity,
requireNativeComponent,
} from 'react-native';

const CounterView = requireNativeComponent("CounterView");

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 })
}

render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={[styles.wrapper, styles.border]}
onPress={this.increment}
>
<Text style={styles.button}>
{this.state.count}
</Text>
</TouchableOpacity>

<CounterView style={ styles.wrapper } count={2} />
</View>
);
}

}
const styles = StyleSheet.create({
container: {
flex: 1, alignItems: "stretch"
},
wrapper: {
flex: 1, alignItems: "center", justifyContent: "center"
},
border: {
borderColor: "#eee", borderBottomWidth: 1
},
button: {
fontSize: 50, color: "orange"
}
});

export default App;

Expose a Component Event Emitter

接下來使用一個 function 來讓 native 使用

CounterView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import UIKit
class CounterView: UIView {

@objc var onUpdate: RCTDirectEventBlock?

@objc var count: NSNumber = 0 {
didSet {
button.setTitle(String(describing: count), for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(button)
increment()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

lazy var button: UIButton = {
let b = UIButton.init(type: UIButton.ButtonType.system)
b.titleLabel?.font = UIFont.systemFont(ofSize: 50)
b.autoresizingMask = [.flexibleWidth, .flexibleHeight]
b.addTarget(
self,
action: #selector(increment),
for: .touchUpInside
)

let longPress = UILongPressGestureRecognizer(
target: self,
action: #selector(sendUpdate(_:))
)
b.addGestureRecognizer(longPress)

return b
}()

@objc func sendUpdate(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
if onUpdate != nil {
onUpdate!(["count": count])
}
}
}

@objc func increment() {
count = count.intValue + 1 as NSNumber
}
}

If you have to send any data to a RCTDirectEventBlock method, you must return a [AnyHashable:Any] structure. This means that you can’t pass a String or Int directly, you have to put them in a Dictionary.

CounterViewManager.m

1
2
3
4
5
6
#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE(CounterViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(count, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onUpdate, RCTDirectEventBlock)
@end

在 header 也要增加一行

Expose methods on the ViewManager

CounterApp-Bridging-Header.h

1
2
3
4
#import "React/RCTBridgeModule.h"
#import "React/RCTViewManager.h"
#import "React/RCTEventEmitter.h"
#import "React/RCTUIManager.h"

CounterViewManager.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@objc(CounterViewManager)
class CounterViewManager: RCTViewManager {
override static func requiresMainQueueSetup() -> Bool {
return true
}

override func view() -> UIView! {
return CounterView()
}

@objc func updateFromManager(_ node: NSNumber, count: NSNumber) {

DispatchQueue.main.async { // 2
let component = self.bridge.uiManager.view( // 3
forReactTag: node // 4
) as! CounterView // 5
component.update(value: count) // 6
}
}
}

CounterView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import UIKit

class CounterView: UIView {

@objc var onUpdate: RCTDirectEventBlock?

@objc var count: NSNumber = 0 {
didSet {
button.setTitle(String(describing: count), for: .normal)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(button)
increment()
}

static func requiresMainQueueSetup() -> Bool {
return true
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

lazy var button: UIButton = {
let b = UIButton.init(type: UIButton.ButtonType.system)
b.titleLabel?.font = UIFont.systemFont(ofSize: 50)
b.autoresizingMask = [.flexibleWidth, .flexibleHeight]
b.addTarget(
self,
action: #selector(increment),
for: .touchUpInside
)

let longPress = UILongPressGestureRecognizer(
target: self,
action: #selector(sendUpdate(_:))
)
b.addGestureRecognizer(longPress)

return b
}()

@objc func update(value: NSNumber) {
count = value
}

@objc func sendUpdate(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
if onUpdate != nil {
onUpdate!(["count": count])
}
}
}

@objc func increment() {
count = count.intValue + 1 as NSNumber
}
}

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/

import React from 'react';
import {
StyleSheet,
View,
Text,
UIManager,
TouchableOpacity,
requireNativeComponent,
} from 'react-native';

const CounterView = requireNativeComponent("CounterView");

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 })
}
update = e => {
this.setState({
count: e.nativeEvent.count
})
}

_onUpdate = event => {
if (this.props.onUpdate) {
this.props.onUpdate(event.nativeEvent);
}
};

updateNative = () => {
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.counterRef), // 1
UIManager["CounterView"].Commands.updateFromManager, // 2
[this.state.count] // 3
);
}

render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={[styles.wrapper, styles.border]}
onPress={this.increment}
>
<Text style={styles.button}>
{this.state.count}
</Text>
</TouchableOpacity>

<CounterView
style={ styles.wrapper }
count={this.state.count}
onUpdate={this._onUpdate}
ref={ref => this.ref = ref}
/>
</View>
);
}

}
const styles = StyleSheet.create({
container: {
flex: 1, alignItems: "stretch"
},
wrapper: {
flex: 1, alignItems: "center", justifyContent: "center"
},
border: {
borderColor: "#eee", borderBottomWidth: 1
},
button: {
fontSize: 50, color: "orange"
}
});

export default App;

參考文章

Swift in React Native

Moleculer-Start

Usage

Create your first microservice

demo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { ServiceBroker } = require("moleculer");

const broker = new ServiceBroker();

broker.createService({
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
}
}
});

broker.start()
// Call service
.then(() => broker.call("math.add", { a: 5, b: 3 }))
.then(res => console.log("5 + 3 =", res))
.catch(err => console.error(`Error occured! ${err.message}`));
1
2
3
4
5
6
7
8
9
10
11
[2019-06-03T07:54:26.366Z] INFO  ********/BROKER: Moleculer v0.13.9 is starting...
[2019-06-03T07:54:26.369Z] INFO ********/BROKER: Node ID: ********
[2019-06-03T07:54:26.370Z] INFO ********/BROKER: Namespace: <not defined>
[2019-06-03T07:54:26.370Z] INFO ********/REGISTRY: Strategy: RoundRobinStrategy
[2019-06-03T07:54:26.372Z] INFO ********/BROKER: Serializer: JSONSerializer
[2019-06-03T07:54:26.373Z] INFO ********/BROKER: Registered 10 internal middleware(s).
[2019-06-03T07:54:26.390Z] INFO ********/REGISTRY: '$node' service is registered.
[2019-06-03T07:54:26.392Z] INFO ********/REGISTRY: 'math' service is registered.
[2019-06-03T07:54:26.394Z] INFO ********/BROKER: ServiceBroker with 2 service(s) is started successfully.
5 + 3 = 8
[2019-06-03T07:54:26.400Z] INFO ********/BROKER: ServiceBroker is stopped. Good bye.

看到上方範例會啟用一個 microservice

計算出 5 + 3 = 8

之後結束這個程式

Create a Moleculer project

Install Nats

需要先安裝 Nats

如果您是選擇其他的 transporters 也需要安裝其他的套件

目前有提供的

  • Nats - 推薦使用
  • MQTT
  • Redis
  • NATS streaming (試驗)
  • Kafka (試驗)

Mac

1
$ brew install gnatsd

Go

Initial Project

有提供一個 Cli tool

install moleculer-cli

1
$ npm i moleculer-cli -g

create a new project

1
$ moleculer init project moleculer-demo

client 會提供幾個選項讓你選擇

1
2
3
4
5
6
7
8
9
10
? Add API Gateway (moleculer-web) service? // 是否使用 api gateway
? Would you like to communicate with other nodes? // 是否需要和其他 nodes 溝通
? Select a transporter NATS (recommended) // 使用哪一種 transporter 工具
? Would you like to use cache? 是否要使用 cache
? Select a cacher solution
? Add Docker files? 是否要使用 Docker
? Use ESLint to lint your code? 是否使用 ESLint
? Setup unit tests with Jest? Unitest framework
Create 'moleculerdemo' folder...
? Would you like to run 'npm install'?

可以依據個人的需求選擇

然後就可以得到一個專案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── README.md
├── moleculer.config.js
├── package-lock.json
├── package.json
├── public
│   ├── banner.png
│   ├── favicon.ico
│   └── index.html
├── services
│   ├── api.service.js
│   └── greeter.service.js
└── test
└── unit
└── greeter.spec.js
1
$ yarn dev // 啟動一個 service

Broker

ServiceBrokerMoleculer 中主要的 component

他會處理幾件 nodes 之間溝通的事情

  • actions
  • emits
  • events
  • communicates

Broker Options

Ping

對遠端的 nodes 使用 broker.ping 來確認遠端 nodes 的狀態

回傳值是一個 Promise

Example

ping 一個 node 並設定 1S 的 timeout

1
broker.ping("node-123", 1000).then(res => broker.logger.info(res));

output

1
2
3
4
5
{ 
nodeID: 'node-123',
elapsedTime: 16,
timeDiff: -3
}
1
timeDiff 是兩個節點之間系統時間的誤差值

也可以同時 ping 多個 nodes

1
broker.ping(["node-100", "node-102"]).then(res => broker.logger.info(res));

output

1
2
3
4
5
6
7
8
9
10
11
12
{ 
"node-100": {
nodeID: 'node-100',
elapsedTime: 10,
timeDiff: -2
},
"node-102": {
nodeID: 'node-102',
elapsedTime: 250,
timeDiff: 850
}
}

ping 所有的 nodes

1
broker.ping().then(res => broker.logger.info(res));

output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

{
"node-100": {
nodeID: 'node-100',
elapsedTime: 10,
timeDiff: -2
} ,
"node-101": {
nodeID: 'node-101',
elapsedTime: 18,
timeDiff: 32
},
"node-102": {
nodeID: 'node-102',
elapsedTime: 250,
timeDiff: 850
}
}

broker properties

broker methods

Services

Services 代表 Moleculer 中的微服務

可以定義多個 action 並且訂閱 ‘event’

建立新的 service 必須先定義好 schema

這些 schema 類似 component of Vuejs

  • name
  • version
  • settings
  • methods
  • events

Actions

基本的 schema

1
2
3
4
{
name: "posts",
version: 1
}

定義多個 actions

1
2
3
4
5
6
7
8
9
10
11
12
{
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},

sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
}

name 是必須要定義的參數

當你呼叫這個 api 時,是第一部分的 route 組成元素

1
2
3
4
5
// 可以在設定中 disable service name prefix

{
$noServiceNamePrefix: true
}

version 不是必要的參數

可以讓同樣的 service 跑不同的 version 做 api 版本控制

可以是 NumberString

1
2
3
4
5
6
7
{
name: "posts",
version: 2,
actions: {
find() {...}
}
}

在上方的範例中若是要呼叫這隻 API route 為 GET /v2/posts/find

Settings

settings 是一個 store

你可以在裡面做各種設定

使用 this.settings 取得你的 setting object

setting options

Mixins

Mixins 是一個可以在 Moleculer 中可以重複使用的 function

Service 的 constructor 會自動合併這些 mixins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ApiGwService = require("moleculer-web");

module.exports = {
name: "api",
mixins: [ApiGwService]
settings: {
// Change port setting
port: 8080
},
actions: {
myAction() {
// Add a new action to apiGwService service
}
}
}

合併的規則

Lifecycle events

當 service 生命週期各自會 trigger 的 function

  • startd
  • stopped
  • created

Dependencies

當你的 service 有依賴到其他 service 的時候

可以利用 Dependencies 來做處理(待捕)

Hot reloading services

在開發過程中需要使用 hot reloading 的機制有兩種方式

1
2
3
4
5
const broker = new ServiceBroker({
hotReload: true
});

broker.loadService("./services/test.service.js");

或是 command

1
$ moleculer-runner --hot ./services/test.service.js

Local variables

如果你需要一些 Local variables

可以在 created 中宣告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const http = require("http");

module.exports = {
name: "www",

settings: {
port: 3000
},

created() {
// Create HTTP server
this.server = http.createServer(this.httpHandler);
},

started() {
// Listening...
this.server.listen(this.settings.port);
},

stopped() {
// Stop server
this.server.close();
},

methods() {
// HTTP handler
httpHandler(req, res) {
res.end("Hello Moleculer!");
}
}
}

ES6 classses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const Service = require("moleculer").Service;

class GreeterService extends Service {

constructor(broker) {
super(broker);

this.parseServiceSchema({
name: "greeter",
version: "v2",
meta: {
scalable: true
},
dependencies: [
"auth",
"users"
],

settings: {
upperCase: true
},
actions: {
hello: this.hello,
welcome: {
cache: {
keys: ["name"]
},
params: {
name: "string"
},
handler: this.welcome
}
},
events: {
"user.created": this.userCreated
},
created: this.serviceCreated,
started: this.serviceStarted,
stopped: this.serviceStopped,
});
}

// Action handler
hello() {
return "Hello Moleculer";
}

// Action handler
welcome(ctx) {
return this.sayWelcome(ctx.params.name);
}

// Private method
sayWelcome(name) {
this.logger.info("Say hello to", name);
return `Welcome, ${this.settings.upperCase ? name.toUpperCase() : name}`;
}

// Event handler
userCreated(user) {
this.broker.call("mail.send", { user });
}

serviceCreated() {
this.logger.info("ES6 Service created.");
}

serviceStarted() {
this.logger.info("ES6 Service started.");
}

serviceStopped() {
this.logger.info("ES6 Service stopped.");
}
}

module.exports = GreeterService;

React Native-Platform

Platform

Platform Specific Code

在初期的時候可以利用檔案模式來做整合

可以將檔案名稱命名為 .ios.js.android.js

然後在不同的平台上 require 不同的檔案

Example

1
2
BigButton.ios.js
BigButton.android.js

然後就可以在你想要的地方 import 檔案

1
import BigButton from './BigButton';

因為 React Native 是跨平台的 Framework

畢竟 IOS Android 之間還是有相當的不同之處

可以利用 Platform 這個模組來統一的整理

1
2
3
4
5
import {Platform, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
height: Platform.OS === 'ios' ? 200 : 100,
});

Platform module 會依據平台來執行相對應的程式碼

如果是 ios 的話 height 是 200

如果是 android 的話 height 是 100

另外也有 Platform.select 可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Platform, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});

上述程式碼中使用到 container Style 的都會有 flex: 1 的參數

但是在 ios 中會是紅色

android 中會是藍色的背 景色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react';
import { StyleSheet, Text, View, Platform } from 'react-native';

const SpecificPlatformComponent = Platform.select({
ios: () => <Text>I am use IOS</Text>,
android: () => <Text>I am use Android</Text>,
});

export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<SpecificPlatformComponent />
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

或是像是這樣也可以依據不同的平台引用 component

OS Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { StyleSheet, Text, View, Platform } from 'react-native';

const SpecificPlatformComponent = Platform.select({
ios: () => <Text>I am use IOS</Text>,
android: () => <Text>I am use Android</Text>,
});

const SpecificPlatformVersionComponent = Platform.select({
ios: () => <Text>my Iphone Version is {Platform.Version}</Text>,
android: () => <Text>my Android Version is {Platform.Version}</Text>,
});


export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<SpecificPlatformComponent />
<SpecificPlatformVersionComponent />
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

Truffle-pet-user-interface

Truffle pet demo

延續

建立一個前端介面

上一篇初始化的程式中也包含了前端的程式碼

但是只有一部分

需要做一些補全

  1. 打開 /src/js/app.js
  2. app.js 裡面已經有 一個物件叫做 App 控制我們的前端
  3. init() 負責 load data
  4. initWeb3() web3 lib他可以取回使用者帳號的資訊,發送交易需求
  5. 移除 initWeb3() 中的註解 加上下面這段程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
initWeb3: async function() {
if(window.ethereum) {
App.web3Provider = window.ethereum;

try {
await window.ethereum.enable();
} catch (error) {
console.error("User denied account access")
}
} else if(window.web3) {
App.web3Provider = window.web3.currentProvider;
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
}

web3 = new Web3(App.web3Provider);

return App.initContract();
},
  • 先檢查瀏覽器中是否有 ethereum provider 如果有的話就建立自己的 web3 物件來取得帳號資訊,但是依舊要呼叫 ethereum.enable()
  • 如果 ethereum 不存在,檢查 window.web3 是否存在來引用舊版的 provider
  • 如果沒有的話就是測試使用 localhost:7545

Instantiating the contract

處理好 web3 的初始化之後

現在需要來解決一下如何實際在前端取得合約的資料

truffle 有一個 lib 可以協助處理這件事情: truffle-contract

他會在 有新的 migrations 同步合約上的資訊

所以你不用修改合約的位址

  1. /src/js/app.js 中有 initContract 的函式
1
2
3
4
5
6
7
8
9
10
11
12
initContract: function() {
$.getJSON('Adoption.json', function(data) {
const AdoptionArtifact = data;
App.contracts.Adoption = TruffleContract(AdoptionArtifact);

App.contracts.Adoption.setProvider(App.web3Provider);

return App.markAdopted();
});

return App.bindEvents();
},
  • 先檢索在鏈上的合約文件,AdoptionArtifact 是合約的內容資訊,包含位址 DApp 接口(ABI)
  • 一但我們取得 Artifact 會將他 pass 給 TruffleContract(). 他會產生一個新的物件,這個物件會提供一下 method 讓我們可以跟合約溝通
  • 產生的 實例會設定給 App.web3Provider 以方便 web3使用
  • 然後呼叫 APP 的 markAdopted() function 封裝這個是因為方便在合約改變的時候可以同時改變 UI
1
2
Note: ABI: Application Binary Interface 應用程式機器碼介面
ABI 是一個 Javascript 對象,用來定義如何跟合約相互溝通的接口

Getting The Adopted Pets and Updating The UI

/src/js/app.js 中移除 markAdopted 中的註解並填入下述程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
markAdopted: function(adopters, account) {
let adoptionInstance;

App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
return adoptionInstance.call();
}).then(function(adopters) {
for(i=0; i<adopters.length; i++) {
if(adopters[i] !== '0x0000000000000000000000000000000000000000') {
$('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
}
}
}).catch(function(error) {
console.log(error.message)
});
},

這一段程式碼中闡述了

  • 同意部署 Adoption 合約,然後呼叫 getAdopters() 在這個實例中
  • 因為宣告了adoptionInstance 在最外面,所以在之後可以呼叫他
  • call() 可以讓我們讀取資料而不用發送一個交易,代表我們不用花費任何乙太幣
  • 呼叫 getAdopters() 使用一個迴圈訪問所有的 pet 確定是否已經有了位址,因為以太使用 16 個位元的初始值,而不是用 null
  • 如果找到有相對應位址的就禁用按鈕並將按鈕的文字改為 成功 以便使用者了解這些資訊
  • 所有的錯誤都會顯示在 console

Handling the adopt() Function

依舊是在 /src/js/app.js 中,移除 handleAdopt 函式中的註解,替換為下列程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
handleAdopt: function(event) {
event.preventDefault();

let petId = parseInt($(event.target).data('id'));

let adoptionInstance;

web3.eth.getAccounts(function(error, accounts) {
if(error) {
console.log(error);
}

let account = accounts[0];

App.contracts.Adoption.deployed()
.then(function(instance) {
adoptionInstance = instance;
return adoptionInstance.adopt(petId, {from: account});
}).then(function(result) {
return App.markAdopted();
}).catch(function(error) {
console.log(error.message);
});
});
}
  • 利用 web3 取得使用者帳號列,含有 error 檢查是否錯誤,若無誤 取第一個帳號
  • 在這裡已經取得部署成功的合約,將它 指定給 adoptionInstance 這個變數,這一次我們會發送一個交易請求而且必須要有 from 這個位址,這個動作會產生一些費用,在乙太中 這個費用叫做 gas,這是一種手續費用,在你儲存或是計算的時候需要付出部分 gas adopt() 中含有 寵物的 ID 和一個物件,這些資訊會被儲存在 account
  • 發送交易後的結果是一個物件,如果沒有錯誤的話會呼叫 markAdopted() 來同步 UI 和儲存的資訊

最簡單的方式是透過 MetaMask 這個在 chrome 和 firefox 都有相關的擴充套件

  1. 安裝 MetaMask
  2. 點擊同意
  3. 閱讀如何使用
  4. 輸入密碼
  5. 完成
  6. 在 MetaMask 中選擇 New RPC URL 並且輸入 http://127.0.0.1:7545

Installing and configuring lite-server

可以起一個 local 的 service 來看看結果

  1. 編輯 bs-config.json 改為下述程式碼
1
2
3
4
5
{
"server": {
"baseDir": ["./src", "./build/contracts"]
}
}
1
$ npm run dev

Result

Image

|