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,就是專案需要所以簡單地摸了一下,踩了坑就記錄一下,歡迎不大家吝嗇指教,只是希望可以噴得小力一點!