本教程概述了“Elm 程序的架构”,你在全部 Elm 程序中都能看到它,从 TodoMVC、dreamwriter 到 NoRedInk 以及 CircuitHub 在生产环境中运行的代码。这种基本模式不管用在编写 Elm 或 JS 前端代码时都颇有用。前端
Elm 架构是无限嵌套组件的简单模式,对于模块化、代码重用和测试都颇有效。并且,这种模式能够很容易地用模块化的方式建立复杂的 Web 应用程序。咱们将经过 8 个例子,一步步学习它的核心原则和模式:react
本教程在某些方面上能够称得上前无古人,后无来者!它教会你必要的概念和想法,让开发例子7和例子8变得超级简单。你这笔基础投资绝对是物超所值得的!
在这些示例的架构中有一个很是有趣的地方:它会从 Elm 中 天然浮现 出来。Elm 语言的设计自己致使你走向这个架构,不管你是否已阅读本文件,知道它的好处与否。我只是在使用 Elm 时偶然发现了这种模式,并深深地被它的简单和强悍所震惊。
注意: 要使用此教程,必须和代码一块儿学习。安装 Elm 并 Fork 这个项目。在本教程的每一个例子中都给出了如何运行项目代码的指令。
每一个 Elm 程序的逻辑将被分为三个彻底分离的部分:
model
update
view
你能够很是放心地使用下面的脚手架,而后为你的具体需求不断增长实现细节。
若是你是第一次阅读 Elm 代码,请查看 elm 语言官方文档 它涵盖了从语法到 “函数式思惟”。或者这份 完整指南 的前两章能够帮你快速入门。
-- MODEL type alias Model = { ... } -- UPDATE type Action = Reset | ... update : Action -> Model -> Model update action model = case action of Reset -> ... ... -- VIEW view : Model -> Html view = ...
本教程都是关于这种模式的变化和扩展。
咱们的第一个例子是一个简单的计数器,它能够递增或递减。
这段代码 以一个很是简单的模型开始。咱们只须要跟踪一个数字:
type alias Model = Int
当须要更新咱们的模型时,事情又一次变得简单。咱们定义一组能够执行的动做,以及一个 update
函数来实际执行这些动做:
type Action = Increment | Decrement update : Action -> Model -> Model update action model = case action of Increment -> model + 1 Decrement -> model - 1
请注意,Action
这个 union type 没有作任何事。它简单地描述了可能的行动。若是有人认为当按下某个按钮时咱们的计数器应该增长一倍,咱们只须要增长一个新的 Action
。这意味着这段代码很是清楚 model
该如何变化。任何阅读此代码的人将马上知道那些是容许的,哪些不是。此外,他们将知道如何以一致的方式添加新的功能。
最后,咱们建立了一个 view
来展现 Model
。咱们使用 [elm-html][] 来建立一些在浏览器中显示的 HTML。咱们先建立一个最外层的 div,它内含:一个减量按钮,显示当前计数的 div,和一个增量按钮。
view : Signal.Address Action -> Model -> Html view address model = div [] [ button [ onClick address Decrement ] [ text "-" ] , div [ countStyle ] [ text (toString model) ] , button [ onClick address Increment ] [ text "+" ] ] countStyle : Attribute countStyle = ...
比较棘手的是 view
的 Address
函数。咱们会在下一章深刻讲解它!如今,我只是想让你注意到 这段代码彻底只是声明。咱们使用 Model
生成 Html
。就是这样,在任什么时候候,咱们不会手工改变 DOM,这给了一些库 更大的自由度作出更聪明的优化 而且使渲染速度更快。这简直疯了!并且, view
是一个普通的函数,因此咱们建立 view
时能够获得 Elm 的模块系统,测试框架和库。
这种模式是架构 Elm 程序的精髓。咱们从如今开始看到每个例子都将只是对这个基本模式的略微变化: Model
,update
,view
。
几乎全部的 Elm 程序都会有一个简短的代码,用它来驱动整个应用程序。在本教程的每一个例子中,该代码被命名为 Main.elm
。虽然是个反例,但它依然颇有趣:
import Counter exposing (update, view) import StartApp.Simple exposing (start) main = start { model = 0, update = update, view = view }
咱们使用 StartApp
这个库把初始 model
的 update
和 view
链接起来。它只是对 Elm 的 signals 作了一个小封装,因此你还不须要深刻研究它的原理。
装配应用的关键概念是 Address
。每一个事件处理器在 view
函数中获得一个特定的地址,而且和数据块一块儿传递过来。StartApp
监听全部传给这个地址的消息,而后把它们发送给 update
函数。model
得到更新, 而 [elm-html][] 负责渲染和高效的修改。
这意味着,Elm 程序中的数据只会在一个方向上流动,相似这样:
蓝色部分是咱们 Elm 程序的核心,这正是 model/update/view,咱们一直在讨论的模式。使用 Elm 编程,你能够一直呆在这个舒服的盒子里面,并取得很大的进步。
注意,咱们 不执行 送回应用程序的 action
。咱们只是转发一些数据。这种分离是一个关键的细节,使咱们的逻辑彻底从视图代码中分离出来。
在上一个例子里咱们搞了一个计数器,若是增长到两个计数器时这个模式会怎样变化呢?咱们能继续保持模块化吗?
若是咱们能彻底重用 例子1 的代码就再好不过了。Elm 架构最疯狂的就是:咱们能够一句不变地重用代码。当咱们实现 例子1 的 Counter
模块时,它包括了全部细节,因此咱们能够在任何地方使用它。
module Counter (Model, init, Action, update, view) where type Model init : Int -> Model type Action update : Action -> Model -> Model view : Signal.Address Action -> Model -> Html
编写模块代码其实彻底是在建立一种很强的抽象。咱们期待的是提供合适的函数接口,可是隐藏具体执行过程。从 Counter
模块的外部咱们只能看到一些基础的值: Model
、init
、Action
、update
和 view
。咱们彻底不用关心这些是如何实现的。事实上,也不可能知道这些是如何实现的。这意味着没人须要依赖这些不公开的实现细节。
咱们本能够彻底复制 Counter
模块, 但咱们仍是使用它的一部分来实现 CounterPair
。 像往常同样, 咱们从一个 Model
开始:
type alias Model = { topCounter : Counter.Model , bottomCounter : Counter.Model } init : Int -> Int -> Model init top bottom = { topCounter = Counter.init top , bottomCounter = Counter.init bottom }
咱们的 Model
纪录了两个计数器, 其中一个是须要在屏幕上显示的。这个 Model
彻底描述了应用全部的状态。咱们还有一个 init
函数能够在任何地方建立一个新的 Model
。
下一步来描述下咱们想要支持的 Actions
。咱们须要的功能是:重置全部的计数器,更新顶部的计数器,或者更新下面的计数器。
type Action = Reset | Top Counter.Action | Bottom Counter.Action
请注意,咱们的 [union type][] 是参考 Counter.Action
类型,但咱们并不知道那些 action
的细节。当咱们建立 update
函数时,主要工做是路由这些 Counter.Actions
到正确的地方:
update : Action -> Model -> Model update action model = case action of Reset -> init 0 0 Top act -> { model | topCounter = Counter.update act model.topCounter } Bottom act -> { model | bottomCounter = Counter.update act model.bottomCounter }
因此最后要作的事情就是建立一个 view
函数显示两个计数器和两个重置按钮。
view : Signal.Address Action -> Model -> Html view address model = div [] [ Counter.view (Signal.forwardTo address Top) model.topCounter , Counter.view (Signal.forwardTo address Bottom) model.bottomCounter , button [ onClick address Reset ] [ text "RESET" ] ]
请注意,咱们能够在两个计数器之中复用 Counter.view
函数,给每一个计数器建立一个转发地址。大致上,这里作的事情实际上是:“让这俩计数器给全部向外传递的消息打上 Top
或 Bottom
标志,以便区分”
这就是全部的工做。最屌的是咱们能够一层又一层地保持嵌套。咱们能够建立 CounterPair
模块,暴露关键值和方法,而后建立 CounterPairPair
或者任何其余咱们须要的。
两个计数器已经很屌了,一个能够随意添加和删除的计数器队列会怎么样呢?这种模式还有效吗?
甚至咱们能够彻底像 例子1 和 例子2 里那样复用 Counter
!
module Counter (Model, init, Action, update, view)
这意味着咱们能够开始建立 CounterList
模块了。 像往常同样, 咱们从 Model
开始:
type alias Model = { counters : List ( ID, Counter.Model ) , nextID : ID } type alias ID = Int
如今,咱们的 model
有了一个计数器队列,每一个计数器有一个惟一的 ID。这些 ID 使咱们能够区别它们,因此若是咱们要更新 4 号计数器,咱们能够很轻松的找到它。(当咱们考虑优化渲染时,这个 ID 也给了咱们一些 key
的便利,然而它并非这个教程的重点!)咱们的 modal
还包含一个 nextID
帮助咱们指定 ID 给每个新增的计数器。
如今咱们能够定义一组 Action
来操做 model
。咱们但愿能够添加计数器,删除计数器,以及更新特定的计数器。
type Action = Insert | Remove | Modify ID Counter.Action
咱们的 Action
union type 使人震惊的接近高阶描述。下面咱们能够定义 update
函数了。
update : Action -> Model -> Model update action model = case action of Insert -> let newCounter = ( model.nextID, Counter.init 0 ) newCounters = model.counters ++ [ newCounter ] in { model | counters = newCounters, nextID = model.nextID + 1 } Remove -> { model | counters = List.drop 1 model.counters } Modify id counterAction -> let updateCounter (counterID, counterModel) = if counterID == id then (counterID, Counter.update counterAction counterModel) else (counterID, counterModel) in { model | counters = List.map updateCounter model.counters }
这里有对每种状况的高阶描述:
Insert
— 首先咱们创造一个新的计数器,并把它当在计数器队列的最后。而后咱们给 nextID
加一,以便下一次添加时有一个新的ID。
Remove
— 删除计数器列表的第一个成员。
Modify
— 遍历全部计数器,当找到匹配的 ID 时,用所给的 Action
操做这个计数器。
下面惟一要作的就是定义 view
。
view : Signal.Address Action -> Model -> Html view address model = let counters = List.map (viewCounter address) model.counters remove = button [ onClick address Remove ] [ text "Remove" ] insert = button [ onClick address Insert ] [ text "Add" ] in div [] ([remove, insert] ++ counters) viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html viewCounter address (id, model) = Counter.view (Signal.forwardTo address (Modify id)) model
这里的 viewCounter
函数比较有趣。它必须使用同一个 Counter.view
函数,但在这里咱们提供了一个转发地址来标记全部的消息和正在渲染的计数器的 ID。
实际上,当咱们建立 view
函数时,咱们映射 viewCounter
到全部的计数器,而后建立添加和删除按钮直接返回 address
。
这个 ID 的玩法能够用在任何你须要数目可变的子模块时。计数器是简单的,可是这种模式能够彻底不变的在用户信息,tweets,新闻列表或者产品列表上复用。
OK,在一个动态的计数器列表上保持简单和模块化是很屌的,可是若是不要一个通用的删除按钮,而是每一个计数器有一个单独的删除按钮呢?它会把事情搞糟吗?
不, 它仍然有效.
在这里,咱们的目标是找到一种新的方法给每一个计数器添加一个删除按钮。有趣的是,咱们能够继续使用原有的 view
函数并添加一个新的 viewWithRemoveButton
函数,这个函数为咱们依赖的 Model
提供一个微小的变化。屌屌屌,咱们不用重复任何代码更不用作任何疯狂的继承和重载。咱们只是给公开的 API 添加了一个函数暴露新的功能!
module Counter (Model, init, Action, update, view, viewWithRemoveButton, Context) where ... type alias Context = { actions : Signal.Address Action , remove : Signal.Address () } viewWithRemoveButton : Context -> Model -> Html viewWithRemoveButton context model = div [] [ button [ onClick context.actions Decrement ] [ text "-" ] , div [ countStyle ] [ text (toString model) ] , button [ onClick context.actions Increment ] [ text "+" ] , div [ countStyle ] [] , button [ onClick context.remove () ] [ text "X" ] ]
viewWithRemoveButton
函数添加了一个额外的按钮。请注意 增长/减小 按钮发送消息给 actions
地址,可是删除按钮发送消息给 remove
这个地址。咱们发给 remove
的消息实际上是在说:“嘿,不管谁拥有我,请删掉我!” 这个计数器的拥有者负责删除。
既然咱们有了新的 viewWithRemoveButton
, 咱们能够建立一个新的 CounterList
模块把全部独立的计数器放在一块儿。这个 Model
和 栗子3 中的同样: 带各自 ID 的计数器列表。
type alias Model = { counters : List ( ID, Counter.Model ) , nextID : ID } type alias ID = Int
咱们的 action
稍有不一样。不是删除一个旧的计数器,而是删除特定的一个,因此 Remove
须要一个 ID。
type Action = Insert | Remove ID | Modify ID Counter.Action
update
函数和 栗子3 中的很是像。
update : Action -> Model -> Model update action model = case action of Insert -> { model | counters = ( model.nextID, Counter.init 0 ) :: model.counters, nextID = model.nextID + 1 } Remove id -> { model | counters = List.filter (\(counterID, _) -> counterID /= id) model.counters } Modify id counterAction -> let updateCounter (counterID, counterModel) = if counterID == id then (counterID, Counter.update counterAction counterModel) else (counterID, counterModel) in { model | counters = List.map updateCounter model.counters }
在 Remove
时,咱们取出拥有该 ID 的计数器。不然,退出直接退出并保持原来那样。
最后,咱们咱们把萌宝宝们都放进 view
中:
view : Signal.Address Action -> Model -> Html view address model = let insert = button [ onClick address Insert ] [ text "Add" ] in div [] (insert :: List.map (viewCounter address) model.counters) viewCounter : Signal.Address Action -> (ID, Counter.Model) -> Html viewCounter address (id, model) = let context = Counter.Context (Signal.forwardTo address (Modify id)) (Signal.forwardTo address (always (Remove id))) in Counter.viewWithRemoveButton context model
在 viewCounter
函数中, 咱们构造了 Counter.Context
来传递全部必需的转发地址。在两种状况下分别声明 Counter.Action
以便咱们知道哪一个计数器须要修改或删除。
基础模式 — 任何事都是围绕 Model
建立出来的,包括更新 model
的函数, 以及 model
的 view
。任何事均可以看做基础模式的变体。
嵌套 Modules — 转发地址使基础模式的嵌套变的简单,彻底隐藏实现细节。咱们能够无限深地嵌套这种模式,而且每一层只须要知道下一层在发生什么。
添加上下文 — 有时对 modal
进行 update
或者 view
操做时须要额外的信息。咱们随时能够添加 Context
给这些函数并传递全部的附加信息而不须要改变 Model
。
update : Context -> Action -> Model -> Model view : Context' -> Model -> Html
在嵌套的每一层,咱们均可觉得每一个子模块衍生出所需的 Context
。
测试变的简单 — 咱们建立的全部函数都是 纯洁函数。这样测试 update
函数变的极其简单。不须要特别的初始化、模拟、配置步骤,你只要带着你想要测试的参数直接调用函数便可。
咱们已经讲了如何建立可无限嵌套的组件,但当咱们在某个组件里发出一个 HTTP 请求时会发生什么呢?与数据库通讯呢?这个栗子使用 elm-effects
包 来建立一个简单的组件,这个组件能够从 giphy.com 获取随机的可爱喵星人的 gif。
若是看了 这个栗子的实现, 你会注意到它和 栗子1 中的代码很是接近。它的 Model
很是典型:
type alias Model = { topic : String , gifUrl : String }
咱们须要知道要查找的 topic
值和当前展现的 gifUrl
。这里惟一新颖的东西是 init
和 update
的类型:
init : String -> (Model, Effects Action) update : Action -> Model -> (Model, Effects Action)
并不是只是返回一个新的 Model
咱们还返回一些咱们须要执行的效果。因此咱们将会使用 Effects
API,看起来像这样:
module Effects where type Effects a none : Effects a -- don't do anything task : Task Never a -> Effects a -- request a task, do HTTP and database stuff
Effects
类型本质上是一个包含了一些会在以后执行的独立任务的数据类型。让咱们经过分析这里的 update
来更深刻了解下这是怎么工做的:
type Action = RequestMore | NewGif (Maybe String) update : Action -> Model -> (Model, Effects Action) update msg model = case msg of RequestMore -> ( model , getRandomGif model.topic ) NewGif maybeUrl -> ( Model model.topic (Maybe.withDefault model.gifUrl maybeUrl) , Effects.none ) -- getRandomGif : String -> Effects Action
因此用户能够经过点击 “More Please!” 按钮来触发 RequestMore
,当服务器响应请求后它会给咱们一个 NewGif
的 action
。咱们在 update
函数中处理这两种状况。
在这里 RequestMore
第一次返回已经存在的 model
。用户只是点击了一个按钮,这时并无任何改变。咱们还使用 getRandomGif
函数建立了一个 Effects Action
。咱们立刻将会知道 getRandomGif
是如何定义的。到此为止,咱们只需知道当一个 Effects Action
运行时,会有一系列 Action
值产生并被传递给整个应用。因此 getRandomGif model.topic
最终会产生像这样的一个 action like
:
NewGif (Just "http://s3.amazonaws.com/giphygifs/media/ka1aeBvFCSLD2/giphy.gif")
它返回一个 Maybe
由于向服务器发出的请求可能失败。那个 Action
将会原路返回给 update
函数。因此当咱们执行 NewGif
时,咱们只是更新当前的 gifUrl
,若是他能够被更新。当请求失败后,咱们只是停留在当前的 model.gifUrl
。
咱们看到一样的事情发生在 init
函数中,它定义了初始时的 modal
而且经过 giphy.com 的 API 请求一个特定话题的 GIF。
init : String -> (Model, Effects Action) init topic = ( Model topic "assets/waiting.gif" , getRandomGif topic ) -- getRandomGif : String -> Effects Action
再一次,当随机的 GIF 下载完成,它会产生一个 Action
发送给 update
函数。
注意: 以前咱们使用的是来自 the start-app package 的
StartApp.Simple
模块,可是如今请升级到StartApp
模块。它能够处理更实际的 web 应用中的复杂状况。它有 更优雅的 API。更相当重要的改变是它能够处理咱们新的init
和update
类型。
这个例子中一个相当重要的方面是 getRandomGif
函数,它描述了如何获得一张随机的 GIF。它使用了 任务 和 Http
库, 我会尽力概述它是如何运作的。让咱们看定义:
getRandomGif : String -> Effects Action getRandomGif topic = Http.get decodeImageUrl (randomUrl topic) |> Task.toMaybe |> Task.map NewGif |> Effects.task -- The first line there created an HTTP GET request. It tries to -- get some JSON at `randomUrl topic` and decodes the result -- with `decodeImageUrl`. Both are defined below! -- -- Next we use `Task.toMaybe` to capture any potential failures and -- apply the `NewGif` tag to turn the result into a `Action`. -- Finally we turn it into an `Effects` value that can be used in our -- `init` or `update` functions. -- Given a topic, construct a URL for the giphy API. randomUrl : String -> String randomUrl topic = Http.url "http://api.giphy.com/v1/gifs/random" [ "api_key" => "dc6zaTOxFJmzC" , "tag" => topic ] -- A JSON decoder that takes a big chunk of JSON spit out by -- giphy and extracts the string at `json.data.image_url` decodeImageUrl : Json.Decoder String decodeImageUrl = Json.at ["data", "image_url"] Json.string
一旦咱们写了上面这些,咱们就能够在 init
和 update
函数中复用 getRandomGif
。
有趣的是,getRandomGif
返回的任务是永远不会失败的。缘由是任何可能的失败必须被明确的处理,咱们不但愿任何任务静静地失败。
我试图确切地解释下它是如何实现的,虽然这对于整个项目的正常运行并不特别重要。Okay,这样每一个 Task
有一个失败的类型和一个成功的类型。例如,一个 HTTP 任务可能有类型如:Task Http.Error String
,咱们能够在失败时返回一个 Http.Error
或者成功时返回一个 String
。这样能够优雅地把一组任务串在一块儿而不用过多的担忧出错。如今,假设咱们的组件请求了一个任务,可是任务失败了。会发生什么呢?谁会被通知?如何恢复?经过设置失败类型为 Never
,咱们强制任何可能的错误变成成功类型,这样它们就能够被组件明确的处理了。在这个例子里,咱们用 Task.toMaybe : Task x a -> Task y (Maybe a)
因此 update
函数精确的处理了 HTTP 失败。这意味着任务不能静默的失败,你永远精确的处理着未知的错误。
好了,结果搞定了,可是 嵌套 的结果呢?你是否思考过这个问题?!这个例子彻底重用栗子5中的 GIF 查看器的代码建立了两个独立的 GIF 查看器。
你阅读 这个实现代码 时,会注意到它和栗子2中的两个计数器的代码几乎同样。Model
被定义为两个 RandomGif.Model
的值:
type alias Model = { left : RandomGif.Model , right : RandomGif.Model }
这让咱们能够独立地分别跟踪它们。咱们的 action
只是路由消息到正确的自模块。
type Action = Left RandomGif.Action | Right RandomGif.Action
有趣的是,咱们实际上使用了 Left
and Right
标签在 update
和 init
函数中。
-- Effects.map : (a -> b) -> Effects a -> Effects b update : Action -> Model -> (Model, Effects Action) update action model = case action of Left msg -> let (left, fx) = RandomGif.update msg model.left in ( Model left model.right , Effects.map Left fx ) Right msg -> let (right, fx) = RandomGif.update msg model.right in ( Model model.left right , Effects.map Right fx )
因此不论在哪一个分支中调用 RandomGif.update
函数时都会返回一个新 model
和一些被咱们称做 fx
的操做。咱们像往常同样返回一个更新过的 model
,可是须要在操做上作一些额外的工做。并不是直接返回它们,咱们使用 Effects.map
函数把他们转化为一种 Action
。这工做很像 Signal.forwardTo
,让咱们标记这些值以便肯定如何路由。
init
函数也是同样。咱们提供一个 topic
给每一个随机 GIF 查看器,而后获得一个初始的 model
和一些 effects
。
init : String -> String -> (Model, Effects Action) init leftTopic rightTopic = let (left, leftFx) = RandomGif.init leftTopic (right, rightFx) = RandomGif.init rightTopic in ( Model left right , Effects.batch [ Effects.map Left leftFx , Effects.map Right rightFx ] ) -- Effects.batch : List (Effects a) -> Effects a
在这里咱们并不是只用 Effects.map
来标记合适的结果,还要用 Effects.batch
函数来把他们归并到一块儿。全部请求的任务将会被生成而且独立运行,因此左边和右边两个 effects
会同时被处理。
这个例子实现了一个随机 GIF 查看器的队列,你能够本身为他设置话题。并且,咱们彻底重用了 RandomGif
模块的核心。
仔细看看 它的代码 你会发现它和 例子3 几乎一致。咱们把全部子模块放进一个关联了 ID 的列表,并依据这些 ID 来进行操做。惟一新鲜的是咱们使用 Effects
在 init
和 update
函数中,把他们和 Effects.map
以及 Effects.batch
放在一块儿。
若是你对它的实现细节还不够清楚,请建立一个 issue。
如今,咱们已经看到了带任务的组件能够很轻松地嵌套在一块儿,可是用它如何实现动画呢?
颇有趣,它们彻底同样!(或许你已经再也不感到惊奇了,相同的模式在这里也适用,真是一个可爱的模式!)
这个例子是两个可点击的方块。当你点击一个方块时,它旋转 90 度。整体上,这里的代码是对 例子2 和 例子6 的调整,咱们保留了全部的动画逻辑在 SpinSquare.elm
里面,而且在 SpinSquarePair.elm
里屡次复用它。
全部新的和有趣的东西都发生在 SpinSquare
里,因此咱们来关注下这里的代码。首先咱们须要一个 model:
type alias Model = { angle : Float , animationState : AnimationState } type alias AnimationState = Maybe { prevClockTime : Time, elapsedTime: Time } rotateStep = 90 duration = second
因此 model 的核心是方块当前的 angle
和一些用来记录每一个动画要作什么的 animationState
。若是没有动画就是 Nothing
,可是若是有动做发生,它就变为:
prevClockTime
— 用于计算时间差的最近时间。它帮咱们精确地肯定上一帧后过了多少毫秒。
elapsedTime
— 0 到 duration
之间的一个数字,告诉咱们当前动画已经进行了多久。
常量 rotateStep
只是声明每次点击转变多少度。你能够随意修改它,而不会影响正常运行。
如今,update
里发生了一些有趣的事:
type Action = Spin | Tick Time update : Action -> Model -> (Model, Effects Action) update msg model = case msg of Spin -> case model.animationState of Nothing -> ( model, Effects.tick Tick ) Just _ -> ( model, Effects.none ) Tick clockTime -> let newElapsedTime = case model.animationState of Nothing -> 0 Just {elapsedTime, prevClockTime} -> elapsedTime + (clockTime - prevClockTime) in if newElapsedTime > duration then ( { angle = model.angle + rotateStep , animationState = Nothing } , Effects.none ) else ( { angle = model.angle , animationState = Just { elapsedTime = newElapsedTime, prevClockTime = clockTime } } , Effects.tick Tick )
有两种 Action
咱们须要处理:
Spin
标示一个用户点击了方块,请求一次旋转。因此在 update
函数中,若是没有正在进行的动画,咱们就请求一个时间戳,并把状态设置为一个动画正在进行。
Tick
标示咱们已经获得了一个时间戳,因此咱们须要进行一次动画。在 update
函数中,这意味着咱们须要更新 animationState
。因此,首先,咱们检查当前是否有正在进行的动画。若是有,咱们只是计算出 newElapsedTime
的值,经过把当前的 elapsedTime
加上一个时间差。若是当前通过的时间大于 duration
,咱们就中止动画并请求一个新的时间戳。不然,咱们更新动画状态,也请求一个新的时间戳。
再一次,随着写了这么多相似的代码,审视一遍它们,咱们会发现一个通用的模式。发现它时你必定很激动!
终于,不管如何咱们有了一个有趣的 view
函数!这个例子有了一个优雅又充满活力的动画,而咱们只是在时间线上增长了 elapsedTime
而已。这是怎么作到的呢?
view
的代码自己就是一个标准的 elm-svg
,能够制做一些漂亮的可点击图形。 代码中 最牛X 的是 toOffset
,它计算了当前 AnimationState
的旋转的度数。
-- import Easing exposing (ease, easeOutBounce, float) toOffset : AnimationState -> Float toOffset animationState = case animationState of Nothing -> 0 Just {elapsedTime} -> ease easeOutBounce float 0 rotateStep duration elapsedTime
咱们使用 @Dandandan 的 easing 库,它使得对数字、颜色、点以及其余任何疯狂的东西的 补间排序 变得很简单。
因此 ease
函数从 0 到 duration
之间取出一个数。而后它把它转变成一个 0 到 rotateStep
(咱们以前的代码里已经把它设置为 90 度了)之间的一个数。在这里你还提供了一个 补间
给 easeOutBounce
这意味着随着它从 0 到 duration
变化,咱们会获得一个从 0 到 90 变化的数字。太疯狂了!尝试替换 easeOutBounce
为 另外一个补间 看看是什么效果!
从这儿开始,咱们把全部东西都拼装到了一块儿成为 SpinSquarePair
, 而它的代码几乎与 例子2 和 例子6 的如出一辙。
好了,这就是用这些工具实现动画的基础!若是把全部东西都摆在这儿,可能不够清晰,因此当你有了更多的经验,请让咱们知道你的收获。但愿咱们能够把她变得更简单!
注意: 我期待咱们能够在这些核心思想之上构建一些抽象概念。这个例子作了一些基础的事情,可是我打赌随着咱们继续为它作出的工做,咱们能够找到一些优雅的模式使它更简单。若是你以为它如今仍是很复杂,请试着让它变得更好,并把你的想法告诉咱们吧!