Tengo una aplicación React Native que voy a construir que usa WebSockets. Tengo una biblioteca WebSocket escrita en JavaScript y simplemente la estoy reutilizando para este proyecto, lo cual es fantástico.

Mi pregunta es, siendo nuevo en React / React Native, ¿cuál es la mejor práctica para configurar y mantener todo el tráfico que atraviesa el WebSocket?

Inicialmente, mi idea era crear el websocket en el componente principal de la aplicación, algo como esto:

export default class App extends Component {

  constructor(props) {
    super(props);
    this.ws = new WebSocket;
  }

  componentWillMount() {
    console.log(this.ws);
  }

  render() {
    console.log("We are rendering the App component.....");

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Hello, world</Text>  
      </View>
    );
  }
}

La clase real de WebSocket contendría todo el manejo de conexión respectivo:

ws.onopen = () => {
  // connection opened
  ws.send('something'); // send a message
};

ws.onmessage = (e) => {
  // a message was received
  console.log(e.data);
};

ws.onerror = (e) => {
  // an error occurred
  console.log(e.message);
};

ws.onclose = (e) => {
  // connection closed
  console.log(e.code, e.reason);
};

Mi pregunta es, dado que los datos que provienen de WebSocket serán aplicables para el estado a través de muchos componentes en la aplicación React Native, pero no es una clase que se extenderá React.Component, no interactúo con Redux en {{X1 }} ¿clase? ¿Muevo todo el manejo de la conexión WebSocket al componente App y envío acciones a Redux?

¿Cuál es el patrón común aquí para crear una instancia de mi clase WebSocket y asegurar que todo el tráfico en él se pase correctamente a Redux para que el estado de todos los componentes se canalice correctamente?

24
randombits 28 oct. 2017 a las 23:49

3 respuestas

La mejor respuesta

Grandes respuestas aquí hasta ahora. Solo quería agregar que donde guarda sus datos realmente debería ser una decisión basada en qué tipo de datos es. James Nelson tiene un excelente artículo sobre este tema al que me refiero regularmente.

Para su caso, hablemos de los primeros 3 tipos de estado:

  1. Datos
  2. Estado de comunicación
  3. Estado de control

Datos

Su conexión WebSocket es genérica y técnicamente podría devolver cualquier cosa, pero es probable que los mensajes que recibe sean datos. Por ejemplo, supongamos que está creando una aplicación de chat. Entonces, el registro de todos los mensajes que se han enviado y recibido serían los datos. Debe almacenar estos datos en redux con un reductor messages:

export default function messages(state = [], action) {
    switch (action.type) {
        case 'SEND_MESSAGE': 
        case 'RECEIVE_MESSAGE': {
            return [ ...state, action.message ];
        } 

        default: return state;
    }
}

No tenemos que (y no deberíamos) tener ninguna lógica WebSocket en nuestros reductores, ya que son genéricos y no nos importa de dónde provienen los datos.

Además, tenga en cuenta que este reductor puede manejar el envío y la recepción exactamente de la misma manera. Esto se debe a que nuestro reductor de estado de comunicación maneja la comunicación de red por separado.

Estado de comunicación

Como está utilizando WebSockets, los tipos de estado de comunicación que desea rastrear pueden diferir de mi ejemplo. En una aplicación que usa una API estándar, rastrearía cuando una solicitud se está cargando , falló o exitosa .

En nuestro ejemplo de aplicación de chat, es probable que desee realizar un seguimiento de estos estados de solicitud cada vez que envíe un mensaje, pero también podría haber otras cosas que desee clasificar como estado de comunicación.

Nuestro reductor network puede usar las mismas acciones que el reductor messages:

export default function network(state = {}, action) {
    switch (action.type) {
        case 'SEND_MESSAGE': {
            // I'm using Id as a placeholder here. You'll want some way
            // to tie your requests with success/failure receipt.
            return { 
                ...state, 
                [action.id]: { loading: true }
            };
        } case 'SEND_MESSAGE_SUCCESS': {
            return { 
                ...state, 
                [action.id]: { loading: false, success: true }
            };
        } case 'SEND_MESSAGE_FAILURE': {
            return { 
                ...state, 
                [action.id]: { loading: false, success: false }
            };
        }

        default: return state;
    }
}

De esta manera, podemos encontrar fácilmente el estado de nuestras solicitudes, y no tenemos que molestarnos con la carga / éxito / falla en nuestros componentes.

Sin embargo, es posible que no le importe el éxito o el fracaso de una solicitud dada, ya que está utilizando WebSockets. En ese caso, su estado de comunicación podría ser si su socket está conectado o no. Si eso le suena mejor, simplemente escriba un reductor connection que responda a las acciones en abrir / cerrar.

Estado de control

También necesitaremos algo para iniciar el envío de mensajes. En el ejemplo de la aplicación de chat, este es probablemente un botón de envío que envía cualquier texto que esté en un campo de entrada. No demostraré todo el componente, ya que usaremos un componente controlado.

La conclusión aquí es que el estado de control es el mensaje antes que se envía. El fragmento de código interesante en nuestro caso es qué hacer en handleSubmit:

class ChatForm extends Component {
    // ...
    handleSubmit() {
        this.props.sendMessage(this.state.message);
        // also clear the form input
    }
    // ...
}

const mapDispatchToProps = (dispatch) => ({
    // here, the `sendMessage` that we're dispatching comes
    // from our chat actions. We'll get to that next.
    sendMessage: (message) => dispatch(sendMessage(message))
});

export default connect(null, mapDispatchToProps)(ChatForm);

Entonces, eso aborda donde va todo nuestro estado. Hemos creado una aplicación genérica que podría utilizar acciones para llamar a fetch para una API estándar, obtener datos de una base de datos o cualquier otra fuente. En su caso, desea usar WebSockets. Entonces, esa lógica debería vivir en tus acciones.

Comportamiento

Aquí, creará todos sus controladores: onOpen, onMessage, onError, etc. Todavía pueden ser bastante genéricos, ya que ya tiene su utilidad WebSocket configurada por separado .

function onMessage(e) {
    return dispatch => {
        // you may want to use an action creator function
        // instead of creating the object inline here
        dispatch({
            type: 'RECEIVE_MESSAGE',
            message: e.data
        });
    };
}

Estoy usando thunk para la acción asíncrona aquí. Para este ejemplo en particular, eso podría no ser necesario, pero probablemente tenga casos en los que desee enviar un mensaje, luego manejar el éxito / fracaso y enviar múltiples acciones a sus reductores desde una sola acción sendMessage. Thunk es genial para este caso.

Conectando todo junto

Finalmente, tenemos todo configurado. Todo lo que tenemos que hacer ahora es inicializar el WebSocket y configurar los oyentes apropiados. Me gusta el patrón que sugirió Vladimir, configurar el zócalo en un constructor, pero parametrizaría sus devoluciones de llamada para que pueda entregar sus acciones. Entonces su clase WebSocket puede configurar todos los oyentes.

Al hacer que la clase WebSocket sea un singleton, puede enviar mensajes desde el interior de su acciones sin necesidad de gestionar referencias al socket activo. También evitará contaminar el espacio de nombres global.

Al usar la configuración de singleton, cada vez que llame a new WebSocket() por primera vez, se establecerá su conexión. Entonces, si necesita que la conexión se abra tan pronto como se inicie la aplicación, la configuraría en componentDidMount de App. Si una conexión diferida está bien, puede esperar hasta que su componente intente enviar un mensaje. La acción creará un nuevo WebSocket y se establecerá la conexión.

19
Luke 2 nov. 2017 a las 16:44

Puede crear una clase dedicada para WebSocket y usarla en todas partes. Es un enfoque simple, conciso y claro. Además, ¡tendrás todo lo relacionado con websockets encapsulado en un solo lugar! Si lo desea, incluso puede crear singleton fuera de esta clase, pero la idea general es esta:

class WS {
  static init() {
    this.ws = new WebSocket('ws://localhost:5432/wss1');
  }
  static onMessage(handler) {
    this.ws.addEventListener('message', handler);
  }
  static sendMessage(message) {
    // You can have some transformers here.
    // Object to JSON or something else...
    this.ws.send(message);
  }
}

Solo ha ejecutado init en algún lugar de index.js o app.js:

WS.init();

Y ahora puede enviar mensajes libremente desde cualquier capa de aplicación, desde cualquier componente, desde cualquier lugar:

WS.sendMessage('My message into WebSocket.');

Y reciba datos de vuelta de WebSocket:

WS.onMessage((data) => {
  console.log('GOT', data);
  // or something else or use redux
  dispatch({type: 'MyType', payload: data});
});

¡Así que puedes usarlo en todas partes, incluso en redux en cualquier acción o en otro lugar!

14
V. Kovpak 1 nov. 2017 a las 15:31

No hay pautas oficiales al respecto. Creo que usar un componente es confuso porque no se representará, y supongo que si usa Redux desea compartir los datos de WebSocket en cualquier lugar de la aplicación.

Puede asignar la función de envío a su administrador de Websocket.

const store = createStore(reducer);

const ws = new WebSocketManager(store.dispatch, store.getState);

Y use this.dispatch dentro de sus métodos de clase.

// inside WebSocketManager class
constructor(dispatch, getState) {
    this.dispatch = dispatch;
    this.getState = getState;
}

También puede usar middlewares para manejar los efectos secundarios, creo que es la forma recomendada. Hay dos bibliotecas geniales que puede buscar:

redux-saga

redux-observable

2
Freez 16 nov. 2019 a las 05:51