初识Elm
过年期间一直宅在家,打算学一门新语言,之前看见关注的TJ大神经常在star Elm相关的项目,因此打算尝试一下。
本文首先学习Elm相关语法,然后尝试使用Elm开发简单的Web应用。相关代码均位于github上。参考
- elm,Elm官方文档
- elmprogramming,一个不错的Elm学习网站
- 如何评价Elm语言
Elm语言
安装
# 全局安装
npm i elm -g
# 进入终端模式,可以学习语法等
elm repl
# 在中的关模式下退出repl
:exit
基本语法
参考:elm-core
一些注意事项
- 数字相加使用
+
,字符串拼接使用++
- 字符串使用双引号
""
函数
-- 函数定义,参数放在函数后面,使用空格分隔
greet name age =
"hello " ++ name ++ ", your age is:" ++ (String.fromInt age)
-- 函数调用,多参数传入时同上
greet "shymean" 10
条件判断
if xxx then
else
当需要判断多个分支时,连续使用if...else...
if xxx then
else if xxx2 then
else
let ... in
参考:elm let in
在查看一些项目源码时,会发现文档中未曾提及的一种语法let ... in
。
在编写函数时,我们可能会需要一些变量或辅助函数来计算返回值,这些变量仅在该函数内有效,let ... in
可以让我们实现这种效果
-- 接收两个元组参数,每个元组表示一个矩形的宽高,该函数返回两个矩形的面积和
addAreas : (Int, Int) -> (Int, Int) -> Int
addAreas rect1 rect2 =
let
(w1, h1) = rect1 -- 解构赋值
(w2, h2) = rect2
area w h = w * h -- 可以定义辅助函数
in
(area w1 h1) + (area w2 h2) -- in中只允许计算表达式,不允许出现额外的定义和赋值操作
内置数据结构
List
列表是一组数据类型相同的元素集合,参考module-List
names = ["a", "b"]
-- List对应的一些方法
List.isEmpty names
List.length names
Elm并没有提供根据索引值直接访问列表元素的方法,不过在0.12.1
中加入了Array
模块,因此可以使用Array.get
实现
import Array
arr = Array.fromList names
-- 获取arr索引值为0的元素
Array.get 0 arr
Tuples
元组中每个元素的类型可以不同
(True, "hello string")
Records
当数据类型比较复杂时,相比于元组,字典是更好的选择。
john = {first = "xx", last = "Hor", age = 19}
-- 访问字典中的某个属性
john.age
-- 也可以通过属性访问函数访问属性
.age john
-- 更新字典中某个属性
{john | age = 20}
{john | age = john.age + 1}
-- 更新后的数据为{ age = 20, first = "xx", last = "Hor" }
需要注意的是,更新后返回的是一个新的字典,不会影响原始数据;为了减少内存占用,Elm内部实现了两个字段公享其余为变化字段所占用的内存。
类型声明
函数参数类型
函数参数类型声明为什么使用多个箭头?
在函数类型这里提到,从概念上将,每个函数都可以只接受一个参数,并返回一个新的函数
String.repeat
<function> : Int -> (String -> String)
String.repeat 4
<function> : String -> String
在来看一个例子
List.map
<function> : (a -> b) -> List a -> List b
因此,函数参数类型的声明都是通过箭头来进行分隔多个参数的,
- 对于类型为函数的参数而言,需要使用括号将其类型包裹
- 每个函数都可以只接受一个参数,并返回一个新的函数,对于单参数而言,可以省略对应的括号
type alias
类型别名,用于将一个包含多个字段的数据结构简化为一个简短的类型名称
type alias User =
{ name : String
, age : Int
}
实际上会声明一个User
函数,按顺序传入字段会返回对应的数据实例
> User
<function> : String -> Int -> User
> User "txm" 12
{ age = 12, name = "txm" } : User
Custom Types
自定义类型,可以创建多个类型的集合,每个类型实际上是一个函数,可以直接定义其参数类型
-- 下面是一个网络请求的状态集合,包含失败、加载中和成功三种状态,
-- 其中成功返回的数据类型是一个包含name和description的record
type Profile
= Failure
| Loading
| Success { name : String, description : String }
> Success
<function> : { description : String, name : String } -> Profile
此时就可以使用Profile
作为数据类型
getProfil: Profile -> String
getProfil profile =
case profile of
Failure ->
"sorry fail"
Loading ->
"request is loading"
Success name description ->
"success" ++ name ++ "," ++ description
模块
参考:模块系统
导出模块
一个模块是一个单独的文件,在文件顶部通过module ... exposing ...
暴露整个模块
module Post exposing (Post, estimatedReadTime, encode, decoder)
-- 下面是关于Post、estimatedReadTime、encode和decoder的实现,未被exposing方法仅在该文件中可访问
-- ...
引入模块
使用import
引入模块,同时可以使用模块别名和按需引入
import Post
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder
import Post as P
-- P.Post, P.estimatedReadTime, P.encode, P.decoder
import Post exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- Post.Post, Post.estimatedReadTime, Post.encode, Post.decoder
import Post as P exposing (Post, estimatedReadTime)
-- Post, estimatedReadTime
-- P.Post, P.estimatedReadTime, P.encode, P.decoder
Elm应用
有一些可供学习的elm应用项目,如
- elm-todomvc,在官方文档中经常出现的一个demo
- elm-hn,一个使用elm实现的hacker news web应用
Hello World
参考:elm-init
mkdir elm-demo
cd elm-demo
# 初始化项目,自动生成elm.json和src目录
elm init
然后在项目src
目录下新建Main.elm
文件,作为整个应用的入口文件。(建议使用webstorm
进行项目开发,其中的语法高亮和代码定义跳转比较有用)
-- 整个应用需要暴露main方法
module Main exposing (main)
-- 引入Html模块,可以使用div和h1等标签方法
import Html exposing (..)
-- 引入Attributes,这样就可以通过style修改行内样式
import Html.Attributes exposing (..)
main =
div []
[h1 [style "color" "red"] [text "Hello world"]
, p [] [text "by Elm"]
]
然后在项目根目录开启调试模式elm reactor
,此时浏览器会打开整个项目的目录浏览模式,点击src/Main.elm
文件,此时elm-compiler
会进行编译,并将整个结果渲染到浏览器中。
UI组件
同理,我们也可以实现将节点封装为组件
main =
div []
[h1 [style "color" "red"] [text "Hello World"]
, p [] [text "by Elm"]
, viewList ["a", "b", "c"] -- 使用列表组件,实际上仅仅只是函数的调用
]
-- 封装列表组件
viewList: List String-> Html msg
viewList strings =
ul [] (List.map viewItem strings)
viewItem: String -> Html msg
viewItem string =
li [] [text string]
可以看见,Elm中的组件可以理解为React中的函数组件。
项目结构
从elm architecture可以了解到Elm的架构,主要包括
- Model,保存整个应用的状态state
- View,将Model中的状态转换成HTML并展示
- Update,基于
message
更新state的方式
了解Flux
、Redux
或者Vuex
都不难理解这种数据流转流程(貌似Redux等皆是从Elm中借鉴的相关思路)。
下面是从官方文档中整合的一个包含表单和计数器的Demo,从中可以看见
module Counter exposing (..)
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Html.Attributes exposing (..)
-- MAIN
-- 实现init、update和view方法
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{
count : Int
}
init : Model
init =
{
count = 0
}
-- UPDATE
type Msg = Increment | Decrement | Reset
| Input String
update : Msg -> Model -> Model
update msg model =
case msg of
-- 增加
Increment ->
{model | count = model.count + 1}
-- 减少
Decrement ->
{model | count = model.count - 1}
-- 重置
Reset ->
{model | count = 0}
-- 直接调整
Input num ->
{model | count = Maybe.withDefault 0 <| String.toInt num}
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, span [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
, button [ onClick Reset ] [ text "reset" ]
, viewInput model
]
viewInput: Model -> Html Msg
viewInput model =
input [ placeholder "counter text", value (String.fromInt model.count), onInput Input ] []
整理一下
- 定义了类型别名Model,实际上是一个包含
count
字段的字典,同时定义了init方法返回Model初始值 - 定义了view方法,该方法接收Model作为参数,生成VDOM,
- 将Model中的数据渲染到HTML中
- 注册
onClick
、onInput
等事件,当事件触发时将发送对应消息
- 定义update方法,接收事件消息,并根据消息类型更新Model中的数据
可以看见整个代码都是声明式的,我们唯一的工作是编写函数,声明整个应用的初始状态、更新后的数据状态即可。其余的所有工作都由Elm处理了。
性能优化
关于性能优化Elm提出了两个观点
- 使用
Html.lazy
和Html.keyed
方法减少DOM操作 - 减少代码资源大小避免加载缓慢,elm编译器已经帮我们做了这个工作
elm/html
这个包用来渲染和更新视图,其内部实现了Virtula DOM
,
- 在初始化时会调用
init
获取初始model,调用view
获取初始VDOM,然后渲染html - 在更新时,model发生变化,此时会重新调用view获取更新后的VDOM,然后进行diff,收集变化,最后将变化映射到HTML中
Html.lazy
纯函数的一个重要作用是:same input, same output
,相同的输入可以获得相同的输出。
因此使用Html.lazy
当数据相同时,可以跳过vnode构建过程,节省重新生成vnode带来的开销,这个过程类似于React中的PureComponent
Html.keyed
针对列表元素的插入、移除和排序,Html.keyed
可以用来实现根据相同key的元素进行diff(而不是默认按照元素索引值进行diff)
小结
本文首先学习了Elm的基础语法,了解了内置数据结构、类型声明和模块系统,然后了解了如何使用Elm构建简单的Web应用。
尽管Elm是一门不那么年轻的语言了,但就目前的学习体验看来,Elm是一门非常有趣且简洁的语言,使用其构建Web应用貌似十分有吸引力,抛开webpack
、JavaScript
,这是一种完全不同的开发体验。
当然,构建一个Web应用还需要很多额外的工作,如
- 样式管理、模块与组件、路由系统
- 网络、JSON、DOM/BOM接口通信
- 测试与生产环境打包
关于这些内容,将在后面的学习过程中进一步整理。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。