Olmo Cómodo e incómodo. Composición

Seguimos hablando de Elm 0.18 .


Olmo Cómodo e incómodo
Olmo Cómodo e incómodo. Json.Encoder y Json.Decoder
Olmo Cómodo e incómodo. Http, Tarea


En este artículo, consideraremos los problemas de la arquitectura de aplicaciones de Elm y las posibles opciones para implementar el enfoque de desarrollo de componentes.


Como tarea, considere la implementación de una ventana desplegable que permita a un usuario registrado agregar una pregunta. En el caso de un usuario anónimo, sugiere primero iniciar sesión o registrarse.


También asumimos que posteriormente puede ser necesario implementar la recepción de otros tipos de contenido de usuario, pero la lógica de trabajar con usuarios autorizados y anónimos seguirá siendo la misma.


Composición incómoda


El código fuente de una implementación ingenua . Como parte de esta implementación, almacenaremos todo en un modelo.


Todos los datos necesarios para la autorización y el sondeo de usuarios están en el mismo nivel en el modelo. La misma situación con los mensajes (Msg).


type alias Model = { user: User , ui: Maybe Ui -- Popup is not open is value equals Nothing , login: String , password: String , question: String , message: String } type Msg = OpenPopup | LoginTyped String | PasswordTyped String | Login | QuestionTyped String | SendQuestion 

El tipo de interfaz se describe como tipo de unión Ui, que se usa con el tipo Quizás.


 type Ui = LoginUi -- Popup shown with authentication form | QuestionUi -- Popup shown with textarea to leave user question 

Por lo tanto, ui = Nothing describe la ausencia de una ventana emergente, y la ventana emergente Just está abierta con una interfaz específica.

En la función de actualización, el par coincide, el mensaje y los datos del usuario. Se realizan diferentes acciones dependiendo de este par.


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of 

Por ejemplo, cuando hace clic en el botón "Abrir ventana emergente", se genera un mensaje OpenPopup. El mensaje de OpenPopup en la función de actualización se maneja de varias maneras. Para un usuario anónimo, se genera un formulario de autorización, y para un usuario autorizado, un formulario en el que puede dejar una pregunta.


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of -- Anonymous user message handling section (OpenPopup, Anonymous) -> ( { model | ui = Just LoginUi, message = "" }, Cmd.none) -- Authenticated user message handling section (OpenPopup, User userName) -> ( { model | ui = Just QuestionUi, message = "" }, Cmd.none) 

Obviamente, este enfoque puede tener problemas con el crecimiento de las funciones de la aplicación:


  1. No hay agrupación de datos en el modelo y los mensajes. Todo se encuentra en un plano. Por lo tanto, no hay límites de componentes; un cambio en la lógica de una parte probablemente afectará al resto;
  2. La reutilización del código es posible según el principio de copiar y pegar con todas las consecuencias resultantes.

Composición conveniente


El código fuente es una implementación conveniente . En el marco de esta implementación, intentaremos dividir el proyecto en componentes independientes. Dependencias permitidas entre componentes.


Estructura del proyecto:


  1. en la carpeta Tipo se declaran los tipos personalizados;
  2. los componentes personalizados se declaran en la carpeta Componente;
  3. Punto de entrada del proyecto del archivo Main.elm;
  4. Los archivos login.json y preguntas.json se utilizan como datos de prueba para la respuesta del servidor a la autorización y para guardar información sobre la pregunta, respectivamente.

Componentes personalizados


Cada componente, basado en la arquitectura del lenguaje, debe contener:


  1. Modelo
  2. Mensajes (Msg)
  3. resultado de la ejecución (Retorno);
  4. función de inicialización (init);
  5. función de mutación (actualización);
  6. Ver función.

Cada componente puede contener una suscripción si es necesario.


imagen
Fig. 1. Diagrama de actividad de componentes


Inicialización


Cada componente debe activarse, es decir debe ser recibido:


  1. modelo
  2. El comando o la lista de comandos que deberían inicializar el estado del componente
  3. Resultado de ejecución. El resultado de la ejecución en el momento de la inicialización puede ser necesario para validar la autorización del usuario, como en los ejemplos de este artículo.

La lista de argumentos de la función de inicialización (init) depende de la lógica del componente y puede ser arbitraria. Puede haber varias funciones de inicialización. Supongamos que, para un componente de autorización, se pueden proporcionar dos opciones de inicialización: con un token de sesión y con datos de usuario.


El código que usa el componente, después de la inicialización, debe pasar comandos a elm runtime usando la función Cmd.map .


Mutación


Se debe invocar la función de componente de actualización para cada mensaje de componente. Como resultado de la ejecución, la función devuelve un triple:


  1. nuevo modelo o nuevo estado (Modelo);
  2. comando o lista de comandos para Elm runtime (Cmd Msg). Los comandos pueden ser comandos para ejecutar solicitudes HTTP, interactuar con puertos y más;
  3. resultado de la ejecución (Quizás Volver). Quizás type tiene dos estados Nothing y Just a. En nuestro caso, nada, no hay resultado, solo un: hay un resultado. Por ejemplo, para la autorización, el resultado puede ser Just (datos de usuario autenticados): el usuario ha iniciado sesión con datos de datos de usuario.

Después de la mutación, el código que usa el componente debe actualizar el modelo del componente y pasar comandos al tiempo de ejecución de Elm usando la función Cmd.map .


Argumentos necesarios para la función de actualización, de acuerdo con la arquitectura de la aplicación Elm:


  1. Mensaje (mensaje)
  2. modelo (Modelo).

Si es necesario, la lista de argumentos puede complementarse.


Sumisión


Se llama a la función de vista cuando es necesario insertar la vista de componente en la vista general de la aplicación.


El argumento requerido para la función de vista debe ser el modelo de componente. Si es necesario, la lista de argumentos puede complementarse.


El resultado de ejecutar la función de vista debe pasarse a la función Html.map .


Integración de aplicaciones


El ejemplo describe dos componentes: autenticación y pregunta . Componentes de los principios descritos anteriormente. Considere cómo se pueden integrar en la aplicación .


Primero, determinemos cómo debería funcionar nuestra aplicación. Hay un botón en la pantalla, cuando haces clic en él:


  1. para un usuario no autorizado, se muestra un formulario de autorización, después de la autorización: un formulario de publicación de preguntas;
  2. Para un usuario autorizado, se muestra un formulario de colocación de preguntas.

Para describir la aplicación que necesita:


  1. Modelo
  2. Mensajes (Msg)
  3. punto de inicio de la aplicación (principal);
  4. función de inicialización (init);
  5. función de mutación;
  6. función de presentación;
  7. función de suscripción

Modelo


 type alias Model = { user: User , ui: Maybe Ui } type Ui = AuthUi Component.Auth.Model | QuestionUi Component.Question.Model 

El modelo contiene información sobre el usuario (usuario) y el tipo de interfaz actual (ui). La interfaz puede estar en el estado predeterminado (Nada) o en uno de los componentes Just a.


Para describir los componentes, utilizamos el tipo Ui, que asocia (etiqueta) cada modelo de componente con una variante específica de un conjunto de tipos. Por ejemplo, una etiqueta AuthUi asocia un modelo de autorización (Component.Auth.Model) con un modelo de aplicación.


Mensajes


 type Msg = OpenPopup | AuthMsg Component.Auth.Msg | QuestionMsg Component.Question.Msg 

En los mensajes, debe etiquetar todos los mensajes componentes e incluirlos en los mensajes de la aplicación. Las etiquetas AuthMsg y QuestionMsg enlazan los mensajes del componente de autorización y la pregunta del usuario, respectivamente.


Se necesita un mensaje OpenPopup para procesar una solicitud para abrir una interfaz.


Función principal


 main : Program Never Model Msg main = Html.program { init = init , update = update , subscriptions = subscriptions , view = view } 

El punto de entrada de la aplicación se describe típicamente para una aplicación Elm.


Función de inicialización


 init : ( Model, Cmd Msg ) init = ( initModel, Cmd.none ) initModel : Model initModel = { user = Anonymous , ui = Nothing } 

La función de inicialización crea un modelo de inicio y no requiere la ejecución de comandos.


Función de mutación


Código fuente de la función
 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of (OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands ) (AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands ) (QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands ) _ -> ( model, Cmd.none ) 

Porque el modelo y los mensajes a la aplicación están conectados, procesaremos un par de mensajes (Msg) y el tipo de interfaz (model.ui: Ui).


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of 

Lógica de trabajo


Si se recibe un mensaje de OpenPopup y la interfaz predeterminada se especifica en el modelo (model.ui = Nothing), entonces inicializamos el componente Auth. Si el componente Auth informa que el usuario está autorizado, inicializamos el componente Pregunta y lo guardamos en el modelo de la aplicación. De lo contrario, guardamos el modelo de componente en el modelo de aplicación.


 (OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands ) 

Si se recibe un mensaje con AuthMsg, se recibe una etiqueta y la interfaz de autorización se especifica en el modelo (model.ui = Just (AuthUi authModel)), entonces pasamos el mensaje componente y el modelo componente a la función Auth.update. Como resultado, obtenemos un nuevo modelo de componentes, comandos y resultados.


Si el usuario está autorizado, inicializamos el componente Pregunta, de lo contrario, actualizamos los datos de la interfaz en el modelo de la aplicación.


 (AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands ) 

De manera similar al componente Auth, se procesan los mensajes para el componente Pregunta. Si la pregunta se publica con éxito, la interfaz cambia al valor predeterminado (model.ui = Nothing).


 (QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands ) 

Todos los demás casos son ignorados.


  _ -> ( model, Cmd.none ) 

Función de visualización


 view : Model -> Html Msg view model = case model.ui of Nothing -> div [] [ div [] [ button [ Events.onClick OpenPopup ] [ text "Open popup" ] ] ] Just (AuthUi authModel) -> Component.Auth.view authModel |> Html.map AuthMsg Just (QuestionUi questionModel) -> Component.Question.view questionModel |> Html.map QuestionMsg 

Dependiendo del tipo de interfaz (model.ui), la función de presentación genera una interfaz predeterminada o llama a la función de presentación del componente y asigna el tipo de mensaje del componente al tipo de mensaje de la aplicación (Html.map).


Función de suscripción


 subscriptions : Model -> Sub Msg subscriptions model = Sub.none 

Sin suscripción


Siguiente


Este ejemplo, aunque un poco más conveniente, pero bastante ingenuo. Lo que falta:


  1. bloqueo de la interacción con la aplicación durante el proceso de descarga;
  2. validación de datos. Requiere una conversación separada;
  3. Realmente ventana emergente con la capacidad de cerrar.

Source: https://habr.com/ru/post/es424341/


All Articles