【问题标题】:sending signal from subcomponent in elm从 elm 中的子组件发送信号
【发布时间】:2015-06-23 17:40:00
【问题描述】:

我正在 Elm 中制作一个小型应用程序。它在屏幕上显示一个计时器,当计时器达到零时,它会播放声音。我无法弄清楚如何从计时器向声音播放器发送消息(?)。

在架构上,我有三个模块:一个代表计时器的Clock 模块,一个可以播放音频的PlayAudio 模块,以及一个将Clock 模块和PlayAudio 模块联系在一起的Main 模块。

理想情况下,当时钟达到零时,我想做一些事情,比如从Clock 模块发送信号。当时钟归零时,Clock 会向Main 发送信号,Main 会将其转发给PlayAudio

但是,通过阅读 Elm 文档,似乎不鼓励使用 Main 以外的任何东西来处理信号。所以这引出了我的第一个问题。对这种状态变化进行建模的好方法是什么? Clock 中的 update 函数是否应该返回它是否已经结束? (这就是我在下面的做法,但我非常愿意接受有关如何做得更好的建议。)

我的第二个问题是关于如何让声音播放。我将使用原始 Javascript 来播放声音,我相信这意味着我必须使用端口。但是,我不确定如何与子模块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:

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <script src="js/elm.js" type="text/javascript"></script>
  <link rel="stylesheet" href="style.css">
 </head>
 <body>
        <script type="text/javascript">
                 var todomvc = Elm.fullscreen(Elm.Main);
                todomvc.ports.playSound.subscribe(function() {
                                setTimeout(function() {
                                        document.getElementById('audiotag').play();
                                }, 50);
                });
        </script>
 </body>
</html>

【问题讨论】:

    标签: javascript timer architecture frp elm


    【解决方案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 的互操作变得更加容易,因此一旦有人创建了与声音库的绑定,您应该能够取消该端口。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多