2023年9月13日 星期三

react-router一般加載與懶加載

 在原先規劃系統設計是去讀src/pages/裡面有index.jsx?的頁面,搞得跟next.js類似

事實上這樣也不難,在vite.config.js這樣寫就好

import { defineConfig } from "vite"
import { GlobSync } from "glob"

export default defineConfig(({ mode }) => {
return {
......
define: {
__PAGES_ROUTES__: GlobSync("src/pages/**/index.{js,jsx}").found.map((x) => x.replace(/src\/pages\/(.*)\/index.jsx?/, "$1")),
},
}
})


 import loadable from "@loadable/component"
使用loadable進行懶加載
 ......
        __PAGES_ROUTES__.map((route, i) => {
const Component = loadable(() => import(`./pages/${route}`), {
     fallback: <LayoutPage center={() => ""} />,
})
return <Route path={route} key={i} element={<Component />} />
})

但今天收到一個難題需求,老闆希望把懶加載拔掉,減短切換頁的等待時間,因為不可能把Component在後端讀好傳到前端來,所以上面那方法就作廢,但總不會要我把上百個Component import進來吧,後來想到vite有附glob給我,稍微研究一下改成以下

const modules = import.meta.glob("./pages/**/index.{js,jsx}", {
import: "default",
eager: true,
})
......
    Object.keys(modules).map((route, i) => {
const path = route.replace(/\.\/pages\/(.*)\/index.jsx?/, "$1")
const Component = modules[route]
return <Route path={path} key={i} element={<Component />} />
})}

這樣改唯一的問題是,yarn dev第一次進入頁面等待時間會比較久一點,之後改動要等編譯時間也會久一點。當然在build的客戶端是沒有差的,是開發上的爽度差別而已
補充:用判斷讓import.meta.glob不要執行也沒用,他是屬於編譯層級的,所以一定會跑,要快只能註解掉
然後不能有在./pages/...../index.js是沒有export default的喔,不然會爆掉

總之就是這樣拉,打完收工

react-router動畫切換

 我挺訝異vue-router原生支援的動畫換頁效果,在react-router6竟然沒有支援,官網老實說也寫得不是很清楚,畢竟這版本目前來說還很新,也沒有別人寫好的套件可以直接使用,只好上網爬文怎麼實現

基本上就是參考了這個網站(https://dev.to/fazliddin04/react-router-v6-animated-transitions-diy-3e6l)把最終效果實踐出來

正文開始

// src/style/route-animate.module.scss
.fadeIn {
animation: 0.4s fadeIn forwards;
}

.fadeOut {
animation: 0.2s fadeOut forwards;
}

@keyframes fadeIn {
from {
opacity: 0.6;
transform: translate(-3rem, 0px);
}
to {
opacity: 1;
transform: translate(0px, 0px);
}
}

@keyframes fadeOut {
from {
opacity: 1;
transform: translate(0px, 0px);
}
to {
opacity: 0;
transform: translate(-3rem, 0px);
}
}

// src/App.jsx
import route_styles from "./style/route-animate.module.scss"
......
const RoutesWithAnimate = () => {
const animateEnum = {
fadeIn: "fadeIn",
fadeOut: "fadeOut",
}
const location = useLocation()
const [displayLocation, setDisplayLocation] = useState(location)
const [transitionStage, setTransistionStage] = useState(animateEnum.fadeIn)
useEffect(() => {
    // 防止有變化query的頁面但route沒有變化
if (location.pathname !== displayLocation.pathname) {
setTransistionStage(animateEnum.fadeOut)
} else if (location !== displayLocation) {
setDisplayLocation(location)
}
}, [location, displayLocation])


return (
<div
className={classNames("min-h-full", route_styles[transitionStage])}
onAnimationEnd={() => {
if (transitionStage === animateEnum.fadeOut) {
setTransistionStage(animateEnum.fadeIn)
setDisplayLocation(location)
}
}}
>
<Routes location={displayLocation}>
<Route path={"/home"} element={<Home />} />
        <Route path={"/about"} element={<About />} />
<Route path="*" element={<Navigate to="/home" replace />} />
</Routes>
</div>
)
}

藉由把location暫存放入Routes裡面,利用時間差做到動畫效果,但終究沒辦法同時存在兩個location,所以沒辦法做到同時兩個route畫面出現在畫面上的效果,類似輪動這樣,這可能需要靠htmlToCanvas達成類似的效果或是等其他人出套件了,總之專案做到這裡就被打掉了....沒錯你沒看錯,被打掉了,因為有人覺得淡入淡出很醜,好吧就這樣吧,做個紀錄,紀錄自己曾經做過。

踩過的小坑
if (location !== displayLocation)

參考的網站判斷原來長這樣,但是這會造成一個問題,如果只是query切換沒有切換pathname,然後畫面中假設彈窗顯示,然後彈窗會去取query的值,這時因為Routes的location還沒有轉換,所以取到的query會是舊的。

例如
router變化:/home?id=1 => /home?id=2
希望達成效果:/home?id=1 => 開始淡出 => 彈窗抓取qeury: id = 2 => 開始淡入
實際執行狀況:/home?id=1 => 開始淡出 => 彈窗抓取qeury: id = 1 => 淡出跑完(setDisplayLocation) => /home?id=2 => 開始淡入 => 淡入跑完

先不要管為什麼系統這樣設計,總之~就是這樣拉~

2023年8月31日 星期四

next.js初體驗小記

 在某些機緣巧合下開啟了第一次接觸Next.js,照官方指令創建專案第一感覺是:好慢!

說好的比vite快兩百倍呢==,我看他底層還是用webpack在打包,可能之前在講的打包工具還沒上線吧。

我用過Nuxt.js,一開始想說兩者屬於同樣性質的框架,使用上應該不會差太多吧,結果還真的差不多。因為網路上已經有很多文章說明Next.js的使用這邊就不再贅述。

這裡只想小小吐槽兩件事

第一點,只要有使用到事件的,基本上無法再server site被render,所以必須在專案最上方寫上“use client“,讓框架知道這個檔案只適用於client端渲染,但.....這是for檔案,如果我畫面上只有一個tag有事件觸發,我就必須把這個tag單獨用component包起來,並讓他在client端渲染,何其不方便,你看看人家Astro.js,當然這又是另外一個故事了。照這邏輯看起來,幾乎沒有頁面可以use server啊....這樣的設計到底為了什麼==


第二點,在Next.js框架是不能ajsx出去要資料的,會發生CORS,處理方式有兩個:

第一就是使用rewrites進行網址改寫達到proxy的效果

第二就是打自己的路由,例如我在/app/api/user/routes.ts寫入

export async function GET() {
return NextResponse.json(await useApi("contact").getList())
}
export async function POST(req: NextRequest) {
const { contact } = await req.json()
return NextResponse.json(await useApi("contact").create(contact))
}
export async function PATCH(req: NextRequest) {
const { id, info } = await req.json()
return NextResponse.json(await useApi("contact").update({ id, ...info }))
}
export async function DELETE(req: NextRequest) {
return NextResponse.json(await useApi("contact").delete({ id: req.nextUrl.searchParams.get("id")! }))
}

但這有個問題,如果api接收的params寫在網址上,例如/api/user/1,1在server這邊是讀不到的,真不知道是他NextRequest沒寫好還是怎樣,我知道有人會說要開個[變數]資料夾去接收變數,xxxx這我也試過了,router取params的變數這功能只存在client端。或是知道怎麼做的可以留言給我,總之我試了很久是找不到方法。

因為上述問題,所以client打的網址必須放在參數裡讓server讀到,變成/api/user?id=1,然後再由server放進params裡去打api,這也導致client跟server端打的網址可能會不一樣,但打自己,server在打出去,還有兩層邏輯....選這方法的人大概率不是蠢就是壞,這樣邏輯會四散各地,還要為client跟server寫兩次打api方法。

而且方法二執行起來,意思跟proxy差不多,所以理所當然我選擇了方法一。

兩天用下來我發現,或可真的可以好好學習一下Astro.js,MPA VS SPA之爭,可惜台灣學習資源很少。

Astro.js是最近才聽到的框架,好像也行之有年,也是ssr框架,然後也支援其他框架寫出來的component,所以可以進行整合。與以往SPA只有單一入口不同,會把不同pages最後分別導出成各自的index.html,撰寫邏輯跟api也都不難,有機會真的想要更深入研究一下

以上圖槽完Next.js心情舒服多了,來捆


html快取

快取大概是前端諸多夢魘之一吧,通常我們能做的有限,在現在打包軟體的加持下,js,css等檔案已經幾乎不可能有快取,因為後面會有hash,那唯一還是有可能被快取的檔案就是入口的index.html

今天又被客戶反應有快取,原本理智線已經快斷掉的我,突然被提醒meta能否設定一下讓html不要被快取,於是參考一下別人的網站列出下面四行

<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache" content="no-cache" />

就加一下,靜待日後觀察吧~

2023年7月24日 星期一

history.back的小坑

原來原生在window.history.back(),回到上一頁後,在“很多環境”是預設不會重新載入的,加速瀏覽,所以在上一頁就必須加上以下內容,幫助畫面重新載入

window.onpageshow = function(event) { if (event.persisted) { window.location.reload(); } };

因為小編spa寫久了,從來沒遇過這個問題,今天踩到做一下紀錄


2023年7月14日 星期五

webpack轉換vite使用心得

 最近把公司專案從webpack4搬到vite上,所以研究了一下,只能說我非常喜歡vite的風格,不像webpack有一大堆看不懂的設定跟一堆plugin,設定起來非常快速跟方便。

不過兩之間還是有一定的差距,不可能只搬打包工具而程式碼不動,這裡就列出幾個會有影響的程式碼

require
vite是不支援require的,就算裝了套件vite-plutin-require,require進來的也只會有路徑而已,但svg可以透過vite-plugin-svgr把require進來的東西轉成component,但如果要做到dynamic import也只能透過異步加載(loadable)的方式來處理

process.env
在webpack可以用這種寫法將環境變數注入到程式碼中,vite當然也可以,但寫法不同,是用import.meta.env,如果不想改寫法,可以在vite.config.js裡面設定define把process.env.API_HOST: 127.0.0.1之類的,讓vite專案也看得懂process.env.XXXX
ps. 如果使用import.meta.env記得要將環境變數以VITE_開頭才讀取得到喔

static public資料夾
webpack好像不特別設定是不會打包靜態資料夾的,可以進行另外處理,但在vite有個publicDir,預設為public,在打包時就會把public裡面的東西打包進目標資料夾的根目錄,例如/public/image => /dist/image
如果不想打包可以設為false

image
在專案中如果是css設定background-image: url(...),vite會自動判斷最終這張圖片是否是存在靜態資料夾,如果不是則是會被打包成圖片檔+hash的檔名,等於把路徑寫在css裡,路徑跟圖片是會被強迫編譯的,這點跟webpack就不一樣,少了一點彈性,沒辦法拿來做文章。
如果在html裡,<img src="/image/logo.png"/>則不會被編譯,當然如果你網址是指到你的根目錄這完全不會有任何問題,但如果你的網址有前綴,例如https://test.com/web/xxxxx,這樣/image/logo.png就會找不到圖片,因為圖片在/web/image/logo.png,所以解決方式是使用import logo from "/public/image/logo.png"
<img src={logo} />
這樣打包完src就會被編譯成/web/image/logo.png

如果像上述情況有前綴,在vite.config.js裡面有個base: "/web",這樣所有的引入都會加上前綴/web

我有試過官方提供new URL("image path").href,在固定路徑的情況下可以,但如果是dynamic path就會變成undefined,試了好幾次也沒辦法,只好放棄

曾經我有試過require("image path")的方式把引入變成路徑,一開始有成功,但重開專案之後又失敗了,也是很神奇的一次經驗。

大致上遇到要調整的就這些,只能說vite真的是很神,設定簡單,也不用裝一堆外掛,編譯速度又快

之後有空也可以pwa的部分

100%與100vh的差異

除了網路上大家常說有沒有內容,會不會被撐開的差異外,今天在實際操作上還真的遇到一點差異,而平常不會注意到的

這張就是我把高度設為100vh的狀況,container是用flex完全置中,但上面卻會有一塊空白,導致確定按鈕會有點超出介面

這張就是我高度設定100%,在完全置中情況下,上下空白幾乎是一樣的

為什麼會有這樣些微的差距是因為vh是表示螢幕的高度,而非扣除掉原生軟體介面,剩餘的高度,所以第一張圖來看,如果把網址列及控制列算進去,他確實是完全置中沒錯,但這不是我們要的效果,所以乖乖設成100%吧