Jak nastavit klienta Websocket s JavaScriptem

Jak vytvořit opakovaně použitelnou funkci, která vytvoří klienta websocket, který se připojí k existujícímu serveru websocket za účelem odesílání a přijímání zpráv.

Začínáme

Pokud jste to ještě neudělali – a nemáte svůj vlastní existující websocket server, ke kterému byste se mohli připojit – doporučujeme vám absolvovat náš doprovodný výukový program Jak nastavit server Websocket pomocí Node.js a Express.

Pokud jste již dokončili tento tutoriál nebo máte websocket server, se kterým byste chtěli testovat, pro tento tutoriál použijeme CheatCode Next.js Boilerplate jako výchozí bod pro zapojení našeho klienta websocket :

Terminál

git clone https://github.com/cheatcode/nextjs-boilerplate.git

Po naklonování kopie projektu cd do něj a nainstalujte jeho závislosti:

Terminál

cd nextjs-boilerplate && npm install

Dále musíme nainstalovat jednu další závislost, query-string , který použijeme k analýze parametrů dotazu z naší adresy URL, aby je předali spolu s naším připojením na websocket:

Terminál

npm i query-string

Nakonec spusťte vývojový server:

Terminál

npm run dev

Díky tomu jsme připraveni začít.

Vytvoření klienta websocket

Naštěstí pro nás jsou moderní prohlížeče nyní nativně podpora webových zásuvek. To znamená, že při nastavování připojení nemusíme záviset na žádných speciálních knihovnách klienta.

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  // We'll write our code here...
};

export default websocketClient;

Zde začínáme specifikovat našeho klienta websocket. Nejprve si všimněte, že vytváříme funkci s názvem websocketClient které hodláme importovat jinam do našeho kódu. Myšlenka je taková, že v závislosti na naší aplikaci můžeme mít více bodů použití pro webové zásuvky; tento vzorec nám umožňuje udělat to bez musíte zkopírovat/vložit velké množství kódu.

Když se podíváme na funkci, nastavujeme ji tak, aby přijala dva argumenty:options , objekt obsahující některá základní nastavení pro klienta websocket a onConnect , funkci zpětného volání, kterou můžeme volat po navázali jsme spojení se serverem (důležité, pokud vytváříte uživatelské rozhraní, které chce/potřebuje připojení websocket vytvořené před načtením celého uživatelského rozhraní).

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  let url = settings?.websockets?.url;
  let client = new WebSocket(url);

  client.addEventListener("open", () => {
    console.log(`[websockets] Connected to ${settings?.websockets?.url}`);
  });

  client.addEventListener("close", () => {
    console.log(`[websockets] Disconnected from ${settings?.websockets?.url}`);
    client = null;
  });
};

export default websocketClient;

Když vytvoříme tělo naší funkce, musíme nastavit připojení našeho klienta k serveru websocket. Za tímto účelem jsme zde importovali /settings/index.js soubor v kořenovém adresáři standardu, který jsme naklonovali na začátku tutoriálu. Tento soubor obsahuje funkci, která stahuje konfigurační data pro náš front-end ze souboru specifického pro prostředí umístěného ve stejné složce na /settings z kořenového adresáře projektu.

Pokud se podíváte do této složky, jsou k dispozici dva ukázkové soubory settings-development.json a settings-production.json . První je navržen tak, aby obsahoval vývoj nastavení prostředí, zatímco druhé je navrženo tak, aby obsahovalo produkci nastavení prostředí. Tento rozdíl je důležitý, protože ve svém vývojovém prostředí chcete používat pouze testovací klíče a adresy URL, abyste se vyhnuli narušení produkčního prostředí.

/settings/settings-development.json

const settings = {
  [...]
  websockets: {
    url: "ws://localhost:5001/websockets",
  },
};

export default settings;

Pokud otevřeme /settings/settings-development.json soubor, přidáme novou vlastnost do settings objekt, který je exportován ze souboru s názvem websockets . Nastavíme toto vlastnost rovna jinému objektu, s jediným url vlastnost nastavena na adresu URL našeho serveru websocket. Zde používáme adresu URL, od které očekáváme existenci z jiného tutoriálu CheatCode pro nastavení serveru websocket, na který jsme odkazovali na začátku tohoto kurzu.

Pokud používáte svůj vlastní existující websocket server, nastavíte jej zde. Je třeba poznamenat, že když se připojujeme k serveru websocket, předponu naší URL je ws:// místo http:// (ve výrobě bychom použili wss:// pro bezpečné připojení, stejně jako používáme https:// ). Je to proto, že websockets jsou nezávislým protokolem na protokolu HTTP. Kdybychom tomu dali předponu http:// , naše připojení by selhalo s chybou prohlížeče.

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  let url = settings?.websockets?.url;
  let client = new WebSocket(url);

  client.addEventListener("open", () => {
    console.log(`[websockets] Connected to ${settings?.websockets?.url}`);
  });

  client.addEventListener("close", () => {
    console.log(`[websockets] Disconnected from ${settings?.websockets?.url}`);
    client = null;

    if (options?.onDisconnect) {
      options.onDisconnect();
    }
  });
};

export default websocketClient;

Zpátky v našem klientském kódu nyní načteme naši webovou adresu URL ze souboru nastavení a uložíme ji do proměnné url deklarováno pomocí let (uvidíme později proč). Dále k navázání spojení k tato adresa URL v jiné proměnné těsně pod ní client (také pomocí let ), voláme na new WebSocket() předávání url pro náš server. Zde WebSocket() je nativní rozhraní API prohlížeče.

Zde pro něj nevidíte import, protože technicky vzato, když se náš kód načte v prohlížeči, globální window kontext již má WebSocket definovaný jako proměnná.

Dále pod naším client připojení přidáme pár posluchačů událostí JavaScriptu pro dvě události, u kterých předpokládáme client vysílat:open a close . Ty by měly být samozřejmé. První je zpětné volání, které se spustí, když se otevře připojení k našemu serveru websocket , zatímco druhý se spustí vždy, když se naše připojení k serveru websocket zavře .

Ačkoli to není nutné v technickém smyslu, je důležité mít je k dispozici pro zpětnou komunikaci sobě (a dalším vývojářům), že připojení bylo úspěšné nebo že bylo připojení ztraceno. Druhý scénář nastane, když se server websocket stane nedostupným nebo úmyslně uzavře spojení s klientem. Obvykle se to stane, když se server restartuje, nebo když interní kód vykopne konkrétního klienta ("proč" pro toto kopnutí závisí na aplikaci a nic není integrováno do specifikace websockets).

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  let url = settings?.websockets?.url;

  if (options.queryParams) {
    url = `${url}?${queryString.stringify(options.queryParams)}`;
  }

  let client = new WebSocket(url);

  client.addEventListener("open", () => {[...]});

  client.addEventListener("close", () => {[...]});

  const connection = {
    client,
    send: (message = {}) => {
      if (options.queryParams) {
        message = { ...message, ...options.queryParams };
      }

      return client.send(JSON.stringify(message));
    },
  };

  if (onConnect) onConnect(connection);

  return connection;
};

export default websocketClient;

Tady jsme toho přidali docela dost. Zpátky nahoře si všimněte, že jsme přidali očekávání pro hodnotu options.queryParams může být přítomen v options objekt předaný jako první argument našemu websocketClient funkce.

Protože připojení websocket nám neumožňují předat tělo, jako můžeme s požadavkem HTTP POST, omezujeme se na předávání parametrů připojení (informace, které lépe identifikují připojení jako userId nebo chatId ) jako řetězec dotazu bezpečný pro adresu URL. Zde říkáme „pokud nám předá objekt queryParams v možnostech chceme tento objekt převést na řetězec dotazu bezpečný pro adresu URL (něco, co vypadá jako ?someQueryParam=thisIsAnExample ).

Zde je použití let přichází to, co jsme naznačili dříve. Pokud jsme předali queryParams v našem options , chceme aktualizovat naši adresu URL, aby zahrnovala tyto. V tomto kontextu je "aktualizace" na url proměnnou, kterou jsme vytvořili. Protože chceme znovu přiřadit obsah této proměnné řetězci včetně našich parametrů dotazu, musíme použít let proměnná (nebo, pokud chcete jít ze staré školy, var ). Důvodem je, že pokud použijeme známější const (což znamená konstantní ) a pokusil se spustit url = '${url}?${queryString.stringify(options.queryParams)}'; kódu, JavaScript by vyvolal chybu, že nemůžeme změnit přiřazení konstanty.

Využitím našeho queryParams objekt, importujeme queryString balíček, který jsme přidali dříve a používá jeho .stringify() metoda pro generování řetězce pro nás. Za předpokladu, že adresa URL našeho základního serveru je ws://localhost:5001/websockets a předáme options.queryParams hodnota se rovná { channel: 'cartoons' } , bude naše adresa URL aktualizována na ws://localhost:5001/websockets?channel=cartoons .

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  [...]

  let client = new WebSocket(url);

  client.addEventListener("open", () => {[...]});

  client.addEventListener("close", () => {[...]});

  const connection = {
    client,
    send: (message = {}) => {
      if (options.queryParams) {
        message = { ...message, ...options.queryParams };
      }

      return client.send(JSON.stringify(message));
    },
  };

  if (onConnect) onConnect(connection);

  return connection;
};

export default websocketClient;

Zpět na konec naší funkce jsme přidali nový objekt connection jako const který zahrnuje dvě vlastnosti:client který je nastaven na client proměnná obsahující naše připojení websocket a send , nastavte na vlastní funkci, kterou definujeme, aby nám pomohla odesílat zprávy.

Jedním ze základních konceptů websocket serveru je schopnost posílat zprávy tam a zpět mezi klientem a serverem (přemýšlejte o svém websocketovém spojení jako o kusu provázku se dvěma plechovkami připojenými k jednomu konci). Když posíláme zprávy – buď z klienta nebo ze serveru – musíme je přetypovat (což znamená nastavit jako nebo transformovat na jiný typ dat) jako hodnotu řetězce 'like this' .

Zde je naše send funkce je přidána jako pohodlí, které nám pomáhá zefektivnit předávání celých objektů jako řetězce. Myšlenka je taková, že když použijeme náš kód, po volání našeho websocketClient funkce, obdržíme zpět toto connection objekt. V našem kódu pak budeme moci volat connection.send({ someData: 'hello there' }) bez musíme objekt, který předáváme, ručně stringifikovat.

Kromě toho, že tento kód zpřesňuje naši zprávu, zahrnuje také jakékoli queryParams které byly předány. To je užitečné, protože na tyto hodnoty možná budeme potřebovat odkazovat, když zpracováváme připojení klienta na našem serveru websocket, nebo kdykoli obdržíme zprávu od připojeného klienta (např. předáme ID uživatele spolu se zprávou identifikovat, kdo to poslal).

Těsně předtím, než vrátíme connection v dolní části naší funkce si všimněte, že podmíněně voláme onConnect (funkce zpětného volání, která se bude nazývat po je navázáno naše spojení). Technicky vzato zde nečekáme na navázání skutečného spojení, než zavoláme zpětné volání.

Připojení websocket by se mělo vytvořit téměř okamžitě, takže v době, kdy je tento kód vyhodnocen, můžeme očekávat existenci klientského připojení. V případě, že bylo připojení k serveru pomalé, rádi bychom zvážili přesunutí volání na onConnect uvnitř zpětného volání posluchače událostí pro open událost výše.

/websockets/client.js

import queryString from "query-string";
import settings from "../settings";

const websocketClient = (options = {}, onConnect = null) => {
  let url = settings?.websockets?.url;

  if (options.queryParams) {
    url = `${url}?${queryString.stringify(options.queryParams)}`;
  }

  let client = new WebSocket(url);

  client.addEventListener("open", () => {
    console.log(`[websockets] Connected to ${settings?.websockets?.url}`);
  });

  client.addEventListener("close", () => {
    console.log(`[websockets] Disconnected from ${settings?.websockets?.url}`);
    client = null;
  });

  client.addEventListener("message", (event) => {
    if (event?.data && options.onMessage) {
      options.onMessage(JSON.parse(event.data));
    }
  });

  const connection = {
    client,
    send: (message = {}) => {
      if (options.queryParams) {
        message = { ...message, ...options.queryParams };
      }

      return client.send(JSON.stringify(message));
    },
  };

  return connection;
};

export default websocketClient;

Ještě jedna věc, kterou bychom se měli vplížit. Zatímco jsme nastavili našeho klienta websocket k odesílání zprávy, zatím jsme jej nenastavili pro příjem zprávy.

Když je zpráva odeslána připojeným klientům (pokud není zpracována záměrně, zpráva odeslaná serverem websocket bude odeslána všem připojení klienti), tito klienti obdrží tuto zprávu prostřednictvím message událost na jejich client připojení.

Zde jsme přidali nový posluchač událostí pro message událost. Podmíněně, za předpokladu, že byla odeslána skutečná zpráva (v event.data pole) a že máme onMessage funkci zpětného volání v našich možnostech, nazýváme tuto funkci a předáváme JSON.parse verze zprávy. Pamatujte, že zprávy jsou odesílány tam a zpět jako řetězce. Zde vycházíme z předpokladu, že zpráva, kterou jsme obdrželi z našeho serveru, je stringifikovaný objekt a my ji chceme převést na objekt JavaScript.

To je vše pro naši realizaci! Nyní nechejte našeho klienta používat a ověřte, že vše funguje podle očekávání.

Pomocí klienta websocket

Abychom mohli našeho klienta používat, zapojíme novou komponentu stránky do standardního vzoru, který jsme naklonovali na začátku tohoto tutoriálu. Pojďme vytvořit novou stránku na /pages/index.js nyní a podívejte se, co musíme udělat pro integraci našeho klienta websocket.

/pages/index.js

import React from "react";
import PropTypes from "prop-types";
import websocketClient from "../websockets/client";

import StyledIndex from "./index.css";

class Index extends React.Component {
  state = {
    message: "",
    received: [],
    connected: false,
  };

  componentDidMount() {
    websocketClient(
      {
        queryParams: {
          favoritePizza: "supreme",
        },
        onMessage: (message) => {
          console.log(message);
          this.setState(({ received }) => {
            return {
              received: [...received, message],
            };
          });
        },
        onDisconnect: () => {
          this.setState({ connected: false });
        },
      },
      (websocketClient) => {
        this.setState({ connected: true }, () => {
          this.websocketClient = websocketClient;
        });
      }
    );
  }

  handleSendMessage = () => {
    const { message } = this.state;
    this.websocketClient.send({ message });
    this.setState({ message: "" });
  };

  render() {
    const { message, connected, received } = this.state;

    return (
      <StyledIndex>
        <div className="row">
          <div className="col-sm-6">
            <label className="form-label">Send a Message</label>
            <input
              className="form-control mb-3"
              type="text"
              name="message"
              placeholder="Type your message here..."
              value={message}
              onChange={(event) =>
                this.setState({ message: event.target.value })
              }
            />
            <button
              className="btn btn-primary"
              onClick={this.handleSendMessage}
            >
              Send Message
            </button>
          </div>
          <div className="row">
            <div className="col-sm-12">
              <div className="messages">
                <header>
                  <p>
                    <i
                      className={`fas ${connected ? "fa-circle" : "fa-times"}`}
                    />{" "}
                    {connected ? "Connected" : "Not Connected"}
                  </p>
                </header>
                <ul>
                  {received.map(({ message }, index) => {
                    return <li key={`${message}_${index}`}>{message}</li>;
                  })}
                  {connected && received.length === 0 && (
                    <li>No messages received yet.</li>
                  )}
                </ul>
              </div>
            </div>
          </div>
        </div>
      </StyledIndex>
    );
  }
}

Index.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Index;

Proberme zde obecnou myšlenku a pak se zaměřme na věci s websocketem. To, co zde děláme, je nastavení komponenty React, která vykresluje vstup, tlačítko a seznam zpráv přijatých z našeho serveru websocket. Abychom demonstrovali využití našeho klienta, připojíme se ke klientovi a poté budeme odesílat zprávy na server. Očekáváme (podíváme se na to později), že nám náš server pošle zpět zprávu formou ping pongu, kde server potvrdí náš zprávu odesláním své vlastní.

V render() zde používáme kombinaci Bootstrapu (součástí standardního modelu, který jsme naklonovali pro tento tutoriál) a malého kousku vlastního CSS implementovaného pomocí styled-components prostřednictvím <StyledIndex /> komponentu, kterou jsme importovali v horní části našeho souboru komponenty.

Specifika CSS zde nejsou důležitá, ale nezapomeňte přidat následující soubor na /pages/index.css.js (dávejte pozor na příponu .css.js, aby import ve vaší komponentě stále fungoval na /pages/index.js ). Kód, který ukážeme dále, bude fungovat i bez něj, ale nebude vypadat jako příklad, který uvádíme níže.

/pages/index.css.js

import styled from "styled-components";

export default styled.div`
  .messages {
    background: var(--gray-1);
    margin-top: 50px;

    header {
      padding: 20px;
      border-bottom: 1px solid #ddd;
    }

    header p {
      margin: 0;

      i {
        font-size: 11px;
        margin-right: 5px;
      }

      .fa-circle {
        color: lime;
      }
    }

    ul {
      padding: 20px;
      list-style: none;
      margin: 0;
    }
  }
`;

Zpět v komponentě se chceme zaměřit na dvě metody:naše componentDidMount a handleSendMessage :

/pages/index.js

import React from "react";
import PropTypes from "prop-types";
import websocketClient from "../websockets/client";

import StyledIndex from "./index.css";

class Index extends React.Component {
  state = {
    message: "",
    received: [],
    connected: false,
  };

  componentDidMount() {
    websocketClient(
      {
        queryParams: {
          favoritePizza: "supreme",
        },
        onMessage: (message) => {
          console.log(message);
          this.setState(({ received }) => {
            return {
              received: [...received, message],
            };
          });
        },
        onDisconnect: () => {
          this.setState({ connected: false });
        },
      },
      (websocketClient) => {
        this.setState({ connected: true }, () => {
          this.websocketClient = websocketClient;
        });
      }
    );
  }

  handleSendMessage = () => {
    const { message } = this.state;
    this.websocketClient.send({ message });
    this.setState({ message: "" });
  };

  render() {
    const { message, connected, received } = this.state;

    return (
      <StyledIndex>
        [...]
      </StyledIndex>
    );
  }
}

Index.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Index;

Zde v componentDidMount zavoláme naše websocketClient() funkci, kterou jsme importovali z našeho /websockets/client.js soubor. Když jej zavoláme, předáme dva očekávané argumenty:za prvé options objekt obsahující nějaký queryParams , onMessage funkce zpětného volání a onDisconnect zpětné volání a za druhé onConnect funkce zpětného volání, která obdrží naši instanci klienta websocket, jakmile bude k dispozici.

Pro queryParams, zde jen předáváme pár příkladů dat, abychom ukázali, jak to funguje.

V onMessage callback, přijmeme zprávu (nezapomeňte, že to bude objekt JavaScriptu analyzovaný z řetězce zprávy, který obdržíme ze serveru) a poté ji nastavíme na stav naší komponenty zřetězením s existujícími zprávami, které máme received . Zde je ...received část říká "přidat existující přijaté zprávy do tohoto pole." Ve skutečnosti získáme pole objektů zpráv obsahujících dříve přijaté zprávy i zprávu, kterou nyní přijímáme.

Nakonec pro options , přidáme také onDisconnect zpětné volání, které nastaví connected stavu na komponentě (použijeme to pro určení úspěšného připojení) na false pokud ztratíme spojení.

Dole v onConnect callback (druhý argument předaný do websocketClient() ) zavoláme na this.setState() nastavení connected na true a pak – což je důležitá část – přiřadíme websocketClient instance nám předána prostřednictvím onConnect callback a nastavte jej na komponentě React instance jako this.websocketClient .

Důvod, proč to chceme udělat, je v handleSendMessage . Tato zpráva je volána při každém stisknutí tlačítka v našem render() metoda se klikne. Po kliknutí získáme aktuální hodnotu pro message (nastavili jsme to na stav jako this.state.message kdykoli se vstup změní) a poté zavolejte na this.websocketClient.send() . Pamatujte, že send() funkce, kterou zde voláme, je stejná, jakou jsme propojili a přiřadili k connection objekt zpět v /websockets/client.js .

Zde předáme naši zprávu jako součást objektu a očekáváme .send() převést to na řetězec před odesláním na server.

To je maso a brambory. Dole v render() jednou naše this.state.received pole má nějaké zprávy, vykreslíme je jako obyčejné <li></li> značky dole v <div className="messages"></div> blokovat.

S tím, když načteme naši aplikaci do prohlížeče a navštívíme http://localhost:5000 , měli bychom vidět náš jednoduchý formulář a (za předpokladu, že náš websocket server běží) pod vstupem stav "Připojeno"! Pokud odešlete zprávu, měli byste vidět odpověď ze serveru.

Poznámka :Znovu, pokud jste nedokončili tutorial CheatCode o nastavení serveru websocket, ujistěte se, že jste se řídili pokyny tam, abyste měli funkční server a ujistěte se, že jej spouštíte.

Zabalení

V tomto tutoriálu jsme se naučili, jak nastavit klienta websocket pomocí nativního prohlížeče WebSocket třída. Naučili jsme se, jak napsat funkci wrapper, která vytvoří připojení k našemu serveru, zpracuje parametry dotazu a zpracuje všechny základní události websocket včetně:open , close a message .

Také jsme se naučili, jak zapojit našeho klienta websocket do komponenty React a jak posílat zprávy přes tohoto klienta z formuláře v naší komponentě.