[{"data":1,"prerenderedAt":530},["ShallowReactive",2],{"insight-\u002Finsights\u002Fpermissions-are-not-a-final-step":3,"insight-related-\u002Finsights\u002Fpermissions-are-not-a-final-step":164},{"id":4,"title":5,"body":6,"category":152,"date":153,"description":154,"draft":155,"extension":156,"meta":157,"navigation":158,"path":159,"readingTime":160,"seo":161,"stem":162,"__hash__":163},"insights\u002Finsights\u002Fpermissions-are-not-a-final-step.md","權限不是上線前才補的東西",{"type":7,"value":8,"toc":145},"minimark",[9,13,16,20,23,30,36,42,45,48,51,54,111,114,120,123,126,129,132,135,138,141],[10,11,12],"p",{},"在系統開發裡，權限常常被排在很後面。先把畫面做出來、先讓流程跑得動，權限「之後再加」。",[10,14,15],{},"聽起來很合理，但實際上這個順序有一個問題：權限不是一層可以外掛的東西，它會決定資料怎麼查、怎麼存、怎麼分頁。等到功能都做完才回頭處理，通常會發現要改的不是幾行判斷，而是整個資料存取的寫法。",[17,18,19],"h2",{"id":19},"三個不同層次的權限",[10,21,22],{},"把權限分開來想，會比較容易決定每一層要做什麼。",[10,24,25,29],{},[26,27,28],"strong",{},"介面層","：這個按鈕要不要顯示。這是最表面的一層，也是唯一一層可以被使用者繞過的。",[10,31,32,35],{},[26,33,34],{},"應用層","：這個請求可不可以執行。伺服器收到請求時要自己判斷，不能相信前端已經檢查過。",[10,37,38,41],{},[26,39,40],{},"資料層","：這筆資料可不可以被讀到。資料庫本身也應該有規則，這樣即使應用層有漏洞，也不會直接把整張表倒出去。",[10,43,44],{},"只做第一層，等於沒有做。三層都做，才有辦法在某一層出錯的時候還有東西擋著。",[17,46,47],{"id":47},"從最小權限開始",[10,49,50],{},"比較安全的預設是：一開始什麼都不給，再依角色一項一項加回來。",[10,52,53],{},"反過來做（先全部開放，再逐一關掉）幾乎一定會漏掉東西，而且漏掉的地方通常不會有人回報，因為使用者不會抱怨「我看到了不該看的資料」。",[55,56,61],"pre",{"className":57,"code":58,"language":59,"meta":60,"style":60},"language-sql shiki shiki-themes github-light github-light","-- 不是「這個角色不能看什麼」，\n-- 而是「這個角色只能看什麼」\nselect * from orders\nwhere store_id = current_store_id();\n","sql","",[62,63,64,73,79,96],"code",{"__ignoreMap":60},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"sJ8bj","-- 不是「這個角色不能看什麼」，\n",[65,74,76],{"class":67,"line":75},2,[65,77,78],{"class":71},"-- 而是「這個角色只能看什麼」\n",[65,80,82,86,89,92],{"class":67,"line":81},3,[65,83,85],{"class":84},"sCydW","select",[65,87,88],{"class":84}," *",[65,90,91],{"class":84}," from",[65,93,95],{"class":94},"sKWpL"," orders\n",[65,97,99,102,105,108],{"class":67,"line":98},4,[65,100,101],{"class":84},"where",[65,103,104],{"class":94}," store_id ",[65,106,107],{"class":84},"=",[65,109,110],{"class":94}," current_store_id();\n",[17,112,113],{"id":113},"前端隱藏不是安全機制",[10,115,116,119],{},[62,117,118],{},"v-if=\"isAdmin\""," 只是讓畫面乾淨，不是保護。任何人打開開發者工具都可以看到前端拿到了哪些資料。",[10,121,122],{},"判斷方式很簡單：如果把這個請求直接用 curl 打一次，會發生什麼事？如果答案是「會拿到資料」，那這個地方就沒有真的被保護。",[17,124,125],{"id":125},"環境也是權限的一部分",[10,127,128],{},"正式環境的金鑰不應該出現在開發環境，開發用的測試帳號也不應該出現在正式資料庫。這兩件事聽起來很基本，但在時間趕的時候特別容易被跳過。",[10,130,131],{},"比較實際的做法是一開始就把環境變數分好，讓「拿錯金鑰」這件事在流程上就做不到，而不是靠記得。",[17,133,134],{"id":134},"什麼時候處理",[10,136,137],{},"我們的習慣是在畫資料表的時候就一起決定：這張表有哪些角色會碰到、各自能做什麼。",[10,139,140],{},"這個階段做這件事幾乎不花時間，因為還沒有任何程式碼需要改。等到上線前一週才想，成本就完全不一樣了。",[142,143,144],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sCydW, html code.shiki .sCydW{--shiki-default:#D73A49;--shiki-dark:#D73A49}html pre.shiki code .sKWpL, html code.shiki .sKWpL{--shiki-default:#24292E;--shiki-dark:#24292E}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":60,"searchDepth":81,"depth":81,"links":146},[147,148,149,150,151],{"id":19,"depth":75,"text":19},{"id":47,"depth":75,"text":47},{"id":113,"depth":75,"text":113},{"id":125,"depth":75,"text":125},{"id":134,"depth":75,"text":134},"資安","2026-08-05","「先把功能做出來，權限之後再說」是很常見的順序，但它幾乎一定會讓你在最後一週重寫一次資料存取邏輯。",false,"md",{},true,"\u002Finsights\u002Fpermissions-are-not-a-final-step",7,{"title":5,"description":154},"insights\u002Fpermissions-are-not-a-final-step","zWtmc13aqFoDwMCfylZRS7XYckT8GRrG34i03fYoBkQ",[165,280],{"id":166,"title":167,"body":168,"category":271,"date":272,"description":273,"draft":155,"extension":156,"meta":274,"navigation":158,"path":275,"readingTime":276,"seo":277,"stem":278,"__hash__":279},"insights\u002Finsights\u002Fprocess-before-features.md","先問流程，再問功能",{"type":7,"value":169,"toc":265},[170,173,176,179,184,187,192,195,200,203,206,209,222,225,228,231,234,237,240,243,247,250,259,262],[10,171,172],{},"第一次和企業聊系統的時候，最常拿到的是一份功能清單。上面寫著「要有登入」「要有報表」「要有通知」。清單本身沒有問題，問題是它通常是從別人的系統抄來的，而不是從自己的流程長出來的。",[10,174,175],{},"所以我們現在習慣先把功能清單放在旁邊，改問另一組問題。",[17,177,178],{"id":178},"先問的三件事",[10,180,181],{},[26,182,183],{},"這件事現在是誰在做？",[10,185,186],{},"不是「哪個部門」，是哪一個人。找到那個人，通常也就找到了所有的例外狀況。",[10,188,189],{},[26,190,191],{},"他現在用什麼做？",[10,193,194],{},"答案很常是 Excel、LINE、紙本，或是三個一起。這不是落後，而是資訊。它代表現有工具至少在某個地方是好用的，不然不會活這麼久。",[10,196,197],{},[26,198,199],{},"做錯的時候會怎樣？",[10,201,202],{},"有些環節做錯只是重做一次，有些做錯會直接影響出貨或收錢。這個問題的答案，會決定哪裡需要防呆、哪裡可以放寬。",[17,204,205],{"id":205},"功能清單的問題",[10,207,208],{},"功能清單描述的是系統，流程描述的是人。兩者之間往往差了一段沒有人記錄下來的東西：",[210,211,212,216,219],"ul",{},[213,214,215],"li",{},"那個「先傳照片到群組給主管看一下」的步驟",[213,217,218],{},"那個「星期五下午單子會特別多」的節奏",[213,220,221],{},"那個「新人前兩週都會做錯同一件事」的現象",[10,223,224],{},"這些東西不會出現在需求文件裡，但它們決定了系統上線之後會不會被繞過。",[17,226,227],{"id":227},"一個實際的例子",[10,229,230],{},"有一種很常見的需求是「我要一個報表系統」。",[10,232,233],{},"如果直接照做，結果通常是一個有很多圖表的頁面，然後三個月後沒有人打開。",[10,235,236],{},"但如果先問「你做這份報表是為了回答什麼問題」，答案可能是「我只是想知道這週有沒有哪一天特別忙」。",[10,238,239],{},"那麼需要的可能不是報表系統，而是一個每週寄一次、只有五行字的信。",[10,241,242],{},"這兩件事的開發成本差了不只十倍，而後者比較有機會真的被看。",[17,244,246],{"id":245},"流程問清楚之後功能反而好決定","流程問清楚之後，功能反而好決定",[10,248,249],{},"把流程攤開之後，功能清單通常會發生兩件事：",[251,252,253,256],"ol",{},[213,254,255],{},"有一半的功能會被刪掉，因為它們解決的是不存在的問題。",[213,257,258],{},"會冒出兩三個原本沒寫的功能，而且它們才是真正的重點。",[10,260,261],{},"這個過程不需要很久。多數情況下，把一天的實際工作走過一次，大概兩個小時就夠了。",[10,263,264],{},"比起花兩個月做一個沒人用的系統，這兩個小時很便宜。",{"title":60,"searchDepth":81,"depth":81,"links":266},[267,268,269,270],{"id":178,"depth":75,"text":178},{"id":205,"depth":75,"text":205},{"id":227,"depth":75,"text":227},{"id":245,"depth":75,"text":246},"企業數位化","2026-08-12","很多系統做完之後沒人用，不是因為功能不夠，而是因為它從一開始就照著功能清單設計，而不是照著人實際的工作方式設計。",{},"\u002Finsights\u002Fprocess-before-features",6,{"title":167,"description":273},"insights\u002Fprocess-before-features","7-zsY3oWaYKvoW50feBNsUNNqp91NdKpd-dtowIB1rU",{"id":281,"title":282,"body":283,"category":522,"date":523,"description":524,"draft":155,"extension":156,"meta":525,"navigation":158,"path":526,"readingTime":396,"seo":527,"stem":528,"__hash__":529},"insights\u002Finsights\u002Fhow-much-javascript-does-a-website-need.md","一個企業網站到底需要多少 JavaScript",{"type":7,"value":284,"toc":515},[285,288,291,294,297,300,303,307,310,321,324,327,330,473,480,483,486,489,500,503,506,509,512],[10,286,287],{},"做企業形象網站的時候，一個很值得先問的問題是：這個網站真正需要「互動」的地方有幾個？",[10,289,290],{},"答案通常是三個以內：導覽選單、捲動進場、還有一兩個展開收合。",[10,292,293],{},"其他部分基本上是文字、圖片與連結。",[17,295,296],{"id":296},"那為什麼還要用框架",[10,298,299],{},"因為維護。同一份導覽列要出現在八個頁面上，用元件寫一次比複製八次好；SEO 的 metadata 要跟著頁面走，用框架處理比手動維護八份 HTML 好。",[10,301,302],{},"框架的價值在於開發與維護方式，不在於執行時要送多少 JavaScript 到瀏覽器。這兩件事其實可以分開。",[17,304,306],{"id":305},"靜態產生-少量-hydration","靜態產生 + 少量 hydration",[10,308,309],{},"比較舒服的組合是：",[210,311,312,315,318],{},[213,313,314],{},"頁面在建置階段就產生好 HTML，訪客第一次連進來拿到的就是完整內容",[213,316,317],{},"需要互動的元件才在瀏覽器端接手",[213,319,320],{},"純展示的區塊不需要任何 JavaScript",[10,322,323],{},"這樣做的好處很直接：第一次載入快、SEO 沒有問題、即使 JavaScript 載入失敗，內容仍然看得到。",[17,325,326],{"id":326},"動畫的部分",[10,328,329],{},"大部分進場動畫都可以用 CSS 完成：",[55,331,335],{"className":332,"code":333,"language":334,"meta":60,"style":60},"language-css shiki shiki-themes github-light github-light","@media (prefers-reduced-motion: no-preference) {\n  [data-reveal] {\n    opacity: 0;\n    transform: translateY(18px);\n  }\n  [data-reveal].is-visible {\n    opacity: 1;\n    transform: none;\n    transition: opacity .85s, transform .85s;\n  }\n}\n","css",[62,336,337,345,357,372,394,400,415,426,438,462,467],{"__ignoreMap":60},[65,338,339,342],{"class":67,"line":68},[65,340,341],{"class":84},"@media",[65,343,344],{"class":94}," (prefers-reduced-motion: no-preference) {\n",[65,346,347,350,354],{"class":67,"line":75},[65,348,349],{"class":94},"  [",[65,351,353],{"class":352},"se37E","data-reveal",[65,355,356],{"class":94},"] {\n",[65,358,359,363,366,369],{"class":67,"line":81},[65,360,362],{"class":361},"sMN4m","    opacity",[65,364,365],{"class":94},": ",[65,367,368],{"class":361},"0",[65,370,371],{"class":94},";\n",[65,373,374,377,379,382,385,388,391],{"class":67,"line":98},[65,375,376],{"class":361},"    transform",[65,378,365],{"class":94},[65,380,381],{"class":361},"translateY",[65,383,384],{"class":94},"(",[65,386,387],{"class":361},"18",[65,389,390],{"class":84},"px",[65,392,393],{"class":94},");\n",[65,395,397],{"class":67,"line":396},5,[65,398,399],{"class":94},"  }\n",[65,401,402,404,406,409,412],{"class":67,"line":276},[65,403,349],{"class":94},[65,405,353],{"class":352},[65,407,408],{"class":94},"]",[65,410,411],{"class":352},".is-visible",[65,413,414],{"class":94}," {\n",[65,416,417,419,421,424],{"class":67,"line":160},[65,418,362],{"class":361},[65,420,365],{"class":94},[65,422,423],{"class":361},"1",[65,425,371],{"class":94},[65,427,429,431,433,436],{"class":67,"line":428},8,[65,430,376],{"class":361},[65,432,365],{"class":94},[65,434,435],{"class":361},"none",[65,437,371],{"class":94},[65,439,441,444,447,450,453,456,458,460],{"class":67,"line":440},9,[65,442,443],{"class":361},"    transition",[65,445,446],{"class":94},": opacity ",[65,448,449],{"class":361},".85",[65,451,452],{"class":84},"s",[65,454,455],{"class":94},", transform ",[65,457,449],{"class":361},[65,459,452],{"class":84},[65,461,371],{"class":94},[65,463,465],{"class":67,"line":464},10,[65,466,399],{"class":94},[65,468,470],{"class":67,"line":469},11,[65,471,472],{"class":94},"}\n",[10,474,475,476,479],{},"JavaScript 只需要負責一件事：判斷元素進到畫面了沒有。一個 ",[62,477,478],{},"IntersectionObserver"," 就夠了，不需要動畫函式庫。",[10,481,482],{},"真正需要引入動畫套件的情況是時間軸控制、複雜的序列或是與捲動位置綁定的連續動畫。如果只是淡入，那大概是殺雞用牛刀。",[17,484,485],{"id":485},"一個判斷標準",[10,487,488],{},"在加入任何函式庫之前，可以先問：",[251,490,491,494,497],{},[213,492,493],{},"這個功能沒有它做不到嗎？",[213,495,496],{},"它會增加多少載入量？",[213,498,499],{},"三年後還會有人維護它嗎？",[10,501,502],{},"三題裡面有兩題答不出來，通常就不該加。",[17,504,505],{"id":505},"這不是效能潔癖",[10,507,508],{},"網站慢一點，多數訪客不會抱怨，他們只會離開，而你不會知道。",[10,510,511],{},"而且載入量這件事有個特性：它只會往上長，不會自己變少。每個「先加進來再說」的套件，最後都會變成沒有人敢刪的東西。所以在一開始少加一點，會比之後再來瘦身容易很多。",[142,513,514],{},"html pre.shiki code .sCydW, html code.shiki .sCydW{--shiki-default:#D73A49;--shiki-dark:#D73A49}html pre.shiki code .sKWpL, html code.shiki .sKWpL{--shiki-default:#24292E;--shiki-dark:#24292E}html pre.shiki code .se37E, html code.shiki .se37E{--shiki-default:#6F42C1;--shiki-dark:#6F42C1}html pre.shiki code .sMN4m, html code.shiki .sMN4m{--shiki-default:#005CC5;--shiki-dark:#005CC5}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":60,"searchDepth":81,"depth":81,"links":516},[517,518,519,520,521],{"id":296,"depth":75,"text":296},{"id":305,"depth":75,"text":306},{"id":326,"depth":75,"text":326},{"id":485,"depth":75,"text":485},{"id":505,"depth":75,"text":505},"技術選型","2026-07-22","大部分形象網站真正需要互動的地方不超過三個，但它們常常載入了一整套前端框架與動畫函式庫。",{},"\u002Finsights\u002Fhow-much-javascript-does-a-website-need",{"title":282,"description":524},"insights\u002Fhow-much-javascript-does-a-website-need","R08ApjvjFxM1RjX6ESvKnDm_yfO4t9R9OVIfygDAcS0",1787664334202]