0%

WebSocket Service and React

了解了 React useState 與 useEffect 掛鉤,現在來實作一個可以實際應用的範例。

我們要使用先前建立的 WebSocket Service and RabbitMQ 即時訊息通知來建立一個 Web App 即時訊息通知功能。透過這個 WebScocket 伺服器可以讓你從任何地方送出訊息,並即時顯示在你的 APEX Web Application 上。

加入基本的原始碼

建立一個新的 APEX Page,在 Page Properties 的 JavaScript File URLs 加入三個原始碼。

File URLs
1
2
3
https://unpkg.com/react@16.13.1/umd/react.development.js
https://unpkg.com/react-dom@16.13.1/umd/react-dom.development.js
https://unpkg.com/@babel/standalone/babel.min.js

應用程序模組

然後在 Page Properties 的 JavaScript Function and Global Variable Declaration 加入我們自己的程式碼。

Demo module
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
var Demo = (() => {
const uuid = () => {
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
};

const toLoalISOString = (date) => {
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
pad = function (num) {
var norm = Math.floor(Math.abs(num));
return (norm < 10 ? '0' : '') + norm;
};
return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate()) +
'T' + pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds()) +
dif + pad(tzo / 60) +
':' + pad(tzo % 60);
};

const roles = {
"roleDemo": {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3c3NpZCI6IjAwMDEzY2Y1LTcxOWItNDAxZC05YWUyLWRjZjJkYTVlZTRmYyIsIk5hbWUiOiJyb2xlRGVtbyIsImlhdCI6MTU5NjQ0MDYxNH0.KYIuu2xqITmStn1Yy1cu2xuGOcd1CuXYKpLGAU9OmhI',
wssid: '00013cf5-719b-401d-9ae2-dcf2da5ee4fc'
},
"roleOne": {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3c3NpZCI6ImY0YTk0MDkxLTgxYWItNDRiOC1hZWNlLTYzNTI2ZWU4OGFlNiIsIk5hbWUiOiJyb2xlT25lIiwiaWF0IjoxNTk2NDQwNzA3fQ.IFdERyYpwAs3zLx9eUznqLdbuq1xjLF3snw3etgbTpQ',
wssid: 'f4a94091-81ab-44b8-aece-63526ee88ae6'
},
"roleOther": {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3c3NpZCI6ImRlYjg4N2ZhLTNlODQtNGEyYi1hNzZmLTA2OWY4MzU0ZmE0MyIsIk5hbWUiOiJyb2xlT3RoZXIiLCJpYXQiOjE1OTY0NDA3ODd9.jSnLlUeYfCpCR7_bg5nlPeQGKwFtI8IEa-NPsWfgtIw',
wssid: 'deb887fa-3e84-4a2b-a76f-069f8354fa43'
}
};

const dataSample = {
id: uuid(),
title: 'Title 標頭',
message: 'Hello, Tainan. 自己從瀏覽器送出的訊息。'
};

const socket = (roleName = "roleDemo") => {
const { token } = roles[roleName];
const url = `ws://10.11.25.138:4040/?token=${token}`;
const ws = new WebSocket(`ws://10.11.25.138:4040/?token=${token}`);

const subscribe = (callback) => {
ws.onmessage = ({ data }) => {
try {
const parsedData = JSON.parse(data);
callback(parsedData);
} catch(err) {
console.log(err);
callback(null);
}
};
};

const sendTo = (roleName = "roleDemo", data = dataSample) => {
const { wssid } = roles[roleName];
const message = { wssid, ...data, created: Date.now() };

return ws.send(JSON.stringify(message));
};

return { subscribe, sendTo };
};

return { uuid, toLoalISOString, socket, ...Demo };
})(Demo || {});

這裡列出 3 個可用的測試角色,你可以申請自己的 token 與 wssid。這個範例預設會使用 roleDemo 訂閱 WebSocket Service。sendTo 函式會用來測試送出訊息,預設也是會送給 roleDemo 自己。也用一個預設的訊息範例 dataSample。這些訊息的資料欄位請視你的所需自行調整。

現在可以測試一下我們的模組。開啟瀏覽器的 Console:

1
2
3
4
5
6
7
const wss = Demo.socket();

wss.subscribe(console.log);

wss.sendTo();

{"wssid":"00013cf5-719b-401d-9ae2-dcf2da5ee4fc","id":"e5adf55e-1949-45c7-8cd5-d4c47ae1bb28","title":"Title 標頭","message":"Hello, Tainan. 自己從瀏覽器送出的訊息。","created":1602638703748}

React 組件

建立一個新的 Region,我們現在可以在 Region Properties Source 的 Text 中加 React 程式碼。

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
<div id="react-container"></div>

<script type="text/babel">
const { useState, useEffect } = React;
const { render } = ReactDOM;
const { toLoalISOString, socket } = Demo;
const wss = socket("roleDemo");

const useWssMessages = () => {
const [messages, setMessages] = useState([]);
const addMessage = message => setMessages(allMessages => (message ? [message, ...allMessages] : allMessages));

useEffect(() => {
wss.subscribe(addMessage);
}, []);

return messages;
};

const Message = ({title, message, created}) => {
return (
<p>Title: {title} Content: {message} AT: {toLoalISOString(new Date(created))}</p>
);
};

const MessageList = () => {
const messages = useWssMessages();
if (!messages.length) return <div>No Messages Listed.</div>

return (
<>
<p>Messages: {messages.length}</p>
{
messages.map((message, i) => <Message key={i} {...message} />)
}
</>
);
};

const App = () => (
<>
<h3>WebSocket instant messages</h3>
<MessageList />
</>
);

render(
<App />,
document.getElementById("react-container")
);
</script>

開啟瀏覽器的 Console:

1
wss.sendTo();

你的 APEX Page 畫面應該馬上會顯示訊息。

使用 GraphQL API 送出訊息

透過 GraphQL API ,其實你可以從任何的地方使用 http 協定送出即時訊息。

可以使用 GraphQL PlayGround 送出測試資料。

GrlphQL API
1
2
3
4
5
6
7
8
9
10
11
// url: GraphQL http://10.11.25.138:4000/v1/graphql

// Mutation
mutation sendToQueue($queue: String!, $message: String!) {
sendToQueue(queue: $queue, message: $message)
}

// Query Variables
{"queue":"wss.notification.demo",
"message":"{\"wssid\":\"00013cf5-719b-401d-9ae2-dcf2da5ee4fc\",\"title\":\"Hello Tainan! 台南!\",\"message\":\"GraphQL http://10.11.25.138:4000/v1/graphql API\",\"created\":1602641256154}"
}

現在從 Oracle 資料庫透過 GraphQL API 送出訊息。

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
declare
l_http_request Utl_Http.req;
l_http_response Utl_Http.resp;
l_url varchar2(255) := 'http://10.11.25.138:4000/v1/graphql';
l_buffer varchar2(32767);
l_item varchar2(1024);
l_queue varchar2(32) := 'wss.notification.demo';
l_wssid varchar2(36) := '00013cf5-719b-401d-9ae2-dcf2da5ee4fc';
l_query varchar2(32767);

procedure send(empno_in in number)
is
rec emp%rowtype;
l_message varchar2(1024);
begin
select * into rec
from emp
where empno = empno_in;

l_message := '"{'||
'\"wssid\":' ||'\"'||l_wssid||'\",'||
'\"id\":' ||'\"'||rec.empno||'-'||rec.deptno||'-'||rec.job||'\",'||
'\"title\":' ||rec.empno||','||
'\"message\":' ||'\"'||rec.ename||'\",'||
'\"created\":'||'\"'||to_char(sysdate,'yyyy-mm-dd"T"hh24:mi:ss')||'\"'||
'}"';

l_query := '{"query":"mutation sendToQueue($queue: String!, $message: String!)' ||
' {\n sendToQueue(queue: $queue, message: $message)\n}",' ||
'"variables":{"queue":"' || l_queue || '","message":' || l_message ||'}}';

--dbms_output.put_line(l_query);

l_http_request := utl_http.begin_request(l_url, 'POST','HTTP/1.1');
utl_http.set_header(l_http_request, 'user-agent', 'mozilla/4.0');
utl_http.set_header(l_http_request, 'content-type', 'application/json; charset=utf-8');
utl_http.set_header(l_http_request, 'Content-Length', lengthb(l_query));
utl_http.set_body_charset(l_http_request, charset => 'utf-8');
utl_http.write_text(l_http_request, l_query);
l_http_response := utl_http.get_response(l_http_request);

-- process the response from the HTTP call
begin
loop
utl_http.read_line(l_http_response, l_buffer);
dbms_output.put_line(l_buffer);
end loop;
utl_http.end_response(l_http_response);
exception
when utl_http.end_of_body
then
utl_http.end_response(l_http_response);
end;
exception
when no_data_found
then
dbms_output.put_line('No data found.');
end;
-- main body
begin
for rec in (select rownum, empno from emp where empno = 7654)
loop
send(rec.empno);
end loop;
end;
/

接收與送出所需要的資料欄位請視所需自行調整。

你每次收到的訊息如果有可能都有不同的資料欄位,可修改 Message 組件:

Message
1
2
3
4
5
6
7
8
9
const Message = ( props ) => {
return (
<p>
{Object.keys(props).map((prop, i) =>
(<span key={i}> { prop.toUpperCase() }: { props[prop] } </span>)
)}
</p>
);
};