2023年12月18日 星期一

記錄一下這四天的奇幻旅程

前言

因為有點空檔,所以想說可以在線上找個pt工作賺點外快,所以我在網上聯絡到一個HR(老闆?PM?),因為他從未表達他的身份所以我也無從得知,而且他回話總是很慢,留言給他大概都要半天後才會得到回覆,總之他給我一小時500的費用讓我試著做做看。之後為了方便辨識,我都稱呼他為A先生

他們公司有自行開了一個看板系統,可以自行上去認領要做的任務,打上評估時間,只要通過審核之後就可以開始開工。

首先A先生派了一個專案給我,因為我週末有安排,中間抽空看教學,花了兩到三天的時間,最後因為該環境限制要amd64架構的cpu(裡面有要架mssql),我用的是mac arm64 cpu所以最後還是沒架起來,一直到週日晚上,A先生又給了我另外一個專案,我花了一個早上時間把環境架起來,然後開始看任務,但任務卡上幾乎沒什麼說明,所以我就開始提問,然後又是半天一回,最後他還補了一句:12/25要進行驗收,我說:等一下,我這週要出國,這我之前就說過了!他回:沒關係,有多少做多少!

講是這樣講,我也很想趕快做點事情,但是我完全看不懂任務看板,我也不知道PM是誰,總之A先生就是我聯絡的窗口,理所應當我也所有問題都問他,但這效率真的不行,你們要後端開發系統,不用開個會交代一下嗎!你是真的覺得一張表格圖就可以搞懂系統全貌那也真的太高估我了。搞得我自己壓力很也大,經思考我覺得好像這不是一個很健康的狀況,我花了三天的時間,雖然不是全時段在線,但全部加總也花了不少時間,我一行程式碼都沒寫到,這些完全是無償的,所以為了長遠著想,並且為了出國可以放心遊玩,所以這是我人生第一次只上班四天就離職,真D雷~

不幸中的大幸,我在這過程中還是有學到一點東西,做一下紀錄

VSCode有個套件叫Dev Containers,安裝完後左下角會有個類似><的綠色按鈕


這圖案點下去會出現幾個選項

New dev container, Attach to running container...

這次使用到第二個,意思是直接進到container裡面進行開發,這樣就可以在php有版本限制的情況下開發了,算是只要想辦法把理想的開發環境build成image就好,減少個體環境的差異。

另外現在在container裡面怎麼連外面的服務呢?在terminal匡那邊有一個ports的tab,可以在裡面設定VSCode port forward,夠過VSCode連結到外面的服務,這構想也算很酷了,如果不是這次體驗我大概率一輩子也不會用到。

後記

這是我摸的第一個專案,還有第二個,第二個就單純一點,laravel10的專案,結合inertiajs,變成一個全端框架但.....我覺得很難用,比較酷的是vite也有出laravel的plugin讓前後端結合得更好,但~有空再來說吧

2023年12月13日 星期三

有趣的小東西MutationObserver

這應該也不是什麼太新鮮的玩意兒,2019年的產物,無意間在我的最愛翻到的,跟大家分享下
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script>
const div = document.querySelector("div");
const observer = new MutationObserver(function (mutations) {
mutations.forEach((record) => {
console.log(record);
});
});

observer.observe(div, {
childList: true,
attributes: true,
characterData: true,
});

function changeContent() {
div.textContent = "Hi I'm Tom";
}

function changeColor() {
div.style.color = "red";
}
</script>
</head>
<body>
<button onclick="changeContent()">改變內容</button>
<button onclick="changeColor()">改變顏色</button>
<div>Hi I'm Max</div>
</body>
</html>

簡單說他可以偵測DOM的變化,有變化就觸發方法,以現在前端框架滿天飛的時代,好像沒啥鳥用,不過有趣就記錄一下。

2023年12月10日 星期日

proxy遇到lagacy browser的小坑

前言

還記得前幾天我用signal包了react用的vue hook套件嗎?才講完沒試過兩天就出事了

不過好里加在不是什麼大包,事情是這樣的

內文

有個Android 8 chrome 70 webview的用戶(簡單說就是android的webview)回報資料顯示有問題,我看這版本起碼也是七八年前的產品,但照道理來說都已經支援Proxy了怎麼可能會出什麼大錯,再track bug的過程中發現,我寫的資料陣列被過濾掉了

const buildPaymentDataByCType = (paymentConfig) => {
// payment is proxy object
const payment = initData.withdrawList.find((x) => x.CType === paymentConfig.C_TYPE)
    // console.log(payment) // exists
return
{
... payment,
// ellipsis
},
)
}
const normalWayPlatforms = computed(() =>
[
buildPaymentDataByCType(ORDER_PAYMENT.BANK),
].filter((x) => x.Name)
)
console.log(normalWayPlatforms.length) // 0??

我在console.log(payment)都是有東西的,理論上應該都要找得到相對應物件,但為什麼會被filter掉,所以我試著不要filter直接在console.log出來,發現問題出在{...x}擴展運算子並沒有生效!!

難道是老舊瀏覽器不支援這ES6語法?所以我又隨便寫了一個

const a = { a: "a" }
console.log({ ...a })

發現這樣寫是正常的,所以應該只是因為Proxy,所以多了一層,然後擴展運算子當時還不支援擴展Proxy,94這麽簡單。那這樣要怎麼搞定呢?

我嘗試一下Object.assign還是可以把Proxy擴展出來,所以就改成這樣

const buildPaymentDataByCType = (paymentConfig) => {
const payment = initData.withdrawList.find((x) => x.CType === paymentConfig.C_TYPE)
return Object.assign(
{
// ellipsis
},
payment
)
}

OK打玩收工

後記

過程中我有嘗試用lagacy plugin把程式碼轉成es5但似乎都沒有成功,vite似乎不希望程式碼可以編譯到es6以下,總之vite還是有他不方便的地方啊,祝大家都可以準時下班~

2023年12月9日 星期六

針對某公司前端面試做個紀錄

前言

我絕對不會說我去面試K什麼M什麼體什麼X的公司,整體面試感受不太好
HR不夠積極跟友善,面試前一小時臨時改面試時間,面試開始前八分鐘才叫我提前五分鐘到,結果面試官也是準點進來,然後面試官整場用我不熟悉的語法(純粹大陸用語跟台灣用語不同)來問問題,所以很多問題我也只能重複進行確認,然後問完話也沒有要讓我提問,說沒有問題了匆匆結束會議,然後也沒有打算讓我跟HR二面,就在你問我答,足足問了22分鐘結束面試....好吧,非常有效率給你一個讚!

內文

針對被問幾個我印象比較深刻或回答沒這麼好的問題進行一次紀錄

1. 輸入網址到畫面渲染,中間過程如何....
又是這種沒有標準答案的鳥問題

2.vue v-for跟v-if可以一起用嗎?
我回答可以,對方接著問誰先誰後,我說太久沒用我忘了,試過就知道(但對方顯然對這答案不買單)
最近去翻筆記,確實是可以,然後在底層v-for會先執行再執行v-if

3.js的垃圾回收機制是什麼?
老實說我會寫不會記憶體洩漏的程式,所以從來沒想過這問題,所以我去查一下
答案是沒有被引用的物件會被瀏覽器回收,所以如果有物件相互引用就會造成記憶體洩漏(memory leaks)

4.Promise的狀態有什麼?
又是一題會寫但不知道答案的問題,我用這麼久還沒遇過有人問這問題,去查一下答案是
pending執行中
reject操作失敗
resolve操作成功

5.react useRef可以用來幹嘛
記錄變數,指向DOM

6.react useState, useRef,當state更新但畫面沒有更新,可以透過ref回復到先前狀態嗎?
我回答:理論上是可以啊。對方突然很驚訝回我:可以!那要怎麼做。我:就看你邏輯怎麼寫吧...他的回答讓我感覺我的回答是錯的,這就是沒有程式碼在那邊盲問的缺點,另外我倒是很想問你是怎麼做到更新state然後畫面沒有更新

小結

其實他還問了很多,但大多我都會答,應該是都對拉,雖然最後我被打上T3的等級(對方要T4),然後被大砍期待薪資,不過貴公司996的工作時數我就算沒被砍我也大概率不會進去(是真的996),然後工時打在JD上我以為是常識,只能說我真的孤陋寡聞,還是在我面試完後拼命追問HR才告訴我。只能說這間公司除了遠端,其他沒有任何一點吸引我的地方

2023年12月8日 星期五

nginx X-Frame-Options & proxy

前言
我都覺得我快變成維運了,全端工程師已經夠可憐了,最近寫的文章全部都是為運相關....

內文
好好一個案子說“弱掃”掃描不過有安全性問題

其中一條說
Missing Anti-clickjacking Header: 在HTTP Header加入X-Frame-Options: DENY或SAMEORIGIN
然後附上一段參考網址
https://a42033.gitbooks.io/system/content/security/user/X_Frame_Options.html

我去研究一下發現這是關於自己往也能否被iframe嵌入的設定,理論上當然要不行,或者同域名才可以
所以我參考這個網頁

因為我伺服器是用nginx,所以我就只打關鍵字拉
在default.conf裡面加上
add_header X-Frame-Options DENY;
這樣就搞定了。


至於還有一個比較大條的,安全性等級比較高的問題寫到

Cloud Metadata Potentially Exposed
參考網址
https://www.nginx.com/blog/trust-no-one-perils-of-trusting-user-input/

老實說他只舉出錯誤的寫法,沒有指出正確該怎麼寫.....所以咧~~
最後我看留言,有一個可憐的老哥跟我有一樣的問題:所以呢,是要怎麼解決!!然後他給出他修改的範例,請作者指教。好在作者還真的有回應他,並且是正向的。(媽的他寫對就說一句“對”很難嗎,我看完google translate文鄒鄒落落長,都不知道你在說什麼)

簡單結論一下,文章指出如果直接寫
proxy_pass http://$host;
這樣會有相對的風險,官方“不建議”這樣寫,所以比較好的寫法是透過upstream去指定host
upstream a_node_app {
server 127.0.0.1:3011;
}
我記得這是用來做low balance的,所以就是我指定到upstream的一個host,再由他幫忙轉到我要的位址,這樣就可以了。

後記
文章給的另一個解法是可以架防火牆,至於怎麼用....等我哪天真的轉維運再告訴你

2023年12月3日 星期日

我受夠了react的hook,我想寫vue啊~

前言

你是否有跟我一樣的困擾呢,覺得react class component的setState寫法相當繁瑣,覺得react hook的的queue也不夠直覺,為什麼不能直接update state然後畫面就自動render需要透過一個setter,然後資料異動還不是同步的,也不是異步,竟然是透過queue......這是我一直以來的心聲。我今天沒有要戰,純粹是就職以來的一些小心得,作為一套老牌框架,我承認他的市佔率,但我覺得如果以便利性讓我挑選,我一定不會選react,但礙於現有的社群要他大改幾乎是不太可能,就算不論三大框架現在市場上也充斥著太多套創新的框架(qwik, svelte, solid, astro....),所以要用一個形容詞來形容這套框架只能說它“過時”了,but!最可怕的but來了,公司就愛用嘛~所以有沒有什麼辦法?

老實說我很驚訝竟然查不到相關的資料,還是大家都超滿意react,沒遇到什麼奇怪的坑,我是異類,總之~辦法還是有的,首先我在腦海中搜索想到了奶綠茶有介紹過preact/signal,奶綠大大事我很喜歡的一位工程師,我大學時還買過他的書,不過那是另外一個故事了

內文

preact跟react是不一樣的框架,但其中的api友87%像,signal使用起來就像是vue裡面的ref(太詳細的內容自己去看原始碼),然後看網誌說明signal無法直接使用在react,特別是底層是vite打包的狀況下(該死)

所以不得已我又開始尋找其他法,後來發現preact/signal有出preact/react-signal特別優化的版本,這樣應該是妥了吧,官方都直接出優化了,直接使用在公司專案結果爆出getSnapshop is undefined......我去查了一下原始碼也一無所獲,歸功於react亂七八糟......抱歉我不該戰,是莫名其妙...抱歉,是我孤陋寡聞不得而知的底層運作機制,所以我放棄了這麼一條路

難道我就該止步於此繼續使用這該死又難寫......抱歉我不該戰,我要繼續使用這我用了很痛苦的框架嗎?當然不是,既然連官方都靠不住,那就只能自己來寫了,一開始我嘗試使用Object.defineProperty去寫(沒錯就是vue2那套,想看原始碼也附註解在裡面),反正所有東西都丟到value下面就好了吧,但也發生了跟vue2一樣的問題,沒有註冊過的屬性就不會reactive,但如果使用Proxy怕太新UC那些奇怪的瀏覽器不支援

沒錯公司要求產品要支援UC,講到這裡我想花一點篇幅抱怨一下,建議看到這裡的工程師,珍惜生命,遠離UC,下次面試記得要問主管公司有沒有要求要支援UC,有人說UC就像手幾板的ie我覺得他說錯了,他比ie還不如!不僅沒辦法debug,之前發生一次白屏,在我一行一行除錯,最後問題竟然是html排序它不滿意,不是什麼沒有加上</>這種低級錯誤喔,就是單純排序它不滿意,當時的狀況就類似以下:
<div>
    hello
    <div>hello2</div>
<div>
然後我把它改成
<div>hello<div>
<div>hello2</div>
就正常了.....我還以為我又用什麼新的寫法或物件戳到它的G點,Fuck!這我真的沒辦法誒!!

好拉,工作還是要繼續下去,但這還在研發階段之不支援我就先不考慮,之後不支援大不了不用!所以你以為我就乖乖刻輪子嗎?當然沒有,我想到既然底層都是Proxy那我用signal就好拉,那畫面不會reactive的問題怎麼處理?奶綠茶大大的網誌有提到react18有出一個新的hook是useSyncExternalStore 這個hook可以控制畫面要不要渲染,網誌裡面有提到大大如何包裝這個function,我試著去改寫但我一直嘗試不成功,因為我一直把變數放在function內,所以在rerender時function會重跑一遍,所以變數每次都會被重置(Fuck我就說寫法很不直覺吧),當我想到這問題時已經好幾天過去了,如果不想變數被重置,那就使用原生的hook來解決吧,最後產出來的code就如下

沒錯,前面說這麼多我只是想要抱怨,最終結果在下面,大家如果為了自己身體著想,可以直接拿去使用

我模擬出一些比較常用的hook,其他我也很少用就不做了,畢竟這就像那些砲轟我的網友說的,就是vue皮react骨,我也沒有不承認這件事,但更新資料畫面就會自動更新就是爽,也不用在那邊setState後還要在useEffect等狀態更新才能在function裡面執行我想執行的內容,這寫法真的很蠢......抱歉我不該戰,這寫法真的很不直覺

好吧廢話不多說直接上程式碼

import { useReactive } from "@reactivedata/react"
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react"
import { signal, effect, computed as preactComputed } from "@preact/signals-core"

export const onMounted = (fn) => {
useEffect(() => {
fn()
}, [])
}
export const onUnmounted = (fn) => {
useEffect(() => {
return () => {
fn()
}
}, [])
}

export const ref = (initValue) => {
const valueRef = useRef(initValue)
const versionRef = useRef(0)
function createStore(signal) {
let onChangeNotifyReact
const unsubscribe = effect(() => {
valueRef.current = signal.value
versionRef.current++
onChangeNotifyReact?.()
})
return {
subscribe(listener) {
onChangeNotifyReact = listener
return () => {
unsubscribe()
}
},
getSnapshop() {
return versionRef.current
},
}
}
const res = signal(valueRef.current)
const store = useMemo(() => {
return createStore(res)
}, [res])
useSyncExternalStore(store.subscribe, store.getSnapshop)
return res
// const _value = useRef(initValue)
// const [ref, setRef] = useState({ value: initValue })
// Object.defineProperty(ref, "value", {
// get() {
// return _value.current
// },
// set(newValue) {
// _value.current = newValue
// setRef({ value: newValue })
// },
// })
// return ref
}
export const reactive = useReactive
export const computed = (computeFn) => {
const res = preactComputed(computeFn)
function createStore(res) {
let onChangeNotifyReact
const unsubscribe = effect(() => {
res.value
onChangeNotifyReact?.()
})
return {
subscribe(listener) {
onChangeNotifyReact = listener
return () => {
unsubscribe()
}
},
getSnapshop() {
return res.value
},
}
}
const store = useMemo(() => {
return createStore(res)
}, [res])
useSyncExternalStore(store.subscribe, store.getSnapshop)
return res.value
}

export const watch = (sSatcher, fn) => {
const watcher = typeof sSatcher === "object" ? sSatcher : [sSatcher]
const value = useMemo(
() =>
watcher.map((x) => {
return typeof x == "function" ? x() : x
}),
[watcher]
)
const ref = useRef(value)
if (value.toString() !== ref.current.toString()) {
ref.current = value
}
useEffect(fn, ref.current)
}

export { effect }

這樣onMounted, onUnmounted也解決了useEffect裡面不能放async的function,那警告看了真的很礙眼,支援就不要吵,不支援就直接抱錯,在那邊吵什麼吵,然後為什麼不多包一層就好,我也不知道,可能礙於什麼東方神秘力量

reactive就直接用別人寫好的吧

watch就是watch

computed這麼好用的功能怎麼能不實現(但我沒實現getter、setter,沒錯因為我不常用)

ref就用useRef去解決變數被重置問題,我們來比較一下差別
// normal way
const [value, setValue] = useState(0)
setValue(1)
console.log(value) // 0 wtf!
useEffect(() => {
    console.log(value) // 1
}, [value])

// better way
const [value, setValue] = useState(0)
const newValue = 1
setValue(newValue)
console.log(newValue) // 1 .....why so troublesome?

// new way
const state = ref(0)
state.value++
console.log(state.value) // 1
// 484很直覺!484很爽!484很好寫


後記

最重要UC, QQ這些中國大牌瀏覽器到底有沒有支援Proxy呢~~答案是有的,至少上線到現在沒有傳出什麼災情
謎之聲:連Promise.race跟any都可以拔掉,竟然支援Proxy,真是可喜可賀~
不過想想也是拉,現在vue3都用Proxy寫了,如此泱泱大國總不會想跟自國工程師對著幹吧!

最後,我沒有實際測過使用起來的效能,我相信應該不會太好,畢竟有內容更新畫面就要重新渲染一次,原本react18 queue的優化都沒用了,但我才不管,開發體驗最重要!!事實上,你如果注重效能就不會選擇react框架了,那這篇就先到這裡拉,希望大家都可以準時下班,see u later~

12/18更新

computed並非是及時的,useSyncExternalStore只用來更新畫面,所以如果要用在邏輯演算中,用useMemo效果可能比較好,之後再來思考看有沒有機會debug,下面有個使用範例,這真令人有點難過

const users = [{ birthday: 2020-01-01}, {birthday:"2022-02-02"}]

let index = 0

const user = computed(() => users[index].birthday) 

console.log(user.birthday) // 2020-01-01

index = 1

console.log(user.birthday) // 2020-01-01 來不及更新


2023年11月20日 星期一

vue2 event hook destroyed

在官方建議寫法一般是這樣

mounted() {
this.$root.$on("getSelf", this.getSelf)
},
destroyed() {
this.$root.$off("getSelf")
},

但你也可以這樣寫

mounted() {
this.$root.$on("getSelf", this.getSelf)
this.$on("hook:destroyed", () => this.$root.$off("getSelf"))
},

這樣寫有什麼好處呢,主要防止邏輯四散,每個人寫作習慣不同,mounted跟destroyed不一定就在旁邊,有人mounted會寫一大串,最造成閱讀上沒這麼直覺。你可以在宣告偵聽時就確定他會在component destroyed時被取消偵聽減少資源浪費。這寫法就跟golang的defer類似。

其實早在很久前就有這樣寫了,只是沒記下來真的會忘記,到最近又碰到vue想要寫才把這招翻出來,所以就做個紀錄

2023年10月26日 星期四

docker,或者說docker-compoes踩的小坑,nginx host name指向問題

先附上我寫的docker-compose

情境說明
我在構思前後端架構時,後端用nodejs,然後前端用nginx,這沒什麼問題,然後因為要走ssl,我懶得把ssl key也copy到後端專案,打算直接在nginx用proxy的方式做掉,OK事情就是這樣發生了,先附上docker-compose跟nginx設定

# docker-compose.yml
version: "3.6"

services:
node-api:
build:
context: .
dockerfile: Dockerfile
container_name: stemi_node_api
volumes:
- ./tmp:/app/tmp
working_dir: /app
command: sh -c "yarn dev"
ports:
- 8000:3333
depends_on:
- python-api
environment:
- PYTHON_API_HOST=python-api

nginx:
build:
context: web
dockerfile: Dockerfile
ports:
- "443:443"
- "3333:3333"
container_name: nginx
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx:/etc/nginx/conf.d
- ../ssl/:/ssl
depends_on:
- node-api

# nginx/default.conf
# ...前端設定省略,因為這不是今天踩坑的主題
server {
listen 3333 ssl;
server_name localhost;
ssl_certificate /ssl/certificate.crt;
ssl_certificate_key /ssl/private.key;

# 根目錄
location / {
proxy_pass http://node-api:8000;
}
}

這一切乍看之下都很完美,但實際在打https://127.0.0.1:3333卻會報502,意思就是打不到東西,我一開始是想是不是node-api讀不到的關係,查了很多關於links, depends_on, networks的文件,最後進到nginx container裡面實際下指令nslookup node-api也確實有,ping也ping得到

/app # nslookup node-api
Server:         127.0.0.11
Address:        127.0.0.11:53
Non-authoritative answer:
Non-authoritative answer:
Name:   node-api
Address: 172.23.0.2

/app # ping node-api
PING node-api (172.23.0.2): 56 data bytes
64 bytes from 172.23.0.2: seq=0 ttl=64 time=0.465 ms
64 bytes from 172.23.0.2: seq=1 ttl=64 time=0.387 ms
^C

後來我發現,我啟動的container也只有兩個,但ip卻有三個
172.23.0.1
172.23.0.2 <= node-api
172.23.0.3 <= nginx
ok問題來了,照上面指令2應該就是node-api,3應該是nginx,那1是什麼呢?我繼續下指令curl -G http://172.23.0.1:8000

/app # curl -G http://172.23.0.1:8000 {"code":0,"data":"ok"}/app #

矮油這下問題嚴重了,我一度以為docker出問題,把host name指向錯誤的主機,想上網找但關鍵字也不知道怎麼下

後來在苦無對策之際,最後試著下一個指令

/app # curl -G http://node-api:3333
{"code":0,"data":"ok"}/app #

一切都真相大白了,我一直以為container是在各自的容器(我不知道這專有名詞是啥,只是講container會搞混,在我想像中他形式比較像沙箱跟區網的概念,將箱子也不好聽,所以就叫中文的容器吧),藉由expose把port開出來讓外界使用,事實上如果用docker-compose那大家就會在同一個容器內,所以網內互打是打原本的port不是export的port,所以1應該就是最後所有container的集合....吧,所以最終只要這樣修改就好。

# nginx/default.conf
  location / {
proxy_pass http://node-api:3333; # 把port改掉
}

因此最終node-api的ports設定也可以拿掉。


我也不是專業的devpos,就是專案需要所以簡單地摸了一下,踩了坑就記錄一下,歡迎不大家吝嗇指教,只是希望可以噴得小力一點!

2023年9月25日 星期一

url retry

 需求是某些地區只能打domain,有些地區只能打ip,所以pm要求我寫一個retry的功能,當一個不通在打另外一個

export const orderPostRetryHandler = async (callApi) => {
return retryHandler(callApi, [
{ baseUrl: "url1", maxTimes: 2 },
{ baseUrl: "url2", maxTimes: 2 },
])
}

export const retryHandler = (callApi, retrySettings) => {
let tryTimes = 1
let index = 0
const tryFunc = async () => {
const { baseUrl, maxTimes } = retrySettings[index]
try {
const res = await callApi(baseUrl)
if (res.status == -1) {
throw res
}
return res
} catch (e) {
tryTimes++
if (tryTimes > maxTimes) {
if (++index >= retrySettings.length) {
throw e
}
tryTimes = 1
}
return tryFunc()
}
}
return tryFunc()
}

但這有個問題,就是等打了兩遍都打不通時間都過去十秒了,真的有人願意等這麼久嗎?所以我突發奇想,為什麼不兩個網址都打,哪個通就走哪個,所以我又改成以下

export const urlChecker = (urls) => {
let passUrl = ""
return async () => {
if (passUrl) return passUrl
const res = await Promise.any(urls.map((url) => fetch(url)))
passUrl = res.url
return passUrl
}
}
const getActivatedUrl = await urlChecker("url1", "url2")
fetch(`${await getActivatedUrl()}/api/`)

這樣體驗好多了

1/19調整一下,如果使用any回來的是無效網址那就會壞掉
所以須改用Promise.all(urls.map(url => axios.get(url, {timeout: 3000})).then(urls => urls.filter(x =>x)[0])



2023年9月20日 星期三

fetch timeout

 前言:上篇提到我加了@vite/plugin-lagacy讓畫面成功渲染了,但打某支api卻會報錯promise rejection.....

我一開始想說是不是沒有支援Promise,我一直嘗試在polyfills塞入各種套件,但始終沒有解決。一直到我後來思考,不對啊,首頁就打了數隻api都沒事,為什麼到該頁打該api才會報!應該是該api有問題,但看來看去底層是一樣的,為什麼只有這隻會報.....

最終找不到辦法,因為我手上也頂多只有6s手機,無法進行除錯,只好到這個網站:https://vikyd.github.io/download-chromium-history-version/#/,下載舊版的chromium看能否模擬事件發生。好在在46版時發生了,然後我在報錯訊息中多看到幾個字“AbortController”,原來是我為了讓fetch有timeout,所以使用了這個物件,但這個物件載舊版瀏覽器上沒有,所以報錯,並非是沒有promise polyfill,所以我把AbortController加上一些判斷如下

if (typeof AbortController == "function") {
const controller = new AbortController()
const timeout = 5000
setTimeout(() => {
controller.abort()
}, timeout)
obj = { ...obj, signal: controller.signal }
}

fetch(url, obj)
.then((res) => {
......
return res.json()
})

變成有就5秒timeout沒有就預設30秒,呵呵,事情就解決了!!撒花~~



vite支援老舊瀏覽器

 前言:公司客服反映有八年前android(chrome)跟十年前5s(safari),打開網頁看不到畫面,換台3000塊以上的手機很難嗎==這要求跟要我支援ie差不多吧,好在客戶群沒很大,老闆就沒逼很急,所以花了點時間研究,做一下紀錄。

簡單說就是vite有出一個plugin:@vite/plugin-lagacy,看名字應該看得出來是為了骨灰級瀏覽器而有的,新的瀏覽器都支援ESM,然而不支援ESM的瀏覽器只好使用pollyfill去讓他被支援,詳細配置如下

// vite.config.js
plugins: [
......
legacy({
targets: ["defaults", "ie 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
}),
],

基本上配置這樣就可以了,我都支援到ie11了總可以放過我了吧。確實在同事口中傳回來捷報說有畫面了,但是!人生最可怕的就是這個但是,但是在打某支api會出現,promise rejection.....

只好下一篇待續

windows iis支援webp

 前言:在公司前輩的要求提醒之下,把png檔都轉為webp檔,說這樣除了檔案較小,也可以增加載入的速度(據說是協議有特別優化過)。在開發期間都相當順利,沒想到上正式機卻發生圖出不來,原因是因為測試機是linux,正式機是window(別問我為什麼,每間公司都有本難念的經),然而在iis上預設是不支援webp的,之後的是可想而知,所以我做一下紀錄。

點開MIME類型


點擊右上角增加

輸入相關資訊


直接送出即可。

或是直接寫進web.config
<configuration> 
    <system.webServer> 
        <staticContent> 
            <mimeMap fileExtension=".webp" mimeType="image/webp" /> 
        </staticContent> 
    </system.webServer> 
</configuration>

小記:後續主管還提到精靈圖,就是把所有圖片壓在同一張圖片,用css去切割,減少request次數,只是真的有需要做到這樣嗎==,好在webp效果不明顯,後續就沒有繼續做下去了

參考網站:https://www.itnota.com/serving-webp-image-iis/

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%吧



2023年6月12日 星期一

AdonisJs上傳base64圖片

 小編在開發專案時遇到ReactNative套件相容問題無法取得File,因此無法正常post FormData的狀況,無奈之下只好跟前端協議改送base64編碼過來後端進行decode,在此做一下紀錄

export class AvatarUploadImageValidator {
@validate(schema.string(), {
required: File7000.IMAGE_ERROR.toString(),
string: File7000.IMAGE_ERROR.toString(),
})
public image: string
}

因為Adonisjs沒有特別驗證base64編碼,就簡單收一段文字

export default class FileService {
async uploadBase64({ base64, path, trx }) {
const [_full, contentType, image] = base64.match(/data:(image\/\w+);base64,(.*)/)
const ext = contentType.split('/')[1]
if (!ext || !contentType || !image) {
throw new ApiException(CommonCodes.FORMAT_ERROR, 'avatar image format error')
}
const decodedImage = Buffer.from(image, 'base64')

const filePath = `public/${path}/${DateTime.now().toFormat('yyyyMMddHHmmss')}_${generateCode(3)}.${ext}`
await Drive.put(filePath, decodedImage, {
contentType,
})
const fileUrl = await Drive.getUrl(filePath)
return File.create(
{
url: fileUrl,
},
{ client: trx }
)
}
}


先跟前端溝通好傳完整的編碼進來(不要只有圖片的部分),然後使用正則把需要的資訊分別提取出來,之後使用原生Buffer.from進行decode base64,剩下就是設定路徑及檔案名稱,最後再使用Adonisjs所提供的Drive進行上傳到s3,並取得網址,再來就是資料庫操作,如此打完收工。