[{"data":1,"prerenderedAt":556},["ShallowReactive",2],{"blog-posts":3},[4,152],{"id":5,"title":6,"body":7,"description":128,"extension":129,"meta":130,"navigation":140,"ogImage":141,"path":143,"robots":144,"schemaOrg":145,"seo":148,"sitemap":149,"stem":150,"__hash__":151},"content\u002Fblog\u002Ffather-to-son.md","沒有一定要用Pinia? 簡單說對資料管理的理解",{"type":8,"value":9,"toc":120},"minimark",[10,19,23,38,42,57,68,72,83,97,107,110,113],[11,12,13,14,18],"p",{},"先打個預防針，本文是建立在",[15,16,17],"strong",{},"基礎不足及可能沒有搞懂工具的目的","產生的錯誤用法，請斟酌壓抑怒火XD",[20,21,22],"h2",{"id":22},"前言",[11,24,25,26,29,30,33,34,37],{},"會有這個體悟是來自於在做 side project 時知道 ",[15,27,28],{},"tanstack query"," 這個工具後，對於",[15,31,32],{},"非同步的資料處理","有了進一步的認識後，也讓我想到，以前是否都",[15,35,36],{},"搞錯Pinia的使用方式","呢?",[20,39,41],{"id":40},"原本我的使用方式","原本我的使用方式?",[11,43,44,45,48,49,52,53,56],{},"原本在寫 Vue 時，會",[15,46,47],{},"無腦的導入後並將各模組所需要的資料、方法做包裝進 store","，完全沒有考慮到",[15,50,51],{},"這些資料是不是其他非父子模組的地方也需要","，只覺得這樣包起來很有規矩(?\n認知提升(?\n當認知稍微提升之後想到，",[15,54,55],{},"既然我沒有其他模組要用，為什麼不用 props 傳進去就好?"," 幹嘛寫一個store 增加維護成本跟記憶體負擔?",[11,58,59,60,63,64,67],{},"還有一點是，如果資料來源是透過 API 拿到的，",[15,61,62],{},"我該怎麼保證拿到的資料都是最新的?"," ",[15,65,66],{},"無法掌握資料更新的地方就是個風險","，變成每次其實還是要打一次API去確保拿到的資料是最新的。",[20,69,71],{"id":70},"後來我怎麼做","後來我怎麼做?",[11,73,74,75,78,79,82],{},"當有了上面的想法後，我思考",[15,76,77],{},"是不是我原本對於模組資料傳遞的設計就是錯誤的","，所以開始在每次開發前",[15,80,81],{},"先著墨在資料傳遞上而不是一昧的寫程式","。",[11,84,85,86,89,90,93,94,82],{},"假設我很明確的知道A、B模組互為父子模組，那就",[15,87,88],{},"在父模組宣告變數即可，子模組需要什麼傳下去即可","。\n",[15,91,92],{},"可以控管資料來源是單一的","，也因為其他模組根本也沒有要使用到，",[15,95,96],{},"等卸載組件後就釋出記憶體了",[11,98,99,100,103,104,82],{},"也意識到",[15,101,102],{},"既然在沒有做快取的情況下，那不同模組拿相同資料就直接打API拿就好","，根本也不用特別存起來，",[15,105,106],{},"如此一來也不用擔心重新整理後資料被洗掉、資料是否不同步的情況",[11,108,109],{},"當然這時知道有個工具叫 tanstack query 可以做這件事情可能有更好的處理方式又是另外一回事了。",[20,111,112],{"id":112},"結語",[11,114,115,116,119],{},"從這件事情後我學到一件事情，",[15,117,118],{},"永遠要清楚知道自己使用的工具設計的目的是什麼","XD (還有要讀官方文件)",{"title":121,"searchDepth":122,"depth":122,"links":123},"",2,[124,125,126,127],{"id":22,"depth":122,"text":22},{"id":40,"depth":122,"text":41},{"id":70,"depth":122,"text":71},{"id":112,"depth":122,"text":112},"反思是否濫用工具或是思考是否因為工具而固化。","md",{"date":131,"category":132,"tags":133,"headline":135,"author":136,"datePublished":139},"2026-07-25","Frontend",[134,132],"Vue3","前端資料管理",{"@type":137,"name":138},"Person","Jamespig","206-07-25",true,{"url":142},"https:\u002F\u002Fimage.jamespig.com\u002FFather-To-Son.png","\u002Fblog\u002Ffather-to-son",null,[146],{"@type":147},"TechArticle",{"title":6,"description":128},{"loc":143,"lastmod":131},"blog\u002Ffather-to-son","JRQrHI8KTMzlnVInxrZYPvYdzDveWpkOc3y4pLw4Egk",{"id":153,"title":154,"body":155,"description":540,"extension":129,"meta":541,"navigation":140,"ogImage":544,"path":546,"robots":144,"schemaOrg":547,"seo":552,"sitemap":553,"stem":554,"__hash__":555},"content\u002Fblog\u002Frefactor-multistep-form.md","重構多步驟表單流程：從 Pinia 管理到局部狀態綁定的實踐",{"type":8,"value":156,"toc":530},[157,160,163,166,169,185,189,249,252,255,258,261,268,277,425,429,455,474,515,517,526],[11,158,159],{},"在開發互動式網頁應用程式時，我們經常會遇到需要分步執行的複雜表單（例如：建立商店、安裝流程等）。這類功能通常包含多個步驟，每個步驟的資料可能互相依賴，且允許使用者暫時儲存或中途跳離。",[11,161,162],{},"在面對這類需求時，我們應該如何設計狀態與資料流，才能在擴充與維護之間取得平衡呢？",[11,164,165],{},"本文分享我遇到的情況及作法。",[20,167,168],{"id":168},"原始架構回顧與痛點",[11,170,171,172,176,177,180,181,184],{},"在最初的開發中，原作者採用了集中式的狀態管理。主要結構是一個主視窗檔案（",[173,174,175],"code",{},"x.vue","），並搭配一個主 Store（",[173,178,179],{},"x.store.js","）以及五個對應步驟的 Store（",[173,182,183],{},"step1~5.store.js","）。",[186,187,188],"h3",{"id":188},"原作者的處理方式與設計邏輯",[190,191,192,223,233,236],"ul",{},[193,194,195,196,199,200,203,204,207,208,211,212,214,215,218,219,222],"li",{},"集中式初始化與賦值：初始化時，呼叫 ",[173,197,198],{},"API"," 取得所有表單的初始值並存入 ",[15,201,202],{},"主 Store"," 。接著透過 ",[173,205,206],{},"watch"," 監聽 ",[15,209,210],{},"currentStep（當前步驟)"," 的變化。當使用者切換步驟時，會自動將 ",[15,213,202],{}," 裡的狀態，直接賦值給對應 ",[15,216,217],{},"子步驟的 Store","（ ",[15,220,221],{},"不透過內部 action 更新"," ）。",[193,224,225,226,229,230,232],{},"跨步驟資料傳遞：若 Step 1 填寫的資料需要帶入 Step 2，則預先在 ",[173,227,228],{},"step2.store.js"," 中宣告承接欄位，並在 ",[173,231,206],{}," 切換步驟時，將資料從 Step 1 的狀態傳遞過去。",[193,234,235],{},"流程控制：使用陣列記錄步驟順序，並根據陣列的 Index 來判斷目前處於哪個步驟。",[193,237,238,239,242,243,246,247,82],{},"業務邏輯處理：各步驟的 ",[15,240,241],{},"驗證規則"," 與 ",[15,244,245],{},"提交"," 都封裝在個別的 Store 裡面；圖片上傳則是在使用者操作當下直接呼叫 ",[173,248,198],{},[186,250,251],{"id":251},"痛點分析",[11,253,254],{},"雖然這種方式將各步驟的邏輯模組化，但隨著步驟增加，全域狀態與監聽器（watch）的交互會變得相當複雜。",[11,256,257],{},"當我們需要追蹤特定資料的變動來源時，容易產生過高的認知負擔，且隱式的狀態賦值容易造成非預期的副作用。",[20,259,260],{"id":260},"重構後的架構與設計",[11,262,263,264,267],{},"為了讓架構更輕量且易於追蹤，我捨棄了不需要全域共享的 Store 機制，改為在主檔案中統一管理狀態，並透過 ",[173,265,266],{},"v-model"," 將資料雙向綁定至各個子組件。\n重構後的核心程式碼結構",[269,270,275],"pre",{"className":271,"code":273,"language":274},[272],"language-text","\u003Ctemplate>\n  \u003CStep1\n    v-if=\"currentStep === 'S1'\"\n    ref=\"componentRef\"\n    :initial-data=\"getInitialData()\"\n    v-model=\"step1Data\"\n  \u002F>\n  \u003CStep2\n    v-if=\"currentStep === 'S2'\"\n    ref=\"componentRef\"\n    :initial-data=\"getInitialData()\"\n    v-model=\"step2Data\"\n  \u002F>\n\u003C\u002Ftemplate>\n","text",[173,276,273],{"__ignoreMap":121},[269,278,282],{"className":279,"code":280,"language":281,"meta":121,"style":121},"language-javaScript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { ref, reactive, computed } from 'vue';\n\n\u002F\u002F 宣告所有表單所需資料\nconst componentRef = ref(null);\nconst xData = reactive({}); \nconst step1Data = reactive({});\nconst step2Data = reactive({});\n\n\u002F\u002F 根據後端資料或初始狀態計算出流程\nconst stepFlow = computed(() => {\n  \u002F\u002F 處理流程判斷邏輯\n});\n\n\u002F\u002F 整理每個表單所需的初始值\nconst getInitialData = (stepName) => {\n  \u002F\u002F 回傳初始化資料\n};\n\nconst prevStep = () => { \u002F* 切換至上一步 *\u002F };\nconst nextStep = async () => {\n  if (componentRef.value?.submitForm) {\n    await componentRef.value.submitForm();\n  }\n};\n","javaScript",[173,283,284,292,297,303,309,315,321,327,332,338,344,350,356,361,367,373,379,385,390,396,402,408,414,420],{"__ignoreMap":121},[285,286,289],"span",{"class":287,"line":288},"line",1,[285,290,291],{},"import { ref, reactive, computed } from 'vue';\n",[285,293,294],{"class":287,"line":122},[285,295,296],{"emptyLinePlaceholder":140},"\n",[285,298,300],{"class":287,"line":299},3,[285,301,302],{},"\u002F\u002F 宣告所有表單所需資料\n",[285,304,306],{"class":287,"line":305},4,[285,307,308],{},"const componentRef = ref(null);\n",[285,310,312],{"class":287,"line":311},5,[285,313,314],{},"const xData = reactive({}); \n",[285,316,318],{"class":287,"line":317},6,[285,319,320],{},"const step1Data = reactive({});\n",[285,322,324],{"class":287,"line":323},7,[285,325,326],{},"const step2Data = reactive({});\n",[285,328,330],{"class":287,"line":329},8,[285,331,296],{"emptyLinePlaceholder":140},[285,333,335],{"class":287,"line":334},9,[285,336,337],{},"\u002F\u002F 根據後端資料或初始狀態計算出流程\n",[285,339,341],{"class":287,"line":340},10,[285,342,343],{},"const stepFlow = computed(() => {\n",[285,345,347],{"class":287,"line":346},11,[285,348,349],{},"  \u002F\u002F 處理流程判斷邏輯\n",[285,351,353],{"class":287,"line":352},12,[285,354,355],{},"});\n",[285,357,359],{"class":287,"line":358},13,[285,360,296],{"emptyLinePlaceholder":140},[285,362,364],{"class":287,"line":363},14,[285,365,366],{},"\u002F\u002F 整理每個表單所需的初始值\n",[285,368,370],{"class":287,"line":369},15,[285,371,372],{},"const getInitialData = (stepName) => {\n",[285,374,376],{"class":287,"line":375},16,[285,377,378],{},"  \u002F\u002F 回傳初始化資料\n",[285,380,382],{"class":287,"line":381},17,[285,383,384],{},"};\n",[285,386,388],{"class":287,"line":387},18,[285,389,296],{"emptyLinePlaceholder":140},[285,391,393],{"class":287,"line":392},19,[285,394,395],{},"const prevStep = () => { \u002F* 切換至上一步 *\u002F };\n",[285,397,399],{"class":287,"line":398},20,[285,400,401],{},"const nextStep = async () => {\n",[285,403,405],{"class":287,"line":404},21,[285,406,407],{},"  if (componentRef.value?.submitForm) {\n",[285,409,411],{"class":287,"line":410},22,[285,412,413],{},"    await componentRef.value.submitForm();\n",[285,415,417],{"class":287,"line":416},23,[285,418,419],{},"  }\n",[285,421,423],{"class":287,"line":422},24,[285,424,384],{},[186,426,428],{"id":427},"架構解析重構亮點與狀態機的關聯","架構解析：重構亮點與狀態機的關聯",[430,431,432],"ol",{},[193,433,434,435,438,439,442,443,454],{},"狀態機（Finite State Machine, FSM）概念的引入",[436,437],"br",{},"\n在重構後的架構中，我 ",[15,440,441],{},"捨棄了依賴陣列索引（Index）"," 的控制方式，改為使用與後端配合的 ",[15,444,445,446,449,450,453],{},"字串標識（如 ",[173,447,448],{},"S1",", ",[173,451,452],{},"S2"," 等）"," 來標示步驟。\n這種做法與有限狀態機（FSM）的概念不謀而合：",[190,456,457,465],{},[193,458,459,460,462,463,82],{},"明確的狀態節點：系統只會處於明確定義的節點（State）中。例如 ",[173,461,448],{},"、",[173,464,452],{},[193,466,467,468,470,471,473],{},"受控的轉換：流程控制只允許合法的轉換發生（例如從 ",[173,469,448],{}," 進入 ",[173,472,452],{},"）。這避免了因陣列順序變動或索引計算錯誤而產生的非預期狀態，大幅降低了除錯的難度。",[430,475,476,488,509],{"start":122},[193,477,478,479,481,482,242,485,487],{},"移除不必要的全域狀態管理",[436,480],{},"\n當資料不需要跨頁面共享時，引入全域 Store 反而會增加耦合度。改用 ",[173,483,484],{},"reactive",[173,486,266],{}," 後，資料流向變得非常明確，開發者能直覺地掌握哪個變數在何處被修改。",[193,489,490,491,493,494,496,497,500,501,504,505,508],{},"職責分離與單一職責原則",[436,492],{},"\n我刻意避免在主檔案 ",[173,495,175],{}," 中處理各步驟的細節邏輯。 ",[15,498,499],{},"主檔案只專注於流程控制（下一步、上一步、暫存狀態）"," ，而表單的 ",[15,502,503],{},"內部驗證與細部處理則由各子組件負責","。\n子組件統一向外暴露（expose）",[173,506,507],{},"submitForm"," 方法，讓父層只需呼叫該方法即可觸發驗證或請求，無須判斷當前步驟的型態。",[193,510,511,512,514],{},"優化圖片上傳體驗",[436,513],{},"\n在原架構中，圖片會在操作當下直接上傳，如果使用者中途取消或未完成流程，可能會產生未被使用的孤立檔案。新做法改為在最終按下「送出」時統一上傳，確保資料的完整性，也省去處理取消上傳（abort）的額外負擔。",[20,516,112],{"id":112},[11,518,519,520,522,523,525],{},"這次重構的目的是為了讓程式碼更具可讀性與可維護性。",[436,521],{},"\n透過將局部邏輯交還給各個步驟組件、統一表單提交行為，並結合狀態機的設計思維，我成功降低了多步驟表單的維護成本。",[436,524],{},"\n在此記錄自己的處理方法，希望這個實踐能為未來的我及觀看者處理類似複雜表單時提供一些靈感！",[527,528,529],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":121,"searchDepth":122,"depth":122,"links":531},[532,536,539],{"id":168,"depth":122,"text":168,"children":533},[534,535],{"id":188,"depth":299,"text":188},{"id":251,"depth":299,"text":251},{"id":260,"depth":122,"text":260,"children":537},[538],{"id":427,"depth":299,"text":428},{"id":112,"depth":122,"text":112},"將依賴 Pinia 的多步驟表單重構，提升程式碼的可讀性與職責清晰度。",{"date":542,"category":132,"tags":543},"2026-05-07",[134,132],{"url":545},"https:\u002F\u002Fimage.jamespig.com\u002FMulti-Step-Form.png","\u002Fblog\u002Frefactor-multistep-form",[548],{"@type":147,"headline":549,"author":550,"datePublished":551},"多步驟表單流程",{"@type":137,"name":138},"206-05-07",{"title":154,"description":540},{"loc":546,"lastmod":131},"blog\u002Frefactor-multistep-form","FfqCH8PmhpdnkhaHDhO53cRe8XKwu3RqS4IxWrS8WJQ",1785052461533]