随着代码量的增加,相关逻辑日渐复杂,需要维护的状态和传递的消息也迅速的增加起来。
Elm 的架构文档中并没有详细说明如何组织比较复杂的项目,我调查中看到的文章中的方案也大多仍然需要模块间的耦合,实际使用中并不能得到满意的效果。
首先需要做到的是代码层面的分离,模块内部实现细节的修改对外部来说尽量不可见,减少代码的耦合程度,便于开发。
下一个阶段的目标是模块的可重用性,除了简单的函数层面的重用,在更高层次上也有很多相似性,例如如果由于应用场景的考虑,需要发布多个微信小程序的话,其中有不少逻辑是可以共用的,例如微信端用户登录、信息获取,服务后台的 Session 管理,等等。
模块之间的交互应尽量简单,可以用可维护的方式进行组织。
个人的习惯是先从数据开始设计,在 Model 的部分先做分割,之后进行 Msg 的设计,宗旨是把聚合度高的部分放在一起,封装成独立的模块。
多个模块需要彼此协调才能完成完整的应用逻辑,根据具体情况有以下的情境
某个模块需要外部提供所需的数据,有几种处理的方法,可以根据具体需要进行选择
对于子模块来说,其实不用了解事件的具体来源,可以是模块自身,可以是其它模块,或是应用层面的用户输入。只要把自身的生命周期管理好即可,由于 Elm 架构的函数式和不可变特性,一般来说调试也很方便,只要观察 Msg 的序列以及相应的 Model 的变化往往就能找到问题所在。
由于所有的微信小程序都需要进行用户身份的管理,在 elm-wx-app 中提供了一个基本的身份认证子模块,在 API 调用之上提供了更高一层的接口。
下面列出了部分的代码,结构相对比较简单,感兴趣的话可以 Clone 完整的版本。
(目前的版本还比较简单,接口也没有完全固定下来)
type alias Type = { systemInfo : SystemInfo.Type , userCode : String , userInfo : UserInfo.Type , userSecret : UserSecret.Type , tabs : List UiTab.Type , currentTabKey : UiTab.Key , pages : List UiPage.Type }
type Msg = DoInit | DoGetSystemInfo | DoCheckSession | DoLogin | DoLoadWxModel | DoGetUserInfo | GetSystemInfoMsg (Result Error GetSystemInfo.Msg) | CheckSessionMsg (Result Error CheckSession.Msg) | LoginMsg (Result Error Login.Msg) | LoadWxModelMsg (Result Error WxModel.Type) ...
update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of DoInit -> ( model , cmd DoGetSystemInfo ) DoGetSystemInfo -> ( model , GetSystemInfo.cmd GetSystemInfoMsg ) DoCheckSession -> ( model , CheckSession.cmd CheckSessionMsg ) DoLogin -> ( model , Login.cmd LoginMsg ) ...
wrapper 的细节请看 elm-component-updater 的实现代码,基本上是从主模型中访问子模型(get),调用子模块的 update,之后再把返回的子模型更新到主模型中(set)
wrapper : Wrapper Msg Wx.Msg wrapper = wrap WxMod { get = Just << .wx , set = /modModel model -> { model | wx = modModel } , update = Wx.update , react = reaction } cmd msg = toCmd msg |> Cmd.map wrapper
reaction 的目的是对于特定的子模块事件产生相应的外部事件,来达到对其他模块的控制。
reaction modMsg modModel model = model ! [] |> case modMsg of Wx.PopPageMsg pageKey (Ok _) -> case List.length modModel.pages of 0 -> addCmd <| cmd <| Wx.SwitchTab "dialogue" _ -> noOperation _ -> noOperation
首先是在 Model 中包含子模块的部分
type alias Type = { rev : Int , wx : Wx.Model ...
import Updater import WxApp.Mod as Wx type alias Delegate = (Updater Model Msg) type Msg = WxMod Delegate | WxMsg Wx.Msg
WxMod 代表由 Wrapper 处理的事件,WxMsg 则是普通事件,需要在 update 中转换为 Wrapper 事件。
这里做区分的原因是在 Elm 中无法循环 import,在被 WxApp Wrapper 引用的代码中如果也需要通知 WxApp 的模块,则只能产生一个 WxMsg 类型的事件。
import Wrapper.Wx as Wx updateMod : Msg -> (Model, Cmd Msg) -> (Model, Cmd Msg) updateMod msg (model, cmd) = case msg of WxMod delegate -> delegate model WxMsg msg -> (model, Wx.cmd msg) ...
可以看到这里对于 WxMsg 类型的事件使用 wrapper 做了一次转换,略显繁琐,不确定是否有更好的方式。
实际开发中应用以上方式写了不少代码,在模块的分隔上感觉还是一种不错的方法。
在需要调整模块结构的情况下,Elm 作为静态类型语言提供了很大的帮助,编译器可以发现不匹配的接口,重构起来有一气呵成的感觉。