初识Elm

过年期间一直宅在家,打算学一门新语言,之前看见关注的TJ大神经常在star Elm相关的项目,因此打算尝试一下。

<!--more-->

本文首先学习Elm相关语法,然后尝试使用Elm开发简单的Web应用。相关代码均位于github上。参考

1. Elm语言

1.1. 安装

# 全局安装
npm i elm -g

# 进入终端模式,可以学习语法等
elm repl

# 在中的关模式下退出repl
:exit

1.2. 基本语法

参考: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 

1.3. 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中只允许计算表达式,不允许出现额外的定义和赋值操作

1.4. 内置数据结构

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内部实现了两个字段公享其余为变化字段所占用的内存。

1.5. 类型声明

函数参数类型

函数参数类型声明为什么使用多个箭头?

函数类型这里提到,从概念上将,每个函数都可以只接受一个参数,并返回一个新的函数

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

1.6. 模块

参考:模块系统

导出模块

一个模块是一个单独的文件,在文件顶部通过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

2. Elm应用

有一些可供学习的elm应用项目,如

  • elm-todomvc,在官方文档中经常出现的一个demo
  • elm-hn,一个使用elm实现的hacker news web应用

2.1. 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会进行编译,并将整个结果渲染到浏览器中。

2.2. 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中的函数组件。

2.3. 项目结构

elm architecture可以了解到Elm的架构,主要包括

  • Model,保存整个应用的状态state
  • View,将Model中的状态转换成HTML并展示
  • Update,基于message更新state的方式

了解FluxRedux或者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中
    • 注册onClickonInput等事件,当事件触发时将发送对应消息
  • 定义update方法,接收事件消息,并根据消息类型更新Model中的数据

可以看见整个代码都是声明式的,我们唯一的工作是编写函数,声明整个应用的初始状态、更新后的数据状态即可。其余的所有工作都由Elm处理了。

2.4. 性能优化

关于性能优化Elm提出了两个观点

  • 使用Html.lazyHtml.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)

3. 小结

本文首先学习了Elm的基础语法,了解了内置数据结构、类型声明和模块系统,然后了解了如何使用Elm构建简单的Web应用。

尽管Elm是一门不那么年轻的语言了,但就目前的学习体验看来,Elm是一门非常有趣且简洁的语言,使用其构建Web应用貌似十分有吸引力,抛开webpackJavaScript,这是一种完全不同的开发体验。

当然,构建一个Web应用还需要很多额外的工作,如

  • 样式管理、模块与组件、路由系统
  • 网络、JSON、DOM/BOM接口通信
  • 测试与生产环境打包

关于这些内容,将在后面的学习过程中进一步整理。