web-dev-qa-db-fra.com

Architecture dans une application native réagissant à l'aide de WebSockets

J'ai une React application native que je vais construire en utilisant WebSockets. J'ai une bibliothèque WebSocket écrite en JavaScript et je la réutilise simplement pour ce projet, ce qui est fantastique .

Ma question est, étant nouveau dans React/React Native, quelle est la meilleure pratique pour configurer et maintenir tout le trafic passant par le WebSocket?

Au départ, mon idée était de créer le websocket dans le composant principal de l'application, quelque chose comme ceci:

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 classe WebSocket réelle contiendrait toute la gestion de connexion respective:

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);
};

Ma question est, puisque les données provenant de WebSocket seront applicables pour l'état via de nombreux composants dans l'application React Native, mais ce n'est pas une classe qui étendra React.Component, je n'interagis pas avec Redux dans la classe WebSocket? Dois-je déplacer toute la gestion des connexions WebSocket vers le composant App et y envoyer des actions vers Redux?

Quel est le modèle courant ici pour instancier ma classe WebSocket et garantir que tout le trafic qu'il contient est correctement transmis à Redux afin que l'état de tous les composants soit correctement acheminé?

24
randombits

Excellentes réponses jusqu'ici. Je voulais juste ajouter que vous conservez vos données devrait vraiment être une décision basée sur ce type de données. James Nelson a n excellent article sur ce sujet auquel je me réfère régulièrement.

Pour votre cas, parlons des 3 premiers types d'état:

  1. Les données
  2. État de communication
  3. État de contrôle

Les données

Votre connexion WebSocket est générique et pourrait techniquement renvoyer n'importe quoi, mais il est probable que les messages que vous recevez sont des données. Par exemple, supposons que vous créez une application de chat. Ensuite, le journal de tous les messages qui ont été envoyés et reçus serait les données. Vous devez stocker ces données dans redux avec un réducteur messages:

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

        default: return state;
    }
}

Nous n'avons pas (et nous ne devrions pas) avoir de logique WebSocket dans nos réducteurs, car ils sont génériques et ne se soucient pas de la provenance des données.

Notez également que ce réducteur est capable de gérer l'envoi et la réception exactement de la même manière. En effet, la communication réseau est gérée séparément par notre réducteur d'état de communication.

État de communication

Puisque vous utilisez WebSockets, les types d'état de communication que vous souhaitez suivre peuvent différer de mon exemple. Dans une application qui utilise une API standard, je suivrais quand une demande chargement , échouait , ou réussi .

Dans notre exemple d'application de chat, vous souhaiterez probablement suivre ces états de demande chaque fois que vous envoyez un message, mais vous pouvez également classer d'autres éléments comme état de communication.

Notre réducteur network peut utiliser les mêmes actions que le réducteur 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 cette façon, nous pouvons facilement trouver l'état de nos demandes, et nous n'avons pas à nous soucier du chargement/du succès/de l'échec de nos composants.

Cependant, vous pourriez ne pas vous soucier du succès/de l'échec d'une demande donnée car vous utilisez WebSockets. Dans ce cas, votre état de communication peut être juste si votre socket est connecté ou non. Si cela vous semble mieux, écrivez simplement un réducteur connection qui répond aux actions lors de l'ouverture/fermeture.

État de contrôle

Nous aurons également besoin de quelque chose pour lancer l'envoi de messages. Dans l'exemple de l'application de chat, il s'agit probablement d'un bouton d'envoi qui envoie le texte contenu dans un champ de saisie. Je ne montrerai pas le composant entier, car nous utiliserons un composant contrôlé .

Le point à retenir ici est que l'état de contrôle est le message avant il est envoyé. Le morceau de code intéressant dans notre cas est ce qu'il faut faire dans 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);

Donc, cela concerne tout notre état disparaît. Nous avons créé une application générique qui pourrait utiliser des actions pour appeler fetch pour une API standard, obtenir des données d'une base de données ou un certain nombre d'autres sources. Dans votre cas, vous souhaitez utiliser WebSockets. Donc, cette logique devrait vivre dans vos actions.

Actions

Ici, vous allez créer tous vos gestionnaires: onOpen, onMessage, onError, etc. Ceux-ci peuvent toujours être assez génériques, car vous avez déjà votre utilitaire WebSocket mis en place séparément.

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
        });
    };
}

J'utilise thunk pour l'action asynchrone ici. Pour cet exemple particulier, cela peut ne pas être nécessaire, mais vous aurez probablement des cas où vous souhaitez envoyer un message, puis gérer la réussite/l'échec et envoyer plusieurs actions à vos réducteurs à partir d'une seule action sendMessage. Thunk est idéal pour ce cas.

Câblage tout ensemble

Enfin, nous avons tout mis en place. Il ne nous reste plus qu'à initialiser le WebSocket et configurer les écouteurs appropriés. J'aime le modèle suggéré par Vladimir - configurer la socket dans un constructeur - mais je paramétrerais vos rappels afin que vous puissiez rendre vos actions. Ensuite, votre classe WebSocket peut configurer tous les écouteurs.

En créant la classe WebSocket n singleton , vous pouvez envoyer des messages depuis l'intérieur de vos actions sans avoir à gérer les références au socket actif. Vous éviterez également de polluer l'espace de noms global.

En utilisant la configuration singleton, chaque fois que vous appelez new WebSocket() pour la première fois, votre connexion sera établie. Donc, si vous avez besoin que la connexion soit ouverte dès que l'application démarre, je la configurerais dans componentDidMount de App. Si une connexion paresseuse est correcte, vous pouvez simplement attendre que votre composant essaie d'envoyer un message. L'action créera un nouveau WebSocket et la connexion sera établie.

19
Luke

Vous pouvez créer une classe dédiée pour WebSocket et l'utiliser partout. C'est une approche simple, concise et claire. De plus, vous aurez tous les trucs liés aux websockets encapsulés en un seul endroit! Si vous le souhaitez, vous pouvez même créer un singleton à partir de cette classe, mais l'idée générale est la suivante:

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);
  }
}

Vous n'avez exécuté init que quelque part dans index.js ou app.js:

WS.init();

Et maintenant, vous pouvez envoyer librement des messages à partir de n'importe quelle couche d'application, de n'importe quel composant, de n'importe quel endroit:

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

Et recevez des données de WebSocket:

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

Vous pouvez donc l'utiliser partout, même en redux, dans n'importe quelle action ou ailleurs!

14
V. Kovpak

Il n'y a pas de directives officielles à ce sujet. Je pense que l'utilisation d'un composant est source de confusion car il ne sera pas rendu, et je suppose que si vous utilisez Redux, vous souhaitez partager les données de websocket n'importe où dans l'application.

Vous pouvez confier la fonction de répartition à votre gestionnaire Websocket.

const store = createStore(reducer);

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

Et utilise this.dispatch dans vos méthodes de classe.

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

Vous pouvez également utiliser des middlewares pour gérer les effets secondaires, je pense que c'est la méthode recommandée. Il existe deux grandes bibliothèques que vous pouvez consulter:

redux-saga

redux-observable

2
Freez