Los puertos permiten la comunicación entre Elm y JavaScript.
Los puertos se utilizan con mayor frecuencia para WebSockets y localStorage. Centrémonos en el ejemplo de WebSockets.
Aquí tenemos prácticamente el mismo HTML que hemos usado en las páginas anteriores, pero con un poco de código JavaScript adicional. Creamos una conexión a wss://echo.websocket.org que simplemente repite lo que le enviamos.
<!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>
Llamamos a Elm.Main.init() como en todos nuestros ejemplos de interoperabilidad, pero esta vez estamos usando el objeto app resultante. Nos suscribimos al puerto sendMessage y enviamos mensajes al puerto messageReceiver.
Observa las líneas que utilizan la palabra clave port en el archivo Elm correspondiente. Así es como definimos los puertos que acabamos de ver en 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"
Fíjate que la primera línea dice "port module" en lugar de simplemente "module". Esto permite definir puertos dentro de un módulo. El compilador ofrece una pista si es necesario, ¡así que esperemos que nadie se quede atascado en esto!
Bien, ¿pero qué ocurre con las declaraciones de puerto para sendMessage y messageReceiver?
La declaración sendMessage nos permite enviar mensajes desde Elm.
port sendMessage: String -> Cmd msg
Aquí declaramos que queremos enviar valores de tipo String, pero podríamos enviar cualquier tipo que funcione con indicadores.
A partir de ahí, podemos usar sendMessage como cualquier otra función. Si tu función de actualización genera un comando sendMessage "hello", lo recibirás en JavaScript:
app.ports.sendMessage.subscribe(function(message) {
socket.send(message);
});
Este código JavaScript se suscribe a todos los mensajes salientes. Puedes suscribir varias funciones y cancelar suscripciones por referencia, pero generalmente recomendamos mantener la configuración estática.
También recomendamos enviar mensajes más completos, en lugar de crear múltiples puertos individuales. Esto podría implicar tener un tipo personalizado en Elm que represente todo lo que necesites comunicar a JavaScript, y luego usar Json.Encode para enviarlo a una única suscripción de JavaScript. Muchos consideran que esto crea una separación de responsabilidades más clara. El código de Elm gestiona claramente parte del estado, y JavaScript gestiona otra parte.
La declaración messageReceiver nos permite escuchar los mensajes que llegan a Elm.
messageReceiver: (String -> msg) -> Sub msg
Estamos indicando que vamos a recibir valores de tipo String, pero también podemos escuchar cualquier tipo que pueda llegar a través de indicadores o puertos de salida. Simplemente reemplazamos el tipo String por uno de los tipos que pueden cruzar el límite.
Podemos usar messageReceiver como cualquier otra función. En nuestro caso, llamamos a messageReceiverRecv al definir nuestras suscripciones porque queremos recibir mensajes entrantes de JavaScript. Esto nos permitirá recibir mensajes como Recv "¿cómo estás?"` en nuestra función de actualización.
En JavaScript, podemos enviar datos a este puerto cuando queramos:
socket.addEventListener("message", function(event) {
app.ports.messageReceiver.send(event.data);
});
En este caso, enviamos mensajes cada vez que el websocket recibe uno, pero también se pueden enviar en otros momentos. Quizás también estemos recibiendo mensajes de otra fuente de datos. Está bien, ¡y Elm no necesita saber nada al respecto! Simplemente envía las cadenas a través del puerto correspondiente.
Los puertos sirven para establecer límites claros. Definitivamente, no intentes crear un puerto para cada función de JavaScript que necesites. Puede que te guste mucho Elm y quieras hacerlo todo con él, cueste lo que cueste, pero los puertos no están diseñados para eso. En su lugar, concéntrate en preguntas como "¿quién controla el estado?" y usa uno o dos puertos para enviar y recibir mensajes. Si te encuentras en un escenario complejo, incluso puedes simular valores de Msg enviando JavaScript como `{ tag: "active-users-changed", list: ... }`, donde tienes una etiqueta para cada variante de información que podrías enviar.
Aquí tienes algunas pautas sencillas y errores comunes:
Se recomienda enviar `Json.Encode.Value` a través de puertos. Al igual que con las banderas, ciertos tipos básicos también pueden pasar a través de puertos. Esto es de la época anterior a los decodificadores JSON, y puedes leer más al respecto aquí.
Todas las declaraciones de puertos deben aparecer en un módulo de puerto. Probablemente sea mejor organizar todos los puertos en un solo módulo para que la interfaz sea más fácil de visualizar en un solo lugar.
Los puertos son para aplicaciones. Un módulo de puerto está disponible en las aplicaciones, pero no en los paquetes. Esto garantiza que los desarrolladores de aplicaciones tengan la flexibilidad que necesitan, pero el ecosistema de paquetes está completamente escrito en Elm. Creemos que esto creará un ecosistema y una comunidad más sólidos a largo plazo, y analizaremos las ventajas y desventajas en profundidad en la siguiente sección sobre los límites de la interoperabilidad entre Elm y JavaScript.
Los puertos pueden eliminarse como código muerto. Elm tiene un sistema de eliminación de código muerto bastante agresivo y eliminará los puertos que no se utilicen dentro del código Elm. El compilador desconoce lo que sucede en JavaScript, así que intente conectar las cosas en Elm antes que en JavaScript.











