端口
端口允许 Elm 和 JavaScript 之间进行通信。
端口可能最常用于 WebSocket
和 localStorage
。让我们关注 WebSocket
示例。
JavaScript 中的端口
在这里,我们的 HTML 与以前页面中使用的非常相似,但其中增加了一些额外的 JavaScript 代码。我们创建到 wss://echo.websocket.org
的连接,该连接会重复发送给它的任何内容。你可以在 实时示例 中看到,这使我们能够创建聊天室的骨架
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm + Websockets</title>
<script type="text/javascript" src="elm.js"></script>
</head>
<body>
<div id="myapp"></div>
</body>
<script type="text/javascript">
// Start the Elm application.
var app = Elm.Main.init({
node: document.getElementById('myapp')
});
// Create your WebSocket.
var socket = new WebSocket('wss://echo.websocket.org');
// When a command goes to the `sendMessage` port, we pass the message
// along to the WebSocket.
app.ports.sendMessage.subscribe(function(message) {
socket.send(message);
});
// When a message comes into our WebSocket, we pass the message along
// to the `messageReceiver` port.
socket.addEventListener("message", function(event) {
app.ports.messageReceiver.send(event.data);
});
// If you want to use a JavaScript library to manage your WebSocket
// connection, replace the code in JS with the alternate implementation.
</script>
</html>
我们在所有互操作示例中都调用 Elm.Main.init()
,但这一次我们实际上正在使用结果 app
对象。我们正在订阅 sendMessage
端口,并且正在发送到 messageReceiver
端口。
这些对应于 Elm 端编写的代码。
Elm 中的端口
查看在对应的 Elm 文件中使用 port
关键字的行。这是我们定义刚在 JavaScript 端看到的端口的方式。
port module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as D
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- PORTS
port sendMessage : String -> Cmd msg
port messageReceiver : (String -> msg) -> Sub msg
-- MODEL
type alias Model =
{ draft : String
, messages : List String
}
init : () -> ( Model, Cmd Msg )
init flags =
( { draft = "", messages = [] }
, Cmd.none
)
-- UPDATE
type Msg
= DraftChanged String
| Send
| Recv String
-- Use the `sendMessage` port when someone presses ENTER or clicks
-- the "Send" button. Check out index.html to see the corresponding
-- JS where this is piped into a WebSocket.
--
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DraftChanged draft ->
( { model | draft = draft }
, Cmd.none
)
Send ->
( { model | draft = "" }
, sendMessage model.draft
)
Recv message ->
( { model | messages = model.messages ++ [message] }
, Cmd.none
)
-- SUBSCRIPTIONS
-- Subscribe to the `messageReceiver` port to hear about messages coming in
-- from JS. Check out the index.html file to see how this is hooked up to a
-- WebSocket.
--
subscriptions : Model -> Sub Msg
subscriptions _ =
messageReceiver Recv
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text "Echo Chat" ]
, ul []
(List.map (\msg -> li [] [ text msg ]) model.messages)
, input
[ type_ "text"
, placeholder "Draft"
, onInput DraftChanged
, on "keydown" (ifIsEnter Send)
, value model.draft
]
[]
, button [ onClick Send ] [ text "Send" ]
]
-- DETECT ENTER
ifIsEnter : msg -> D.Decoder msg
ifIsEnter msg =
D.field "key" D.string
|> D.andThen (\key -> if key == "Enter" then D.succeed msg else D.fail "some other key")
请注意,第一行显示的是 port module
而不是 module
。这使得可以在给定模块中定义端口。如果需要,编译器会对此提供提示,因此希望没有人会对此感到困惑!
好的,但是 sendMessage
和 messageReceiver
的 port
声明是怎么回事?
传出消息 (Cmd
)
sendMessage
声明允许我们发送消息离开 Elm。
port sendMessage : String -> Cmd msg
在此,我们声明想要发送String
值,但我们可以发送适用于标志的任何类型。我们在前一页中讨论了这些类型,你可以查看这个localStorage
示例,了解将Json.Encode.Value
发送到 JavaScript 的过程。
在这里,我们可以将sendMessage
用作任何其他函数。如果你的update
函数产生sendMessage "hello"
命令,你可以在 JavaScript 端关注该命令
app.ports.sendMessage.subscribe(function(message) {
socket.send(message);
});
此 JavaScript 代码已订阅所有传出消息。你可以按引用订阅
多项函数并取消订阅
函数,但我们通常建议保留静态内容。
我们还建议发送更丰富的信息,而不是创建大量独立的端口。也许这意味着在 Elm 中拥有一个自定义类型,该类型表示可能需要告知 JS 的所有内容,然后使用Json.Encode
将其发送到单个 JS 订阅中。许多人发现这可以更清楚地划分职责范围。Elm 代码明显拥有某些状态,而 JS 明显拥有其他状态。
传入消息 (Sub
)
messageReceiver
声明使我们能够侦听传入 Elm 的消息。
port messageReceiver : (String -> msg) -> Sub msg
我们表示我们将接收String
值,但同样,我们可以侦听可通过标志或传出端口传入的任何类型。只需将String
类型换成可以跨越边界的类型即可。
同样,我们可以像使用任何其他函数一样使用messageReceiver
。在我们自己的用例中,我们在定义我们的subscriptions
时调用messageReceiver Recv
,因为我们希望了解来自 JavaScript 的任何传入消息。这将使我们能够在update
函数中获得像Recv "how are you?"
这样的消息。
在 JavaScript 端,我们能够在任何时候将内容发送到此端口
socket.addEventListener("message", function(event) {
app.ports.messageReceiver.send(event.data);
});
碰巧,我们无论何时都不发送,而每当 websocket 获取消息时都会进行发送,但你也可以在其他时候发送。也许我们也从另一个数据源获取消息。这没关系,而且 Elm 无需了解任何有关此内容的信息!只需通过相关端口发送字符串即可。
备注
端口用于创建强边界!一定不要为所需的每个 JS 函数尝试创建一个端口。你可能非常喜欢 Elm,并希望不惜一切代价在 Elm 中完成所有操作,但端口并非为此而设计。取而代之的是,专注于“谁拥有该状态?”等问题,并使用一个或两个端口来来回回发送消息。如果你处于复杂情景中,你甚至可以通过发送类似于{ tag: "active-users-changed", list: ... }
的 JS 来模拟Msg
值,其中你为可能发送的所有信息变体设置一个标签。
以下是一些简单的准则与常见错误
建议通过端口发送
Json.Encode.Value
。与标志类似,某些核心类型也可以通过端口传递。这个机制来自于 JSON 解码器出现之前,详细信息请参阅此处。所有
port
声明都必须出现在port module
中。最好将你的所有端口整理到一个port module
中,以便于在一处查看接口。端口专用于应用程序。
port module
可用于应用程序,但不可用于包。这确保了应用程序作者拥有必需的灵活性,而包生态系统则完全采用 Elm 编写。我们认为这可以从长期来看创建一个更强的生态系统和社区,并且我们将在有关 Elm/JS 互操作的 限制 的即将到来的部分中深入探讨权衡利弊。端口还可以死代码消除。Elm 具有非常激进的 死代码消除,并且它将删除未在 Elm 代码中使用的端口。编译器不知道 JavaScript 中发生了什么,因此请尝试在 JavaScript 之前在 Elm 中连接事物。
我希望这些信息能帮助你找到将 Elm 嵌入你的现有 JavaScript 中的方法!这无法与完全用 Elm 重写一样令人兴奋,但历史表明,这是一个更有效的策略。