我在Elm做了一个小应用程序。它在屏幕上显示一个计时器,当计时器到达零时,它会播放声音。我无法弄清楚如何从定时器向声音播放器发送消息(?)。
在架构上,我有三个模块:一个Clock
模块代表定时器,一个PlayAudio
模块可以播放音频,一个Main
模块将Clock
连接在一起} module和PlayAudio
module。
理想情况下,当时钟到达零时,我想做一些事情,比如从Clock
模块发送信号。当时钟到达零时, Clock
会向Main
发送信号,然后将信号转发至PlayAudio
。
然而,通过阅读Elm文档,似乎不鼓励使用除Main
以外的任何其他信息来处理信号。这就引出了我的第一个问题。建模这种状态变化的好方法是什么?来自update
的{{1}}函数是否应该返回它是否已经结束? (这就是我在下面的方式,但我会对如何做得更好的建议持开放态度。)
我的第二个问题是如何让声音发挥。我将使用原始Javascript来播放声音,我相信这意味着我必须使用端口。但是,我不确定如何与我的子模块Clock
中Main
中定义的端口进行交互。
以下是我正在使用的代码。
PlayAudio
:
Clock.elm
module Clock (Model, init, Action, signal, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type ClockState = Running | Ended
type alias Model =
{ time: Time
, state: ClockState
}
init : Time -> Model
init initialTime =
{ time = initialTime
, state = Running
}
-- UPDATE
type Action = Tick Time
update : Action -> Model -> (Model, Bool)
update action model =
case action of
Tick tickTime ->
let hasEnded = model.time <= 1
newModel = { model | time <-
if hasEnded then 0 else model.time - tickTime
, state <-
if hasEnded then Ended else Running }
in (newModel, hasEnded)
-- VIEW
view : Model -> Html
view model =
div []
[ (toString model.time ++ toString model.state) |> text ]
signal : Signal Action
signal = Signal.map (always (1 * second) >> Tick) (every second)
:
PlaySound.elm
module PlaySound (Model, init, update, view) where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import LocalChannel (..)
import Signal
import Time (..)
-- MODEL
type alias Model =
{ playing: Bool
}
init : Model
init =
{ playing = False
}
-- UPDATE
update : Bool -> Model -> Model
update shouldPlay model =
{ model | playing <- shouldPlay }
-- VIEW
view : Model -> Html
view model =
let node = if model.playing
then audio [ src "sounds/bell.wav"
, id "audiotag" ]
[]
else text "Not Playing"
in div [] [node]
:
Main.elm
module Main where
import Debug (..)
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Html.Lazy (lazy, lazy2)
import Json.Decode as Json
import List
import LocalChannel as LC
import Maybe
import Signal
import String
import Time (..)
import Window
import Clock
import PlaySound
---- MODEL ----
-- The full application state of our todo app.
type alias Model =
{ clock : Clock.Model
, player : PlaySound.Model
}
emptyModel : Model
emptyModel =
{ clock = 10 * second |> Clock.init
, player = PlaySound.init
}
---- UPDATE ----
type Action
= NoOp
| ClockAction Clock.Action
-- How we update our Model on a given Action?
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
ClockAction clockAction ->
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in { model | clock <- newClock
, player <- newPlaySound }
---- VIEW ----
view : Model -> Html
view model =
let context = Clock.Context (LC.create ClockAction actionChannel)
in div [ ]
[ Clock.view context model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map view model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel allSignals
allSignals : Signal Action
allSignals = Signal.mergeMany
[ Signal.map ClockAction Clock.signal
, Signal.subscribe actionChannel
]
initialModel : Model
initialModel = emptyModel
-- updates from user input
actionChannel : Signal.Channel Action
actionChannel = Signal.channel NoOp
port playSound : Signal ()
port playSound = ???
:
index.html
答案 0 :(得分:1)
这种方法看起来非常有原则,符合Elm Architecture post的指导原则。在该文档的最后是一个名为One last pattern的部分,它完全按照您的要求执行操作:如果需要从组件向另一个组件发出信号,请让更新功能返回一对。
所以我认为你做得对。当然,在如此小的应用程序中如此严格地遵循这种架构确实增加了样板/相关代码比率。
无论如何,您需要进行的唯一更改是Main.elm
。实际上,您并不需要Channel
从子组件向Main发送消息,因为Main会启动组件并将更新功能连接在一起。因此,您可以使用组件更新功能的额外输出,并将其从模型信号中分离到端口中。
---- UPDATE ----
-- How we update our Model on a given Action?
update : Clock.Action -> Model -> (Model, Bool)
update clockAction model =
let (newClock, hasEnded) = Clock.update clockAction model.clock
newPlaySound = PlaySound.update hasEnded model.player
in ( { model | clock <- newClock
, player <- newPlaySound }, hasEnded)
---- VIEW ----
view : Model -> Html
view model =
div [ ]
[ Clock.view model.clock
, PlaySound.view model.player
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main = Signal.map (view << fst) model
-- manage the model of our application over time
model : Signal Model
model = Signal.foldp update initialModel Clock.signal
initialModel : Model
initialModel = emptyModel
port playSound : Signal ()
port playSound =
model
|> Signal.map snd
|> Signal.keepIf ((==) True)
|> Signal.map (always ())
最后注释: Elm 0.15 is out,至少会简化您的导入。但更重要的是,与Elm内部的JavaScript(无端口)互操作变得更加容易,因此只要有人创建了与声音库的绑定,您就应该能够取消该端口。