React Native Cache PartI

LRU Cache

Cache 是兩個存取速度差異的硬體

同步兩者資料的架構

LRU 是 Least Recently Used 的縮寫

表示最近較少使用的會優先被替換掉

實作邏輯

LRU 快取是一個固定容量的 map

如果快取是滿的時候

仍需要插入一個新的元素

找出最近最少使用的元素來替換

而不會增加 Cache 的容量大小

  • 需要一個類似hashmap 的架構
  • 一種將所有元素按照訪問順徐來儲存,有效率的移動元素可以藉由雙向連結

需要有兩個操作

  • #.set
  • #.get

可以參考 lru github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const LRU = require('lru');

const cache = new LRU(2),
evicted

cache.on('evict',function(data) { evicted = data });

cache.set('foo', 'bar');
cache.get('foo'); //=> bar

cache.set('foo2', 'bar2');
cache.get('foo2'); //=> bar2

cache.set('foo3', 'bar3'); // => evicted = { key: 'foo', value: 'bar' }
cache.get('foo3'); // => 'bar3'
cache.remove('foo2') // => 'bar2'
cache.remove('foo4') // => undefined
cache.length // => 1
cache.keys // => ['foo3']

cache.clear() // => it will NOT emit the 'evict' event
cache.length // => 0
cache.keys // => []

雙向鍊表

雙向鍊表

既可以從頭到尾訪問,也可以從尾到頭訪問

一個節點會有前面的 ref 也會有一個向後的 ref

image

  • 雙向鍊表不僅有 head 指向第一個節點,而且有 tail 指向最後一個節點
  • 每一個節點由三部分組成:item儲存數據、prev指向前一個節點、next指向後一個節點
  • 雙向鍊表的第一個節點的 prev 指向null
  • 雙向鍊表的最後一個節點的 next 指向null

雙向鍊錶常見的操作:

  • append(element)
    • 雙向鍊表尾部添加一個新的元素
  • inset(position,element)
    • 雙向鍊表的特定位置插入一個新的元素
  • get(element)
    • 獲取對應位置的元素
  • indexOf(element)
    • 返回元素在鍊錶中的 index,如果雙向鍊表中沒有元素就返回 -1
  • update(position,element)
    • 修改某個位置的元素
  • removeAt(position)
    • 從雙向鍊表的特定位置移除一項
  • isEmpty()
    • 如果雙向鍊表中不包含任何元素,返回trun,如果雙向鍊表長度大於 0 則返回 false
  • size()
    • 返回雙向鍊表包含的元素個數,與 陣列 的length屬性類似
  • toString()
    • 由於雙向鍊表中需要重寫繼承自JavaScript對象默認的toString方法,只輸出元素的值
  • forwardString()
    • 返回正向訪問節點字符串形式
  • backwordString()
    • 返回反向訪問的節點的字符串形式

React-Native-Notification-Android

Android

Initial React Native App

1
$ react-native init rn_notification

Install

1
$ yarn add @react-native-firebase/app @react-native-firebase/messaging

Firebase

Download GoogleService-Info.plist

configure Android

index.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
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import messaging from '@react-native-firebase/messaging';

async function requestUserPermission() {
try {
const messageinstance = messaging();
const authStatus = await messageinstance.requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;

if (enabled) {
await messageinstance.registerDeviceForRemoteMessages();
const token = await messageinstance.getToken();
console.log('token:', token);
}
} catch(error) {
console.log("requestUserPermission -> error", error)
}
}

requestUserPermission();
AppRegistry.registerComponent(appName, () => App);

APNS Service

wiki Apns Service

Next - Android push notification

參考資訊

react-native-permissions

issue

issue2

firebase

React-Native-Notification-IOS

IOS

Initial React Native App

1
$ react-native init rn_notification

Install

1
2
$ yarn add @react-native-firebase/app @react-native-firebase/messaging
$ cd ios && pod install && cd ..

Firebase

Download GoogleService-Info.plist

podFile

1
2
3
4
5
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

$FirebaseSDKVersion = '6.29.0'
... other settings

AppDelegate.m

1
2
3
4
...import
#import <Firebase.h>
... others settings

index.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
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import messaging from '@react-native-firebase/messaging';

async function requestUserPermission() {
try {
const messageinstance = messaging();
const authStatus = await messageinstance.requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;

if (enabled) {
await messageinstance.registerDeviceForRemoteMessages();
const token = await messageinstance.getToken();
console.log('token:', token);
}
} catch(error) {
console.log("requestUserPermission -> error", error)
}
}

requestUserPermission();
AppRegistry.registerComponent(appName, () => App);

APNS Service

wiki Apns Service

Image

Demo

Image

Next - Android push notification

參考資訊

react-native-permissions

issue

issue2

firebase

Socketcluster-client-AuthorizationEngine

需求

處理 JWT 的時候如果在瀏覽器可以將 token 存在 localStorage 中的 socketcluster.authToken

但是如果在 React Native 中沒有 localStorage 的模組

可以使用 jest-localstorage-mock

來處理這個問題

但是這樣很醜

希望可以自己控制 Authorization 流程

所以去爬了一下 source code

資訊在參考資料

這部分文件沒有寫得很清楚

所以花了一個篇幅來記錄一下如何客製化 Authorization

Server

server.js

agOptions 中加入 authKey: SCC_AUTH_KEY

這時候就會把這個參數帶入 agServer.signatureKey

所有對外的服務可以放入同樣的 authKey

彼此就可以共用同樣的 token

Login

1
2
3
4
5
6
7
8
9
expressApp.get('/login', async (req, res) => {
const signedTokenString = await agServer.auth.signToken(
myTokenData,
agServer.signatureKey
)
res.status(200).json({
token: signedTokenString,
})
})

Websocket flow

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
agServer.setMiddleware(
agServer.MIDDLEWARE_INBOUND,
async (middlewareStream) => {
for await (let action of middlewareStream) {
let authToken = action.socket.authToken
if (isEmpty(authToken)) {
const notAllowError = new Error('not allow')
notAllowError.name = 'InvalidActionError'
action.block(notAllowError)
action.request.error(notAllowError)
console.log('AL: action.request.error', action.request.error)
return
}
try {
await agServer.auth.verifyToken(bearerToken, agServer.signatureKey)
} catch (error) {
const notAllowError = new Error('not allow')
notAllowError.name = 'InvalidActionError'
action.block(notAllowError)
action.request.error(notAllowError)
console.log('AL: action.request.error', action.request.error)
return
}

action.allow()
}
}
)

websocket 連線上之前可以在 inbound 的 middleware 中做檢查

HTTP flow

因為 API 每個 Route 希望可以更彈性的來處理驗證問題

可以依據 Express 的一般驗證模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const jwtverify = async (req, res, next) => {
try {
const bearerHeader = req.headers['authorization']
if (!bearerHeader) throw new Error('unauthorization')

const bearer = bearerHeader.split(' ')
const bearerToken = bearer[1]
req.user = await agServer.auth.verifyToken(
bearerToken,
agServer.signatureKey
)
next()
} catch (error) {
return res.status(400).json({ message: error.message })
}
}

expressApp.get('/health-check', jwtverify, (req, res) => {
res.status(200).send('OK')
})

可以在需要驗證的 route 加入 驗證的 Middleware

Client

Browser

最簡單的方式

1
localStorage.setItem('socketcluster.authToken', token)

另外也可以 AuthEngine 可以自訂

source code 在參考資料中可以參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let socket = socketClusterClient.create({
secure: false,
authTokenName: "socketcluster.authToken",
authEngine: {
_internalStorage: {
"socketcluster.authToken": ${token}
},
isLocalStorageEnabled: true,
saveToken: (name, token, options) => {
this._internalStorage[name] = token;
return Promise.resolve(token);
},
removeToken: function(name) {
const loadPromise = this.loadToken(name);
delete this._internalStorage[name];
return loadPromise;
},
loadToken: function(name) {
const token = this._internalStorage[name] || null;
return Promise.resolve(token);
}
},
});

secure 是否要使用 wss

authTokenName 設定 _internalStorage[name] 的 name

authEngine 可以自行定義針對 authToken 的行為

React Native

因為在 React Native 沒有 localStorage

所以無法利用 localStorage 處理

但是可以利用 jest-localstorage-mock

來建立

但是這個做法比較不優

所以會選用 AuthEngine 來處理

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
let socket = SocketClusterClient.create({
hostname: hostname(ip),
port: 1234,
secure: false,
authTokenName: 'socketcluster.authToken',
authEngine: {
_internalStorage: {
'socketcluster.authToken': token,
},
isLocalStorageEnabled: true,
saveToken: (name, token, options) => {
this._internalStorage[name] = token
return Promise.resolve(token)
},
removeToken: function (name) {
const loadPromise = this.loadToken(name)
delete this._internalStorage[name]
return loadPromise
},
loadToken: function (name) {
const token = this._internalStorage[name] || null
return Promise.resolve(token)
},
},
})

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

這時候會發生這個問題

1
2
TypeError: Invalid attempt to iterate non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.

oh!shit!

所以跑去研究了 Symbol.iterator

結果是因為 array like 的問題

所以找不到 [Symbol.iterator]() method

解決方案只要用 Array.from 轉換型態就可以了

所以變成

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
const connectSocketCluster = async () => {
try {
let socket = SocketClusterClient.create({
hostname: hostname(ip),
port: 1234,
secure: false,
authTokenName: 'socketcluster.authToken',
authEngine: {
_internalStorage: {
'socketcluster.authToken': token,
},
isLocalStorageEnabled: true,
saveToken: (name, token, options) => {
this._internalStorage[name] = token
return Promise.resolve(token)
},
removeToken: function (name) {
const loadPromise = this.loadToken(name)
delete this._internalStorage[name]
return loadPromise
},
loadToken: function (name) {
const token = this._internalStorage[name] || null
return Promise.resolve(token)
},
},
})

;(async () => {
const errorChannel = socket.listener('error')
try {
for await (let { error } of Array.from(errorChannel)) {
}
} catch (error) {}
})()
} catch (error) {
console.log('connectSocketCluster -> error.message', error.message)
}
}

connectSocketCluster()

終於可以連上了

以及可以做基本的驗證

只要同一個 token 就可以在各個服務中聯繫

如果還要開其他的 broker

只要注意 SSC_AUTH_KEY 的一致性

就可以基本上保證彼此之間的 token 共用

參考資料

socketcluster

authengine

auth.js

iterable

SocketCluster-scc

SCC Guide

SCC 是服務的集合

讓你可以快速的將 SocketCluster 快速的部屬到多台機器上

讓彼此可以任意溝通

並且可以利用 Kubernetes 上面優化運行

SCC 基本上可以分成幾個服務

Scc-worker

可以建立多個數量的 service 必須指定一個 scc-state service 做連結

Scc-broker

可以建立多個數量的 service 必須指定一個 scc-state service 做連結

這是特殊的後端 service 和 scc-worker 最大的不同是

scc-broker 主要是在與多個前端做訊息的溝通

所有的 sub/pub channels 可以平均的在 scc-broker 平均分配流量

Scc-state

一個 Cluster 應該只會有一個 Scc-state service

主要在管理各個 Service 的狀態

讓他們可以自動 重新啟動或是服務管理

當有新的 Scc-broker 加入的時候

Scc-state 會通知所有的 Scc-worker

這樣就可以重新平衡流量

Scc-state 關閉或是不可用的時候

SCC可以繼續運行而不會中斷服務

k8s

未來會補充

部屬順序

scc-state >> scc-worker >> scc-broker

參考資源

SCC Guide

SociekCluster-Storage

Mongodb

以 Mongodb 為範例

1
2
3
4
$ git clone git@github.com:SocketCluster/scc-broker.git scc-mongo-broker && cd scc-mongo-broker
$ yarn install
$ yarn add mongoose dotenv
$ mkdir models

models/tankModel.js

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

const schema = new mongoose.Schema({ name: 'string', size: 'string' });
const Tank = mongoose.model('Tank', schema);
// Tank.create({ size: 'small' }, function (err, small) {
// if (err) return handleError(err);
// // saved!
// });


Tank.find().then(result => {
console.log('AL: result', result)
}).catch(error => {
console.log('AL: error', error)
})

module.exports = Tank;

server.js

1
2
3
4
5
6
7
8
9
10
11
const dotenv = require('dotenv');
dotenv.config();
const mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/test?poolSize=4';
mongoose.connect(uri, {useNewUrlParser: true, useUnifiedTopology:true});
// mongoose.createConnection(uri, {useUnifiedTopology: true});

const Tank = require('./models/tankModel');
dotenv.config();

參考資料

mongoose

scc-broker

sc-redis

sc-rabbitmq

sc-error

Redux-Part-III

前情提要

角色

  • 生產者 - 產出任務 store.dispatch
  • 消費者 - 消費任務 saga function
  • channel - 暫存任務的地方

Redux-Saga 的組成

  • createMiddleware
  • effects
  • Channel

createMiddleware

基於 redux 所以要建立一個 sagaMiddleware

createMiddleware

會回傳一個 sagaMiddleware

sagaMiddleware

sagaMiddleware 有一個 run 的參數

他是之前說過的 Generator Runner

sagaRunner

在這個 Function 中 會利用 saga 產生 iterator

Channel

之前在生產者產生 task 之後需要有一個 channel 來暫存

這就是暫存的地方

預設會有一個 channel(之後有機會再說)

自己產生的話就可以用 actionChannel

watcher.js

1
2
3
4
5
6
7
8
9
10
11
import types from "../constants/actionTypes";
import { take, call, takeLatest, actionChannel } from 'redux-saga/effects';
import { loginSaga, logoutSaga } from './authSaga';

export function* watchLogin() {
yield takeLatest(types.LOGIN, loginSaga);
}

export function* watchLogout() {
yield takeLatest(types.LOGOUT, logoutSaga);
}

一般需要使用動併發的時候可以這樣處理

但是因為使用的是 takeLatest

所以當有重複的 Action 的時候

他會取消上一個 Action

但是如果我們希望可以一個一個處理

所有還沒處理到的 Action 先暫存一個地方

希望能有一個 queue 的機制

這時候可以利用 actionChannel

new watcher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import types from "../constants/actionTypes";
import { take, call, takeLatest, actionChannel } from 'redux-saga/effects';
import { loginSaga, logoutSaga } from './authSaga';

export function* watchLogin() {
const requestChan = yield actionChannel(types.LOGIN);
while(true) {
const actionObject = yield take(requestChan);
yield call(loginSaga, actionObject);
}
}

export function* watchLogout() {
yield takeLatest(types.LOGOUT, logoutSaga);
}

上述的是利用 channel 暫存 task

由於使用 call 來做強制執行完之後

再由 while(true) 會重複執行下一個新的 task

Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import types from '~/constants/actionTypes';
import { connect } from 'react-redux';
import LoginScreen from './view';

const loginAction = payload => ({
type: types.LOGIN,
payload
});

const mapStateToProps = ({ auth }) => ({
auth
});

const mapDispatchToProps = dispatch => ({
handleLogin: payload => {
dispatch(loginAction(payload))
},
});

export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);

在 container 有描述 dispatch 產生新的 task

再由 saga 進行消費

參考資料

createMiddleware

Recipes

RNSkelton

Redux-Part-II

Thunk

經過上一篇了解了 Redux 和如何實做 Middleware 之後

再來看看 Thunk 到底做了什麼事情呢?

What is Thunk?

Thunk 是一個 Function 主要功用是將結果延遲到需要的時候再執行這個 Function 來獲取這份資料

1
2
3
4
5
function foo() {
return 1 + 2;
}

const x = 1 + 2;

這是最簡單的一個 Thunk

這時候來看一下 React Thunk 到底在 applyMiddleware 做了什麼事情?

React Thunk

它其實是多包了一層 Function 來暫時阻止 action 進入 Store

來達到有機會完成非同步的 Action

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
import { createStore, applyMiddleware } from 'redux';
import React, {useState, useEffect} from 'react';
import thunk from 'redux-thunk';
import { View, Text } from 'react-native';

const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};


const store = createStore(counter, applyMiddleware(thunk));

function increment() {
console.log('increment');
return {
type: 'INCREMENT',
};
}

function incrementAsync() {
console.log('incrementAsync');
return (dispatch) => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}

const LoginScreen = () => {
const [value, setValue] = useState(null);
useEffect(() => {
incrementAsync()(store.dispatch);

const unsubscribe = store.subscribe(() => {
const v = store.getState();
console.log('LoginScreen -> v', v)
setValue(v);
})
return () => unsubscribe();
}, [])
return (
<View style={{flex: 1}}>
<Text>{String(value)}</Text>

</View>
);
};

thunk 的 middleware 會多包覆一層 傳送 dipspatch

所以在執行 Action 的當下 Reducer 不會收到通知

而是在在中間曾發送 Action

來達到通知 Reducer 修改 Store 的效果

另外也可以做一些變形

讓這一層可以統一執行

範例

middleware 中有一個 promiseMiddleware.js 專門處理非同步事件

Action 可以很單純的回傳一個物件

覺的是一種不錯的作法

Saga

Saga Pattern

Saga Pattern 並不是 Redux 產生的

各個程式語言都可以去實做這個 Pattern

這個 Pattern 想解決的是 LLT (Long Live Transaction) 的問題

尚未解決的是當產生了一個 Transaction 的同時也應該會產出一個 Compensation

Redux Saga 使用的2是 Generator Function 而不是 Async/ Await Function

所以要先了解這兩個的差異性

Async/Await And Gernerator

Redux Saga

如何透過 Generator 管理任務

一個一般的 function 依序列出 step1 step2 step3

1
2
3
4
5
6
7
function work() {
console.log('step1');
console.log('step2');
console.log('step3');
}

work();

但是如果是要用 Generator Function 來實做的話

1
2
3
4
5
6
7
8
9
10
function* generatorWork() {
yield console.log('step1');
yield console.log('step2');
yield console.log('step3');
}

const work = generatorWork();
work.next(); //step1
work.next();//step2
work.next();//step3

當每次呼叫 next 會回傳一個 {done: false, value: 'step1'} 的物件

done 是一個 Boolean

代表是否完成

Generator Runner

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 axios from "axios";

function runner(genFn) {
const itr = genFn();

function next(arg) {
let result = itr.next(arg);

if (result.done) {
return arg;
} else {
return Promise.resolve(result.value).then(next);
}
}

return next();
}

function* genFn() {
const USER_URI = "https://reqres.in/api/users";
let res = yield axios.get(USER_URI);
const userId = res.data.data[0].id;
yield axios.get(`${USER_URI}/${userId}`);
}

const result = runner(genFn);
Promise.resolve(result).then(res => console.log(res.data));

Redux-Saga 是這樣來管理每一個任務

它可以執行的對象相當豐富

  • effect
  • iterator
  • promise
  • 一般的程式碼

Producer And Consumer

Redux-saga 其實背後的原理是 Producer 和 Consumer

image

但是不知道是歷史因素還是什麼原因

image

如果你閱讀 redux-saga 的原始碼

你會看到 channel 是用來管理非同步任務的緩衝區 (Buffer)

裡面提供了 produce 與 consume 的函式

  • channel.take()

    • 生產者 (Producer): 把任務放到 channel 中
  • channel.put()

    • 消費者 (Consumer): 呼叫了 store.dispatch() 後執行的函式,會從 channel 中選擇符合 pattern 的任務執行

    WTF

take建立一個 Effect 描述

指示 middleware 在 Store 等待指定的 action

Generator 會暫停

直到一個符合 pattern 的 action 被 dispatch

簡單來說 take 是用來註冊處理非同步的函式

take 會 將處理非同步的函式所生成的 iterator 用 generator runner 包裝起來

最後呼叫 channel.take(cb)

以 callback 的形式儲存在 channel 中

在 redux-saga 的實作中

被儲存在 channel 中的 callback 稱作 taker

這些原理講得有點抽象

下個章節來看一下 Redux-saga 的 Source code 吧

Redux-Part-I

Redux

React Redux

Flux 的實作之一

有一個共同的 Store 儲存資料

透過 Middleware 來控制工作流

發起 Action 來通過工作流之後

透過 Reducer 來修改 Store 內的資料

再藉由 Store 的資料來顯示 Screen

搭配的 Package 有很多種不同的配合與實現

  • redux-thunk
  • redux-saga

主要都是在處理 Async Actions

redux-thunk skelton

redux-saga skelton

兩個骨架範例

基本上每一個 Async Action 都是單一的 Promise

但是不代表只能夠有一個 Promise

也可以多個 Promise 但是需要使用 Promise.all 做群組

有清楚的成功與失敗

所以建立一個非同步的 Action 應該會有三個 action types

  • Request - 顯示 loading
  • Success - 成功 (關閉 loading)
  • Error - 失敗 (關閉 loading)

不要跳脫一個完整的週期

image

在一個 Action 內 在發送另外一個 非同步 Action

Install And Setup

Basic Usage

1
$ yarn add redux
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
import {createStore} from 'redux';
import React, {useState, useEffect} from 'react';
import {Navigation} from 'react-native-navigation';
import { View, Text, Button, StyleSheet, Icon } from 'react-native';

const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};


const store = createStore(counter);

const LoginScreen = () => {
const [value, setValue] = useState(null);
useEffect(() => {
setTimeout(() => {
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
}, 2000)
const unsubscribe = store.subscribe(() => {
const v = store.getState();
console.log('LoginScreen -> v', v)
setValue(v);
})
return () => unsubscribe();
}, [])
return (
<View style={styles.root}>
<Text>{String(value)}</Text>
<Button
title='Login'
color='#710ce3'
onPress={() => Navigation.setRoot(mainRoot)}
/>
</View>
);
};

counter 是 Reducer

Reducer 都是一個函式

會修改 Store 裡面的某一個部分的值

再把修改結果回傳

但是這樣最基本的 Redux 只能同步的修改 Store 的資料

{ type: 'INCREMENT' } 是 Action Object

Middleware

但是上述的流程只能處理同步的 Action

非同步的需要多做一點點事情

這時候需要 Middleware 的幫助

Middleware 概念就是一個一個的生產線

每一個 Function 都是處理某些細節

再利用這個 Middleware 排定順序

Compose

redux compose

1
2
3
4
5
const compose = (...fns) => (...payload) => {
if(fns.length === 0) return payload;
if(fns.length === 1) return fns[0](...payload);
return funcs.reduce((a, b) => (...payload) => a(b(...payload)))
}

composepipe 是 Middleware 的其中兩種應用

針對 Dispatch 做擴充

目前 dispatch 只能做同步 Action

但是並不符合我們的需求

Monkeypatching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default (...middlewares) => (reducer, initialState) => {
const store = createStore(reducer, initialState);
const next = store.dispatch;

const middlePayload = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
};

const chain = middlewares.map(middleware => middleware(middlewareAPI));
const dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
}
};

可以一步一步慢慢了解 Redux 如何形成 Middleware 的過程

最後可以看到這個範例

The Final Approach

接下來了解如何實做 Redux 中的 Middleware 之後

再來看看加入 Thunk , Saga 概念的 Data flow

參考資源

Nodejs 範例

Middleware

React-Native-Navigation

React-Native-Navigation

Install

依據以下動作即可完成
Install

Basic Usage

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
// In index.js of a new project
const { Navigation } = require('react-native-navigation');
const React = require('react');
const { View, Text, StyleSheet } = require('react-native');

const HomeScreen = (props) => {
return (
<View style={styles.root}>
<Text>Home Screen</Text>
</View>
);
};

//可以直接在 options 設定參數
HomeScreen.options = {
topBar: {
title: {
text: 'Home',
color: 'white'
},
background: {
color: '#4d089a'
}
}
}

Navigation.registerComponent('Home', () => HomeScreen);

Navigation.events().registerAppLaunchedListener(async () => {
Navigation.setRoot({
root: {
stack: {
children: [
{
component: {
name: 'Home'
}
}
]
}
}
});
});

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

:::info
Navigation.registerComponent 會建立一個 uniqueId CompoenntId 這個 Id 會是換頁的主要依據
:::

導頁

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
// In index.js of a new project
const { Navigation } = require('react-native-navigation');
const React = require('react');
const { View, Text, StyleSheet } = require('react-native');
const { Button } = require('react-native');

const HomeScreen = (props) => {
return (
<View style={styles.root}>
<Text>Home Screen</Text>
<Button
title='Push Settings Screen'
color='#710ce3'
onPress={() => Navigation.push(props.componentId, {
component: {
name: 'Settings',
options: {
topBar: {
title: {
text: 'Settings'
}
}
}
}
})}/>
</View>
);
};

const SettingScreen = (props) => {
return (
<View style={styles.root}>
<Text>Setting Screen</Text>
</View>
);
};


HomeScreen.options = {
topBar: {
title: {
text: 'Home',
color: 'white'
},
background: {
color: '#4d089a'
}
}
}

Navigation.registerComponent('Home', () => HomeScreen);
Navigation.registerComponent('Settings', () => SettingScreen);

Navigation.events().registerAppLaunchedListener(async () => {
Navigation.setRoot({
root: {
stack: {
children: [
{
component: {
name: 'Home'
}
}
]
}
}
});
});

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

App Theme

使用的 Style Framwork 是 react-native-elements

裡面也有 Theme

React Native Navigation 也可以設定 Theme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Navigation.setDefaultOptions({
statusBar: {
backgroundColor: '#4d089a'
},
topBar: {
title: {
color: 'white'
},
backButton: {
color: 'white'
},
background: {
color: '#4d089a'
}
}
});

Tab Stack

一般的App 都會有 Bottom 的 Tap navigation

在 React Native Navigation 中

把剛剛的 Home Settings 頁面換成兩個 Tab Statck

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const { Navigation } = require('react-native-navigation');
const React = require('react');
const { View, Text, Button, StyleSheet } = require('react-native');

const HomeScreen = (props) => {
return (
<View style={styles.root}>
<Text>Hello React Native Navigation 👋</Text>

<Button
title='Push Settings Screen'
color='#710ce3'
onPress={() => Navigation.push(props.componentId, {
component: {
name: 'Settings'
}
})} />
</View>
);
};
HomeScreen.options = {
topBar: {
title: {
text: 'Home'
}
},
bottomTab: {
text: 'Home'
}
};

const SettingsScreen = () => {
return (
<View style={styles.root}>
<Text>Settings Screen</Text>
</View>
);
}
SettingsScreen.options = {
topBar: {
title: {
text: 'Settings'
}
},
bottomTab: {
text: 'Settings'
}
}

Navigation.registerComponent('Home', () => HomeScreen);
Navigation.registerComponent('Settings', () => SettingsScreen);

Navigation.setDefaultOptions({
statusBar: {
backgroundColor: '#4d089a'
},
topBar: {
title: {
color: 'white'
},
backButton: {
color: 'white'
},
background: {
color: '#4d089a'
}
},
bottomTab: {
fontSize: 14,
selectedFontSize: 14
}
});

Navigation.events().registerAppLaunchedListener(async () => {
Navigation.setRoot({
root: {
bottomTabs: {
children: [
{
stack: {
children: [
{
component: {
name: 'Home'
}
},
]
}
},
{
stack: {
children: [
{
component: {
name: 'Settings'
}
}
]
}
}
]
}
}
});
});

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

Bottom Tab 會有 HomeSettings

但是在 Home 會有 Button

點擊後還是會 push Settings 進入 Home Stack

這是一般的 App Navigation 的邏輯

Replace Root

Navigation 也提供了覆蓋 Root Stack 的 function

Navigation.setRoot(${rootObject})

執行這個 function 會將 Root 整個覆蓋

範例如下

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const { Navigation } = require('react-native-navigation');
const React = require('react');
const { View, Text, Button, StyleSheet } = require('react-native');

const LoginScreen = () => {
return (
<View style={styles.root}>
<Button
title='Login'
color='#710ce3'
onPress={() => Navigation.setRoot(mainRoot)}
/>
</View>
);
};

const HomeScreen = (props) => {
return (
<View style={styles.root}>
<Text>Hello React Native Navigation 👋</Text>

<Button
title='Push Settings Screen'
color='#710ce3'
onPress={() => Navigation.push(props.componentId, {
component: {
name: 'Settings'
}
})} />
</View>
);
};
HomeScreen.options = {
topBar: {
title: {
text: 'Home'
}
},
bottomTab: {
text: 'Home'
}
};

const SettingsScreen = () => {
return (
<View style={styles.root}>
<Text>Settings Screen</Text>
</View>
);
}
SettingsScreen.options = {
topBar: {
title: {
text: 'Settings'
}
},
bottomTab: {
text: 'Settings'
}
}

Navigation.registerComponent('Login', () => LoginScreen);
Navigation.registerComponent('Home', () => HomeScreen);
Navigation.registerComponent('Settings', () => SettingsScreen);

const mainRoot = {
root: {
bottomTabs: {
children: [
{
stack: {
children: [
{
component: {
name: 'Home'
}
},
]
}
},
{
stack: {
children: [
{
component: {
name: 'Settings'
}
}
]
}
}
]
}
}
};

const loginRoot = {
root: {
component: {
name: 'Login'
}
}
};


Navigation.setDefaultOptions({
statusBar: {
backgroundColor: '#4d089a'
},
topBar: {
title: {
color: 'white'
},
backButton: {
color: 'white'
},
background: {
color: '#4d089a'
}
},
bottomTab: {
fontSize: 14,
selectedFontSize: 14
}
});
Navigation.events().registerAppLaunchedListener(async () => {
Navigation.setRoot(loginRoot);
});

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

基本上定義了一個 loginRootmainRoot

在登入的時候如果成功則切換到 mainRoot

登出的時候則再度切換回 loginRoot

來簡單的實現了登入登出機制

但是在實際的產品中這樣卻是不足的

因為會先看到 LoginScreen

如果是登入狀態

加上在 React Native 讀取 AsyncStorage 的資料都是非同步的

會看到閃一下 再跳到 MainRoot

在使用者體驗上會很糟糕

所以會需要做一些調整來避免這個狀況

但是這篇篇幅太多

留著後面再說吧

|