You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/th/7-bank-project/4-state-management/README.md

30 KiB

สร้างแอปธนาคาร ตอนที่ 4: แนวคิดการจัดการสถานะ

แบบทดสอบก่อนเรียน

แบบทดสอบก่อนเรียน

บทนำ

เมื่อแอปพลิเคชันเว็บเติบโตขึ้น การติดตามการไหลของข้อมูลทั้งหมดกลายเป็นเรื่องท้าทาย โค้ดใดที่ได้รับข้อมูล หน้าต่างๆ ใช้ข้อมูลอย่างไร ที่ไหนและเมื่อไหร่ที่ต้องอัปเดต...ง่ายมากที่จะทำให้โค้ดรกและยากต่อการดูแลรักษา โดยเฉพาะอย่างยิ่งเมื่อคุณต้องแชร์ข้อมูลระหว่างหน้าต่างๆ ของแอป เช่น ข้อมูลผู้ใช้ แนวคิดของ การจัดการสถานะ มีอยู่ในโปรแกรมทุกประเภทมาโดยตลอด แต่เนื่องจากแอปเว็บมีความซับซ้อนมากขึ้นเรื่อยๆ การจัดการสถานะจึงกลายเป็นจุดสำคัญที่ต้องพิจารณาในระหว่างการพัฒนา

ในส่วนสุดท้ายนี้ เราจะมาทบทวนแอปที่เราสร้างขึ้นเพื่อพิจารณาใหม่เกี่ยวกับการจัดการสถานะ โดยรองรับการรีเฟรชเบราว์เซอร์ได้ทุกเมื่อ และสามารถเก็บข้อมูลข้ามเซสชันของผู้ใช้ได้

สิ่งที่ต้องมีมาก่อน

คุณต้องทำส่วน การดึงข้อมูล ของแอปเว็บให้เสร็จสมบูรณ์ก่อนบทเรียนนี้ นอกจากนี้ คุณยังต้องติดตั้ง Node.js และ รันเซิร์ฟเวอร์ API ในเครื่องของคุณเพื่อจัดการข้อมูลบัญชี

คุณสามารถทดสอบว่าเซิร์ฟเวอร์ทำงานได้อย่างถูกต้องโดยรันคำสั่งนี้ในเทอร์มินัล:

curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result

ทบทวนการจัดการสถานะ

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

มีปัญหา 3 ข้อในโค้ดปัจจุบัน:

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

เราสามารถอัปเดตโค้ดของเราเพื่อแก้ไขปัญหาเหล่านี้ทีละข้อ แต่จะทำให้เกิดการซ้ำซ้อนของโค้ดมากขึ้น และทำให้แอปซับซ้อนและยากต่อการดูแลรักษา หรือเราสามารถหยุดคิดสักครู่และพิจารณากลยุทธ์ของเราใหม่

ปัญหาที่เราพยายามแก้ไขจริงๆ คืออะไร?

การจัดการสถานะ คือการหาวิธีที่ดีในการแก้ปัญหาเฉพาะสองข้อ:

  • จะทำให้การไหลของข้อมูลในแอปเข้าใจง่ายได้อย่างไร?
  • จะทำให้ข้อมูลสถานะสอดคล้องกับส่วนติดต่อผู้ใช้เสมอ (และในทางกลับกัน) ได้อย่างไร?

เมื่อคุณจัดการกับปัญหาเหล่านี้แล้ว ปัญหาอื่นๆ ที่คุณอาจมีอาจได้รับการแก้ไขไปแล้วหรือแก้ไขได้ง่ายขึ้น มีวิธีการมากมายในการแก้ปัญหาเหล่านี้ แต่เราจะเลือกวิธีแก้ปัญหาทั่วไปที่ประกอบด้วย การรวมศูนย์ข้อมูลและวิธีการเปลี่ยนแปลงข้อมูล การไหลของข้อมูลจะเป็นดังนี้:

แผนภาพแสดงการไหลของข้อมูลระหว่าง HTML, การกระทำของผู้ใช้ และสถานะ

เราจะไม่ครอบคลุมส่วนที่ข้อมูลกระตุ้นการอัปเดตมุมมองโดยอัตโนมัติ เนื่องจากเกี่ยวข้องกับแนวคิดขั้นสูงของ การเขียนโปรแกรมเชิงปฏิกิริยา ซึ่งเป็นหัวข้อที่ดีสำหรับการศึกษาต่อหากคุณต้องการเจาะลึก

มีไลบรารีมากมายที่มีแนวทางต่างๆ ในการจัดการสถานะ Redux เป็นตัวเลือกยอดนิยม ลองดูแนวคิดและรูปแบบที่ใช้ เนื่องจากมักเป็นวิธีที่ดีในการเรียนรู้ปัญหาที่อาจเกิดขึ้นในแอปเว็บขนาดใหญ่และวิธีแก้ไข

งาน

เราจะเริ่มต้นด้วยการปรับโครงสร้างโค้ดเล็กน้อย แทนที่การประกาศ account:

let account = null;

ด้วย:

let state = {
  account: null
};

แนวคิดคือการ รวมศูนย์ ข้อมูลแอปทั้งหมดของเราในออบเจ็กต์สถานะเดียว ตอนนี้เรามีเพียง account ในสถานะ ดังนั้นจึงไม่ได้เปลี่ยนแปลงมากนัก แต่สร้างเส้นทางสำหรับการพัฒนาในอนาคต

เรายังต้องอัปเดตฟังก์ชันที่ใช้งานมัน ในฟังก์ชัน register() และ login() ให้แทนที่ account = ... ด้วย state.account = ...;

ที่ด้านบนของฟังก์ชัน updateDashboard() ให้เพิ่มบรรทัดนี้:

const account = state.account;

การปรับโครงสร้างนี้เองไม่ได้ทำให้เกิดการปรับปรุงมากนัก แต่แนวคิดคือการวางรากฐานสำหรับการเปลี่ยนแปลงถัดไป

ติดตามการเปลี่ยนแปลงข้อมูล

ตอนนี้เราได้ตั้งค่าออบเจ็กต์ state เพื่อเก็บข้อมูลของเราแล้ว ขั้นตอนต่อไปคือการรวมศูนย์การอัปเดต เป้าหมายคือทำให้ง่ายต่อการติดตามการเปลี่ยนแปลงใดๆ และเมื่อเกิดขึ้น

เพื่อหลีกเลี่ยงการเปลี่ยนแปลงที่เกิดขึ้นกับออบเจ็กต์ state การพิจารณาให้มันเป็น immutable หรือไม่สามารถแก้ไขได้เลยก็เป็นแนวปฏิบัติที่ดี นั่นหมายความว่าคุณต้องสร้างออบเจ็กต์สถานะใหม่หากคุณต้องการเปลี่ยนแปลงอะไรในนั้น การทำเช่นนี้ช่วยป้องกัน ผลข้างเคียง ที่อาจไม่พึงประสงค์ และเปิดโอกาสสำหรับฟีเจอร์ใหม่ๆ ในแอปของคุณ เช่น การทำ undo/redo ในขณะเดียวกันก็ทำให้การดีบักง่ายขึ้น ตัวอย่างเช่น คุณสามารถบันทึกการเปลี่ยนแปลงทุกครั้งที่เกิดขึ้นกับสถานะและเก็บประวัติการเปลี่ยนแปลงเพื่อทำความเข้าใจแหล่งที่มาของบั๊ก

ใน JavaScript คุณสามารถใช้ Object.freeze() เพื่อสร้างเวอร์ชันที่ไม่สามารถแก้ไขได้ของออบเจ็กต์ หากคุณพยายามเปลี่ยนแปลงออบเจ็กต์ที่ไม่สามารถแก้ไขได้ จะเกิดข้อยกเว้นขึ้น

คุณรู้ความแตกต่างระหว่างออบเจ็กต์ที่ไม่สามารถแก้ไขได้แบบ ตื้น และ ลึก หรือไม่? คุณสามารถอ่านเพิ่มเติมได้ ที่นี่

งาน

มาสร้างฟังก์ชันใหม่ updateState():

function updateState(property, newData) {
  state = Object.freeze({
    ...state,
    [property]: newData
  });
}

ในฟังก์ชันนี้ เรากำลังสร้างออบเจ็กต์สถานะใหม่และคัดลอกข้อมูลจากสถานะก่อนหน้าโดยใช้ spread (...) operator จากนั้นเราจะเขียนทับคุณสมบัติเฉพาะของออบเจ็กต์สถานะด้วยข้อมูลใหม่โดยใช้ bracket notation [property] สำหรับการกำหนดค่า สุดท้าย เราล็อกออบเจ็กต์เพื่อป้องกันการแก้ไขโดยใช้ Object.freeze() ตอนนี้เรามีเพียงคุณสมบัติ account ที่เก็บไว้ในสถานะ แต่ด้วยวิธีนี้คุณสามารถเพิ่มคุณสมบัติได้มากเท่าที่คุณต้องการในสถานะ

เราจะอัปเดตการเริ่มต้น state เพื่อให้แน่ใจว่าสถานะเริ่มต้นถูกล็อกด้วย:

let state = Object.freeze({
  account: null
});

หลังจากนั้น อัปเดตฟังก์ชัน register โดยแทนที่ state.account = result; ด้วย:

updateState('account', result);

ทำเช่นเดียวกันกับฟังก์ชัน login โดยแทนที่ state.account = data; ด้วย:

updateState('account', data);

เราจะใช้โอกาสนี้แก้ไขปัญหาข้อมูลบัญชีที่ไม่ได้ถูกล้างเมื่อผู้ใช้คลิก ออกจากระบบ

สร้างฟังก์ชันใหม่ logout():

function logout() {
  updateState('account', null);
  navigate('/login');
}

ใน updateDashboard() แทนที่การเปลี่ยนเส้นทาง return navigate('/login'); ด้วย return logout();

ลองลงทะเบียนบัญชีใหม่ ออกจากระบบ และเข้าสู่ระบบอีกครั้งเพื่อตรวจสอบว่าทุกอย่างยังทำงานได้อย่างถูกต้อง

เคล็ดลับ: คุณสามารถดูการเปลี่ยนแปลงสถานะทั้งหมดได้โดยเพิ่ม console.log(state) ที่ด้านล่างของ updateState() และเปิดคอนโซลในเครื่องมือพัฒนาของเบราว์เซอร์ของคุณ

เก็บสถานะ

แอปเว็บส่วนใหญ่จำเป็นต้องเก็บข้อมูลเพื่อให้ทำงานได้อย่างถูกต้อง ข้อมูลสำคัญทั้งหมดมักจะถูกเก็บไว้ในฐานข้อมูลและเข้าถึงผ่านเซิร์ฟเวอร์ API เช่น ข้อมูลบัญชีผู้ใช้ในกรณีของเรา แต่บางครั้ง การเก็บข้อมูลบางอย่างในแอปฝั่งไคลเอนต์ที่ทำงานในเบราว์เซอร์ก็เป็นสิ่งที่น่าสนใจ เพื่อประสบการณ์ผู้ใช้ที่ดีขึ้นหรือเพื่อปรับปรุงประสิทธิภาพการโหลด

เมื่อคุณต้องการเก็บข้อมูลในเบราว์เซอร์ มีคำถามสำคัญบางข้อที่คุณควรถามตัวเอง:

  • ข้อมูลนี้เป็นข้อมูลที่อ่อนไหวหรือไม่? คุณควรหลีกเลี่ยงการเก็บข้อมูลที่อ่อนไหวในฝั่งไคลเอนต์ เช่น รหัสผ่านของผู้ใช้
  • คุณต้องการเก็บข้อมูลนี้ไว้นานแค่ไหน? คุณวางแผนที่จะเข้าถึงข้อมูลนี้เฉพาะในเซสชันปัจจุบันหรือคุณต้องการให้มันถูกเก็บไว้ตลอดไป?

มีหลายวิธีในการเก็บข้อมูลในแอปเว็บ ขึ้นอยู่กับสิ่งที่คุณต้องการ ตัวอย่างเช่น คุณสามารถใช้ URL เพื่อเก็บคำค้นหา และทำให้สามารถแชร์ระหว่างผู้ใช้ได้ คุณยังสามารถใช้ HTTP cookies หากข้อมูลจำเป็นต้องแชร์กับเซิร์ฟเวอร์ เช่น ข้อมูล การยืนยันตัวตน

อีกตัวเลือกหนึ่งคือการใช้หนึ่งใน API ของเบราว์เซอร์สำหรับการเก็บข้อมูล สองตัวเลือกที่น่าสนใจคือ:

  • localStorage: เป็น Key/Value store ที่ช่วยให้เก็บข้อมูลเฉพาะสำหรับเว็บไซต์ปัจจุบันข้ามเซสชันต่างๆ ข้อมูลที่บันทึกไว้ในนี้จะไม่มีวันหมดอายุ
  • sessionStorage: ทำงานเหมือนกับ localStorage ยกเว้นว่าข้อมูลที่เก็บไว้ในนี้จะถูกลบเมื่อเซสชันสิ้นสุดลง (เมื่อปิดเบราว์เซอร์)

โปรดทราบว่า API ทั้งสองนี้อนุญาตให้เก็บเฉพาะ strings หากคุณต้องการเก็บออบเจ็กต์ที่ซับซ้อน คุณจะต้องแปลงเป็นรูปแบบ JSON โดยใช้ JSON.stringify()

หากคุณต้องการสร้างแอปเว็บที่ไม่ทำงานร่วมกับเซิร์ฟเวอร์ ก็สามารถสร้างฐานข้อมูลในฝั่งไคลเอนต์โดยใช้ IndexedDB API ได้เช่นกัน API นี้เหมาะสำหรับกรณีการใช้งานขั้นสูงหรือหากคุณต้องการเก็บข้อมูลจำนวนมาก เนื่องจากมันซับซ้อนกว่าในการใช้งาน

งาน

เราต้องการให้ผู้ใช้ของเรายังคงล็อกอินอยู่จนกว่าพวกเขาจะคลิกปุ่ม ออกจากระบบ อย่างชัดเจน ดังนั้นเราจะใช้ localStorage เพื่อเก็บข้อมูลบัญชี ก่อนอื่น ให้กำหนดคีย์ที่เราจะใช้เก็บข้อมูลของเรา

const storageKey = 'savedAccount';

จากนั้นเพิ่มบรรทัดนี้ที่ท้ายฟังก์ชัน updateState():

localStorage.setItem(storageKey, JSON.stringify(state.account));

ด้วยวิธีนี้ ข้อมูลบัญชีผู้ใช้จะถูกเก็บไว้และอัปเดตอยู่เสมอ เนื่องจากเรารวมศูนย์การอัปเดตสถานะทั้งหมดไว้ก่อนหน้านี้ นี่คือจุดที่เราเริ่มได้รับประโยชน์จากการปรับโครงสร้างก่อนหน้านี้ทั้งหมด 🙂

เนื่องจากข้อมูลถูกบันทึกไว้ เราจึงต้องดูแลการกู้คืนข้อมูลเมื่อแอปโหลดขึ้นมาอีกครั้ง เนื่องจากเราจะเริ่มมีโค้ดเริ่มต้นมากขึ้น อาจเป็นความคิดที่ดีที่จะสร้างฟังก์ชันใหม่ init ซึ่งรวมถึงโค้ดก่อนหน้านี้ที่ด้านล่างของ app.js:

function init() {
  const savedAccount = localStorage.getItem(storageKey);
  if (savedAccount) {
    updateState('account', JSON.parse(savedAccount));
  }

  // Our previous initialization code
  window.onpopstate = () => updateRoute();
  updateRoute();
}

init();

ที่นี่เรากู้คืนข้อมูลที่บันทึกไว้ และหากมีข้อมูล เราจะอัปเดตสถานะตามนั้น สิ่งสำคัญคือต้องทำสิ่งนี้ ก่อน อัปเดตเส้นทาง เนื่องจากอาจมีโค้ดที่พึ่งพาสถานะระหว่างการอัปเดตหน้า

เรายังสามารถทำให้หน้า Dashboard เป็นหน้าเริ่มต้นของแอปพลิเคชันของเราได้ เนื่องจากตอนนี้เรากำลังเก็บข้อมูลบัญชีไว้แล้ว หากไม่พบข้อมูล แดชบอร์ดจะดูแลการเปลี่ยนเส้นทางไปยังหน้า Login อยู่แล้ว ใน updateRoute() แทนที่ fallback return navigate('/login'); ด้วย return navigate('/dashboard');

ตอนนี้เข้าสู่ระบบในแอปและลองรีเฟรชหน้า คุณควรอยู่ในแดชบอร์ด ด้วยการอัปเดตนี้ เราได้จัดการกับปัญหาเริ่มต้นทั้งหมดของเรา...

รีเฟรชข้อมูล

...แต่เราอาจสร้างปัญหาใหม่ขึ้นมา โอ๊ะ!

ไปที่แดชบอร์ดโดยใช้บัญชี test จากนั้นรันคำสั่งนี้ในเทอร์มินัลเพื่อสร้างธุรกรรมใหม่:

curl --request POST \
     --header "Content-Type: application/json" \
     --data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
     http://localhost:5000/api/accounts/test/transactions

ลองรีเฟรชหน้าแดชบอร์ดในเบราว์เซอร์ของคุณตอนนี้ เกิดอะไรขึ้น? คุณเห็นธุรกรรมใหม่หรือไม่?

สถานะถูกเก็บไว้อย่างไม่มีกำหนดด้วย localStorage แต่ก็หมายความว่ามันไม่เคยอัปเดตจนกว่าคุณจะออกจากระบบและเข้าสู่ระบบอีกครั้ง!

กลยุทธ์หนึ่งที่เป็นไปได้ในการแก้ไขปัญหานี้คือการโหลดข้อมูลบัญชีใหม่ทุกครั้งที่โหลดหน้าแดชบอร์ด เพื่อหลีกเลี่ยงข้อมูลที่ล้าสมัย

งาน

สร้างฟังก์ชันใหม่ updateAccountData:

async function updateAccountData() {
  const account = state.account;
  if (!account) {
    return logout();
  }

  const data = await getAccount(account.user);
  if (data.error) {
    return logout();
  }

  updateState('account', data);
}

เมธอดนี้ตรวจสอบว่าเรากำลังล็อกอินอยู่ในปัจจุบัน จากนั้นโหลดข้อมูลบัญชีใหม่จากเซิร์ฟเวอร์

สร้างฟังก์ชันอีกตัวชื่อ refresh:

async function refresh() {
  await updateAccountData();
  updateDashboard();
}

ฟังก์ชันนี้อัปเดตข้อมูลบัญชี จากนั้นดูแลการอัปเดต HTML ของหน้าแดชบอร์ด นี่คือสิ่งที่เราต้องเรียกใช้เมื่อโหลดเส้นทางแดชบอร์ด อัปเดตการกำหนดเส้นทางด้วย:

const routes = {
  '/login': { templateId: 'login' },
  '/dashboard': { templateId: 'dashboard', init: refresh }
};

ลองรีเฟรชแดชบอร์ดตอนนี้ ควรแสดงข้อมูลบัญชีที่อัปเดตแล้ว


🚀 ความท้าทาย

ตอนนี้เราโหลดข้อมูลบัญชีใหม่ทุกครั้งที่โหลดแดชบอร์ด คุณคิดว่าเรายังจำเป็นต้องเก็บ ข้อมูลบัญชีทั้งหมด ไว้อยู่หรือไม่?

ลองทำงานร่วมกันเพื่อเปลี่ยนสิ่งที่ถูกบันทึกและโหลดจาก localStorage ให้รวมเฉพาะสิ่งที่จำเป็นอย่างยิ่งสำหรับการทำงานของแอป

แบบ

ดำเนินการ "เพิ่มธุรกรรม" ในหน้าต่างการสนทนา

นี่คือตัวอย่างผลลัพธ์หลังจากทำงานเสร็จสิ้น:

ภาพหน้าจอแสดงตัวอย่างหน้าต่างการสนทนา "เพิ่มธุรกรรม"


ข้อจำกัดความรับผิดชอบ:
เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI Co-op Translator แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่แม่นยำ เอกสารต้นฉบับในภาษาต้นทางควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามนุษย์ที่เป็นมืออาชีพ เราจะไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความที่ผิดพลาดซึ่งเกิดจากการใช้การแปลนี้