昨天稍微提到了「有限狀態機」的概念,今天要來看看如何把它跟 Bottender 做個結合。
想要在 JavaScript 裡面使用有限狀態機的話,我會推薦使用 xstate
這個 Library,這個 Library 不但能單獨使用,也有可以在網頁前端場景去整合 React 的 @xstate/react
,我們則是開發了一個 bottender-xstate
來把它跟 Bottender 整合在一起。
在使用 xstate 時,有個一定要做的事情就是定義好有限狀態機的 config,以昨天提到的紅綠燈範例來說,我們設定初始值 (initial
) 是 green
,並定義以下三件事:
green
時,收到 TIMER
的 xstate event,會讓狀態變為 yellow
yellow
時,收到 TIMER
的 xstate event,會讓狀態變為 red
red
時,收到 TIMER
的 xstate event,會讓狀態變為 green
所以就像下面這樣:
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
},
yellow: {
on: {
TIMER: { target: 'red' },
},
},
red: {
on: {
TIMER: { target: 'green' },
},
},
},
};
在跟 Bottender 整合的情況下,我們還需要定義 mapContextToXstateEvent
這個 Function 來把每個發生的 context 轉換成對應的 xstate event,在這個情況下收到 TIMER
字串,我們就當作一次 TIMER
event:
const mapContextToXstateEvent = context => {
if (context.event.text === 'TIMER') {
return 'TIMER';
}
};
(注意:bottender event 跟 xstate event 是不一樣的概念喔,雖然都是叫做 event)
接下來把這整個範例兜起來會是這樣:
const xstate = require('bottender-xstate');
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
},
yellow: {
on: {
TIMER: { target: 'red' },
},
},
red: {
on: {
TIMER: { target: 'green' },
},
},
},
};
const mapContextToXstateEvent = context => {
if (context.event.text === 'TIMER') {
return 'TIMER';
}
};
const StateMachine = xstate({
config,
mapContextToXstateEvent,
});
module.exports = async function App() {
return StateMachine;
};
看看一下目前這樣有些怎樣的效果,當然我們現在都還沒讓機器人講話,所以只能透過 /state
指令看一下 state 的變化,這個狀態一般是存在 context.state.xstate.value
的位置。
第一次收到 TIMER
後,變成黃色 yellow
:
第二次收到 TIMER
後,變成紅色 red
:
第三次收到 TIMER
後,變回最一開始的綠色 green
:
這就是基本的狀態轉移啦!
昨天的文章有提到,我們需要利用下面這四類動作(Action)來描述這個模型的行為:
我們可以透過 onEntry
來定義進入某個 State 時必須執行的 Action,下面我們來讓進入 yellow
時要執行 entryYellow
:
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
},
yellow: {
on: {
TIMER: { target: 'red' },
},
onEntry: 'entryYellow', // 主要是加這行
},
red: {
on: {
TIMER: { target: 'green' },
},
},
},
};
const StateMachine = xstate({
config,
mapContextToXstateEvent,
actions: {
// 還要加這個 Action
entryYellow: async context => {
await context.sendText('變成黃燈啦!');
},
},
});
還有要記得,我們必須在傳進去的 actions
上實作 entryYellow
。
嘗試起來是這樣:
我們可以透過 onExit
來定義離開某個 State 時必須執行的 Action,下面我們來讓離開 green
時要執行 exitGreen
:
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
onExit: 'exitGreen', // 主要是加這行
},
yellow: {
on: {
TIMER: { target: 'red' },
},
onEntry: 'entryYellow',
},
red: {
on: {
TIMER: { target: 'green' },
},
},
},
};
const StateMachine = xstate({
config,
mapContextToXstateEvent,
actions: {
entryYellow: async context => {
await context.sendText('變成黃燈啦!');
},
// 還要加這個 Action
exitGreen: async context => {
await context.sendText('不是綠燈囉!');
},
},
});
一樣記得要實作 exitGreen
。
嘗試起來是這樣:
可以在收到 Event 時執行對應的 Action,但不用轉移 State,下面我們來讓在紅燈時對 WALk
執行 warning
做一個警告:
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
onExit: 'exitGreen',
},
yellow: {
on: {
TIMER: { target: 'red' },
},
onEntry: 'entryYellow',
},
red: {
on: {
TIMER: { target: 'green' },
WALK: { actions: 'warning' }, // 主要是加這行
},
},
},
};
// 修改一下 mapContextToXstateEvent 讓他可以也支援 WALK
const mapContextToXstateEvent = context => {
return context.event.text;
};
const StateMachine = xstate({
config,
mapContextToXstateEvent,
actions: {
entryYellow: async context => {
await context.sendText('變成黃燈啦!');
},
exitGreen: async context => {
await context.sendText('不是綠燈囉!');
},
// 還要加這個 Action
warning: async context => {
await context.sendText('紅燈了,別走!');
},
},
});
嘗試起來是這樣:
可以在轉移 State 時順便執行對應的 Action,下面我們來在由 green
轉 yellow
的過程中執行 fromGreenToYellow
:
const config = {
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: {
target: 'yellow',
actions: 'fromGreenToYellow', // 主要是加這行
},
},
onExit: 'exitGreen',
},
yellow: {
on: {
TIMER: { target: 'red' },
},
onEntry: 'entryYellow',
},
red: {
on: {
TIMER: { target: 'green' },
WALK: { actions: 'warning' },
},
},
},
};
const StateMachine = xstate({
config,
mapContextToXstateEvent,
actions: {
entryYellow: async context => {
await context.sendText('變成黃燈啦!');
},
exitGreen: async context => {
await context.sendText('不是綠燈囉!');
},
warning: async context => {
await context.sendText('紅燈了,別走!');
},
// 還要加這個 Action
fromGreenToYellow: async context => {
await context.sendText('由綠變黃中~~~');
},
},
});
嘗試起來是這樣:
接連五天的介紹,終於把做機器人能用到常見模式都講到了,雖然每個模式都很好用很強大,但卻不是所有情況都是必要的,所以還是老話一句,記得要了解自己的機器人並選擇最適合他的模式來使用!