Real World Halogen
Why Halogen
purescript-halogen
是一个100%d的purescript virtual DOM实现,一个基于组件化的框架。如果不bind其他框架(React ),这是最OK的选择
Why Purescript
- best-in-class type system
- algebraic data types
- generics
- row types
- type classes
函数式的Single-Page App设计
Single-Page App
: 一种基于web的应用或者网站, 这种single page在和用户交互的时候当用户点击某个物件或者按键的时候不会跳转到其他的页面. 使H5应用使用起来跟原生应用一样流畅,没有页面的跳转,所有的交互都在一个页面完成,页面的切换通过route完成。
Strongly-typed languages make excellent choices for domain modeling and Domain-driven design
使用三个内容帮助我们设计应用
- 用例
用户可以进行的操作 - 数据模型
将应用中的信息实体抽象表达 - 转换
使用函数转化数据,或从一个状态变化到另一个状态,这个函数应尽可能pure和small
模型设计原则
- 使用模型来支持业务处理
模型 支持 业务逻辑
业务逻辑 影响 模型设计
- 用类型授予值以意义
home :: String
home = "home"
navigate :: String -> Effect Unit
navigate r = setHashTo r
-- 但是String所代表的值远远多于应用中的route
data Route = Home | Setting
navigate :: Route -> Effect Unit
navigate Home = setHashTo "home"
navigate Settings = setHashTo "settings"
-- or
instance showRoute :: Show Route where
show Home = "home"
show Settings = "settings"
navigate :: Route -> Effect Unit
navigate r = setHashTo $ show r
- 使用自定义类型来进行区别,尽管该类型只是包裹了基础类型
newtype CustomerId = CustomerId Natural
newtype OrderId = OrderId Natural
-- 尽管两种类型的值可能相同,但其意义绝不相同
- 排除掉不合法的状态
-- 联系方式可能是Email或Postal的一种 或两者都有
type ContactInfo = Tuple (Maybe Email) (Maybe Postal)
-- 这种可能会有 Tuple Nothing Nothing, 这并不是合法的数据
data ContactInfo
= EmailOnly Email
| PostalOnly Postal
| Both Email Postal
只要我们保证某类型的所有数据都是合法的,那么所有处理该类型的函数均无需担心因不合法的数据而出错。
-- UserName: 长度在[1, 32]之内的任意字符串,(或更多规则)
-- 只暴露类型,不暴露构造函数
newtype Username = Username String
mkUsername :: String -> Maybe UserName
mkUsername = Username <=< lengthInRange 1 32 <=< otherPrinciple
where
lengthInRange :: Int -> Int -> String -> Maybe String
otherPrinciple :: String -> Maybe String
open record in purescript
-- This type is now an "open record", which means it can be extended
-- with more fields.
type UserProfileFields r =
{ username :: Username
, bio :: Maybe String
, image :: Maybe ProfilePhoto
| r
}
type UserProfile = UserProfileFields ()
data AuthUser = AuthUser Token (UserProfileFields (email :: Email))
表示带有状态的数据 (remote-data)
-- https://pursuit.purescript.org/packages/purescript-remotedata
data RemoteData err res
= NotAsked
| Loading
| Failure err
| Success res
框架设计原则
The ReaderT Design Pattern
- 使用
ReaderT Monad Transformer
来实现一个通用Monad。获得: 一个global read-ony
的环境,包含一个配置信息,例如:logging level, data base connection, credentials - 如果需要全局的可变状态,使用
mutable reference
, 而不要用StateT monad transformer
, See this explanation in the Halogen repo for more details. pure type classes
: 可能在之后运行以副作用,或者在无副作用的环境之下 (Aff
,Effect
)
The Three-Layer App
1. Layer1: The ReaderT Pattern
你可以任意定义pure functions, 但是最终你的应有仍要产生副作用。 这一层就是为此,你的monad
从ReaderT
转换到Aff
。这一层应尽可能小, 这一层都是关于操作的:
- 管理配置
- 发起网络请求
- 实现并发
- 读取数据
2. Layer2: External Services & Dependencies
你需要layer1去实际管理数据库连接等,但是你不应在layer1实现与外部服务一一对应的接口。实际上,应定义纯函数,以便被被layer1调用,以产生副作用。这一层也应尽可能薄, 把业务代理到layer3.
3. Layer3: Pure Business Logic
应用中剩余逻辑应该都是无副作用的,使用纯函数和简单的数据类型来实现业务逻辑。
Halogen Components
Halogen is a type-safe, declarative UI library for PureScript applications.
declarative
type-safe
- Design almost all code in a pure, functional style
- Use type classes to represent capabilities like reading and writing to local storage, routing, and logging
- Implement a thin layer of highly stateful code to actually perform requests, read and write to local storage, and so on — a layer which can easily be swapped out for mocks when testing without changing the functional core.
- Store application-wide information in
ReaderT
and represent global state with aRef
instead of a state monad- Manage UI state and interactions with relatively monolithic components (compared to what you might see in React), preferring pure functions instead of components for parts of the application that don’t require internal state or communication with the browser or other components
Components
通常是带有副作用的外层壳的一部分,但是也不必非要如此,我们肯尽可能使得组件无副作用。一个如下的组件,是一个包裹了状态和事件循环的组件,但是在被Halogen执行之前,依然是无副作用的:
myComponent :: forall m. Component HTML Query Input Message m
虽然这个组件会与DOM交互,但是除此之外,你可以把它当做一个无副作用的状态机。
myComponent :: forall m. LocalStorage m => Component HTML Query Input Message m
现在你的组件可以访问在localstorage相关定义的纯函数了。
render函数也是无副作用的,并且大多数组件都会有几个这样的函数构成的。可以定义一个pure render, 然后被其他组件使用。
-- Given an user profile, render a header with their username (for example)
renderHeader :: forall p i. Profile -> HTML p i
一个通常的Halogen应用由主页组件,其他小组件(页头,时间选择器,图表等等)构成。主页由很多render构成(每个render都是renderPart)
仅有两种设计模式是可复用的,higher-order, renderless
There are only two design patterns that provide true reuse in this sense of the word: higher-order and renderless components.
Higher-order
组件是React社区常用的模式,其使用组件作为参数,赋予其新的行为和状态,然后返回一个新的组件Renderless
组件不含有render函数,然后允许你扩展该组件的行为和状态,这个父组件里面提供render函数