คุณทำไม่ได้
แต่ ... คุณควรใช้ redux-saga :)
คำตอบของ Dan Abramov นั้นถูกต้องredux-thunk
แต่ฉันจะพูดเพิ่มเติมเกี่ยวกับredux-sagaที่ค่อนข้างคล้ายกัน แต่มีประสิทธิภาพมากกว่า
การประกาศ VS แบบบังคับ
- DOM : jQuery มีความจำเป็น / React คือการประกาศ
- Monads : IO มีความจำเป็น / ฟรีเป็นสิ่งที่ประกาศได้
- ผลกระทบ Redux :
redux-thunk
มีความจำเป็น / redux-saga
มีการประกาศ
เมื่อคุณมีมืออันธพาลเช่นไอโอ monad หรือสัญญาคุณไม่สามารถรู้ได้อย่างง่ายดายว่าจะทำอย่างไรเมื่อคุณดำเนินการ วิธีเดียวที่จะทดสอบอันธพาลคือดำเนินการและเยาะเย้ยดิสแพตเชอร์ (หรือโลกภายนอกทั้งหมดถ้ามันโต้ตอบกับสิ่งอื่น ๆ อีกมากมาย ... )
หากคุณใช้ mocks แสดงว่าคุณไม่ได้เขียนโปรแกรมใช้งานได้
หากมองผ่านเลนส์ของผลข้างเคียง mocks คือธงที่รหัสของคุณไม่บริสุทธิ์และในสายตาของโปรแกรมเมอร์ที่ใช้งานได้พิสูจน์ว่ามีบางอย่างผิดปกติ แทนที่จะดาวน์โหลดห้องสมุดเพื่อช่วยให้เราตรวจสอบภูเขาน้ำแข็งไม่เป็นอันตรายเราควรล่องเรือรอบ ๆ คนที่แต่งตัวประหลาด TDD / Java ไม่ยอมใครง่ายๆเคยถามฉันว่าคุณล้อเลียนใน Clojure อย่างไร คำตอบคือเรามักจะไม่ทำ เรามักจะเห็นว่ามันเป็นสัญญาณที่เราจำเป็นต้อง refactor รหัสของเรา
แหล่ง
sagas (ตามที่พวกเขานำมาใช้redux-saga
) เป็นสิ่งที่ประกาศได้และเช่นเดียวกับส่วนประกอบของ monad หรือ React พวกเขาทดสอบได้ง่ายกว่าโดยไม่มีการจำลองใด ๆ
ดูบทความนี้ :
ใน FP ที่ทันสมัยเราไม่ควรเขียนโปรแกรม - เราควรเขียนคำอธิบายของโปรแกรมซึ่งเราสามารถวิปัสสนา, แปลงร่างและตีความตามความประสงค์
(ที่จริงแล้ว Redux-saga เป็นเหมือนลูกผสม: การไหลเป็นสิ่งจำเป็น แต่ผลที่ได้คือการประกาศ)
ความสับสน: การกระทำ / เหตุการณ์ / คำสั่ง ...
มีความสับสนมากมายในโลกหน้าว่าแนวคิดแบ็คเอนด์เช่น CQRS / EventSourcing และ Flux / Redux อาจเกี่ยวข้องกันส่วนใหญ่เป็นเพราะใน Flux เราใช้คำว่า "การกระทำ" ซึ่งบางครั้งอาจเป็นตัวแทนของรหัสบังคับ ( LOAD_USER
) และเหตุการณ์ ( USER_LOADED
) ฉันเชื่อว่าเช่นเดียวกับการจัดหากิจกรรมคุณควรจัดส่งกิจกรรมเท่านั้น
การใช้ sagas ในทางปฏิบัติ
ลองนึกภาพแอปที่มีลิงก์ไปยังโปรไฟล์ผู้ใช้ วิธีการใช้สำนวนนี้ในการจัดการกับมิดเดิลแวร์แต่ละอันคือ:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
เทพนิยายนี้แปลเป็น:
ทุกครั้งที่มีการคลิกชื่อผู้ใช้ดึงโปรไฟล์ผู้ใช้แล้วส่งเหตุการณ์ด้วยโปรไฟล์ที่โหลด
redux-saga
ที่คุณสามารถดูมีประโยชน์บางส่วนของ
การtakeLatest
อนุญาตให้ใช้เพื่อแสดงว่าคุณสนใจที่จะรับข้อมูลชื่อผู้ใช้ล่าสุดที่คลิกเท่านั้น (จัดการปัญหาที่เกิดขึ้นพร้อมกันในกรณีที่ผู้ใช้คลิกเร็วมากในชื่อผู้ใช้จำนวนมาก) สิ่งของประเภทนี้มีความแข็งมาก คุณสามารถใช้takeEvery
ถ้าคุณไม่ต้องการพฤติกรรมนี้
คุณทำให้ผู้สร้างแอ็คชั่นบริสุทธิ์ โปรดทราบว่ามันยังมีประโยชน์ในการรักษา actionCreators (ใน sagas put
และส่วนประกอบdispatch
) เนื่องจากมันอาจช่วยให้คุณเพิ่มการตรวจสอบการกระทำ (ยืนยัน / ไหล / typescript) ในอนาคต
รหัสของคุณสามารถทดสอบได้มากขึ้นเนื่องจากเอฟเฟกต์เป็นสิ่งที่เปิดเผยได้
คุณไม่จำเป็นอีกต่อไปที่ทริกเกอร์ actions.loadUser()
RPC-เช่นสายเช่น UI ของคุณเพียงต้องการส่งสิ่งที่เกิดขึ้น เราเพียง แต่จัดกิจกรรม (ในช่วงเวลาที่ผ่านมา!) และไม่ใช่การกระทำอีกต่อไป ซึ่งหมายความว่าคุณสามารถสร้าง"เป็ด"หรือบริบทที่ถูกผูกมัดแบบแยกส่วนและเทพนิยายสามารถทำหน้าที่เป็นจุดเชื่อมต่อระหว่างส่วนประกอบแบบแยกส่วนเหล่านี้
ซึ่งหมายความว่ามุมมองของคุณง่ายต่อการจัดการเพราะพวกเขาไม่ต้องการที่จะมีเลเยอร์การแปลนั้นระหว่างสิ่งที่เกิดขึ้นกับสิ่งที่ควรจะเกิดขึ้น
ตัวอย่างเช่นลองนึกภาพมุมมองการเลื่อนที่ไม่มีที่สิ้นสุด CONTAINER_SCROLLED
สามารถนำไปสู่NEXT_PAGE_LOADED
แต่เป็นความรับผิดชอบจริง ๆ ของคอนเทนเนอร์ที่เลื่อนได้เพื่อตัดสินใจว่าเราควรโหลดหน้าอื่นหรือไม่ จากนั้นเขาต้องระวังสิ่งที่ซับซ้อนมากขึ้นเช่นโหลดหน้าสุดท้ายสำเร็จหรือไม่หรือมีหน้าเว็บที่พยายามโหลดหรือไม่มีรายการเหลือให้โหลดหรือไม่ ฉันไม่คิดอย่างนั้น: เพื่อให้สามารถนำกลับมาใช้ใหม่ได้สูงสุดคอนเทนเนอร์แบบเลื่อนได้ควรอธิบายว่ามันถูกเลื่อน การโหลดหน้าเว็บเป็น "ผลกระทบทางธุรกิจ" ของการเลื่อนนั้น
บางคนอาจแย้งว่าเครื่องกำเนิดไฟฟ้าสามารถซ่อนสถานะนอกร้าน redux ด้วยตัวแปรท้องถิ่นได้ แต่ถ้าคุณเริ่มดัดแปลงสิ่งที่ซับซ้อนภายใน thunks โดยเริ่มตัวนับ ฯลฯ คุณจะมีปัญหาเดียวกันอยู่แล้ว และมีselect
ผลกระทบที่ตอนนี้อนุญาตให้รับสถานะจากร้านค้า Redux ของคุณ
Sagas สามารถเดินทางข้ามเวลาและยังช่วยให้การบันทึกการไหลที่ซับซ้อนและเครื่องมือ dev ที่กำลังทำงานอยู่ นี่คือบันทึกการไหลของ async แบบง่าย ๆ ที่มีการใช้งานแล้ว:
decoupling
Sagas ไม่เพียง แต่แทนที่ redux thunks พวกเขามาจากแบ็กเอนด์ / กระจายระบบ / จัดหาเหตุการณ์
มันเป็นความเข้าใจผิดที่พบบ่อยมากที่ sagas เป็นเพียงที่นี่เพื่อแทนที่ thux ของคุณด้วยการทดสอบที่ดีขึ้น จริงๆแล้วนี่เป็นเพียงรายละเอียดการดำเนินการของ redux-saga การใช้เอฟเฟกต์การประกาศดีกว่าชุดทดสอบที่ใช้ได้ แต่รูปแบบการผจญภัยสามารถนำไปใช้กับโค้ดที่จำเป็นหรือที่เปิดเผยได้
ในตอนแรกซากะเป็นซอฟต์แวร์ที่อนุญาตให้ประสานงานธุรกรรมที่ดำเนินการมายาวนาน (ความสอดคล้องในที่สุด) และการทำธุรกรรมในบริบทที่แตกต่างกัน
เพื่อทำให้โลกนี้ง่ายขึ้นให้จินตนาการว่ามี widget1 และ widget2 เมื่อคลิกปุ่มบน widget1 นั้นจะมีผลกับ widget2 แทนที่จะเชื่อมต่อ 2 วิดเจ็ตเข้าด้วยกัน (เช่น widget1 ส่งการกระทำที่กำหนดเป้าหมาย widget2), widget1 จะส่งเฉพาะปุ่มที่คลิก จากนั้นเทพนิยายจะฟังปุ่มนี้คลิกจากนั้นอัปเดต widget2 โดยแยกเหตุการณ์ใหม่ที่ widget2 รับรู้
สิ่งนี้เป็นการเพิ่มระดับของการอ้อมที่ไม่จำเป็นสำหรับแอพที่ง่าย แต่ทำให้ง่ายต่อการปรับขนาดแอพพลิเคชั่นที่ซับซ้อน ตอนนี้คุณสามารถเผยแพร่ widget1 และ widget2 ไปยังที่เก็บ npm ที่แตกต่างกันเพื่อให้พวกเขาไม่ต้องรู้เกี่ยวกับกันและกันโดยไม่ต้องให้พวกเขาแชร์การลงทะเบียนระดับโลกของการกระทำ วิดเจ็ต 2 ตอนนี้ถูกผูกไว้กับบริบทที่สามารถอยู่แยกกันได้ พวกเขาไม่ต้องการให้แต่ละคนมีความสอดคล้องกันและสามารถนำกลับมาใช้ในแอปอื่น ๆ ได้เช่นกัน เทพนิยายเป็นจุดเชื่อมต่อระหว่างสองวิดเจ็ตที่ประสานงานพวกมันอย่างมีความหมายสำหรับธุรกิจของคุณ
บทความดีๆเกี่ยวกับวิธีการจัดโครงสร้างแอป Redux ของคุณซึ่งคุณสามารถใช้ Redux-saga ได้ด้วยเหตุผล decoupling:
usecase ที่เป็นรูปธรรม: ระบบแจ้งเตือน
ฉันต้องการให้ส่วนประกอบของฉันสามารถเรียกการแสดงการแจ้งเตือนในแอพได้ แต่ฉันไม่ต้องการให้ส่วนประกอบของฉันเชื่อมโยงกับระบบการแจ้งเตือนที่มีกฎเกณฑ์ทางธุรกิจของตัวเองสูง (การแจ้งเตือนสูงสุด 3 รายการปรากฏขึ้นในเวลาเดียวกันการจัดคิวการแจ้งเตือนเวลาแสดงผล 4 วินาทีเป็นต้น ... )
ฉันไม่ต้องการให้คอมโพเนนต์ JSX ตัดสินใจว่าการแจ้งเตือนจะแสดง / ซ่อนเมื่อใด ฉันแค่ให้ความสามารถในการร้องขอการแจ้งเตือนและปล่อยให้กฎที่ซับซ้อนภายในเทพนิยาย สิ่งของประเภทนี้ค่อนข้างยากที่จะนำไปใช้กับอุโมงค์หรือสัญญา
ฉันอธิบายแล้ว ที่นี่ว่าสิ่งนี้สามารถทำได้กับซากะ
ทำไมถึงเรียกว่าซากะ?
เทพนิยายเป็นคำที่มาจากโลกแบ็กเอนด์ ตอนแรกฉันแนะนำ Yassine (ผู้แต่ง Redux-saga) กับคำนั้นในการสนทนาที่ยาวนานการอภิปรายยาว
เริ่มแรกคำนี้ถูกนำเสนอด้วยกระดาษรูปแบบเทพนิยายที่ควรจะใช้ในการจัดการความสอดคล้องในที่สุดในการทำธุรกรรมกระจาย แต่การใช้งานได้รับการขยายไปยังคำจำกัดความที่กว้างขึ้นโดยนักพัฒนาแบ็กเอนด์เพื่อให้ตอนนี้ก็ครอบคลุม pattern (รูปแบบ saga ดั้งเดิมเป็นรูปแบบเฉพาะของตัวจัดการกระบวนการ)
วันนี้คำว่า "เทพนิยาย" กำลังสับสนเพราะมันสามารถอธิบายสิ่งต่าง ๆ ได้ 2 อย่าง เนื่องจากมันถูกใช้ใน redux-saga มันไม่ได้อธิบายถึงวิธีการจัดการธุรกรรมแบบกระจาย แต่เป็นวิธีการประสานการทำงานในแอปของคุณ redux-saga
อาจถูกเรียกredux-process-manager
อีกอย่างว่า
ดูสิ่งนี้ด้วย:
ทางเลือก
หากคุณไม่ชอบความคิดในการใช้เครื่องกำเนิดไฟฟ้า แต่คุณสนใจรูปแบบการผจญภัยและคุณสมบัติการแยกชิ้นส่วนคุณสามารถบรรลุผลเช่นเดียวกันกับredux-observableซึ่งใช้ชื่อepic
เพื่ออธิบายรูปแบบเดียวกัน แต่ด้วย RxJS หากคุณคุ้นเคยกับ Rx แล้วคุณจะรู้สึกเหมือนอยู่บ้าน
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
ทรัพยากรที่มีประโยชน์ redux-saga
2017 ให้คำแนะนำ
- อย่าใช้ Redux-saga มากเกินไปเพียงเพื่อประโยชน์ในการใช้งานเท่านั้น การเรียกใช้ API ที่ทดสอบได้เท่านั้นไม่คุ้มค่า
- อย่าลบ thunks ออกจากโครงการของคุณสำหรับกรณีที่ง่ายที่สุด
- อย่าลังเลที่จะส่ง thunks
yield put(someActionThunk)
ถ้ามันสมเหตุสมผล
หากคุณกลัวที่จะใช้ Redux-saga (หรือ Redux-observable) แต่ต้องการรูปแบบ decoupling ให้ตรวจสอบredux-dispatch-สมัครสมาชิก : มันอนุญาตให้ฟัง dispatches และทริกเกอร์ dispatches ใหม่ในผู้ฟัง
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});