Cara Membuat Feed Real-Time Menggunakan Phoenix dan React
() translation by (you can also view the original English article)



Dalam tutorial ini, saya akan menunjukkan kepada anda bagaimana kami dapat menggunakan kekuatan React and Phoenix untuk membuat aplikasi feed yang akan direfresh real time saat kami menambahkan feed baru ke database kami.
Pengenalan
Elixir dikenal karena stabilitas dan fitur real-time, dan Phoenix memanfaatkan kemampuan Erlang VM untuk menangani jutaan koneksi bersama sintaks yang indah dan perkakas produktif Elixir. Ini akan membantu kami dalam menghasilkan pembaruan data secara real-time melalui API yang akan digunakan oleh aplikasi React kami untuk menampilkan data pada user interface.
Persiapan
Anda harus menginstal Elixir, Erlang, dan Phoenix. Lebih lanjut tentang itu dapat ditemukan di situs web framework Phoenix. Selain itu, kita akan menggunakan bare-bones React boilerplate karena dipelihara dengan baik dan didokumentasikan dengan baik.
Membuat API Ready
Di bagian ini, kami akan mem-bootstrap aplikasi Phoenix API-only kami dan menambahkan saluran untuk memperbarui API secara real time. Kami hanya akan bekerja dengan feed (ini akan berisi judul dan deskripsi), dan setelah nilainya diubah dalam database, API akan mengirim nilai yang diperbarui ke aplikasi front-end kami.
Bootstrap App
Pertama mari kita bootstrap aplikasi Phoenix.
mix phoenix.new realtime_feed_api--no-html--no-brunch
Ini akan membuat aplikasi Phoenix bare-bones di dalam folder bernama realtime_feed_api. Opsi --no-html
tidak akan membuat semua file statis (yang berguna jika anda membuat aplikasi khusus API), dan opsi --no-brunch
tidak akan menyertakan bundel statis Phoenix, Brunch. Pastikan anda menginstal dependensi ketika ia meminta.
Mari kita masuk ke dalam folder dan membuat database kita.
cd realtime_feed_api
Kami harus menghapus username dan password dari file config/dev.exs kami karena kami akan membuat database kami tanpa username atau password. Ini hanya untuk menjaga hal-hal sederhana untuk posting ini. Untuk aplikasi anda, pastikan anda membuat database terlebih dahulu, dengan username dan password.
mix ecto.create
Perintah di atas akan menciptakan database kita. Sekarang, kita dapat menjalankan server Phoenix kami dan menguji jika semuanya berjalan normal saat ini.
mix phoenix.server
Perintah di atas akan menghidupkan server Phoenix kita, dan kami dapat mengakses http://localhost:4000 untuk melihatnya berjalan. Saat ini, tidak akan ada kesalahan yang ditemukan karena kami belum membuat rute apa pun!
Jangan ragu untuk memverifikasi perubahan dengan commit ku.
Tambahkan Model Feed
Pada langkah ini, kami akan menambahkan model Feed kami ke aplikasi Phoenix kami. Model Feed akan terdiri dari title dan description.
mix phoenix.gen.json Feed feeds title:string description:string
Perintah di atas akan menghasilkan model dan controller Feed kami. Ini juga akan menghasilkan spesifikasi (yang tidak akan kita modifikasi dalam tutorial ini, hanya untuk membuatnya singkat).
Anda perlu menambahkan route /feeds
di file web/router.ex Anda di dalam lingkup api:
resources "/feeds", FeedController, except: [:new, :edit]
Kita juga perlu menjalankan migrasi untuk membuat tabel feeds di basis data kami:
mix ecto.migrate
Sekarang, jika kita pergi ke http://localhost:4000/api/feed, kita akan melihat bahwa API mengirimkan respon kosong karena tidak ada data di tabel feeds kita.
Anda dapat memeriksa commit saya untuk referensi.
Menambahkan Chanel Feed
Pada langkah ini, kami akan menambahkan saluran Feed kami ke aplikasi Phoenix kami. Saluran menyediakan sarana komunikasi dua arah dari klien yang terintegrasi dengan lapisan Phoenix.PubSub
untuk fungsi waktu nyata yang mudah.
mix phoenix.gen.channel feed
Perintah di atas akan menghasilkan file feed_channel.ex di dalam folder web/chanels. Melalui file ini, aplikasi React kami akan menukarkan data yang diperbarui dari database menggunakan soket.
Kita perlu menambahkan saluran baru ke file web/channels/user_socket.ex:
channel "feeds", RealtimeFeedApi.FeedChannel
Karena kami tidak melakukan otentikasi apa pun untuk aplikasi ini, kami dapat memodifikasi file web/saluran/feed_channel.ex kami. Kami akan membutuhkan satu metode join untuk aplikasi React kami untuk bergabung dengan chanel feed, satu metode handles_out untuk mendorong payload melalui koneksi soket, dan satu metode broadcast_create yang akan menyiarkan payload setiap kali feed baru dibuat dalam database.
1 |
def join("feeds", payload, socket) do |
2 |
{:ok, "Joined feeds", socket} |
3 |
end
|
1 |
def handle_out(event, payload, socket) do |
2 |
push socket, event, payload |
3 |
{:noreply, socket} |
4 |
end
|
1 |
def broadcast_create(feed) do |
2 |
payload = %{ |
3 |
"id" => to_string(feed.id), |
4 |
"title" => feed.title, |
5 |
"description" => feed.description |
6 |
}
|
7 |
|
8 |
RealtimeFeedApi.Endpoint.broadcast("feeds", "app/FeedsPage/HAS_NEW_FEEDS", payload) |
9 |
end
|
Tiga metode yang didefinisikan di atas. Dalam method broadcast_create, kami menggunakan app/FeedsPage/HAS_NEW_FEEDS
karena kita akan menggunakan itu sebagai konstan untuk container Redux state, yang akan bertanggung jawab untuk membiarkan aplikasi front-end tahu bahwa ada feed baru dalam database. Kami akan membahas itu ketika kami membangun aplikasi front-end kami.
Pada akhirnya, kita hanya perlu memanggil metode broadcast_change melalui file feed_controller.ex kita setiap kali data baru dimasukkan dalam method create kita. Method create kami akan terlihat seperti ini:
1 |
def create(conn, %{"feed" => feed_params}) do |
2 |
changeset = Feed.changeset(%Feed{}, feed_params) |
3 |
|
4 |
case Repo.insert(changeset) do |
5 |
{:ok, feed} -> |
6 |
RealtimeFeedApi.FeedChannel.broadcast_create(feed) |
7 |
|
8 |
conn
|
9 |
|> put_status(:created) |
10 |
|> put_resp_header("location", feed_path(conn, :show, feed)) |
11 |
|> render("show.json", feed: feed) |
12 |
{:error, changeset} -> |
13 |
conn
|
14 |
|> put_status(:unprocessable_entity) |
15 |
|> render(RealtimeFeedApi.ChangesetView, "error.json", changeset: changeset) |
16 |
end
|
17 |
end
|
Method create bertanggung jawab untuk memasukkan data baru dalam database. Anda dapat memeriksa commit saya untuk referensi.
Menambahkan CORS Support untuk API
Kami perlu menerapkan support ini karena, dalam kasus kami, API dilayani dari http://localhost:4000 tetapi aplikasi front-end kami akan berjalan di http://localhost:3000. Menambahkan CORS support itu mudah. Kita hanya perlu menambahkan cors_plug ke file mix.exs kami:
1 |
defp deps do |
2 |
[
|
3 |
...
|
4 |
{:cors_plug, "~> 1.3"} |
5 |
]
|
6 |
end
|
Sekarang, kami stop server Phoenix kami menggunakan Control-C dan mengambil ketergantungan menggunakan perintah berikut:
mix deps.get
Kita perlu menambahkan baris berikut ke file lib/realtime_feed_api/endpoint.ex kami:
plug CORSPlug
Anda dapat memeriksa commit saya. Kami selesai dengan semua perubahan back-end kami. Mari sekarang fokus pada aplikasi front-end.
Update Data Front-End secara Real Time
Seperti yang disebutkan sebelumnya, kami akan menggunakan react-boilerplate untuk memulai dengan aplikasi front-end kami. Kami akan menggunakan Redux saga yang akan mendengarkan tindakan kami yang dikirim, dan berdasarkan itu, user interface akan memperbarui data.
Karena semuanya sudah dikonfigurasi di boilerplate, kami tidak perlu mengkonfigurasinya. Namun, kami akan menggunakan perintah yang tersedia di boiler untuk men-scaffold aplikasi kami. Pertama mari kita mengkloning repositori:
git clone
https://github.com/react-boilerplate/react-boilerplate.git
realtime_feed_ui
Bootstrap App
Sekarang, kita harus masuk ke folder realtime_feed_ui dan menginstal dependensi.
cd realtime_feed_ui && npm run setup
Ini menginisialisasi proyek baru dengan boiler ini, menghapus sejarah git react-boilerplate
, menginstal dependensi, dan menginisialisasi repositori baru.
Sekarang, mari kita hapus contoh aplikasi yang disediakan oleh boilerplate, dan ganti dengan kode boilerplate terkecil yang diperlukan untuk mulai menulis aplikasi kita:
npm run clean
Kita sekarang dapat memulai aplikasi kita menggunakan npm run start
dan melihatnya berjalan di http://localhost:3000/.
Anda dapat merujuk ke commit saya.
Tambahkan Container yang Diperlukan
Pada langkah ini, kami akan menambahkan dua kontainer baru, FeedsPage dan AddFeedPage, ke aplikasi kami. Container FeedsPage akan menampilkan daftar feed, dan container AddFeedPage akan memungkinkan kita untuk menambahkan feed baru ke database kita. Kami akan menggunakan generator react-boilerplate untuk membuat container kami.
npm run generate container
Perintah di atas digunakan untuk men-scaffold sebuah container di aplikasi kami. Setelah anda mengetikkan perintah ini, ia akan meminta nama komponen, yang akan menjadi FeedsPage dalam kasus ini, dan kami akan menggunakan opsi Component pada langkah berikutnya. Kami tidak akan membutuhkan header, tetapi kami akan membutuhkan actions/constants/selectors/reducer serta sagas untuk arus asynchronous kami. Kita tidak perlu i18n messages untuk aplikasi kita. Kita juga perlu untuk mengikuti pendekatan yang sama untuk membuat container AddFeedPage kami.
Sekarang, kami memiliki banyak file baru untuk digunakan. Ini menghemat banyak waktu. Kalau tidak, kita harus membuat dan mengkonfigurasi semua file ini oleh diri kita sendiri. Juga, generator membuat file test, yang sangat berguna, tetapi kami tidak akan menulis test sebagai bagian dari tutorial ini.
Mari kita tambahkan container dengan cepat ke file routes.js kami:
1 |
{
|
2 |
path: '/feeds', |
3 |
name: 'feedsPage', |
4 |
getComponent(nextState, cb) { |
5 |
const importModules = Promise.all([ |
6 |
import('containers/FeedsPage/reducer'), |
7 |
import('containers/FeedsPage/sagas'), |
8 |
import('containers/FeedsPage'), |
9 |
]);
|
10 |
|
11 |
const renderRoute = loadModule(cb); |
12 |
|
13 |
importModules.then(([reducer, sagas, component]) => { |
14 |
injectReducer('feedsPage', reducer.default); |
15 |
injectSagas(sagas.default); |
16 |
|
17 |
renderRoute(component); |
18 |
});
|
19 |
|
20 |
importModules.catch(errorLoading); |
21 |
},
|
22 |
}
|
Ini akan menambahkan wadah FeedsPage kami ke rute /feed
kami. Kami dapat memverifikasi ini dengan mengunjungi http://localhost:3000 feed. Saat ini, ini akan benar-benar kosong karena kami tidak memiliki apa pun di containers kami, tetapi tidak akan ada kesalahan di konsol browser kami.
Kami akan melakukan yang sama untuk container AddFeedPage kami.
Anda dapat merujuk ke commit saya untuk semua perubahan.
Membangun Feeds Listing Page
Pada langkah ini kita akan membangun FeedsPage yang akan mencantumkan semua feed kami. Demi menjaga tutorial ini kecil, kami tidak akan menambahkan style apa pun di sini, tetapi di akhir aplikasi kami, saya akan membuat commit terpisah yang akan menambahkan beberapa desain ke aplikasi kami.
Mari mulai dengan menambahkan konstanta kami di file app/containers/FeedsPage/constants.js kami:
1 |
export const FETCH_FEEDS_REQUEST = 'app/FeedsPage/FETCH_FEEDS_REQUEST'; |
2 |
export const FETCH_FEEDS_SUCCESS = 'app/FeedsPage/FETCH_FEEDS_SUCCESS'; |
3 |
export const FETCH_FEEDS_ERROR = 'app/FeedsPage/FETCH_FEEDS_ERROR'; |
4 |
export const HAS_NEW_FEEDS = 'app/FeedsPage/HAS_NEW_FEEDS'; |
Kami akan membutuhkan empat konstanta ini:
- Konstanta FETCH_FEEDS_REQUEST akan digunakan untuk menginisialisasi permintaan pengambilan kami.
- Konstanta FETCH_FEEDS_SUCCESS akan digunakan ketika permintaan mengambil berhasil.
- Konstanta FETCH_FEEDS_ERROR akan digunakan ketika mengambil permintaan gagal.
- Konstanta HAS_NEW_FEEDS akan digunakan ketika ada feed baru dalam database kami.
Mari kita menambahkan action kita dalam file app/containers/FeedsPage/actions.js kami:
1 |
export const fetchFeedsRequest = () => ({ |
2 |
type: FETCH_FEEDS_REQUEST, |
3 |
});
|
4 |
|
5 |
export const fetchFeeds = (feeds) => ({ |
6 |
type: FETCH_FEEDS_SUCCESS, |
7 |
feeds, |
8 |
});
|
9 |
|
10 |
export const fetchFeedsError = (error) => ({ |
11 |
type: FETCH_FEEDS_ERROR, |
12 |
error, |
13 |
});
|
14 |
|
15 |
export const checkForNewFeeds = () => ({ |
16 |
type: HAS_NEW_FEEDS, |
17 |
});
|
Semua tindakan ini sudah cukup jelas. Sekarang, kami akan menyusun initialState dari aplikasi kami dan menambahkan reducer di file app/containers/FeedsPage/reducer.js kami:
1 |
const initialState = fromJS({ |
2 |
feeds: { |
3 |
data: List(), |
4 |
ui: { |
5 |
loading: false, |
6 |
error: false, |
7 |
},
|
8 |
},
|
9 |
metadata: { |
10 |
hasNewFeeds: false, |
11 |
},
|
12 |
});
|
Ini akan initialState dari aplikasi kita (state sebelum mengambil data dimulai). Karena kami menggunakan ImmutableJS, kami dapat menggunakan struktur data List untuk menyimpan data abadi kami. Fungsi reducer kami akan menjadi seperti berikut:
1 |
function addFeedPageReducer(state = initialState, action) { |
2 |
switch (action.type) { |
3 |
case FETCH_FEEDS_REQUEST: |
4 |
return state |
5 |
.setIn(['feeds', 'ui', 'loading'], true) |
6 |
.setIn(['feeds', 'ui', 'error'], false); |
7 |
case FETCH_FEEDS_SUCCESS: |
8 |
return state |
9 |
.setIn(['feeds', 'data'], action.feeds.data) |
10 |
.setIn(['feeds', 'ui', 'loading'], false) |
11 |
.setIn(['metadata', 'hasNewFeeds'], false); |
12 |
case FETCH_FEEDS_ERROR: |
13 |
return state |
14 |
.setIn(['feeds', 'ui', 'error'], action.error) |
15 |
.setIn(['feeds', 'ui', 'loading'], false); |
16 |
case HAS_NEW_FEEDS: |
17 |
return state |
18 |
.setIn(['metadata', 'hasNewFeeds'], true); |
19 |
default: |
20 |
return state; |
21 |
}
|
22 |
}
|
Pada dasarnya, apa yang kita lakukan di sini adalah mengubah state kita berdasarkan konstanta dari tindakan kita. Kami dapat menampilkan loader dan pesan kesalahan dengan sangat mudah dengan cara ini. Ini akan menjadi lebih jelas ketika kita menggunakan ini di user Interface kami.
Saatnya untuk membuat selector kami menggunakan reselect, yang merupakan perpustakaan selector untuk Redux. Kami dapat mengekstrak nilai status kompleks dengan sangat mudah menggunakan reselect. Mari tambahkan pemilih berikut ke file app/containers/FeedsPage/selectors.js kami:
1 |
const feeds = () => createSelector( |
2 |
selectFeedsPageDomain(), |
3 |
(titleState) => titleState.get('feeds').get('data') |
4 |
);
|
5 |
|
6 |
const error = () => createSelector( |
7 |
selectFeedsPageDomain(), |
8 |
(errorState) => errorState.get('feeds').get('ui').get('error') |
9 |
);
|
10 |
|
11 |
const isLoading = () => createSelector( |
12 |
selectFeedsPageDomain(), |
13 |
(loadingState) => loadingState.get('feeds').get('ui').get('loading') |
14 |
);
|
15 |
|
16 |
const hasNewFeeds = () => createSelector( |
17 |
selectFeedsPageDomain(), |
18 |
(newFeedsState) => newFeedsState.get('metadata').get('hasNewFeeds') |
19 |
);
|
Seperti yang Anda lihat di sini, kami menggunakan struktur initialState kami untuk mengekstrak data dari state kita. Anda hanya perlu ingat sintaks reselect.
Saatnya untuk menambahkan sagas kami menggunakan redux-saga. Di sini, ide dasarnya adalah bahwa kita perlu membuat sebuah fungsi untuk mengambil data dan fungsi yang lain untuk mengawasi fungsi awal sehingga setiap kali tindakan tertentu dikirim, kita perlu untuk memanggil fungsi awal. Mari kita menambahkan fungsi yang akan mengambil daftar feed dari aplikasi back-end di file app/containers/FeedsPage/sagas.js kami:
1 |
function* getFeeds() { |
2 |
const requestURL = 'http://localhost:4000/api/feeds'; |
3 |
|
4 |
try { |
5 |
// Call our request helper (see 'utils/Request')
|
6 |
const feeds = yield call(request, requestURL); |
7 |
yield put(fetchFeeds(feeds)); |
8 |
} catch (err) { |
9 |
yield put(fetchFeedsError(err)); |
10 |
}
|
11 |
}
|
Di sini, request hanya fungsi util yang melakukan panggilan API kami ke back end kami. Seluruh file tersedia di react-boilerplate. Kami akan membuat sedikit perubahan setelah kami menyelesaikan file sagas.js kami.
Kita juga perlu untuk membuat satu lagi fungsi untuk mengawasi getFeeds fungsi:
1 |
export function* watchGetFeeds() { |
2 |
const watcher = yield takeLatest(FETCH_FEEDS_REQUEST, getFeeds); |
3 |
|
4 |
// Suspend execution until location changes
|
5 |
yield take(LOCATION_CHANGE); |
6 |
yield cancel(watcher); |
7 |
}
|
Seperti yang bisa kita lihat di sini, fungsi getFeeds akan dipanggil ketika kita mengirim tindakan yang berisi konstanta FETCH_FEEDS_REQUEST.
Sekarang, mari salin file request.js dari react-boilerplate ke aplikasi kita di dalam folder app/utils dan kemudian ubah fungsi request:
1 |
export default function request(url, method = 'GET', body) { |
2 |
return fetch(url, { |
3 |
headers: { |
4 |
'Content-Type': 'application/json', |
5 |
},
|
6 |
method, |
7 |
body: JSON.stringify(body), |
8 |
})
|
9 |
.then(checkStatus) |
10 |
.then(parseJSON); |
11 |
}
|
Saya baru saja menambahkan beberapa default yang akan membantu kami dalam mengurangi kode di kemudian hari karena kami tidak perlu melewati metode dan header setiap kali. Sekarang, kita perlu membuat file util lain di dalam folder app/utils. Kita akan memanggil file ini socketSagas.js. Isinya empat fungsi: connectToSocket, joinChannel, createSocketChannel, dan handleUpdatedData.
Fungsi connectToSocket akan bertanggung jawab untuk menghubungkan ke soket API back-end kami. Kami akan menggunakan npm package phoenix. Jadi kita harus menginstalnya:
npm install phoenix --save
Ini akan menginstal paket npm phoenix dan menyimpannya ke file package.json kami. Fungsi connectToSocket kita akan terlihat seperti berikut:
1 |
export function* connectToSocket() { |
2 |
const socket = new Socket('ws:localhost:4000/socket'); |
3 |
socket.connect(); |
4 |
return socket; |
5 |
}
|
Selanjutnya, kita mendefinisikan fungsi joinChannel kami, yang akan bertanggung jawab untuk bergabung dengan chanel tertentu dari back-end. Fungsi joinChannel akan memiliki isi sebagai berikut:
1 |
export function* joinChannel(socket, channelName) { |
2 |
const channel = socket.channel(channelName, {}); |
3 |
channel.join() |
4 |
.receive('ok', (resp) => { |
5 |
console.log('Joined successfully', resp); |
6 |
})
|
7 |
.receive('error', (resp) => { |
8 |
console.log('Unable to join', resp); |
9 |
});
|
10 |
|
11 |
return channel; |
12 |
}
|
Jika proses bergabung berhasil, kami akan mencatat 'Joined successfully' hanya untuk testing. Jika ada kesalahan selama fase penggabungan, kami juga akan mencatatnya hanya untuk keperluan debugging.
createSocketChannel akan bertanggung jawab untuk menciptakan sebuah acara channel dari soket tertentu.
1 |
export const createSocketChannel = (channel, constant, fn) => |
2 |
// `eventChannel` takes a subscriber function
|
3 |
// the subscriber function takes an `emit` argument to put messages onto the channel
|
4 |
eventChannel((emit) => { |
5 |
const newDataHandler = (event) => { |
6 |
console.log(event); |
7 |
emit(fn(event)); |
8 |
};
|
9 |
|
10 |
channel.on(constant, newDataHandler); |
11 |
|
12 |
const unsubscribe = () => { |
13 |
channel.off(constant, newDataHandler); |
14 |
};
|
15 |
|
16 |
return unsubscribe; |
17 |
});
|
Fungsi ini juga akan berguna jika kita ingin berhenti berlangganan dari saluran tertentu.
HandleUpdatedData hanya akan memanggil tindakan yang diteruskan ke situ sebagai argumen.
1 |
export function* handleUpdatedData(action) { |
2 |
yield put(action); |
3 |
}
|
Sekarang, mari tambahkan sisa saga di file app/containers/FeedsPage/sagas.js kami. Kita akan menciptakan dua fungsi lain di sini: connectWithFeedsSocketForNewFeeds dan watchConnectWithFeedsSocketForNewFeeds.
Fungsi connectWithFeedsSocketForNewFeeds akan bertanggung jawab untuk menghubungkan dengan soket back-end dan memeriksa feed baru. Jika ada feed baru, ini akan memanggil fungsi createSocketChannel dari file utils/socketSagas.js, yang akan membuat channel acara untuk soket yang diberikan. Fungsi connectWithFeedsSocketForNewFeeds kami akan berisi hal-hal berikut:
1 |
function* connectWithFeedsSocketForNewFeeds() { |
2 |
const socket = yield call(connectToSocket); |
3 |
const channel = yield call(joinChannel, socket, 'feeds'); |
4 |
|
5 |
const socketChannel = yield call(createSocketChannel, channel, HAS_NEW_FEEDS, checkForNewFeeds); |
6 |
|
7 |
while (true) { |
8 |
const action = yield take(socketChannel); |
9 |
yield fork(handleUpdatedData, action); |
10 |
}
|
11 |
}
|
Dan watchConnectWithFeedsSocketForNewFeeds akan memiliki berikut:
1 |
export function* watchConnectWithFeedsSocketForNewFeeds() { |
2 |
const watcher = yield takeLatest(FETCH_FEEDS_SUCCESS, connectWithFeedsSocketForNewFeeds); |
3 |
|
4 |
// Suspend execution until location changes
|
5 |
yield take(LOCATION_CHANGE); |
6 |
yield cancel(watcher); |
7 |
}
|
Sekarang, kami akan mengikat semuanya dengan file app/containers/FeedsPage/index.js kami. File ini akan berisi semua elemen antarmuka pengguna kami. Mari kita mulai dengan memanggil prop yang akan mengambil data dari bagian belakang di componentDidMount kami:
1 |
componentDidMount() { |
2 |
this.props.fetchFeedsRequest(); |
3 |
}
|
Ini akan mengambil semua umpan. Sekarang, kita perlu memanggil fetchFeedsRequest prop lagi setiap kali hasNewFeeds prop benar (anda bisa merujuk ke initialState reducer kami untuk struktur aplikasi kami):
1 |
componentWillReceiveProps(nextProps) { |
2 |
if (nextProps.hasNewFeeds) { |
3 |
this.props.fetchFeedsRequest(); |
4 |
}
|
5 |
}
|
Setelah ini, kami hanya membuat feed dalam fungsi render kami. Kami akan membuat fungsi FeedsNode dengan konten berikut:
1 |
feedsNode() { |
2 |
return [...this.props.feeds].reverse().map((feed) => { // eslint-disable-line arrow-body-style |
3 |
return ( |
4 |
<div |
5 |
className="col-12" |
6 |
key={feed.id} |
7 |
>
|
8 |
<div |
9 |
className="card" |
10 |
style={{ margin: '15px 0' }} |
11 |
>
|
12 |
<div className="card-block"> |
13 |
<h3 className="card-title">{ feed.title }</h3> |
14 |
<p className="card-text">{ feed.description }</p> |
15 |
</div> |
16 |
</div> |
17 |
</div> |
18 |
);
|
19 |
});
|
20 |
}
|
Dan kemudian, kita dapat memanggil metode ini dalam metode render kami:
1 |
render() { |
2 |
if (this.props.loading) { |
3 |
return ( |
4 |
<div>Loading...</div> |
5 |
);
|
6 |
}
|
7 |
|
8 |
return ( |
9 |
<div className="row"> |
10 |
{this.feedsNode()} |
11 |
</div> |
12 |
);
|
13 |
}
|
Jika kita sekarang pergi ke http://localhost:3000/feeds, kita akan melihat berikut login konsol:
Bergabung Sukses Bergabung feeds
Ini berarti bahwa API umpan kami berfungsi dengan baik, dan kami telah berhasil menghubungkan front-end kami dengan aplikasi back-end kami. Sekarang, kita hanya perlu membuat formulir agar kita dapat memasukkan feed baru.
Jangan ragu untuk merujuk pada commit saya karena banyak hal yang dilakukan dalam commit ini!
Buat Formulir untuk Menambah Feed Baru
Pada langkah ini, kami akan membuat formulir yang melaluinya kami dapat menambahkan umpan baru ke database kami.
Mari kita mulai dengan menambahkan konstanta ke file app/containers/AddFeedPage/constants.js kami:
1 |
export const UPDATE_ATTRIBUTES = 'app/AddFeedPage/UPDATE_ATTRIBUTES'; |
2 |
export const SAVE_FEED_REQUEST = 'app/AddFeedPage/SAVE_FEED_REQUEST'; |
3 |
export const SAVE_FEED_SUCCESS = 'app/AddFeedPage/SAVE_FEED_SUCCESS'; |
4 |
export const SAVE_FEED_ERROR = 'app/AddFeedPage/SAVE_FEED_ERROR'; |
Konstanta UPDATE_ATTRIBUTES akan digunakan ketika kita menambahkan beberapa teks ke kotak input. Semua konstanta lainnya akan digunakan untuk menyimpan judul dan deskripsi feed ke database kami.
Kontainer AddFeedPage akan menggunakan empat action: updateAttribut, saveFeedRequest, saveFeed, dan saveFeedError. Fungsi updateAttributes akan memperbarui atribut feed baru kami. Ini berarti setiap kali kita mengetik sesuatu di kotak input dari judul dan deskripsi feed, fungsi updateAttributes akan memperbarui status Redux kita. Keempat tindakan ini akan terlihat seperti berikut:
1 |
export const updateAttributes = (attributes) => ({ |
2 |
type: UPDATE_ATTRIBUTES, |
3 |
attributes, |
4 |
});
|
5 |
|
6 |
export const saveFeedRequest = () => ({ |
7 |
type: SAVE_FEED_REQUEST, |
8 |
});
|
9 |
|
10 |
export const saveFeed = () => ({ |
11 |
type: SAVE_FEED_SUCCESS, |
12 |
});
|
13 |
|
14 |
export const saveFeedError = (error) => ({ |
15 |
type: SAVE_FEED_ERROR, |
16 |
error, |
17 |
});
|
Selanjutnya, mari kita tambahkan fungsi reducer kami dalam app/containers/AddFeedPage/reducer.js file. InitialState akan terlihat seperti berikut:
1 |
const initialState = fromJS({ |
2 |
feed: { |
3 |
data: { |
4 |
title: '', |
5 |
description: '', |
6 |
},
|
7 |
ui: { |
8 |
saving: false, |
9 |
error: null, |
10 |
},
|
11 |
},
|
12 |
});
|
Dan fungsi reducer akan terlihat seperti:
1 |
function addFeedPageReducer(state = initialState, action) { |
2 |
switch (action.type) { |
3 |
case UPDATE_ATTRIBUTES: |
4 |
return state |
5 |
.setIn(['feed', 'data', 'title'], action.attributes.title) |
6 |
.setIn(['feed', 'data', 'description'], action.attributes.description); |
7 |
case SAVE_FEED_REQUEST: |
8 |
return state |
9 |
.setIn(['feed', 'ui', 'saving'], true) |
10 |
.setIn(['feed', 'ui', 'error'], false); |
11 |
case SAVE_FEED_SUCCESS: |
12 |
return state |
13 |
.setIn(['feed', 'data', 'title'], '') |
14 |
.setIn(['feed', 'data', 'description'], '') |
15 |
.setIn(['feed', 'ui', 'saving'], false); |
16 |
case SAVE_FEED_ERROR: |
17 |
return state |
18 |
.setIn(['feed', 'ui', 'error'], action.error) |
19 |
.setIn(['feed', 'ui', 'saving'], false); |
20 |
default: |
21 |
return state; |
22 |
}
|
23 |
}
|
Selanjutnya, kita akan mengkonfigurasi file app/containers/AddFeedPage/selectors.js kami. Itu akan memiliki empat penyeleksi: title, description, error, dan saving. Seperti namanya, selector ini akan mengekstraksi status ini dari status Redux dan membuatnya tersedia dalam wadah kami sebagai alat peraga.
Keempat fungsi ini akan terlihat seperti berikut:
1 |
const title = () => createSelector( |
2 |
selectAddFeedPageDomain(), |
3 |
(titleState) => titleState.get('feed').get('data').get('title') |
4 |
);
|
5 |
|
6 |
const description = () => createSelector( |
7 |
selectAddFeedPageDomain(), |
8 |
(titleState) => titleState.get('feed').get('data').get('description') |
9 |
);
|
10 |
|
11 |
const error = () => createSelector( |
12 |
selectAddFeedPageDomain(), |
13 |
(errorState) => errorState.get('feed').get('ui').get('error') |
14 |
);
|
15 |
|
16 |
const saving = () => createSelector( |
17 |
selectAddFeedPageDomain(), |
18 |
(savingState) => savingState.get('feed').get('ui').get('saving') |
19 |
);
|
Selanjutnya, mari kita konfigurasi sagas kami untuk container AddFeedPage. Ini akan memiliki dua fungsi: saveFeed dan watchSaveFeed. Fungsi saveFeed akan bertanggung jawab untuk melakukan permintaan POST ke API kami, dan itu akan memiliki hal-hal berikut:
1 |
export function* saveFeed() { |
2 |
const title = yield select(feedTitle()); |
3 |
const description = yield select(feedDescription()); |
4 |
const requestURL = 'http://localhost:4000/api/feeds'; |
5 |
|
6 |
try { |
7 |
// Call our request helper (see 'utils/Request')
|
8 |
yield put(saveFeedDispatch()); |
9 |
yield call(request, requestURL, 'POST', |
10 |
{
|
11 |
feed: { |
12 |
title, |
13 |
description, |
14 |
},
|
15 |
},
|
16 |
);
|
17 |
} catch (err) { |
18 |
yield put(saveFeedError(err)); |
19 |
}
|
20 |
}
|
Fungsi watchSaveFeed akan mirip dengan fungsi jam sebelumnya kami:
1 |
export function* watchSaveFeed() { |
2 |
const watcher = yield takeLatest(SAVE_FEED_REQUEST, saveFeed); |
3 |
|
4 |
// Suspend execution until location changes
|
5 |
yield take(LOCATION_CHANGE); |
6 |
yield cancel(watcher); |
7 |
}
|
Selanjutnya, kita hanya perlu membuat formulir di container kami. Untuk menjaga agar termodulasi, mari buat sub-komponen untuk form. Buat file form.js baru di dalam app/containers/AddFeedPage/sub-components folder (folder sub-components adalah folder baru yang Anda akan harus membuat). Ini akan berisi form dengan satu input box untuk judul feed dan satu textarea untuk deskripsi feed. Method render akan memiliki isi sebagai berikut:
1 |
render() { |
2 |
return ( |
3 |
<form style={{ margin: '15px 0' }}> |
4 |
<div className="form-group"> |
5 |
<label htmlFor="title">Title</label> |
6 |
<input |
7 |
type="text" |
8 |
className="form-control" |
9 |
id="title" |
10 |
placeholder="Enter title" |
11 |
onChange={this.handleChange} |
12 |
name="title" |
13 |
value={this.state.title} |
14 |
/> |
15 |
</div> |
16 |
<div className="form-group"> |
17 |
<label htmlFor="description">Description</label> |
18 |
<textarea |
19 |
className="form-control" |
20 |
id="description" |
21 |
placeholder="Enter description" |
22 |
onChange={this.handleChange} |
23 |
name="description" |
24 |
value={this.state.description} |
25 |
/> |
26 |
</div> |
27 |
<button |
28 |
type="button" |
29 |
className="btn btn-primary" |
30 |
onClick={this.handleSubmit} |
31 |
disabled={this.props.saving || !this.state.title || !this.state.description } |
32 |
>
|
33 |
{this.props.saving ? 'Saving...' : 'Save'} |
34 |
</button> |
35 |
</form> |
36 |
);
|
37 |
}
|
Kami akan membuat dua fungsi lagi: handleChange dan handleSubmit. Fungsi handleChange bertanggung jawab untuk memperbarui status Redux kami setiap kali kami menambahkan beberapa teks, dan fungsi handleSubmit memanggil API kami untuk menyimpan data dalam status Redux kami.
Fungsi handleChange memiliki isi seperti berikut:
1 |
handleChange(e) { |
2 |
this.setState({ |
3 |
[e.target.name]: e.target.value, |
4 |
});
|
5 |
}
|
Dan fungsi handleSubmit akan berisi sebagai berikut:
1 |
handleSubmit() { |
2 |
// doing this will make the component faster
|
3 |
// since it doesn't have to re-render on each state update
|
4 |
this.props.onChange({ |
5 |
title: this.state.title, |
6 |
description: this.state.description, |
7 |
});
|
8 |
|
9 |
this.props.onSave(); |
10 |
|
11 |
this.setState({ |
12 |
title: '', |
13 |
description: '', |
14 |
});
|
15 |
}
|
Di sini, kami menyimpan data dan kemudian membersihkan nilai form.
Sekarang, kembali ke file app/containers/AddFeedPage/index.js kami, kita akan hanya me-render form kami dibuat.
1 |
render() { |
2 |
return ( |
3 |
<div> |
4 |
<Form |
5 |
onChange={(val) => this.props.updateAttributes(val)} |
6 |
onSave={() => this.props.saveFeedRequest()} |
7 |
saving={this.props.saving} |
8 |
/> |
9 |
</div> |
10 |
);
|
11 |
}
|
Sekarang, semua pengkodean kami selesai. Jangan ragu untuk memeriksa commit saya jika anda memiliki keraguan.
Finalisasi
Kami telah menyelesaikan pembangunan aplikasi kita. Sekarang, kami dapat mengunjungi http://localhost:3000/feed/new dan menambahkan feed baru yang akan ditampilkan secara real time di http://localhost:3000/feeds. Kami tidak perlu merefresh halaman untuk melihat feed baru. Anda juga dapat mencoba ini dengan membuka http://localhost:3000/feed pada dua tab secara berdampingan dan mengujinya!
Kesimpulan
Ini akan menjadi contoh aplikasi untuk menunjukkan kekuatan nyata menggabungkan Phoenix dengan React. Kami menggunakan real-time data di kebanyakan tempat sekarang, dan ini mungkin hanya membantu Anda mendapatkan feel untuk mengembangkan sesuatu seperti itu. Saya berharap bahwa anda menemukan tutorial ini berguna.