Sag dari kehidupan

Hari yang baik


Apakah Anda juga memiliki pengembang reaksi yang akrab yang menceritakan kisah luar biasa tentang efek samping di redux? Tidak ?! Bisakah saya menjadi orang ini



Penulis mengambil kebebasan untuk tidak menulis bagian pengantar tentang apa itu perpustakaan redux saga. Dia berharap jika data tidak mencukupi, pembaca yang baik akan menggunakan pencarian Habr atau tutorial resmi . Contoh-contoh ini sangat disederhanakan untuk menyampaikan esensi.


Jadi, mengapa saya menggabungkan Anda semua. Ini akan tentang penggunaan redux saga di ruang terbuka klien tempur. Lebih khusus, tentang kasus yang lebih kompleks dan menarik daripada "terima tindakan => kirim permintaan API => buat tindakan baru".


Saya berharap untuk merangsang studi yang lebih dalam tentang perpustakaan ini oleh sesama warga, serta berbagi kesenangan tentang bagaimana hal-hal asinkron yang canggih menjadi lebih mudah dimengerti dan ekspresif.


Soket web


Use case: menerima pembaruan daftar lowongan yang tersedia dari server secara real time menggunakan model push.


Ini tentu saja tentang penggunaan soket web. Misalnya, ambil socket.io, tetapi sebenarnya API soket tidak masalah di sini.


Dalam kisah-kisah ada hal seperti saluran. Ini adalah bus pesan di mana sumber acara dapat berkomunikasi dengan konsumen mereka. Tujuan utama saluran adalah komunikasi antara kisah-kisah dan mengubah aliran peristiwa asinkron ke dalam struktur yang nyaman untuk bekerja.


Secara default, store adalah saluran acara utama untuk redux saga. Acara datang dalam bentuk aksi. Saluran digunakan untuk bekerja dengan acara bukan dari toko.


Ternyata saluran itu hanya apa yang Anda butuhkan untuk bekerja dengan aliran pesan yang tidak sinkron dari soket. Ayo buat saluran secepatnya!


Tapi pertama-tama, buat soket:


import io from 'socket.io-client'; export const socket = io.connect('/'); 

Sekarang nyatakan daftar acara sederhana:


 export const SocketEvents = { jobsFresh: 'jobs+fresh', }; 

Berikutnya adalah metode pabrik untuk membuat saluran. Kode menciptakan metode untuk berlangganan acara yang menarik bagi kami dari soket, metode untuk berhenti berlangganan, dan, secara langsung, saluran acara itu sendiri:


 import { eventChannel } from 'redux-saga'; import { socket } from '../apis/socket'; import { SocketEvents } from '../constants/socket-events'; export function createFreshJobsChannel() { const subscribe = emitter => { socket.on(SocketEvents.jobsFresh, emitter); return () => socket.removeListener(SocketEvents.jobsFresh, emitter); }; return eventChannel(subscribe); } 

Mari kita menulis kisah yang cukup sederhana, menunggu pembaruan dari soket dan mengubahnya menjadi tindakan yang sesuai:


 import { take, call, put } from 'redux-saga/effects'; import { createFreshJobsChannel } from '../channels/fresh-jobs'; import { JobsActions } from '../actions/jobs'; export function * freshJobsSaga() { const channel = yield call(createFreshJobsChannel); while (true) { const jobs = yield take(channel); const action = JobsActions.fresh(jobs); yield put(action); } } 

Tetap hanya untuk mengikatnya ke root saga:


 import { fork } from 'redux-saga/effects'; import { freshJobsSaga } from './fresh-jobs'; export function * sagas() { yield fork(freshJobsSaga); } 

Google Places Autocomplete


Use case: tampilkan kiat saat pengguna memasuki lokasi geografis untuk pencarian real estat terdekat berikutnya.


Faktanya, kita membutuhkan koordinat, dan pengguna membutuhkan nama yang bisa dibaca manusia dari area yang diinginkan.


Tampaknya bagaimana tugas ini berbeda dari "action => API => action" yang membosankan? Dalam hal pelengkapan otomatis, kami ingin membuat panggilan tidak berguna sesedikit mungkin ke sumber daya eksternal, dan juga menunjukkan kepada pengguna hanya petunjuk yang relevan.


Pertama, kami akan menulis metode API menggunakan Layanan Google Places Autocomplete. Satu hal yang menarik di sini adalah batasan kiat di negara tertentu:


 export function getPlaceSuggestions(autocompleteService, countryCode, query) { return new Promise(resolve => { autocompleteService.getPlacePredictions({ componentRestrictions: { country: countryCode }, input: query, }, resolve); }); } 

Ada metode API yang akan kami tarik, Anda dapat mulai menulis saga. Sudah waktunya untuk mengklarifikasi permintaan yang tidak berguna.


Implementasi di dahi, ketika pengguna mengetik, dan kami untuk setiap perubahan, membaca - untuk setiap karakter, mengirim permintaan ke API - tidak mengarah ke mana pun. Saat pengguna mengetik, dia tidak membutuhkan petunjuk. Tetapi ketika dia berhenti, saatnya untuk melayaninya.


Selain itu, kami tidak ingin berada dalam situasi di mana pengguna mengetik sesuatu, berhenti, permintaan API hilang, pengguna mengetik sesuatu, permintaan lain hilang.


Karena itu, kami membuat perlombaan antara dua permintaan. Acara dapat berkembang dalam dua cara dan keduanya tidak terlalu menyenangkan.


Misalnya, permintaan yang tidak relevan akan berakhir lebih awal dari permintaan saat ini dan untuk sesaat pengguna akan melihat permintaan yang tidak relevan. Tidak menyenangkan, tetapi tidak kritis.


Atau permintaan saat ini untuk menyelesaikan lebih awal dari yang tidak relevan dan setelah berkedip pengguna akan tetap dengan permintaan yang tidak relevan yang sama. Ini sudah kritis.


Tentu saja, kami bukan yang pertama kali menghadapi masalah seperti itu dan teknik yang dikenal sebagai debounce akan membantu kami - pelaksanaan tugas hanya setelah N unit waktu sejak peristiwa terakhir diterima. Ini ada sedikit materi tentang ini.


Dalam redux saga, teknik ini diimplementasikan menggunakan dua efek - delay dan takeLatest . Yang pertama menunda eksekusi saga dengan jumlah milidetik yang ditentukan. Yang kedua memotong eksekusi dari saga yang sudah berfungsi ketika sebuah acara baru tiba.


Mengetahui semua ini, kami akan menulis saga:


 import { delay } from 'redux-saga'; import { put, call, select } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { MapsActions } from '../actions/maps'; import { getPlaceSuggestions } from '../api/get-place-suggestions'; export function placeSuggestionsSaga * ({ payload: query }) { const { maps: { isApiLoaded } } = yield select(); //  API    , //      if (!isApiLoaded) { yield take(MapsActions.apiLoaded); } //     Google Places Autocomplete  store const { maps: { autocompleteService }, countryCode } = yield select(); //    , //        if (query) { yield put(PlaceActions.suggestions([])); yield put(PlaceActions.select(null)); return; } //  250    yield call(delay, 250); //  API  const suggestions = yield call( getPlaceSuggestions, autocompleteService, countryCode, query, ); //  action   const action = PlacesActions.suggestions(suggestions || []); //     store yield put(action); }; 

Seperti pada contoh sebelumnya, hanya mengikatnya ke root saga:


 import { takeLatest } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { placeSuggestionsSaga } from './place-suggestions'; export function * sagas() { yield takeLatest( PlaceActions.changeQuery, placeSuggestionsSaga, ); } 


Use case: tutup daftar drop-down yang ditulis sendiri ketika mengklik di luar wilayah kontrol.


Bahkan, ini adalah emulasi dari perilaku yang dibangun ke dalam browser pilih. Alasan mengapa Anda mungkin memerlukan daftar drop-down yang ditulis pada div akan dibiarkan untuk imajinasi pembaca.


Fitur utama dari tugas yang diselesaikan adalah lewat dari suatu peristiwa di luar kendali, misalnya, ketika mengklik di luar daftar.


Tertebak? Ya, saluran juga akan membantu kami di sini. Dengan menggunakannya, kami akan mengubah acara klik yang muncul ke atas ke tindakan yang sesuai.


Akan menyenangkan untuk memiliki metode pabrik yang membuat saluran untuk acara jendela sembarang. Dan ini dia:


 import { eventChannel } from 'redux-saga'; export function createWindowEventChannel(eventName) { const subscribe = emitter => { window.addEventListener(eventName, emitter); return () => window.removeEventListener(eventName, emitter); }; return eventChannel(subscribe); } 

Kami membuat kisah yang sangat mirip dengan contoh pertama (Anda dapat membuat metode pabrik untuk mereka jika Anda mau):


 import { take, put, call } from 'redux-saga/effects'; import { createWindowEventChannel } from '../channels/window-event'; import { DropdownActions } from '../actions/dropdown'; export function * closeDropdownsSaga() { const channel = yield call(createWindowEventChannel, 'onClick'); while (true) { const event = yield take(channel); const action = DropdownActions.closeAll(event); yield put(action(event)); } } 

Pengurangan yang tertarik akan menempatkan kontrol dalam keadaan tertutup:


 import { handleActions } from 'redux-actions'; import { DropdownActions } from '../actions/dropdown'; export const priceReducer = handleActions({ ..., [DropdownActions.closeAll]: state => ({ ...state, isOpen: false}), }, {}); 

Daftar drop-down itu sendiri harus menghentikan penyebaran acara klik pada setiap bagian internal dan mengirimkan acara penutupan ke toko sendiri. Misalnya, ketika Anda mengklik untuk membuka:


 // components/dropdown.js import React from 'react'; export class Dropdown extends React.Component { ... __open(event) { event.stopPropagation(); this.props.open(); } } // dispatchers/open-price-dropdown.js import { DropdownActions } from '../actions/dropdown'; import { PriceActions } from '../actions/price'; export const openPriceDropdownDispatcher = dispatch => () => { dispatch( DropdownActions.closeAll() ); dispatch( PriceActions.open() ); }; 

Kalau tidak, daftar itu tidak akan terbuka. Hal yang sama berlaku untuk klik saat memilih opsi.


El clasico, pasang saga:


 import { fork } from 'redux-saga/effects'; import { closeDropdownsSaga } from './close-dropdowns'; export function * sagas() { yield fork(closeDropdownsSaga); } 

Notifikasi


Use case: tampilkan pemberitahuan browser tentang ketersediaan lowongan baru, jika tab di latar belakang.


Di tab aktif, pengguna akan melihat perubahan dalam kontrol khusus dan karenanya pemberitahuan tidak sesuai. Tetapi untuk tab latar belakang mungkin berguna. Tentu saja, dengan izin dari pengguna!


Saya juga ingin mengklik notifikasi untuk membuka tab dan menampilkan lowongan baru. Jika pengguna tidak merespons, maka tutup pemberitahuan. Untuk melakukan ini, kita perlu efek lain yang berguna - ras . Ini memungkinkan Anda untuk mengatur balapan di antara beberapa efek lainnya. Dalam kebanyakan kasus, balapan digunakan untuk memberikan batas waktu untuk beberapa operasi.


Kami menghilangkan kode untuk melacak aktivitas tab, karena identitas dengan kode intersepsi klik dari contoh sebelumnya.


Kami akan menulis metode pabrik yang akan membuat saluran untuk meminta persetujuan dari pengguna untuk menerima pemberitahuan:


 import { eventChannel, END } from 'redux-saga'; export function createRequestNotificationPermissionChannel() { const subscribe = emitter => { Notification.requestPermission(permission => { emitter(permission); emitter(END); }); return () => {}; }; return eventChannel(subscribe); } 

Dalam mengejar metode pabrik lain, tetapi dengan saluran untuk menerima pemberitahuan klik:


 import { eventChannel, END } from 'redux-saga'; export function createNotificationClickChannel(notification) { const subscribe = emitter => { notification.onclick = event => { emitter(event); emitter(END); }; return () => notification.onclick = null; }; return eventChannel(subscribe); } 

Kedua saluran adalah sekali pakai dan pemotretan maksimum pada satu acara, setelah itu mereka ditutup.


Kuncinya tetap - saga dengan logika. Periksa apakah tab aktif, minta izin, buat pemberitahuan, tunggu klik atau batas waktu, tampilkan lowongan baru, aktifkan tab, lalu tutup pemberitahuan:


 import { delay } from 'redux-saga'; import { call, select, race, take } from 'redux-saga/effects'; import { createRequestNotificationPermissionChannel } from '../channels/request-notification-permission'; import { createNotificationClickChannel } from '../channels/notification-click'; import { JobsActions } from '../actions/jobs'; export function * notificationsSaga(action) { const { inFocus } = yield select(); if (inFocus) return; const permissionChannel = yield call(createRequestNotificationPermissionChannel); const permission = yield take(permissionChannel); if (permission !== 'granted') return; const notification = new Notification( `You have ${action.payload.jobs.length} new job posts`, { icon: 'assets/new-jobs.png' } ); const clickChannel = yield call(createNotificationClickChannel, notification); const { click, timeout } = yield race({ click: take(clickChannel), timeout: call(delay, 5000), }); if (click) { yield put(JobsActions.show()); window.focus(); window.scrollTo(0, 0); } notification.close(); } 

Pasang saga sebelum melakukan deteksi fitur ini:


 import { takeEvery } from 'redux-saga/effects'; import { JobsActions } from '../actions/jobs'; import { notificationsSaga } from './notifications'; export default function * sagas() { if ( 'Notification' in window && Notification.permission !== 'denied' ) { yield takeEvery(JobsActions.fresh, notificationsSaga); } } 

Bus acara global


Use case: mentransfer kategori peristiwa yang ditentukan antara toko redux.


Bus semacam itu diperlukan jika beberapa aplikasi dengan data umum ada di halaman. Pada saat yang sama, aplikasi dapat diimplementasikan secara independen satu sama lain.


Misalnya, string pencarian dengan filter dan hasil pencarian sebagai aplikasi reaksi terpisah. Saat mengganti filter, saya ingin aplikasi hasil mengetahuinya jika juga ada di halaman.


Kami menggunakan emitor acara standar:


 import EventEmmiter from 'events'; if (!window.GlobalEventBus) { window.GlobalEventBus = new EventEmmiter(); } export const globalEventBus = window.GlobalEventBus; 

EventChannel yang sudah dicintai mengubah emitor standar menjadi saluran:


 import { eventChannel } from 'redux-saga'; import { globalEventBus as bus } from '../utils/global-event-bus'; exports function createGlobalEventBusChannel() { const subscribe = emitter => { const handler = event => emitter({ ...event, external: true }); bus.on('global.event', handler); return bus.removeListener('global.event', handler); }; return eventChannel(subscribe); } 

Kisahnya cukup sederhana - kami membuat saluran dan menerima acara tanpa henti, baik internal maupun eksternal. Jika kami menerima acara internal, kirimkan ke bus, jika acara eksternal - di toko:


 import { take, put, race, call } from 'redux-saga/effects'; import { globalEventBus as bus } from '../utils/global-event-bus'; import { createGlobalEventBusChannel } from '../channels/global-event-bus'; export function * globalEventBusSaga(allowedActions) { allowedActions = allowedActions.map(x => x.toString()); const channel = yield call(createGlobalEventBusChannel); while (true) { const { local, external } = yield race({ local: take(), external: take(channel), }); if ( external && allowedActions.some(action => action === external.type) ) { yield put(external); } if ( local && !local.external && allowedActions.some(action => action === local.type) ) { bus.emit('global.event', local); } } }; 

Dan yang terakhir - menambah kisah dengan peristiwa yang diperlukan:


 import { fork } from 'redux-saga/effects'; import { globalEventBusSaga } from './global-event-bus'; import { DropdownsActions } from '../actions/dropdowns'; import { AreasActions } from '../actions/areas'; export function * sagas() { yield fork(globalEventBusSaga, [ DropdownsActions.closeAll, AreasActions.add, AreasActions.remove, ... ]); } 



Saya berharap dapat menunjukkan bahwa kisah-kisah membuat menggambarkan efek samping yang kompleks menjadi lebih mudah. Jelajahi API perpustakaan, transfer ke kasing Anda, buat pola yang kompleks dari harapan acara dan senanglah. Sampai jumpa di ruang terbuka JS!

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


All Articles