การสร้างไลบรารีการตรวจสอบ React ของคุณเอง: พื้นฐาน (ตอนที่ 1)
เผยแพร่แล้ว: 2022-03-10ฉันเคยคิดว่าไลบรารีตรวจสอบแบบฟอร์มนั้นค่อนข้างเจ๋ง ฉันรู้ว่ามันเป็นความสนใจเฉพาะกลุ่ม แต่เราใช้มันมาก! อย่างน้อยในงานของฉัน สิ่งที่ฉันทำส่วนใหญ่คือการสร้างแบบฟอร์มที่ซับซ้อนไม่มากก็น้อยด้วยกฎการตรวจสอบที่ขึ้นอยู่กับตัวเลือกและเส้นทางก่อนหน้า การทำความเข้าใจว่าไลบรารีการตรวจสอบความถูกต้องของฟอร์มทำงานอย่างไรเป็นสิ่งสำคัญยิ่ง
ปีที่แล้วฉันเขียนห้องสมุดตรวจสอบแบบฟอร์มดังกล่าว ฉันตั้งชื่อมันว่า "การสอบเทียบ" และคุณสามารถอ่านโพสต์บล็อกแนะนำที่นี่ เป็นห้องสมุดที่ดีที่มีความยืดหยุ่นสูงและใช้วิธีที่แตกต่างจากที่อื่นในตลาดเล็กน้อย มีห้องสมุดที่ยอดเยี่ยมอื่น ๆ อีกมากมาย - ของฉันทำงานได้ดีสำหรับ ความ ต้องการของเรา
วันนี้ ผมจะแสดงวิธีเขียน ไลบรารีตรวจสอบความถูกต้องของคุณเอง สำหรับ React เราจะดำเนินการทีละขั้นตอน และคุณจะพบตัวอย่าง CodeSandbox เมื่อเราดำเนินการ ในตอนท้ายของบทความนี้ คุณจะรู้วิธีเขียนไลบรารีตรวจสอบความถูกต้องของคุณเอง หรืออย่างน้อยก็มีความเข้าใจอย่างลึกซึ้งว่าไลบรารีอื่นๆ นำ "เวทมนตร์แห่งการตรวจสอบ" ไปใช้อย่างไร
- ตอนที่ 1: พื้นฐาน
- ส่วนที่ 2: คุณสมบัติ
- ตอนที่ 3: ประสบการณ์
ขั้นตอนที่ 1: การออกแบบ API
ขั้นตอนแรกของการสร้างห้องสมุดคือการออกแบบว่าจะใช้งานอย่างไร มันวางรากฐานสำหรับงานจำนวนมากที่จะเกิดขึ้น และในความคิดของฉัน มันเป็นการตัดสินใจที่สำคัญที่สุดเพียงอย่างเดียวที่คุณจะทำในห้องสมุดของคุณ
การสร้าง API ที่ “ใช้งานง่าย” เป็นสิ่งสำคัญ แต่ยังมีความยืดหยุ่นเพียงพอที่จะทำให้มีการปรับปรุงในอนาคตและกรณีการใช้งานขั้นสูง เราจะพยายามบรรลุเป้าหมายทั้งสองนี้
เราจะสร้าง hook แบบกำหนดเองที่จะยอมรับวัตถุการกำหนดค่าเดียว ซึ่งจะทำให้ตัวเลือกในอนาคตสามารถส่งผ่านได้โดยไม่ทำให้เกิดการเปลี่ยนแปลงที่แตกหัก
หมายเหตุเกี่ยวกับตะขอ
Hooks เป็นวิธีการใหม่ในการเขียน React หากคุณเคยเขียน React มาก่อน คุณอาจจำแนวคิดเหล่านี้บางส่วนไม่ได้ ในกรณีนั้น โปรดดูเอกสารที่เป็นทางการ มันเขียนได้ดีอย่างไม่น่าเชื่อ และนำคุณผ่านพื้นฐานที่คุณต้องรู้
เราจะเรียก hook useValidation
แบบกำหนดเองของเราในตอนนี้ การใช้งานอาจมีลักษณะดังนี้:
const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);
ออบเจ็กต์ config
ยอมรับ fields
prop ซึ่งตั้งค่ากฎการตรวจสอบสำหรับแต่ละฟิลด์ นอกจากนี้ยังรับโทรกลับเมื่อส่งแบบฟอร์ม
วัตถุ fields
มีคีย์สำหรับแต่ละฟิลด์ที่เราต้องการตรวจสอบ แต่ละฟิลด์มีการกำหนดค่าของตัวเอง โดยที่แต่ละคีย์คือชื่อเครื่องมือตรวจสอบ และแต่ละค่าเป็นคุณสมบัติการกำหนดค่าสำหรับตัวตรวจสอบความถูกต้องนั้น อีกวิธีในการเขียนแบบเดียวกันก็คือ:
{ fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }
useValidation
hook ของเราจะส่งคืนอ็อบเจ็กต์ที่มีคุณสมบัติบางอย่าง — getFieldProps
, getFormProps
และ errors
สองฟังก์ชันแรกคือสิ่งที่ Kent C. Dodds เรียกว่า "prop getters" (ดูที่นี่สำหรับบทความที่ยอดเยี่ยมเกี่ยวกับสิ่งเหล่านี้) และใช้เพื่อรับอุปกรณ์ที่เกี่ยวข้องสำหรับฟิลด์แบบฟอร์มที่กำหนดหรือแท็กแบบฟอร์ม errors
prop เป็นอ็อบเจ็กต์ที่มีข้อความแสดงข้อผิดพลาดใด ๆ คีย์ต่อฟิลด์
การใช้งานนี้จะมีลักษณะดังนี้:
const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };
เอาล่ะ! ดังนั้นเราจึงจับ API
- ดูการสาธิต CodeSandbox
โปรดทราบว่าเราได้สร้างการใช้งานจำลองของ useValidation
hook ด้วยเช่นกัน สำหรับตอนนี้ เป็นเพียงการส่งคืนอ็อบเจ็กต์ที่มีอ็อบเจ็กต์และฟังก์ชันที่เราต้องการให้อยู่ที่นั่น ดังนั้นเราจึงไม่ทำลายตัวอย่างการใช้งานของเรา
การจัดเก็บสถานะแบบฟอร์ม
สิ่งแรกที่เราต้องทำคือจัดเก็บสถานะแบบฟอร์มทั้งหมดไว้ใน hook ที่เรากำหนดเอง เราจำเป็นต้องจำค่าของแต่ละฟิลด์ ข้อความแสดงข้อผิดพลาด และการส่งแบบฟอร์มหรือไม่ เราจะใช้เบ็ด useReducer
สำหรับสิ่งนี้ เนื่องจากจะช่วยให้มีความยืดหยุ่นมากที่สุด (และต้นแบบน้อยกว่า) หากคุณเคยใช้ Redux คุณจะเห็นแนวคิดที่คุ้นเคย และถ้าไม่เคย เราจะอธิบายต่อไป! เราจะเริ่มด้วยการเขียนตัวลดซึ่งถูกส่งไปยัง useReducer
hook:
const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }
ตัวลดคืออะไร?
ตัวลดคือฟังก์ชันที่ยอมรับออบเจกต์ของค่าและ "การกระทำ" และส่งคืนอ็อบเจ็กต์ค่าเวอร์ชันเสริม
การดำเนินการเป็นอ็อบเจ็กต์ JavaScript ธรรมดาที่มีคุณสมบัติ type
เรากำลังใช้คำสั่ง switch
เพื่อจัดการกับการกระทำที่เป็นไปได้แต่ละประเภท
"วัตถุของค่า" มักถูกเรียกว่า state และในกรณีของเรา มันคือสถานะของตรรกะการตรวจสอบความถูกต้องของเรา
สถานะของเราประกอบด้วยข้อมูลสามส่วน — values
(ค่าปัจจุบันของฟิลด์แบบฟอร์มของเรา), errors
(ชุดข้อความแสดงข้อผิดพลาดปัจจุบัน) และแฟ isSubmitted
ระบุว่าแบบฟอร์มของเราถูกส่งอย่างน้อยหนึ่งครั้งหรือไม่
ในการจัดเก็บสถานะแบบฟอร์มของเรา เราจำเป็นต้องใช้ส่วนน้อยของ useValidation
hook ของเรา เมื่อเราเรียกใช้เมธอด getFieldProps
เราจำเป็นต้องส่งคืนอ็อบเจ็กต์ด้วยค่าของฟิลด์นั้น ตัวจัดการการเปลี่ยนแปลงเมื่อมีการเปลี่ยนแปลง และชื่อ prop เพื่อติดตามว่าฟิลด์ใดเป็นฟิลด์ใด
function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };
ตอนนี้เมธอด getFieldProps
จะส่งคืนอุปกรณ์ประกอบฉากที่จำเป็นสำหรับแต่ละฟิลด์ เมื่อมีการเริ่มเหตุการณ์การเปลี่ยนแปลง เราตรวจสอบให้แน่ใจว่าฟิลด์นั้นอยู่ในการกำหนดค่าการตรวจสอบความถูกต้องของเรา จากนั้นจึงแจ้งให้ตัวลดของเราทราบถึงการดำเนินการ change
ที่เกิดขึ้น ตัวลดจะจัดการการเปลี่ยนแปลงสถานะการตรวจสอบ
- ดูการสาธิต CodeSandbox
การตรวจสอบแบบฟอร์มของเรา
ไลบรารีการตรวจสอบแบบฟอร์มของเราดูดี แต่ไม่ได้ทำอะไรมากในแง่ของการตรวจสอบค่าแบบฟอร์มของเรา! มาแก้ไขกันเถอะ
เราจะตรวจสอบฟิลด์ทั้งหมดในทุกเหตุการณ์การเปลี่ยนแปลง นี่อาจฟังดูไม่ค่อยมีประสิทธิภาพนัก แต่ในแอปพลิเคชันในโลกแห่งความเป็นจริงที่ฉันเจอ มันไม่ได้เป็นปัญหาจริงๆ
หมายเหตุ เราไม่ได้บอกว่าคุณต้องแสดงทุกข้อผิดพลาดในทุกการเปลี่ยนแปลง เราจะทบทวนวิธีแสดงข้อผิดพลาดเฉพาะเมื่อคุณส่งหรือออกจากช่องข้อมูลในบทความนี้
วิธีเลือกฟังก์ชันตัวตรวจสอบความถูกต้อง
เมื่อพูดถึงเครื่องมือตรวจสอบความถูกต้อง มีห้องสมุดมากมายที่ใช้วิธีการตรวจสอบความถูกต้องทั้งหมดที่คุณต้องการ คุณสามารถเขียนของคุณเองได้หากต้องการ เป็นการออกกำลังกายที่สนุก!
สำหรับโปรเจ็กต์นี้ เราจะใช้ชุดเครื่องมือตรวจสอบที่ฉันเขียนไว้เมื่อนานมาแล้ว — calidators
เครื่องมือตรวจสอบเหล่านี้มี API ต่อไปนี้:
function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;
กล่าวคือแต่ละเครื่องมือตรวจสอบยอมรับวัตถุการกำหนดค่าและส่งคืนเครื่องมือตรวจสอบที่กำหนดค่าอย่างสมบูรณ์ เมื่อฟังก์ชัน นั้น ถูกเรียกใช้ด้วยค่า ฟังก์ชันจะส่งกลับค่า prop ของ message
หากค่านั้นไม่ถูกต้อง หรือ null
หากค่านั้นถูกต้อง คุณสามารถดูวิธีใช้งานเครื่องมือตรวจสอบความถูกต้องเหล่านี้ได้โดยดูจากซอร์สโค้ด
ในการเข้าถึงเครื่องมือตรวจสอบเหล่านี้ ให้ติดตั้งแพ็คเกจตัววัดด้วย npm install calidators
calidators
ตรวจสอบฟิลด์เดียว
จำการกำหนดค่าที่เราส่งผ่านไปยังวัตถุ useValidation
ของเราได้หรือไม่ ดูเหมือนว่านี้:
{ fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }
เพื่อทำให้การใช้งานของเราง่ายขึ้น สมมติว่าเรามีฟิลด์เดียวเท่านั้นที่จะตรวจสอบ เราจะตรวจสอบแต่ละคีย์ของออบเจ็กต์การกำหนดค่าของฟิลด์ และเรียกใช้เครื่องมือตรวจสอบทีละรายการจนกว่าเราจะพบข้อผิดพลาดหรือทำการตรวจสอบเสร็จสิ้น
import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }
ที่นี่ เราได้เขียนฟังก์ชัน validateField
ซึ่งยอมรับค่าที่จะตรวจสอบและกำหนดค่าตัวตรวจสอบความถูกต้องสำหรับฟิลด์นั้น เราวนรอบตัวตรวจสอบทั้งหมด ส่งต่อการกำหนดค่าสำหรับตัวตรวจสอบนั้น และเรียกใช้ หากเราได้รับข้อความแสดงข้อผิดพลาด เราจะข้ามเครื่องมือตรวจสอบที่เหลือและส่งคืน ถ้าไม่ เราลองใช้ตัวตรวจสอบความถูกต้องถัดไป
หมายเหตุ: บนเครื่องมือตรวจสอบ APIs
หากคุณเลือกเครื่องมือตรวจสอบความถูกต้องที่แตกต่างกันด้วย API ที่แตกต่างกัน (เช่น validator.js
ยอดนิยม) ส่วนนี้ของโค้ดอาจดูแตกต่างออกไปเล็กน้อย อย่างไรก็ตาม เพื่อความกระชับ เราปล่อยให้ส่วนนั้นเป็นแบบฝึกหัดที่เหลือสำหรับผู้อ่าน
หมายเหตุ: เปิดสำหรับ…ในลูป
ไม่เคยใช้ for...in
loop มาก่อน? ไม่เป็นไร นี่เป็นครั้งแรกของฉันด้วย! โดยทั่วไป มันจะวนซ้ำปุ่มในวัตถุ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับพวกเขาได้ที่ MDN
ตรวจสอบทุกช่อง
ตอนนี้เราได้ตรวจสอบฟิลด์หนึ่งฟิลด์แล้ว เราควรตรวจสอบฟิลด์ทั้งหมดได้โดยไม่มีปัญหามากเกินไป
function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }
เราได้เขียนฟังก์ชัน validateFields
ที่ยอมรับค่าฟิลด์ทั้งหมดและการกำหนดค่าฟิลด์ทั้งหมด เราวนซ้ำแต่ละชื่อฟิลด์ในการกำหนดค่าและตรวจสอบฟิลด์นั้นด้วยออบเจกต์การกำหนดค่าและค่าของมัน
ถัดไป: บอกตัวลดของเรา
เอาล่ะ ตอนนี้เรามีฟังก์ชันนี้ที่ตรวจสอบข้อมูลทั้งหมดของเราแล้ว มาดึงมันเข้าไปในโค้ดที่เหลือของเรากันเถอะ!
ขั้นแรก เราจะเพิ่มตัวจัดการการดำเนินการ validate
สอบความถูกต้องใน validationReducer
ของเรา
function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }
เมื่อใดก็ตามที่เราทริกเกอร์การดำเนินการ validate
เราจะแทนที่ข้อผิดพลาดในรัฐของเราด้วยสิ่งที่ส่งผ่านควบคู่ไปกับการดำเนินการ
ต่อไป เราจะเรียกใช้ตรรกะการตรวจสอบของเราจากเบ็ด useEffect
:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
เบ็ด useEffect
นี้ทำงานทุกครั้งที่มีการเปลี่ยนแปลง state.fields
หรือ config.fields
ของเรา นอกเหนือจากการเมานต์ครั้งแรก
ระวังแมลง
มีข้อบกพร่องที่ละเอียดอ่อนมากในโค้ดด้านบน เราได้ระบุว่า useEffect
hook ของเราควรรันใหม่ทุกครั้งที่ state.fields
หรือ config.fields
เปลี่ยนไปเท่านั้น ปรากฎว่า “การเปลี่ยนแปลง” ไม่ได้แปลว่าการเปลี่ยนแปลงมูลค่าเสมอไป! useEffect
ใช้ Object.is
เพื่อรับรองความเท่าเทียมกันระหว่างวัตถุ ซึ่งจะใช้ความเท่าเทียมกันในการอ้างอิง นั่นคือ — หากคุณส่งผ่านวัตถุใหม่ที่มีเนื้อหาเดียวกัน วัตถุนั้นจะไม่เหมือนเดิม (เนื่องจากตัววัตถุนั้นเป็นของใหม่)
state.fields
ถูกส่งคืนจาก useReducer
ซึ่งรับประกันความเท่าเทียมกันในการอ้างอิงนี้ แต่การกำหนด config
ของเรามีการระบุแบบอินไลน์ในองค์ประกอบฟังก์ชันของเรา นั่นหมายความว่าอ็อบเจกต์จะถูกสร้างขึ้นใหม่ทุกครั้งที่เรนเดอร์ ซึ่งจะทริกเกอร์ useEffect
ด้านบน!
เพื่อแก้ปัญหานี้ เราจำเป็นต้องใช้ไลบรารี use-deep-compare-effect
โดย Kent C. Dodds คุณติดตั้งด้วย npm install use-deep-compare-effect
และแทนที่ useEffect
call ของคุณด้วยสิ่งนี้แทน สิ่งนี้ทำให้แน่ใจว่าเราทำการตรวจสอบความเท่าเทียมกันอย่างลึกซึ้ง แทนที่จะตรวจสอบความเท่าเทียมกันโดยอ้างอิง
รหัสของคุณจะมีลักษณะดังนี้:
import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };
หมายเหตุเกี่ยวกับผลการใช้งาน
กลายเป็นว่า useEffect
เป็นฟังก์ชันที่น่าสนใจทีเดียว Dan Abramov เขียนบทความยาว ๆ ที่ดีมากเกี่ยวกับความซับซ้อนของ useEffect
การใช้งาน หากคุณสนใจที่จะเรียนรู้ทั้งหมดที่มีเกี่ยวกับเบ็ดนี้
ตอนนี้สิ่งต่าง ๆ เริ่มดูเหมือนห้องสมุดตรวจสอบแล้ว!
- ดูการสาธิต CodeSandbox
การจัดการการส่งแบบฟอร์ม
ส่วนสุดท้ายของไลบรารีการตรวจสอบความถูกต้องของแบบฟอร์มพื้นฐานคือการจัดการสิ่งที่เกิดขึ้นเมื่อเราส่งแบบฟอร์ม ตอนนี้ มันโหลดหน้าซ้ำ และไม่มีอะไรเกิดขึ้น นั่นไม่เหมาะสม เราต้องการป้องกันพฤติกรรมเริ่มต้นของเบราว์เซอร์เมื่อพูดถึงแบบฟอร์ม และจัดการเองแทน เราวางตรรกะนี้ไว้ในฟังก์ชัน getter ของ getFormProps
:
const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };
เราเปลี่ยนฟังก์ชัน getFormProps
เพื่อส่งคืนฟังก์ชัน onSubmit
ซึ่งจะทริกเกอร์ทุกครั้งที่มีทริกเกอร์เหตุการณ์ DOM การ submit
เราป้องกันพฤติกรรมเริ่มต้นของเบราว์เซอร์ ส่งการดำเนินการเพื่อบอกตัวลดของเราว่าเราส่ง และโทรกลับ onSubmit
ที่ให้มาพร้อมกับสถานะทั้งหมด - หากมีให้
สรุป
เราอยู่ที่นั่น! เราได้สร้างไลบรารีตรวจสอบความถูกต้องที่ใช้งานง่ายและยอดเยี่ยม ยังมีงานอีกมากที่ต้องทำก่อนที่เราจะสามารถครองอินเตอร์เว็บได้
- ตอนที่ 1: พื้นฐาน
- ส่วนที่ 2: คุณสมบัติ
- ตอนที่ 3: ประสบการณ์