Performing Effects
到目前为止,我们已经涵盖了很多领域。您知道如何编写Halogen HTML。您可以定义响应用户交互的组件并按类型对组件的每个部分进行建模, 有了这个基础,我们就可以在编写应用程序时继续使用另一个重要的工具: 执行effects。
在本章中,我们将通过两个示例探索如何在您的组件中执行effects: 生成随机数并发出HTTP请求。一旦您知道如何执行effects,您就可以很好地掌握Halogen基础知识.
在我们开始之前,重要的是要知道您只能在评估期间执行effects,例如像handleAction这样使用HalogenM类型的函数. 但是您无法在生成初始状态或渲染期间执行effects。由于您只能在HalogenM中执行effects,因此在深入研究示例之前,让我们简要了解一下它的更多信息。
HalogenM类型
如果您还记得上一章的内容,handleAction函数会返回一个名为HalogenM的类型。这是我们写的handleAction:
handleAction :: forall output m. Action -> HalogenM State Action () output m Unit
HalogenM是Halogen的关键部分,通常称为eval单子。这个monad启用了Halogen特性,如状态、分叉线程、开始订阅等。但它非常有限,仅与Halogen特定的性质有关。事实上,Halogen组件没有内置的effects机制!
相反,Halogen允许您选择要在组件中与HalogenM一起使用的monad。您可以访问HalogenM的所有功能以及您选择的monad支持的任何功能. 这用类型参数m表示,它代表monad。
仅使用Halogen特定性质的组件可以让此类型参数保持打开状态。例如,我们的计数器只更新状态。但是执行effects的组件可以使用 Effect或Aff monad,或者您可以提供自己的自定义monad。
这个handleAction可以使用来自HalogenM的函数,比如modify_,也可以使用来自Effect的effectful函数:
handleAction :: forall output. Action -> HalogenM State Action () output Effect Unit
这个可以使用来自HalogenM的函数以及来自Aff的effectful函数:
handleAction :: forall output. Action -> HalogenM State Action () output Aff Unit
在Halogen中更常见的是对类型参数m使用约束来描述monad可以做什么,而不是选择特定的monad, 这允许您随着应用程序的增长将多个monad混合在一起。例如,大多数Halogen应用程序将通过以下类型签名使用来自Aff的函数:
handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit
这让您可以完成硬编码Aff类型所做的一切,但也可以让您混合其他约束。
最后一件事: 当你为你的组件选择一个monad时,它会出现在你的HalogenM类型、你的Component类型中,如果你正在使用子组件,那么它会出现在你的ComponentHTML类型中:
component :: forall query input output m. MonadAff m => H.Component query input output m
handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit
-- We aren't using child components, so we don't have to use the constraint here, but
-- we'll learn about when it's required in the parent & child components chapter.
render :: forall m. State -> H.ComponentHTML Action () m
一个Effect例子: 随机数
让我们创建一个新的简单组件,每次单击按钮时都会生成一个新的随机数。在您阅读示例时,请注意它如何使用与我们用于编写计数器的相同类型和函数。随着时间的推移,您将习惯于快读Halogen组件的状态、动作和其他类型,以了解其功能的要点,并熟悉标准函数,如initialState、render 和 handleAction。
您可以将此示例粘贴到Try Purescript中以交互方式探索它。您还可以在此存储库的示例目录中查看并运行完整的示例代码。
请注意,我们没有在我们的initialState或render函数中执行任何effects – 例如,我们将我们的状态初始化为Nothing而不是为我们的初始状态生成一个随机数 —— 但是我们可以在我们的handleAction函数(使用HalogenM类型)中自由地执行effects。
module Main where
import Prelude
import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State = Maybe Number
data Action = Regenerate
component :: forall query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = Nothing
render :: forall m. State -> H.ComponentHTML Action () m
render state = do
let value = maybe "No number generated yet" show state
HH.div_
[ HH.h1_
[ HH.text "Random number" ]
, HH.p_
[ HH.text ("Current value: " <> value) ]
, HH.button
[ HE.onClick \_ -> Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall output m. MonadEffect m =>
Action ->
H.HalogenM State Action () output m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random
H.modify_ \_ -> Just newNumber
如您所见,执行effects的组件与不执行effects的组件没有太大区别!我们只做了两件事:
- 我们为组件和
handleAction函数的m类型参数添加了MonadEffect约束. 我们的render函数不需要约束,因为我们没有任何子组件。 - 我们实际上第一次使用了一个
effect:random函数,它来自Effect.Random。
让我们再分解一下这个effect。
-- [1]
handleAction :: forall output m. MonadEffect m =>
Action ->
H.HalogenM State Action () output m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random -- [2]
H.modify_ \_ -> Just newNumber -- [3]
- 我们已经限制了我们的
m类型参数说我们支持任何monad,只要那个monad支持MonadEffect. 这是”我们需要能够在我们的评估代码中使用Effect函数”的另一种说法。 - 随机函数的类型为
Effect Number。但是我们不能直接使用它:我们的组件不支持Effect而是支持任何monad m,只要该monad可以从Effect运行effects. 这是一个细微的区别,但最终我们要求random函数的类型为MonadEffect m => m Number而不是直接为Effect. 幸运的是,我们可以使用LiftEffect函数将任何Effect类型转换为MonadEffect m => m。这是Halogen中的常见模式,因此如果您使用MonadEffect,请记住liftEffect。 modify_函数让你更新状态,它直接来自带有其他状态更新功能的HalogenM。在这里,我们使用它来将新的随机数写入我们的状态。
这是一个很好的示例,说明您可以如何自由地将Effect中的effects与特定于Halogen的函数(如modify_)交织在一起。让我们再做一次,这次使用Aff monad来实现异步效果。
一个Aff例子: HTTP Requests
从Internet上的其他地方获取信息是很常见的。例如,假设我们想使用GitHub的API来获取用户。我们将使用affjax包来发出我们的请求, 它本身依赖于Aff monad来实现异步效果。
不过,这个例子更有趣: 我们还将使用preventDefault函数来防止表单提交刷新页面,该页面在Effect中运行。这意味着我们的示例展示了如何将不同的effects(Effect和Aff)与Halogen函数(HalogenM)交织在一起。
与
Random示例一样,您可以将此示例粘贴到Try Purescript中以交互方式探索它。您还可以在此存储库的examples目录中查看并运行完整的示例代码.
这个组件定义应该看起来很熟悉。我们定义我们的State和Action类型并实现我们的initialState、render和handleAction函数. 我们将它们组合到我们的组件规范中,并将它们变成一个有效的组件H.mkComponent。
再次注意,我们的effects集中在handleAction函数中,并且在构造初始状态或渲染Halogen HTML时没有执行任何effects。
module Main where
import Prelude
import Affjax as AX
import Affjax.ResponseFormat as AXRF
import Data.Either (hush)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
import Web.Event.Event (Event)
import Web.Event.Event as Event
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State =
{ loading :: Boolean
, username :: String
, result :: Maybe String
}
data Action
= SetUsername String
| MakeRequest Event
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = { loading: false, username: "", result: Nothing }
render :: forall m. State -> H.ComponentHTML Action () m
render st =
HH.form
[ HE.onSubmit \ev -> MakeRequest ev ]
[ HH.h1_ [ HH.text "Look up GitHub user" ]
, HH.label_
[ HH.div_ [ HH.text "Enter username:" ]
, HH.input
[ HP.value st.username
, HE.onValueInput \str -> SetUsername str
]
]
, HH.button
[ HP.disabled st.loading
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Fetch info" ]
, HH.p_
[ HH.text $ if st.loading then "Working..." else "" ]
, HH.div_
case st.result of
Nothing -> []
Just res ->
[ HH.h2_
[ HH.text "Response:" ]
, HH.pre_
[ HH.code_ [ HH.text res ] ]
]
]
handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
SetUsername username -> do
H.modify_ _ { username = username, result = Nothing }
MakeRequest event -> do
H.liftEffect $ Event.preventDefault event
username <- H.gets _.username
H.modify_ _ { loading = true }
response <- H.liftAff $ AX.get AXRF.string ("https://api.github.com/users/" <> username)
H.modify_ _ { loading = false, result = map _.body (hush response) }
这个例子特别有趣,因为:
- 它混合了来自多个
monad的函数(preventDefault是Effect,AX.get是Aff,gets和modify_是HalogenM)。我们可以使用liftEffect和liftAff以及我们的约束来确保一切都很好地协同工作。 - 我们只有一个约束,
MonadAff。那是因为任何可以在Effect中运行的东西也可以在Aff中运行,所以MonadAff意味着MonadEffect。 - 我们正在一次评估中进行多个状态更新。
最后一点特别重要:当您修改组件呈现的状态时。这意味着在本次评估期间,我们:
- 将
loading设置为true,这会导致组件重新渲染并显示Working.. - 将
loading设置为false并更新结果,这会导致组件重新渲染并显示结果(如果有的话)。
值得注意的是,因为我们使用的是MonadAff,所以我们的请求不会阻止组件做其他工作,而且我们不必处理回调来获得这种异步超能力。我们在MakeRequest中编写的计算只是暂停,直到我们得到响应,然后继续第二次更新状态。
仅在必要时修改状态并在可能的情况下一起批量更新是一个聪明的主意(就像我们调用modify_一次来更新loading和result字段一样). 这有助于确保您只在需要时重新渲染。
重新审视事件处理
在这个例子中发生了很多事情,所以值得花点时间关注它引入的新事件处理特性。与按钮示例的简单点击处理程序不同, 此处定义的处理程序确实使用了它们所提供的事件数据:
- 用户名输入的值由
onValueInput处理程序(SetUsername操作)使用。 - 在
onSubmit处理程序(MakeRequest操作)中的事件上调用preventDefault。
传递给处理程序的参数类型取决于用于附加它的函数。有时,对于onValueInput,处理程序只是接收从事件中提取的数据 - 在这种情况下是字符串。大多数其他on...函数设置一个处理程序来接收整个事件,或者作为Event类型的值,或者作为像MouseEvent这样的特殊类型。详细信息可以在Halogen.HTML.Events的模块文档中找到;用于事件的类型和函数可以在web-events和web-uievents包中找到。