Komponenten höherer Ordnung reagieren

Kürzlich haben wir Material zu Funktionen höherer Ordnung in JavaScript veröffentlicht, das sich an diejenigen richtet, die JavaScript lernen. Der Artikel, den wir heute übersetzen, richtet sich an React-Entwickler für Anfänger. Es konzentriert sich auf Komponenten höherer Ordnung (HOC).



DRY-Prinzip und Komponenten höherer Ordnung in React


Sie werden im Programmierstudium nicht weit genug vorankommen können und nicht auf das fast kultige Prinzip von DRY stoßen (Wiederholen Sie sich nicht, wiederholen Sie nicht). Manchmal gehen seine Anhänger sogar zu weit, aber in den meisten Fällen lohnt es sich, nach Compliance zu streben. Hier werden wir über das beliebteste Reaktionsentwicklungsmuster sprechen, das die Einhaltung des DRY-Prinzips sicherstellt. Es geht um Komponenten höherer Ordnung. Um den Wert von Komponenten höherer Ordnung zu verstehen, formulieren und verstehen wir zunächst das Problem, das sie lösen sollen.

Angenommen, Sie müssen ein Bedienfeld neu erstellen, das dem Streifenbedienfeld ähnelt. Viele Projekte haben die Eigenschaft, sich nach dem Schema zu entwickeln, wenn bis zum Abschluss des Projekts alles in Ordnung ist. Wenn Sie der Meinung sind, dass die Arbeit fast abgeschlossen ist, stellen Sie fest, dass das Bedienfeld viele verschiedene QuickInfos enthält, die angezeigt werden sollten, wenn Sie mit der Maus über bestimmte Elemente fahren.


Systemsteuerung und QuickInfos

Um diese Funktionalität zu implementieren, können Sie verschiedene Ansätze verwenden. Sie haben sich dazu entschlossen: Bestimmen Sie, ob sich der Zeiger über einer einzelnen Komponente befindet, und entscheiden Sie dann, ob ein Hinweis dafür angezeigt werden soll oder nicht. Es gibt drei Komponenten, die mit ähnlichen Funktionen ausgestattet werden müssen. Dies sind Info , TrendChart und DailyChart .

Beginnen wir mit der Info Komponente. Im Moment ist es ein einfaches SVG-Symbol.

 class Info extends React.Component { render() {   return (     <svg       className="Icon-svg Icon--hoverable-svg"       height={this.props.height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   ) } } 

Jetzt müssen wir diese Komponente in die Lage versetzen, festzustellen, ob sich der Mauszeiger darüber befindet oder nicht. onMouseOver können Sie die onMouseOver und onMouseOut . Die an onMouseOver Funktion wird aufgerufen, wenn der Mauszeiger in den Komponentenbereich gefallen ist, und die an onMouseOut Funktion wird aufgerufen, wenn der Zeiger die Komponente verlässt. Um dies alles so zu organisieren, wie es in React akzeptiert wird, fügen wir der Komponente, die im Status gespeichert ist, die hovering Eigenschaft hinzu, mit der wir die Komponente neu rendern können, indem wir den Tooltip anzeigen oder ausblenden, wenn sich diese Eigenschaft ändert.

 class Info extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id} />         : null}       <svg         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}         className="Icon-svg Icon--hoverable-svg"         height={this.props.height}         viewBox="0 0 16 16" width="16">           <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />       </svg>     </>   ) } } 

Es ist ziemlich gut geworden. Jetzt müssen wir zwei weiteren Komponenten dieselbe Funktionalität hinzufügen - TrendChart und DailyChart . Der obige Mechanismus für die Info Komponente funktioniert einwandfrei. Was nicht kaputt ist, muss nicht repariert werden. Lassen Sie uns dasselbe in anderen Komponenten mit demselben Code neu erstellen. Recyceln Sie den Code für die TrendChart Komponente.

 class TrendChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='trend'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Sie haben wahrscheinlich schon verstanden, was als nächstes zu tun ist. Das gleiche kann mit unserer letzten Komponente gemacht werden - DailyChart .

 class DailyChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='daily'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Jetzt ist alles fertig. Möglicherweise haben Sie bereits etwas Ähnliches über React geschrieben. Dies ist natürlich nicht der schlechteste Code der Welt, aber er folgt nicht besonders gut dem DRY-Prinzip. Wie Sie sehen können, wiederholen wir durch die Analyse des Komponentencodes in jedem von ihnen dieselbe Logik.

Das Problem, mit dem wir konfrontiert sind, sollte jetzt extrem klar werden. Dies ist doppelter Code. Um dies zu lösen, möchten wir die Notwendigkeit beseitigen, denselben Code in Fällen zu kopieren, in denen das, was wir bereits implementiert haben, von einer neuen Komponente benötigt wird. Wie kann man es lösen? Bevor wir darüber sprechen, werden wir uns auf verschiedene Programmierkonzepte konzentrieren, die das Verständnis der hier vorgeschlagenen Lösung erheblich erleichtern. Wir sprechen von Rückrufen und Funktionen höherer Ordnung.

Funktionen höherer Ordnung


Funktionen in JavaScript sind erstklassige Objekte. Dies bedeutet, dass sie wie Objekte, Arrays oder Zeichenfolgen Variablen zugewiesen, als Argumente an Funktionen übergeben oder von anderen Funktionen zurückgegeben werden können.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } addFive(10, add) // 15 

Wenn Sie an dieses Verhalten nicht gewöhnt sind, erscheint Ihnen der obige Code möglicherweise seltsam. Sprechen wir darüber, was hier los ist. Wir übergeben nämlich die Funktion addFive als Argument an die Funktion addFive , benennen sie in addReference und rufen sie dann auf.

Wenn solche Konstruktionen verwendet werden, wird eine als Argument an eine andere übergebene Funktion als Rückruf (Rückruffunktion) bezeichnet, und eine Funktion, die eine andere Funktion als Argument empfängt, wird als Funktion höherer Ordnung bezeichnet.

Die Benennung von Entitäten in der Programmierung ist wichtig, daher wird hier derselbe Code verwendet, in dem die Namen gemäß den von ihnen dargestellten Konzepten geändert werden.

 function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add) 

Dieses Muster sollte Ihnen bekannt vorkommen. Tatsache ist, dass Sie, wenn Sie beispielsweise JavaScript-Array-Methoden verwendet haben, die mit jQuery oder lodash gearbeitet haben, bereits Funktionen und Rückrufe höherer Ordnung verwendet haben.

 [1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') ) 

Kehren wir zu unserem Beispiel zurück. Was ist, wenn wir anstatt nur die Funktion addFive zu erstellen, die Funktion addTwenty , addTwenty und ähnliche erstellen addTen addTwenty Angesichts der addFive Funktion addFive müssen wir ihren Code kopieren und ändern, um die oben genannten Funktionen basierend darauf zu erstellen.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } function addTen (x, addReference) { return addReference(x, 10) } function addTwenty (x, addReference) { return addReference(x, 20) } addFive(10, add) // 15 addTen(10, add) // 20 addTwenty(10, add) // 30 

Es sollte beachtet werden, dass unser Code nicht so albtraumhaft war, aber es ist klar, dass viele Fragmente darin wiederholt werden. Unser Ziel ist es, so viele Funktionen zu erstellen, die den an sie addFive Zahlen ( addFive , addTen , addTwenty usw.) bestimmte Zahlen addTen , wie wir benötigen, und gleichzeitig die Codeduplizierung zu minimieren. Vielleicht müssen wir dazu eine makeAdder Funktion erstellen? Diese Funktion kann eine bestimmte Nummer und einen Link zur add Funktion annehmen. Da der Zweck dieser Funktion darin besteht, eine neue Funktion zu erstellen, die die ihr übergebene Nummer zu der angegebenen hinzufügt, können wir die Funktion makeAdder , eine neue Funktion zurückzugeben, die eine bestimmte Nummer enthält (wie die Nummer 5 in makeFive ) und Zahlen annehmen kann zu dieser Nummer hinzufügen.

Schauen Sie sich ein Beispiel für die Implementierung der oben genannten Mechanismen an.

 function add (x, y) { return x + y } function makeAdder (x, addReference) { return function (y) {   return addReference(x, y) } } const addFive = makeAdder(5, add) const addTen = makeAdder(10, add) const addTwenty = makeAdder(20, add) addFive(10) // 15 addTen(10) // 20 addTwenty(10) // 30 

Jetzt können wir so viele add Funktionen wie nötig erstellen und gleichzeitig den Umfang der Codeduplizierung minimieren.

Wenn es interessant ist, wird das Konzept, dass es eine bestimmte Funktion gibt, die andere Funktionen verarbeitet, so dass sie mit weniger Parametern als zuvor verwendet werden können, als "teilweise Anwendung der Funktion" bezeichnet. Dieser Ansatz wird in der funktionalen Programmierung verwendet. Ein Beispiel für die Verwendung ist die in JavaScript verwendete .bind Methode.

Das alles ist gut, aber was hat React mit dem oben genannten Problem zu tun, den Code für die Verarbeitung von Mausereignissen zu duplizieren, wenn neue Komponenten erstellt werden, die diese Funktion benötigen? Tatsache ist, dass genau wie die Funktion höherer Ordnung makeAdder uns hilft, die Codeduplizierung zu minimieren, die sogenannte „Komponente höherer Ordnung“ uns hilft, das gleiche Problem in einer React-Anwendung zu lösen. Hier wird jedoch alles etwas anders aussehen. Anstelle eines Arbeitsschemas, bei dem eine Funktion höherer Ordnung eine neue Funktion zurückgibt, die einen Rückruf aufruft, kann eine Komponente höherer Ordnung ihr eigenes Schema implementieren. Er kann nämlich eine neue Komponente zurückgeben, die eine Komponente rendert, die die Rolle eines „Rückrufs“ spielt. Vielleicht haben wir schon viele Dinge gesagt, also ist es Zeit, zu Beispielen überzugehen.

Unsere Funktion höchster Ordnung


Diese Funktion hat die folgenden Funktionen:

  • Sie ist eine Funktion.
  • Sie akzeptiert als Argument einen Rückruf.
  • Es gibt eine neue Funktion zurück.
  • Die zurückgegebene Funktion kann den ursprünglichen Rückruf aufrufen, der an unsere Funktion höherer Ordnung übergeben wurde.

 function higherOrderFunction (callback) { return function () {   return callback() } } 

Unsere Komponente höchster Ordnung


Diese Komponente kann wie folgt charakterisiert werden:

  • Es ist eine Komponente.
  • Als Argument nimmt es eine andere Komponente.
  • Es wird eine neue Komponente zurückgegeben.
  • Die zurückgegebene Komponente kann die ursprüngliche Komponente rendern, die an die Komponente höherer Ordnung übergeben wurde.

 function higherOrderComponent (Component) { return class extends React.Component {   render() {     return <Component />   } } } 

HOC-Implementierung


Nachdem wir im Allgemeinen genau herausgefunden haben, welche Aktionen die Komponente höherer Ordnung ausführt, werden wir beginnen, Änderungen an unserem Reaktionscode vorzunehmen. Wenn Sie sich erinnern, besteht der Kern des Problems, das wir lösen, darin, dass der Code, der die Logik der Verarbeitung von Mausereignissen implementiert, auf alle Komponenten kopiert werden muss, die diese Funktion benötigen.

 state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) 

Vor diesem withHover benötigen wir unsere Komponente höherer Ordnung (nennen wir sie withHover ), um den Verarbeitungscode für withHover zu kapseln und die hovering Eigenschaft dann an die Komponenten zu übergeben, die sie rendert. Auf diese Weise können wir verhindern, dass der entsprechende Code dupliziert wird, indem wir ihn in die withHover Komponente withHover .

Letztendlich wollen wir dies erreichen. Wann immer wir eine Komponente benötigen, die eine Vorstellung von ihrer hovering Eigenschaft haben muss, können wir diese Komponente mit withHover an eine Komponente höherer Ordnung withHover . Das heißt, wir möchten mit den unten gezeigten Komponenten arbeiten.

 const InfoWithHover = withHover(Info) const TrendChartWithHover = withHover(TrendChart) const DailyChartWithHover = withHover(DailyChart) 

Wenn dann das, was withHover gerendert wird, ist dies die withHover , an die die hovering Eigenschaft übergeben wird.

 function Info ({ hovering, height }) { return (   <>     {hovering === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } 

Tatsächlich müssen wir jetzt nur noch die withHover Komponente implementieren. Aus dem Vorstehenden ist ersichtlich, dass er drei Aktionen ausführen muss:

  • Nehmen Sie ein Argument zu Komponente.
  • Geben Sie eine neue Komponente zurück.
  • Rendern Sie das Component-Argument, indem Sie die hovering Eigenschaft an es übergeben.

▍ Akzeptieren des Component-Arguments


 function withHover (Component) { } 

▍Eine neue Komponente zurückgeben


 function withHover (Component) { return class WithHover extends React.Component { } } 

▍ Rendern der Komponentenkomponente mit der an sie übergebenen schwebenden Eigenschaft


Jetzt stehen wir vor der folgenden Frage: Wie kommt man zum hovering Grundstück? Tatsächlich haben wir bereits den Code für die Arbeit mit dieser Eigenschaft geschrieben. Wir müssen es nur der neuen Komponente hinzufügen und dann die hovering Eigenschaft an sie übergeben, wenn die Komponente in Form des Component Arguments an die Component höherer Ordnung übergeben wird.

 function withHover(Component) { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component hovering={this.state.hovering} />       </div>     );   } } } 

Ich ziehe es vor, über diese Dinge folgendermaßen zu sprechen (wie in der React-Dokumentation angegeben): Eine Komponente konvertiert Eigenschaften in eine Benutzeroberfläche, und eine Komponente höherer Ordnung konvertiert eine Komponente in eine andere Komponente. In unserem Fall werden wir die Komponenten Info , TrendChart und DailyChart in neue Komponenten DailyChart , die dank der hovering Eigenschaft wissen, ob sich der Mauszeiger über ihnen befindet.

Zusätzliche Hinweise


Zu diesem Zeitpunkt haben wir alle grundlegenden Informationen zu Komponenten höherer Ordnung überprüft. Es gibt jedoch einige wichtigere Dinge zu besprechen.

Wenn Sie sich unser HOC withHover , werden Sie feststellen, dass es mindestens eine Schwachstelle hat. Dies bedeutet, dass bei der Empfängerkomponente der hovering Eigenschaft keine Probleme mit dieser Eigenschaft auftreten. In den meisten Fällen ist diese Annahme wahrscheinlich gerechtfertigt, es kann jedoch vorkommen, dass dies nicht akzeptabel ist. Was ist zum Beispiel, wenn eine Komponente bereits eine hovering Eigenschaft hat? In diesem Fall kommt es zu einer Kollision von Namen. Daher kann eine withHover an der withHover Komponente vorgenommen werden, mit der der Benutzer dieser Komponente angeben kann, welchen Namen die an die Komponenten übergebene hovering Eigenschaft tragen soll. Da withHover nur eine Funktion ist, schreiben wir sie neu, sodass withHover zweites Argument withHover ist, das den Eigenschaftsnamen festlegt, der an die Komponente übergeben werden soll.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Dank des ES6-Standardparametermechanismus setzen wir jetzt den Standardwert des zweiten Arguments als hovering . Wenn der Benutzer der withHover Komponente dies jedoch ändern möchte, kann er in diesem zweiten Argument den Namen übergeben, den er benötigt.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } function Info ({ showTooltip, height }) { return (   <>     {showTooltip === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } const InfoWithHover = withHover(Info, 'showTooltip') 

Problem mit der Hover-Implementierung


Möglicherweise haben Sie ein anderes Problem bei der Implementierung von withHover . Wenn wir unsere Info Komponente analysieren, werden Sie feststellen, dass sie unter anderem die Eigenschaft height akzeptiert. Die Art und Weise, wie wir jetzt alles angeordnet haben, bedeutet, dass die height auf undefined . Der Grund dafür ist, dass die withHover Komponente die Komponente ist, die für das Rendern der ihr als Component Argument übergebenen Komponenten verantwortlich ist. Jetzt übertragen wir keine anderen Eigenschaften als das von uns erstellte hovering auf die Komponentenkomponente.

 const InfoWithHover = withHover(Info) ... return <InfoWithHover height="16px" /> 

Die height Eigenschaft wird an die InfoWithHover Komponente übergeben. Und was ist diese Komponente? Dies ist die Komponente, von der wir mit withHover .

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     console.log(this.props) // { height: "16px" }     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Innerhalb der WithHover Komponente beträgt 16px , aber in Zukunft werden wir mit dieser Eigenschaft nichts mehr tun. Wir müssen diese Eigenschaft an das Component Argument übergeben, das wir rendern.

 render() {     const props = {       [propName]: this.state.hovering,       ...this.props,     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     ); } 

Über die Probleme bei der Arbeit mit Komponenten von Drittanbietern auf höchstem Niveau


Wir glauben, dass Sie bereits die Vorteile der Verwendung von Komponenten höherer Ordnung bei der Wiederverwendung von Logik in verschiedenen Komponenten erkannt haben, ohne denselben Code kopieren zu müssen. Fragen wir uns nun, ob die Komponenten höherer Ordnung Fehler aufweisen. Diese Frage kann positiv beantwortet werden, und wir haben diese Mängel bereits festgestellt.

Bei Verwendung von HOC tritt eine Kontrollinversion auf. Stellen Sie sich vor, wir verwenden eine Komponente höherer Ordnung, die nicht von uns entwickelt wurde, wie den HOC withRouter React Router. Gemäß der Dokumentation withRouter die Eigenschaften für match , location und history an die Komponente, die beim Rendern umbrochen wurde.

 class Game extends React.Component { render() {   const { match, location, history } = this.props // From React Router   ... } } export default withRouter(Game) 

Bitte beachten Sie, dass wir kein Spielelement erstellen (d. H. - <Game /> ). Wir übertragen unsere React Router-Komponente vollständig und vertrauen darauf, dass diese Komponente nicht nur rendert, sondern auch die richtigen Eigenschaften an unsere Komponente weitergibt. Dieses Problem ist uns bereits zuvor begegnet, als wir beim Übergeben der hovering Eigenschaft über einen möglichen Namenskonflikt gesprochen haben. Um dies zu beheben, haben wir beschlossen, dem withHover HOC withHover zu erlauben withHover das zweite Argument zu verwenden, um den Namen der entsprechenden Eigenschaft zu konfigurieren. withRouter wir das HOC eines anderen mit withRouter wir keine solche Gelegenheit. Wenn die match , location oder history Eigenschaften bereits in der Game Komponente verwendet werden, können wir sagen, dass wir kein Glück hatten. Wir müssen diese Namen entweder in unserer Komponente ändern oder die Verwendung von HOC withRouter .

Zusammenfassung


In Bezug auf HOC in React sind zwei wichtige Dinge zu beachten. Erstens ist HOC nur ein Muster. Komponenten höherer Ordnung können nicht einmal als reaktionsspezifisch bezeichnet werden, obwohl sie mit der Architektur der Anwendung zusammenhängen. Zweitens müssen Sie zum Entwickeln von React-Anwendungen keine Kenntnisse über Komponenten höherer Ordnung haben. Sie sind vielleicht nicht mit ihnen vertraut, aber schreiben Sie ausgezeichnete Programme. Wie in jedem Unternehmen kann das Ergebnis Ihrer Arbeit jedoch umso besser sein, je mehr Tools Sie haben. Und wenn Sie Anwendungen mit React schreiben, tun Sie sich selbst einen schlechten Dienst, ohne Ihrem Arsenal HOC hinzuzufügen.

Liebe Leser! Verwenden Sie in React Komponenten höherer Ordnung?

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


All Articles