Phoenix and ReactでTrelloをクロヌンしたす。 パヌト1〜3

画像

Trelloは私のお気に入りのアプリの1぀です。 私はそれを開始以来䜿甚しおおり、その動䜜方法、シンプルさず柔軟性がずおも気に入っおいたす。 新しいテクノロゞヌの研究を開始するたびに、実際の問題を解決するために研究したすべおを実践できる本栌的なアプリケヌションを䜜成し、これらの゜リュヌションを確認するこずを奜みたす。 そのため、 ElixirずそのPhoenixフレヌムワヌクの勉匷を始めたずき、出䌚ったこの玠晎らしい資料をすべお実際に䜿甚し、シンプルだが機胜的なTrelloの献身を実装する方法のガむドずしお共有する必芁があるこずに気付きたした。




翻蚳者からのメモ

幎の初め、゚リクサヌずフェニックスフレヌムワヌクに粟通するこずを決めお、私ぱリクサヌ、フェニックス、Reactを䜿甚したTrelloクロヌンの実装に関する興味深いシリヌズの蚘事でネットに出䌚いたした。 私にはかなり面癜そうで、ロシア語の翻蚳は芋぀かりたせんでしたが、共有したいず思いたした。 最埌に、䞡手が翻蚳に到達したした。


私はReact゚コシステムに完党に䞍慣れであるこずに泚意する必芁がありたす。この郚分は珟状のたたです。 それに加えお、゚リクサヌ/フェニックスのいく぀かの瞬間はこの間に倉化したした-プロゞェクトはただ立ち䞊がっおいたせん。 たた、Angular2 <-> Phoenix Channels <-> Elixir / Phoenix Framework bunchをやっおいるので、Angular2を䜿甚しおフロント゚ンドを実装し、それに関する蚘事を公開するための将来の時間を芋぀けたいず考えおいたす。


私の意芋では、元のサむクルでは蚘事ブロックが短すぎるため、ここでの1぀の出版物には耇数の郚分が含たれ、元のリンクは小芋出しの暪にありたす。


異議がある堎合は、甚語の元の名前を提䟛したす。翻蚳に矛盟がある堎合は、代替文をご容赊ください。 ゚ラヌ、タむプミス、䞍正確さの修正も歓迎したす。


そしお、序文を耇補したこずをおizeび申し䞊げたす。ネタバレの䞋でも、キャットの前に䜜者からメモず序文を入れるこずはできたせんでした。 導入がより重芁であるず決定したした。


技術スタックの玹介ず遞択


オリゞナル


Trelloは私のお気に入りのアプリの1぀です。 私はそれを開始以来䜿甚しおおり、その動䜜方法、シンプルさず柔軟性がずおも気に入っおいたす。 新しいテクノロゞヌの研究を開始するたびに、実際の問題を解決するために研究したすべおを実践できる本栌的なアプリケヌションを䜜成し、これらの゜リュヌションを確認するこずを奜みたす。 そのため、 ElixirずそのPhoenixフレヌムワヌクの勉匷を始めたずき、出䌚ったこの玠晎らしい資料をすべお実際に䜿甚し、シンプルだが機胜的なTrelloの献身を実装する方法のガむドずしお共有する必芁があるこずに気付きたした。


私たちは䜕をする぀もりですか


実際、既存のナヌザヌがログむンしお耇数のボヌドを䜜成し、それらを他のナヌザヌず共有しおリストやカヌドを远加できる1ペヌゞのアプリケヌションを䜜成したす。 ボヌドを衚瀺するず、接続されおいるナヌザヌが衚瀺され、倉曎はすぐに自動的に-Trelloスタむルで-各ナヌザヌのブラりザヌに反映されたす。


珟圚の技術スタック


フェニックスはnpmを䜿甚しお静的リ゜ヌスを管理し、 BrunchたたはWebpackを䜿甚しおすぐにそれらを収集するので、単䞀のコヌドベヌスを維持しながらフロント゚ンドずバック゚ンドを完党に分離するのは非垞に簡単です。 したがっお、バック゚ンドには次のものを䜿甚したす。



そしお、1ペヌゞのフロント゚ンドアプリケヌションを䜜成するには



さらにいく぀かのElixir䟝存関係ずnpmパッケヌゞを䜿甚したすが、プロセスの埌半でそれらに぀いお説明したす。


このスタックはなぜですか


Elixirは、 Erlangに基づいた非垞に高速で匷力な関数型蚀語であり、Rubyに非垞に類䌌した䜿いやすい構文を備えおいたす。 圌は非垞に信頌性が高く、䞊列凊理に特化しおおり、仮想マシンのおかげでErlang Erlang VM 、 BEAM-およそTranslator は数千の䞊列プロセスに察応できたす。 私はElixirを初めお䜿甚するので、ただ孊ぶべきこずがたくさんありたすが、すでに孊んだこずから、非垞に印象的であるず蚀えたす。


珟圚、Elixirで最も人気のあるWebフレヌムワヌクであるPhoenixを䜿甚したす。これは、 Rails Web開発で導入されたポむントや暙準の䞀郚を実装するだけでなく、䞊蚘の静的リ゜ヌスの管理方法など、他の倚くのクヌルな機胜も提䟛したす。そしお、私にずっお最も重芁なこずは、耇雑さや远加の倖郚䟝存関係のないwebsocketを䜿甚した組み蟌みのリアルタむム機胜ですそしお、信じられたすが、時蚈のように機胜したす 。


同時に、 React 、 react-router、およびReduxを䜿甚したす。これは、この組み合わせを䜿甚しお単䞀ペヌゞのアプリケヌションを䜜成し、その状態を管理するのが倧奜きだからです。 CoffieScriptをい぀ものように䜿甚する代わりに、新しい幎 蚘事は2016幎1月初旬-箄Translator にES6ずES7で䜜業したいので、これを開始しお参加する絶奜の機䌚です。


最終結果


アプリケヌションは、4぀の異なるビュヌで構成されたす。 最初の2぀は、登録画面ずログむン画面です。


ログむン


メむン画面には、ナヌザヌ自身のボヌドず、他のナヌザヌが接続したボヌドのリストが含たれたす。


ボヌドリスト


そしお最埌に、ボヌドのプレれンテヌションでは、すべおのナヌザヌが誰がそれに接続しおいるかを確認でき、リストずカヌドを管理できたす。


ボヌドの内容


しかし、十分な話。 2番目のパヌトの準備を開始できるように、ここでやめたしょう。新しいフェニックスプロゞェクトの䜜成方法、ブランチの代わりにWebpackを䜿甚するために倉曎する必芁があるもの、フロント゚ンドのフレヌムワヌクの構成方法に぀いお説明したす。



Phoenix Frameworkプロゞェクトの初期セットアップ


オリゞナル


したがっお、珟圚のテクノロゞヌスタックを遞択したら、新しいPhoenixプロゞェクトを䜜成するこずから始めたしょう。 これを行う前に、 ElixirずPhoenixがすでにむンストヌルされおいる必芁があるため、 むンストヌル手順に぀いおは公匏サむトを䜿甚しおください 。


Webpackを䜿甚した静的リ゜ヌス


Ruby on Railsずは異なり、 Phoenixには独自のリ゜ヌス凊理パむプラむンがありたせんアセットパむプラむン、 䞀郚のロシア語Railsリ゜ヌスは甚語を「ファむルパむプラむン」ずしお翻蚳したす-ほが翻蚳者 。そしお柔軟。 Brunchを䜿甚する必芁がないのは玠晎らしいこずです。これが望たしくない堎合は、 Webpackを䜿甚できたす。 私はBrunchを扱ったこずがないので、代わりにWebpackを䜿甚したす。


Phoenixには、ブランチに必芁なnode.jsがオプションの䟝存関係ずしお含たれおいたすが、Webpackにはnode.jsも必芁なので、埌者をむンストヌルしおください。


ブランチなしで新しいPhoenixプロゞェクトを䜜成したす。


$ mix phoenix.new --no-brunch phoenix_trello ... ... ... $ cd phoenix_trello 

さお、今ではリ゜ヌス構築ツヌルのない新しいプロゞェクトがありたす。 新しいpackage.jsonファむルを䜜成し、開発甚の䟝存関係ずしおWebpackをむンストヌルしたす dev䟝存関係-コメント トランスレヌタヌ 


 $ npm init ... (   Enter         ) ... ... $ npm i webpack --save-dev 

package.jsonは次のようになりたす。


 { "name": "phoenix_trello", "devDependencies": { "webpack": "^1.12.9" }, "dependencies": { }, } 

プロゞェクトの堎合、倚数の䟝存関係が必芁なので、ここですべおをスクロヌルするのではなく、プロゞェクトリポゞトリの゜ヌスファむルを芋お、そこからpackage.jsonコピヌしおください。 次のコマンドを実行しお、すべおのパッケヌゞをむンストヌルする必芁がありたす。


 $ npm install 

たた、 webpack.config.js構成ファむルを远加しお、 webpack.config.jsリ゜ヌスの収集方法を指瀺する必芁がありたす。


 'use strict'; var path = require('path'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var webpack = require('webpack'); function join(dest) { return path.resolve(__dirname, dest); } function web(dest) { return join('web/static/' + dest); } var config = module.exports = { entry: { application: [ web('css/application.sass'), web('js/application.js'), ], }, output: { path: join('priv/static'), filename: 'js/application.js', }, resolve: { extesions: ['', '.js', '.sass'], modulesDirectories: ['node_modules'], }, module: { noParse: /vendor\/phoenix/, loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { cacheDirectory: true, plugins: ['transform-decorators-legacy'], presets: ['react', 'es2015', 'stage-2', 'stage-0'], }, }, { test: /\.sass$/, loader: ExtractTextPlugin.extract('style', 'css!sass?indentedSyntax&includePaths[]=' + __dirname + '/node_modules'), }, ], }, plugins: [ new ExtractTextPlugin('css/application.css'), ], }; if (process.env.NODE_ENV === 'production') { config.plugins.push( new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({ minimize: true }) ); } 

ここでは、2぀のwebpack゚ントリポむントが必芁であるこずを瀺したす。1぀はJavaScript甚、もう1぀はスタむルシヌト甚で、䞡方ずもweb/staticディレクトリにありたす。 出力ファむルはpriv/static䜜成されpriv/static 。 ES6 / 7およびJSXのいく぀かの機胜を䜿甚するため、これらの目的のために䜜成されたいく぀かのプリセットでBabelを䜿甚したす。


最埌のステップは、開発サヌバヌが起動するたびにWebpack を起動するようPhoenixに指瀺するこずです。これにより、Webpackは開発プロセス䞭に倉曎を远跡し、フロント゚ンドビュヌによっお参照される察応するリ゜ヌスファむルを生成できたす。 これを行うには、「observer」の説明をconfig/dev.exs 。


 config :phoenix_trello, PhoenixTrello.Endpoint, http: [port: 4000], debug_errors: true, code_reloader: true, cache_static_lookup: false, check_origin: false, watchers: [ node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color"] ] ... 

開発サヌバヌを起動するず、 Webpackも機胜し、倉曎を远跡しおいるこずがわかりたす。


 $ mix phoenix.server [info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000 Hash: 93bc1d4743159d9afc35 Version: webpack 1.12.10 Time: 6488ms Asset Size Chunks Chunk Names js/application.js 1.28 MB 0 [emitted] application css/application.css 49.3 kB 0 [emitted] application [0] multi application 40 bytes {0} [built] + 397 hidden modules Child extract-text-webpack-plugin: + 2 hidden modules 

別のこず。 priv/static/jsディレクトリを調べるず、 phoenix.jsファむルがphoenix.jsたす。 このファむルにはwebsocketずchannelsを䜿甚するために必芁なものがすべお含たれおいるため、必芁に応じお接続できるようにweb/static/js゜ヌスを䜿甚しおベヌスディレクトリに移動したす


メむンのフロント゚ンド構造


これで、プログラミングを開始するためのすべおができたした。 たず、フロント゚ンドアプリケヌション構造を䜜成するこずから始めたしょう。これには、特に次のパッケヌゞが必芁です。



スタむルシヌトに぀いおはただ修正しおいるため、時間を無駄にする぀もりはありたせんが、通垞はcss-burittoを䜿甚したす。これは個人的な意芋ずしお、適切なSassファむル構造を䜜成するのに非垞に䟿利です。


Reduxリポゞトリreduxストアを構成する必芁があるため、次のファむルを䜜成したす。


 //web/static/js/store/index.js import { createStore, applyMiddleware } from 'redux'; import createLogger from 'redux-logger'; import thunkMiddleware from 'redux-thunk'; import { syncHistory } from 'react-router-redux'; import reducers from '../reducers'; const loggerMiddleware = createLogger({ level: 'info', collapsed: true, }); export default function configureStore(browserHistory) { const reduxRouterMiddleware = syncHistory(browserHistory); const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore); return createStoreWithMiddleware(reducers); } 

実際、3぀のミドルりェアレむダヌを持぀ストアをセットアップしおいたす。



たた、状態レデュヌサヌの組み合わせを枡す必芁があるため、このファむルの基本バヌゞョンを䜜成したす。


 //web/static/js/reducers/index.js import { combineReducers } from 'redux'; import { routeReducer } from 'redux-simple-router'; import session from './session'; export default combineReducers({ routing: routeReducer, session: session, }); 

出発点ずしお必芁なのは、ルヌティングの倉曎を自動的に状態に送信するrouterReducer 、次のようなsession 2぀のコンバヌタヌ routerReducer のみです。


 //web/static/js/reducers/session.js const initialState = { currentUser: null, socket: null, error: null, }; export default function reducer(state = initialState, action = {}) { return state; } 

埌者の初期状態には、蚪問者の認蚌埌に枡すcurrentUserオブゞェクト、チャネルぞの接続に䜿甚するsocket 、およびナヌザヌ認蚌䞭の問題を远跡するためのerrorが含たれたす。


これが完了したら、メむンのapplication.jsファむルに移動しお、 Rootコンポヌネントをレンダリングapplication.jsたす。


 //web/static/js/application.js import React from 'react'; import ReactDOM from 'react-dom'; import { browserHistory } from 'react-router'; import configureStore from './store'; import Root from './containers/root'; const store = configureStore(browserHistory); const target = document.getElementById('main_container'); const node = <Root routerHistory={browserHistory} store={store}/>; ReactDOM.render(node, target); 

ブラりザヌの履歎を含むオブゞェクトを䜜成し、リポゞトリヌを構成し、最埌にメむンアプリケヌションテンプレヌトにRootコンポヌネントを描画したす。これがRootのReduxアダプタヌラッパヌ Providerになりroutes 。


 //web/static/js/containers/root.js import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; import invariant from 'invariant'; import routes from '../routes'; export default class Root extends React.Component { _renderRouter() { invariant( this.props.routerHistory, '<Root /> needs either a routingContext or routerHistory to render.' ); return ( <Router history={this.props.routerHistory}> {routes} </Router> ); } render() { return ( <Provider store={this.props.store}> {this._renderRouter()} </Provider> ); } } 

次に、非垞に単玔なルヌトファむルに぀いお説明したす。


 //web/static/js/routes/index.js import { IndexRoute, Route } from 'react-router'; import React from 'react'; import MainLayout from '../layouts/main'; import RegistrationsNew from '../views/registrations/new'; export default ( <Route component={MainLayout}> <Route path="/" component={RegistrationsNew} /> </Route> ); 

アプリケヌションはMainLayoutコンポヌネント内にMainLayoutれ、 MainLayoutは登録画面を描画したす。 このファむルの最終バヌゞョンは、埌で実装する認蚌メカニズムのために倚少耇雑になりたすが、これに぀いおは埌で説明したす。


最埌に、メむンのPhoenixアプリケヌションテンプレヌトにRootコンポヌネントを描画するhtmlコンテナを远加する必芁がありたす。


 <!-- web/templates/layout/app.html.eex --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content="ricardo@codeloveandboards.com"> <title>Phoenix Trello</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>"> </head> <body> <main id="main_container" role="main"></main> <script src="<%= static_path(@conn, "/js/application.js") %>"></script> </body> </html> 

リンクおよびスクリプトタグは、 Webpackによっお生成された静的リ゜ヌスを参照するこずに泚意しおください。


フロント゚ンドルヌティングを管理するので、メむンテンプレヌトずRootコンポヌネントのみを描画するPageControllerコントロヌラヌのアクションアクション indexむベントハンドラヌにhttpリク゚ストを送信するようにPhoenixに指瀺する必芁がありたす。


 # master/web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", PhoenixTrello do pipe_through :browser # Use the default browser stack get "*path", PageController, :index end end 

今のずころすべおです。 次の出版物では、デヌタベヌス、 Userモデル、および新しいナヌザヌアカりントを䜜成する機胜の最初の移行を䜜成する方法に぀いお説明したす。



ナヌザヌモデルずJWT認蚌


オリゞナル


ナヌザヌ登録


プロゞェクトが完党に構成されたので、デヌタベヌスを移行するためのUserモデルず手順を䜜成する準備ができたした。 このパヌトでは、これを行う方法ず、蚪問者が新しいナヌザヌアカりントを䜜成できるようにする方法に぀いお説明したす。


ナヌザヌモデルず移行


フェニックスは、デヌタベヌスずのやり取りの仲介ずしおEctoを䜿甚したす。 Railsの堎合、EctoはActiveRecordsに䌌おいるず蚀えたすが、異なるモゞュヌル間で同様の機胜を共有しおいたす。


先に進む前に、デヌタベヌスを䜜成する必芁がありたす ただし、その前にconfig/dev.exs -translator commentでデヌタベヌス接続蚭定を構成する必芁がありたす 。


 $ mix ecto.create 

次に、新しい移行およびEctoモデルを䜜成したす。 モデルゞェネレヌタは、モゞュヌルの名前、スキヌムに名前を付けるための耇数圢、およびフォヌム:必須フィヌルド:パラメヌタヌずしお受け取りたす。


 $ mix phoenix.gen.model User users first_name:string last_name:string email:string encrypted_password:string 

結果の移行ファむルを芋るず、Rails移行ファむルずの類䌌性がすぐにわかりたす。


 # priv/repo/migrations/20151224075404_create_user.exs defmodule PhoenixTrello.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :first_name, :string, null: false add :last_name, :string, null: false add :email, :string, null: false add :crypted_password, :string, null: false timestamps end create unique_index(:users, [:email]) end end 

フィヌルドのコンテンツにnull犁止を远加し、電子メヌルフィヌルドの䞀意のむンデックスさえ远加したした。 これは、他の倚くの開発者が行うように、アプリケヌションに䟝存するよりも、デヌタの敎合性に察する責任をデヌタベヌスに移したいためです。 これは個人的な奜みの問題だず思いたす。


それでは、デヌタベヌスにusersテヌブルを䜜成したしょう。


 $ mix ecto.migrate 

Userモデルを詳しく芋おみたしょう。


 # web/models/user.ex defmodule PhoenixTrello.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :first_name, :string field :last_name, :string field :email, :string field :encrypted_password, :string timestamps end @required_fields ~w(first_name last_name email) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) end end 

2぀の䞻芁なセクションが衚瀺されたす。



ご泚意 翻蚳者
Ectoの最新バヌゞョンが曎新されたした。 たずえば、空のアトムは廃止予定ずしおマヌクされおいるため、代わりに空の連想配列マップ %{}䜿甚する必芁がありたす。たた、cast / 4関数はcast / 3およびvalidate_required / 3バンドルに眮き換えるこずをお勧めしたす 。 圓然、最新のPhoenixゞェネレヌタヌはこれらの掚奚事項に埓いたす。


(changeset)


, , , null email. User , , . encrypted_field , .


:


 # web/models/user.ex defmodule PhoenixTrello.User do # ... schema "users" do # ... field :password, :string, virtual: true # ... end @required_fields ~w(first_name last_name email password) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 5) |> validate_confirmation(:password, message: "Password does not match") |> unique_constraint(:email, message: "Email already taken") end end 

, :



. encrypted_password . comeonin , mix.exs :


 # mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project # ... def application do [mod: {PhoenixTrello, []}, applications: [ # ... :comeonin ] ] end #... defp deps do [ # ... {:comeonin, "~> 2.0"}, # ... ] end end 

:


 $ mix deps.get 

comeonin User encrypted_password changeset :


 # web/models/user.ex defmodule PhoenixTrello.User do # ... def changeset(model, params \\ :empty) do model # ...     |> generate_encrypted_password end defp generate_encrypted_password(current_changeset) do case current_changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) _ -> current_changeset end end end 

, . , comeonin encrypted_password , .


ルヌタヌ


, User , , router.ex :api :


 # web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #... pipeline :api do plug :accepts, ["json"] end scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do post "/registrations", RegistrationController, :create end end #... end 

, POST /api/v1/registrations (action) :create RegistrationController , json 
 , :)


コントロヌラヌ


, . , . , , , , front-end json jwt . — , , .


jwt, Guardian, . mix.exs:


 # mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project #... defp deps do [ # ... {:guardian, "~> 0.9.0"}, # ... ] end end 

mix deps.get config.exs:


 # config/confg.exs #... config :guardian, Guardian, issuer: "PhoenixTrello", ttl: { 3, :days }, verify_issuer: true, secret_key: <your guardian secret key>, serializer: PhoenixTrello.GuardianSerializer 

GuardianSerializer , Guardian, :


 # lib/phoenix_trello/guardian_serializer.ex defmodule PhoenixTrello.GuardianSerializer do @behaviour Guardian.Serializer alias PhoenixTrello.{Repo, User} def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } def for_token(_), do: { :error, "Unknown resource type" } def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) } def from_token(_), do: { :error, "Unknown resource type" } end 

, RegistrationController :


 # web/controllers/api/v1/registration_controller.ex defmodule PhoenixTrello.RegistrationController do use PhoenixTrello.Web, :controller alias PhoenixTrello.{Repo, User} plug :scrub_params, "user" when action in [:create] def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) conn |> put_status(:created) |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(PhoenixTrello.RegistrationView, "error.json", changeset: changeset) end end end 

(pattern matching), create "user" . User . , Guardian ( encode_and_sign ) , jwt json . , , json , .


JSON


Phoenix JSON - Poison . Phoenix, - . — User , :


 # web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # ... @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]} # ... end 

json (channel), . !


back-end, , front-end, , , React Redux . .



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


All Articles