การสร้างไลบรารีการตรวจสอบ React ของคุณเอง: พื้นฐาน (ตอนที่ 1)

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ เคยสงสัยหรือไม่ว่าไลบรารีตรวจสอบความถูกต้องทำงานอย่างไร? บทความนี้จะบอกวิธีสร้างไลบรารีตรวจสอบความถูกต้องของคุณเองสำหรับ React ทีละขั้นตอน ส่วนต่อไปจะเพิ่มคุณสมบัติขั้นสูงเพิ่มเติม และส่วนสุดท้ายจะเน้นที่การปรับปรุงประสบการณ์ของนักพัฒนา

ฉันเคยคิดว่าไลบรารีตรวจสอบแบบฟอร์มนั้นค่อนข้างเจ๋ง ฉันรู้ว่ามันเป็นความสนใจเฉพาะกลุ่ม แต่เราใช้มันมาก! อย่างน้อยในงานของฉัน สิ่งที่ฉันทำส่วนใหญ่คือการสร้างแบบฟอร์มที่ซับซ้อนไม่มากก็น้อยด้วยกฎการตรวจสอบที่ขึ้นอยู่กับตัวเลือกและเส้นทางก่อนหน้า การทำความเข้าใจว่าไลบรารีการตรวจสอบความถูกต้องของฟอร์มทำงานอย่างไรเป็นสิ่งสำคัญยิ่ง

ปีที่แล้วฉันเขียนห้องสมุดตรวจสอบแบบฟอร์มดังกล่าว ฉันตั้งชื่อมันว่า "การสอบเทียบ" และคุณสามารถอ่านโพสต์บล็อกแนะนำที่นี่ เป็นห้องสมุดที่ดีที่มีความยืดหยุ่นสูงและใช้วิธีที่แตกต่างจากที่อื่นในตลาดเล็กน้อย มีห้องสมุดที่ยอดเยี่ยมอื่น ๆ อีกมากมาย - ของฉันทำงานได้ดีสำหรับ ความ ต้องการของเรา

วันนี้ ผมจะแสดงวิธีเขียน ไลบรารีตรวจสอบความถูกต้องของคุณเอง สำหรับ 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: ประสบการณ์