[實作筆記] Hexo CI 自動執行 ncu -u 更新相依套件

前情提要

參考前篇
我針對了我 Blog 文章的發佈流程作了改善,
當中我提到其中並沒有用到多麼了不起的技術或新知,
而是整合現有的技術,讓流程更加的自動化。

這次我又想到一個自動化的流程,
之前提到的 npm-check-updates 工具,
雖然可以很輕鬆的更新 npm 套件,但是什麼時候執行就是一個問題了,
可以的話,我希望讓 CI/CD 代勞。

實作

立刻來捲起袖子吧,
首先回顧一下手動作業的流程。

前置條件

全域安裝 npm-check-updates

流程

  1. 拉取專案 git pull
  2. 執行 ncu -u
  3. 如果有異動的話 git commit -m "some message here"
  4. 推上遠端 repo git push

自動化

看看上面的流程,我們知道其實我們只需要一個安裝好 node、git 與 ncu 的環境,

必要條件

接著就依照流程執行命令即可,這裡我覺得正可以展現 Github Workflow 與 Actions 的強大之處,
你的問題,就是大眾會遇到的問題,而且大部份的情況都有對應的 Actions,
我們所需要作的,就是組合這些步驟,並且放入自已的邏輯。
直接看 workflow 範例

1
2
3
4
5
6
7
8
9
10
11
steps:
- name: Checkout
uses: actions/[email protected]
- name: ncu-upgrade
run: |
npm i -g npm-check-updates
ncu -u
- name: Commit & Push changes
uses: actions-js/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

觀注在 steps 的部份,
透過 actions/checkout 取得最新的 remote repository

1
2
- name: Checkout
uses: actions/[email protected]

接下來,執行一些語法,
安裝 npm i -g npm-check-updates
再執行 ncu -u

最後,透過 action-js/push 將異動推回 remote repository

1
2
- name: Commit & Push changes
uses: action-js/[email protected]

下一個問題是,要怎麼取得 git repo 的權限 ?
登登!! 我們可以使用 GITHUB_TOKEN 就輕易取得權限,
有關 GITHUB_TOKEN 的介紹可以參考下方的連結。

參考

(fin)

[實作筆記] Hexo CI/CD 設置

前情提要

我的 blog 一直以來都是用 markdown 加上 hexo 與 github page 建置的靜態網站。
為此,我也寫了一系列的文章;但是我卻沒想過優化 blog 撰寫的流程。

我目前寫作的的流程是這樣的:

首先我會用任何工具補捉自已的想法,手機、筆記本、便利貼、Notion etc…
然後我會用 hackmd.io 來寫草稿。
草稿完成後,我會在自已電腦的 Marsen.Node.Hexo Repo 上完成文章。
接著我會先執行 git push 上版,再接著手動進行 blog 的建置(hexo g) 與部署(hexo d)。

這次要優化的部份是 手動進行 blog 的建置(hexo g) 與部署(hexo d)
我希望寫完 blog 後推上 main 就是發佈了。

進一步的話,可以考慮用分支作草稿流程管理,但先不過早優化。

實作

首先,來了解我們的架構
hexo CI/CD

如圖,可以被畫分為兩個部份,第一個部份是紅框處,
在取得 Hexo Source 後,執行 hexo g 會產生靜態檔案放在 public 資料夾,
藍框處是第二部份,將 public 資料夾部署到 Github Page 的 repository 當中。

就這麼簡單,整個流程只會用到兩個工具 githexo
我們只要透過 docker 準備好安裝這兩個工具的 image 在 CI/CD 上執行即可
我找到了一個 Github Action 的 repository – HEXO-ACTION
依照以下的 SOP 就可以完成 CI/CD 設定

  1. 準備 Deploy Keys 與 Secrets
    執行以下指令

    $ ssh-keygen -t rsa -C “[email protected]

    這時我們會取得一組私鑰與公鑰,
    請在 Github Page repository 的 Settings > Deploy keys 中,加入公鑰(記得要勾選 Allow write access)

    https://github.com/{your_account}/{your_account}.github.io/settings/keys

    然後依照 hexo-action 的說明,你必需在 Hexo Source 的 repository 的 Settings > Secrets > actions 中加入私鑰
    有關 ssh-keygen 與不對稱加密的相關知識這裡就不多說明了,
    簡單的去理解,這組鑰匙是用來對 Github Page Repository 作讀寫,所以公鑰會放在 Github Page Repository 中。
    想嚐試存取 Github Page Repository 的人都會拿到公鑰,但是真得要存取,你得有私鑰才行。
    這個時候,Hexo Source Repository 會將私鑰以 Secrets 的形式存下來,
    在 Actions 執行時,會透過 {secrets.DEPLOY_KEY} 去取得私鑰,小心別弄丟了,不然你要從頭來過。
    DEPLOY_KEY 是 HEXO-ACTION 所規範的 Key Name。

    如果你真的很想修改這個 Key Name,可以把 hexo-action fork 回去自已改

  2. 配置 Github Workflow
    這步驟就相當簡單了,可以直接參考 HEXO-ACTION 的配置,或是看看我的 workflow 配置

後續

一些小錯誤要注意一下

第一點,Hexo Source repository 的 package-lock.json 應該要進版控,
這個檔案本來就應該進版控,不知道為什麼之前被設成 ignore 了,
剛好 hexo-action 有用到 npm ci 語法才發現這個錯誤,相關文章請參考

在 Hexo Source 的設定檔 _config.yml 中的 deploy 區段有兩點要注意,
第一,type 一定要是 git,第二,repository 的路徑請設定為 ssh (不要用 https),
參考以下範例

1
2
3
4
deploy:
type: git
repository: [email protected]:marsen/marsen.github.io.git
branche: master

如此一來,未來我寫好文章,只要 push 上 Hexo Source,就可以打完收工了,
再也不用額外的手動作業囉,這篇文章剛好就是第一篇,馬上來試試看。

心得

相關的技術與知識我都有兩年以上了,
但是一直沒有發現應該將其結合起來,可以省下許多時間,
讓我想到 91 大說的綜效,我應該要讓想像力再開放一些,儘可能的把知識與資訊變成真正的價值。

參考

(fin)

[學習筆記] React useEffect

簡介 useEffect

在 RC(React Component) 當中,useEffect 是一個常用 Hook,用來處理一些副作用(side-Effect)。

用 FP(Functional Programming) 的角度來看,RC 只是依照狀態(state)或是參數(prop)來呈現不同的外觀,就像是一個單純的 Pure Function。
FP 一些常見的副作用如下:

  • I/O (存取檔案、寫 Log 等)
  • 與資料庫互動
  • Http 請求
  • DOM 操作

而 Http 與 DOM 正好是我們開發 RC 最常接觸到的副作用,
useEffect 就是要來解決這個問題。

如何運作

  1. RC 在渲染的時候會通知 React
  2. React 通知 Browser 渲染
  3. Browser 渲染後,執行 Effect

使用範例

下面是 VSCode 外掛產生的程式片段

1
2
3
4
5
6
7
useEffect(() => {
first;

return () => {
second;
};
}, [third]);

我們可以看到 useEffect 需要提供兩個參數,一個函數與一個陣列
這裡有三處邏輯,first、second、third,
為了更好說明概念,順序會稍會有點跳躍,請再參考上面程式範例

相依(dependencies)

third 就是指與此副作用相依的參數,如果不特別加上這個參數,
每次重新渲染都會觸發 Effect 的第一個參數的函數,
如果只提供一個空陣列,就只會在第一次渲染的時候觸發,這很適合用在初始化的情況。
觸發時機:

  • 不傳值,每次渲染時
  • [],只有第一次渲染時
  • [dep1,dep2,…]

注意的事項與 useMemo

useEffect 在判斷觸發的 dependencies 參數,
會有 Primitive 與 Non-primitive 參數的差別。

Primitive: 比如 number、boolean 與 string
Non-primitive: 比如 object、array
這兩種參數的差異在於 JavaScript 在實作時,記憶體的使用方式

Primitive 會直接將值記錄在記憶體區塊中,
Non-primitive,則會另外劃一塊記憶體位置存值,再將這塊記憶體位址存到參數的記憶體位址中。
所以當我們比較的是記憶體位址時,即使內部的值都相同也會回傳 false

參考範例

1
2
3
4
5
const [staff, setStaff] = useState({ name: "", toggle: false });
useEffect(() => {
console.count(`staff updated:${JSON.stringify(staff)}`);
}, [staff]);
//}, [staff.name, staff.toggle]);

每次我們點擊 Change Name 按鈕的時候,都會呼叫改變命名的方法,

1
const handleName = () => setStaff((prev) => ({ ...prev, name: name }));

但是即時我們的 name 沒有改變,仍然觸發 Effect,
這是因為 staff 是一個 Non-Primitive 型別的變數。
一種簡單暴力的方法是將物件展開放入 [] 之中。

這裡我們無法使用展開運算符(Spread Operator)
因為一般的物件沒有 Symbol.iterator 方法

Only iterable objects, like Array, can be spread in array and function parameters.
Many objects are not iterable, including all plain objects that lack a Symbol.iterator method:

物件展開放入 [] 之中的明顯缺點是,當物件變的太大或複雜的時候,
你的 [] 會變得又臭又長。

解法可以使用 useMemo
useMemo 會回傳一個存在記憶體裡的值,
完整範例請參考

1
2
3
4
5
6
7
8
const memo = useMemo(
() => ({ name: staff.name, toggle: staff.toggle }),
[staff.name, staff.toggle]
);

useEffect(() => {
console.count(`staff updated:${JSON.stringify(memo)}`);
}, [memo]);

函數

回到我們的程式片段
first 其實就是處理 Effect 函數,我們可以把 Effect 寫在這裡,
範例中都是寫 console.log 實務上更多會是打 API、fetch etc… 的行為

Clean up functions

useEffect 的第一個參數是一個函數,可以回傳一個 callback function 用來清除 side Effect。

舉例說明:
想像我們有兩個連結用來切換不同的使用者資料,
在切換的過程中會打 api 取得不同使用者(user1 與 user2)的資料,

我們會先點 user1 的連結取 user1 資料(因網速過慢,此時資料還沒回來),
再點 user2 的連結取取 user2 資料(user2 的資料也還沒回來),
這時,user1 資料回來了,畫面會渲染 user1 資料(實際上,這時候我想看 user2 的資料),
再過一會兒,user2 資料回來了,畫面閃爍,渲染 user2 資料。

線上範例看這,使用網速網路會更明顯。

這是表示當我們點擊 user2 的連結時,其實我們已經拋棄了點擊 user1 的結果(對我們來說已經不需要了),
這時候 Clean up function 就是用來處理這個不再需要的 side-effect。

我們可以簡單設定一個 toggle 來處理這個問題

1
2
3
4
5
6
7
8
9
10
11
12
13
useEffect(() => {
let fetching = true;
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((res) => res.json())
.then((data) => {
if (fetching) {
setUser(data);
}
});
return () => {
fetching = false;
};
}, [id]);

我們也可使用 AbortController,
有關 AbortController 的相關資訊可以參考隨附連結,或是留言給我再作介紹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(`https://jsonplaceholder.typicode.com/users/${id}`,{ signal })
.then((res) => res.json())
.then((data) => {
setUser(data);
})
.catch(err=>{
if(err.name === "AbortError){
console.log("cancelled!")
}else{
//todo:handle error
}
});
return () => {
controller.abort();
};
}, [id]);

以上,我們就介紹完了 useEffect 的三個部份(函數、回呼函數與相依數列)與用法。

如果你使用常見的套件 axios 應該怎麼作 clean up,
補充範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
useEffect(() => {
const cancelToken = axios.cancelToken.source();
axios(`https://jsonplaceholder.typicode.com/users/${id}`,{ cancelToken:cancelToken.token })
.then((res) =>
setUser(res.data);
})
.catch(err=>{
if(axios.isCancel(err)){
console.log("cancelled!")
}else{
//todo:handle error
}
});
return () => {
cancelToken.cancel();
};
}, [id]);

why render twice with React.StrictMode

請參考官方文章,
StrictMode 可以幫助開發者即早發現諸如下列的問題:

  • Identifying components with unsafe life-cycles
  • Warning about legacy string ref API usage
  • Warning about deprecated findDOMNode usage
  • Detecting unexpected side effects
  • Detecting legacy context API
  • Ensuring reusable state

小結

  • React.StrictMode 可以幫助你檢查組件的生命周期(不僅僅 useEffect)
  • React.StrictMode 很有用不應該考慮移除它
  • useEffect 包含三個部份
    • 第一個參數(function),處理 side-Effect 的商業邏輯
    • 第一個參數的回傳值(clean-up function),處理 side-Effect 中斷時的邏輯(拋錯、釋放資源 etc…)
    • 第二個參數表示相依的參數陣列
      • 不傳值將會導致每次渲染都觸發副作用
      • 傳空陣列將會只執行一次
      • 相依的參數需注意 Primitive 與 Non-primitive
      • useMemo 可以協助處理 Primitive 與 Non-primitive

參考

[學習筆記] TypeScript Omit 的用法

簡介

本文簡單介紹一下 TypeScript Omit 的用法與範例,
更多資料可以參考官方文件

說明

Omit 字義為忽略、省略,在 TypeScript 中是一種 Utility Type,
使用方法如下:

1
type NewType = Omit<OldType, "name" | "age">;

第一個參數是傳入的 Type, 第二個參數是要忽略的欄位,
並會回傳一個新的 Type, 這是 TypeScript Utility Types 的標準用法。

具體一個例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface LocationX {
address: string;
longitude: number;
latitude: number;
}

interface SpecificLocation extends Omit<LocationX, "longitude" | "latitude"> {
coordinate: {
longitude: number;
latitude: number;
};
}

type AnotherLocation = Omit<SpecificLocation, "coordinate"> & {
x: number;
y: number;
};

const example1: SpecificLocation = {
address: "example first on some where",
coordinate: {
latitude: 100,
longitude: 100,
},
};

const example2: AnotherLocation = {
address: "example 2nd on some where",
};

console.log(example1);
console.log(example2);

說明:

  1. interface 可以用 extends 加上新欄位,借此實現 override 欄位的功能
  2. type 使用 & 作擴展,一樣的方式先省略(Omit)再擴展欄位
  3. 相反的 Utility Type 有 Pick

完整範例

這些 Utility Types 蠻有趣的,有時間應該將它們補完。

20220913 補充

進一步探討 Omit

  1. Omit 適用於 typeinterface

  2. Omit 相反的就是 Pick

  3. OmitExclude 的差別

    • Omit 移除指定 Type 的欄位
    • Exclude 移除 union literal Type 當中的成員
    • Exclude 可以適用於 enum

舉例說明:

下面兩個例子分別使用了 union literalenum,
在這種情況下如需要忽略部份的成員, 請使用 Exclude,
你要用 Omit 也不會報錯, 但是無法達到編譯時期警告的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type TrafficLight = "red" | "yellow" | "green";

type RedLight = Exclude<TrafficLight, "yellow" | "green">;
type GreenLight = Omit<TrafficLight, "red">;

const sighs: {
Danger: RedLight;
Warning: TrafficLight;
Safe: GreenLight;
} = {
Danger: "red", // if you use other words would get error
Warning: "yellow",
Safe: "there is no constraint any string",
};

console.log(sighs);

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum TrafficLight {
red,
yellow,
green,
}

type RedLight = Exclude<TrafficLight, TrafficLight.yellow | TrafficLight.green>;
type GreenLight = Omit<TrafficLight, TrafficLight.red>;

const sighs: {
Danger: RedLight;
Warning: TrafficLight;
Safe: GreenLight;
} = {
Danger: TrafficLight.red, // if you use yellow or green would get error
Warning: TrafficLight.yellow,
Safe: TrafficLight.red,
};

console.log(sighs);

範例

參考

(fin)

[實作筆記] 如何更無痛更新專案中的 npm 相依套件

前情提要

純屬個人偏見,我討厭 Nodejs。
有幾個原因,
首先,使用 javascript 作為開發語言,用弱型別直譯式的語言來開發大型專案是痛苦的。
不論在 TDD、重構或是除錯上…我始終作不到極速開發那樣流暢的感覺,
即使改用 TypeScript,儘管看到 TypeScript 的可能性,這樣的感覺還是揮之不去。
很希望有機會可以跟這樣的高手交流。

再者,node_modules 是出了名的零碎肥大,網路上甚至為它作了迷因,如下圖
node_modules

第三點,專案的 npm 更新是相當麻煩的,也是這篇文章我們要面對的問題,
通常我們的專案,隨著時間開發,會與越來越多 npm 套件,先不論它會不會變成黑洞影響時空,
我最常遇到的問題是,我不知道有哪些套件需要被更新 !??
這些更新有的無關痛養,有的是提昇效能,有的是安全性更新…
不論如何,我的目標是更新更加的流暢,而不要被一些奇怪的問題阻檔。

但實務上,更新之路是異常充滿危機與苦難…

實際案例

在這裡我用一個我的學習用專案來進行演練 Marsen.React.Practice
這個專案是我用來練習 React 相關的開發,相比任何商務專案,相信會小得多。
但是它相依的套件依然多得驚人,有的與字型相關,有的與 TypeScript 型別相關,
有多語系、有 firebase、有 RTK(React-Tool-Kit)、有 Router、有 Form etc…
隨隨便便也有近 30 幾個相依套件需要管理、更新。

比較好的或是有名的套件,或許會官宣一些更新的原因與細節,
但是實際上,更多套件的更新你是不會知道的。

你可以這樣作

1
> npm update

然後 BOOM !! 就爆炸了

npm update

你可以看 log、可以解讀這些錯誤訊息、可以梳理其背後的原因,然後用你寶貴的生命去修復它,
過了幾周或是幾天,新版本的更新又釋出… 那樣的苦痛又要再經歷一次又一次…
或許你會想那就不要更新好了,直到某一天一個致命的漏洞被發佈或是資安審查時強制要求你更新,
或是因為沒有更新,而用不了新的酷功能,這真是兩難,所有的語言都有類似的問題,但 Nodejs 是我覺得最難處理、最令人髮指的一個。

原來有方法

原來有不痛的方法,參考如下
首先執行 npm outdated
npm outdated

如上圖,我們可以發現在套件後面會有 Current/Wanted/Latest 三種版號
分別顯示:目前專案的套件版本/semver 建議的最新版本/Registry 中標注為 latest 的版本
如圖所示,有時候你 Current 版本會比 Latest 版本還新,不用太過揪結與驚訝,
更多可以參考官方文件的說明

接下來我們試著來解決這些套件相依,
我們全域安裝 ncu(npm-check-updates) 這個工具

1
> npm install -g npm-check-updates

執行更新

1
> ncu -u

魔術般完成了更新,沒有任何痛點。
再次執行 npm outdated 將不會看到任何顯示,表示你目前專案的相依套件都在最新的穩定版本
定期執行或是透過 CI/CD 周期性協助你更新並 commit,是不是就會省下了許多時間呢 ?

20220928 後記

參考

(fin)

[實作筆記] Golang DI Wire 使用範例與編輯器 GoLand 設定

前情提要

學習 Golang 一陣子了,最近開始使用在正式的產品上,
老實說我還不覺得有用到它的特色。

在學習程式語言上的一個現實是,我在台灣面臨的商業規模大多瓶頸並不在語言本身,
開發者的寫法(甚至稱不上演算法)、軟硬架構基本上可以解決大部份的問題。

我的背景是 C#、JavaScript(TypeScript) 為主要開發項目,
除此之外,也有寫過 C、C++、Java、Php、VB.NET、Ruby 與 Python

Go 的優勢常見如下:

  1. 讓人容易了解的語意:作為漢語母語者我感受不強烈,我的英文不夠好可以感受到這點(同樣我對 Python 的感受也不深)
  2. 容易上手的多緒:這個有感,相比 C# 的確易懂好寫
  3. 靜態語言: 原本寫 Python 的開發人員可能比較有感,對我來說這是基本(C# 開發者角度)
  4. 高效,快:目前的專案複雜性還未可以感到其差異,或許需要壓測用實際數據比較。
    經驗上是,錯誤的架構或寫法往往才是瓶頸之所在。
  5. 編譯快:在 C# 的不好體驗,巨大單體專案,不包測試編譯就要 2~5 分鐘,不曉得 Go 在這樣情況的表現如何? 不過目前主流開發方式為雲原生,微服務,或許有生之年不會再看巨型單體專案了
  6. 原生測試:這點我覺得真是棒,我的學習之路就是由 Learn Go with tests 開始的
  7. IOP:介面導向程式設計,目前還無法體會其哲學,不過因為其語言的特性會促使人思考,這點我還在慢慢嚐試

一個新的語言我會從測試開始學,
這表示你通常會需要這些工具:測試框架、相依注入、Mocking、語意化 Assert,
本文會專注在使用相依注入的套件 WIRE

WIRE

一些 Q&A

為什麼選用 WIRE ?

google 官方推薦 Wire

還有哪些選擇 ?

有什麼不同

官方推薦,本質上更像代碼生成器(Code Generator)

Clear is better than clever ,Reflection is never clear.
— Rob Pike

示範

參考本篇文章

高耦合版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"bytes"
"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
fmt.Println(message)
}

type HttpClient struct {
logger *Logger
}

func (client *HttpClient) Get(url string) string {
client.logger.Log("Getting " + url)

// make an HTTP request
return "my response from " + url
}

func NewHttpClient() *HttpClient {
logger := &Logger{}
return &HttpClient{logger}
}

type ConcatService struct {
logger *Logger
client *HttpClient
}

func (service *ConcatService) GetAll(urls ...string) string {
service.logger.Log("Running GetAll")

var result bytes.Buffer

for _, url := range urls {
result.WriteString(service.client.Get(url))
}

return result.String()
}

func NewConcatService() *ConcatService {
logger := &Logger{}
client := NewHttpClient()

return &ConcatService{logger, client}
}

func main() {
service := NewConcatService()

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

在上面的程式中,可以明顯看到 ConcatService 相依於 HttpClientLogger
HttpClient 本身又與 Logger 耦合。
這是一種高耦合,在這個例子裡 Logger 還會產生兩份實體,但實際上我們只需要一份。

Golang 實際上不像 C# 有建構子(Constructor)的設計,不過常見的實踐會用大寫 New 開頭的方法作為一種類似建構子的應用,
比如說上面例子的 NewConcatServiceNewHttpClient
我們可以透過這個方法來注入我們相依的服務。

相依注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"bytes"
"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
fmt.Println(message)
}

type HttpClient struct {
logger *Logger
}

func (client *HttpClient) Get(url string) string {
client.logger.Log("Getting " + url)

// make an HTTP request
return "my response from " + url
}

func NewHttpClient(logger *Logger) *HttpClient {
return &HttpClient{logger}
}

type ConcatService struct {
logger *Logger
client *HttpClient
}

func (service *ConcatService) GetAll(urls ...string) string {
service.logger.Log("Running GetAll")

var result bytes.Buffer

for _, url := range urls {
result.WriteString(service.client.Get(url))
}

return result.String()
}

func NewConcatService(logger *Logger, client *HttpClient) *ConcatService {
return &ConcatService{logger, client}
}

func main() {
logger := &Logger{}
client := NewHttpClient(logger)
service := NewConcatService(logger, client)

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

我們把焦點放在 main 函數中,實作實體與注入會在這裡發生,當你的程式變得複雜時,這裡也變得更複雜。
一個簡單的思路是我們可以把重構這些邏輯,
到另一個檔案 container.goCreateConcatService 方法中。

1
2
3
4
5
6
7
8
// container.go
package main

func CreateConcatService() *ConcatService {
logger := &Logger{}
client := NewHttpClient(logger)
return NewConcatService(logger, client)
}
1
2
3
4
5
6
7
8
9
10
func main() {
service := CreateConcatService()

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

接下來我們看看怎麼透過 wire 實作

使用 Wire

安裝 wire

1
go get github.com/google/wire/cmd/wire

接下來改寫 container.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//go:build wireinject

package main

import "github.com/google/wire"

func CreateConcatService() *ConcatService {
panic(wire.Build(
wire.Struct(new(Logger), "*"),
NewHttpClient,
NewConcatService,
))
}

在專案中執行

1
wire

wire 將會產生 wire_gen.go 檔,裡面幫你實作 CreateConcatService 函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from container.go:

func CreateConcatService() *ConcatService {
logger := &Logger{}
httpClient := NewHttpClient(logger)
concatService := NewConcatService(logger, httpClient)
return concatService
}

簡單回顧一下:

  1. 我們需要針對特定的 Service 寫出一個方法的殼

    1
    2
    3
    func CreateConcatService() *ConcatService {
    ////skip
    }
  2. 在方法中透過 wire.Build 加入相依的類別,實際上我們不需要 return 實體,所以用 panic 包起來

    1
    2
    3
    4
    5
    6
    7
    func CreateConcatService() *ConcatService {
    panic(wire.Build(
    wire.Struct(new(Logger), "*"),
    NewHttpClient,
    NewConcatService,
    ))
    }
  3. 執行 wire 建立檔案

  4. 實務上需要這個 Service 時,直接呼叫 Create 方法

    1
    service := CreateConcatService()

GoLand 設定

如果你跟我一樣使用 GoLand 作為主要編輯器,
應該會收到 customer tags 的警告
customer tags

解決方法:
GoLand > Preferences > Build Tags & Vendoring > Editor Constraints > Custom Tags
設定為 wireinject 即可

參考

(fin)

[實作筆記] Dialog Button 實作 React 父子元件交互作用

前情提要

最近我設計了一個 Dialog Button ,過程十分有趣,稍微記錄一下

需求

首先 Dialog Button 的設計上是一個按鈕,被點擊後會開啟一個 Dialog,
它可以是表單、一個頁面、另一個組件或是一群組件的集合。
簡單的說 Dialog 可以是任何的 Component。

用圖說明的話,如下

Dialog Button

藍色表示父層組件,我們會把 Dialog Button 放在這裡
綠色就是 Dialog Button 本身,它會有兩種狀態,Dialog 的隱藏或顯示
紅色則是任意被放入 Dialog Button 的 Component。
也就是說我們會用 attribute 傳遞 Component 給 Dialog Button

注意黃線的部份,我們會幾種互動的行為

  1. Component 自身的行為,比如說計數器 Component 的計數功能
  2. Component 與 Dialog Button 的互動,比如說關閉 Dialog Button
  3. Component 與 Dialog Button 的 Parents Component 互動,比如說重新 Fetch 一次資料

具體的 Use Case 如下,
我在一個表格(Parents Component)中找到一筆資料,
點下編輯(Dialog Button)時會跳出一個編輯器(Component),
編輯器在我錯誤輸入時會提示1 警告訊息,
在修改完畢儲存成功時,會關閉編輯器2並且同時刷新表格資料3
參考下圖
count、close and fetch

實作

首先先作一個陽春的計數器,這個是未來要放入我們 Dialog 之中的 Component,
這裡對有 React 經驗的人應該不難理解,我們透過 useState 的方法來與計數器互動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Counter() {
const handleClick = () => {
setCount(count + 1);
};
const [count, setCount] = useState(0);
return (
<>
Count :{count}
<Stack spacing={2} direction="row">
<Button onClick={handleClick} variant="contained">
Add
</Button>
</Stack>
</>
);
}

接下來我們來實作 Dialog Button,大部份的實作細節我們先跳過
我們來看看如何使用傳入的 Component 並將其與 Dialog 內部的函數銜接起來

1
2
3
<DialogContent>
{cloneElement(props.content, { closeDialog: closeDialog })}
</DialogContent>

這個神奇方法就是 cloneElement
從官方文件可知,我們可以透過 config 參數提供 prop、key 與 ref 給 clone 出來的元素,

1
React.cloneElement(element, [config], [...children]);

所以我們將 closeDialog 方法作為一個 props 傳遞給 dialog 內開啟的子組件,比如說 : Counter

完整程式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function DialogButton(props: DialogButtonProps) {
const [open, setOpen] = useState(false);
const openDialog = () => {
setOpen(true);
};

const closeDialog = () => {
setOpen(false);
};

return (
<>
<Button {...props} onClick={openDialog}>
{props.children}
</Button>
<Dialog
open={open}
onClose={closeDialog}
maxWidth={props.max_width}
fullWidth
>
<DialogTitle>
{props.title}
<IconButton
aria-label="close"
onClick={closeDialog}
sx={{
position: "absolute",
right: 6,
top: 6,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{cloneElement(props.content, { closeDialog: closeDialog })}
</DialogContent>
</Dialog>
</>
);
}

最後我們父層元素,也可以將自身的方法傳遞給 content Component
以達到 Component 與父層元素互動的功能,不需要再經過 Dialog Button 了

parent interact with content

參考

(fin)

[實作筆記] 最近操作 k8s 的常用指令集錦 (透過 GKE 實作)

Docker 相關

在建立映像檔的時候

--build-arg 可以提供參數給 Dockerfile

e.g

1
2
3
docker build \
--build-arg PackageServerUrl="{Url}" \
-t deposit -f ./Dockerfile .

在 container 裡 一些 Linux 常用指令

有時候使用太輕量的 image 會缺少一些工具,
我們可以直接換成有工具的 image。
如果真的需要即時的在 container 裡面操作,
可以參考以下語法
更新 apt-get

apt-get update

透過 apt-get 安裝 curl (其它套件也適用)

apt install curl

印出環境變數
方法一、 後面可以加上指定的變數名

printenv {environment value}

方法二、 效果跟 printenv 一樣

env | grep {environment value}

GKE(k8s)相關指令

準備好 manifests.yml,

GCP 授權

gcloud container clusters get-credentials {kubernetes cluster name} –region {region} –project {project name}

e.g

  • gcloud container clusters get-credentials gke-cluster-qa –region asia-east1 –project my-first-project

取得專案列表

kubectl config get-contexts

切換專案(prod/qa)

kubectl config use-context {namespace}

e.g

kubectl config use-context {prod_namespace}
kubectl config use-context {qa_namespace}

套用 manifests(記得切換環境)

kubectl apply (-f FILENAME | -k DIRECTORY)

e.g

kubectl apply -f ./manifests.yml
kubectl apply -f ./k8s/manifests.yml –namespace=prod

查詢 configMap

  • kubectl -n=qa get configmap {config_name} -o yaml

查詢 secret

  • kubectl -n prod get secret {secret_name} -o json
  • kubectl -n qa get secret {secret_name} -o json

pods port 轉入開發者環境

kubectl -n={namespace} port-forward {service} 80:80

e.g

kubectl -n=qa port-forward service/deposit 80:80

取得資訊

  • kubectl -n=qa get po
  • kubectl -n=qa get deploy
  • kubectl -n=qa get svc

進入 pods terminal 環境

kubectl -n=qa exec -it {pods_name} – /bin/bash

e.g

kubectl -n=qa exec –stdin –tty my-service-5b777f56b8-q7lf7 – /bin/sh

重啟服務

kubectl rollout restart {service_name} -n qa

e.g

kubectl rollout restart my-service-n qa

其它 CI 中用到的指令

綜合應用

設定環境變數,並且置換 yml 檔中的參數,最後 apply 到線上環境

e.g

export K8S_NAMESPACE=qa && envsubst < k8s/manifests.yml | kubectl -n=$K8S_NAMESPACE apply -f -

export K8S_NAMESPACE=qa && envsubst < k8s/configmap.qa.yml | kubectl -n=$K8S_NAMESPACE apply -f -

manifests.yml 的範例

Deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: apps/v1
kind: Deployment
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $SERVICE_NAME
template:
metadata:
labels:
app: $SERVICE_NAME
spec:
containers:
- name: $SERVICE_NAME
image: "asia-east1-docker.pkg.dev/$PROJECT_NAME/docker/$SERVICE_NAME:latest"
imagePullPolicy: Always
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: my-service-config
- secretRef:
name: my-service-secret

Service

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
type: ClusterIP
selector:
app: $SERVICE_NAME
ports:
- port: 8080
targetPort: 8080

ConfigMap

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
namespace: $K8S_NAMESPACE
data:
THIRD_PARTY_URL: "https://third_party_url/"
THIRD_PARTY_TOKEN: TBD
THIRD_PARTY_ID: TBD

Secret

!!要記得作 base64 Encode

1
2
3
4
5
6
apiVersion: v1
kind: Secret
metadata:
name: my-service-secret
namespace: $K8S_NAMESPACE
data: secret.Key:<<must encoded base64>>

manifest 實作上的 tip

更多參考官方文件與範例,設定太多了,不懂就去查
可以用 ---- 串連多份文件,但我實務上不會串 Secret
e.g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $SERVICE_NAME
template:
## 中間省略
----
apiVersion: v1
kind: Service
metadata:
name: $SERVICE_NAME
## 中間省略
----
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
## 以下省略

參考

(fin)

[實作筆記] 12-Factor Config 使用 Golang (with Viper) II

前情提要

在前篇提到了如何透過 Viper 來取得 config 檔與環境變數來組成我們的組態檔。
這篇我們會進一步討論,透過 Go 的強型別特性,取得組態物件,並處理巢狀的結構。

假想情境

假設今天我們要作一個金流服務,
後面可能會介接 PayPal、Strip 等等不同金流的服務,
那我的組態設定上可能就會有不同的階層關係

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "1.10.1",
"Stripe": {
"Enable": true,
"MerchantId": 123,
"ClientId": 123
},
"Paypal": {
"Enable": false,
"MerchantNo": "A003"
}
}

那麼問題來了

  1. 我應該怎麼取用這些組態 ?
    • 可以直接透過 viper.Get("name") 取得值
    • 但是每次都要用組態名稱字串取得值,其實是一個風險;
      比如打錯字,很難有工具有效防範
    • 所以我希望建立一個 type 去定義組態檔,並透過物件去存取,
      這可以有效將寫死字串限定至一次。
  2. 有了上述的方向後,另一個問題是巢狀解構的解析,
    一般來說,我認為組態檔不應該有超過三層的深度,這次就用兩作為範例說明
  3. Viper 本身支援多種組態格式,本篇僅以 envjsonyaml 作範例說明
  4. 在上一篇提到,當存在環境變數時,環境變數的優先度應高於檔案

期待的結果

我們在啟動程式的時候,一次性將組態載入事先定義好的物件之中,
如果有環境變數,優先取用。
如果沒有環境變數,會讀開發者提供的組態檔(env 檔優先,也要能支援 json 或 yaml )
如果完全沒有組態,應該發出警告

定義 Type

首先建立我們的 Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Config struct {
Version string
Stripe StripeType
Paypal PaypalType
}

type StripeType struct {
Number int
Enable bool
}

type PaypalType struct {
MerchantNo string
Enable bool
}

接下來我們如果上一篇宣告 Of 方法用來取得組態

1
2
3
4
5
6
7
8
// define instance
var instance *Config
func Of() *Config {
once.Do(func() {
Load()
})
return instance
}

此時我們要將組態載入,先考慮開發者環境的實體檔案

1
2
3
4
5
6
7
8
9
func Load() {
vp := viper.New()
vp.AddConfigPath(".")
// todo here, change config name & type
vp.SetConfigName("config.json")
vp.SetConfigType("json")
vp.ReadInConfig()
vp.Unmarshal(&instance)
}

實作 env 使用的 config.env 檔案如下

1
2
3
4
5
Version="1.10.1.yaml"
Stripe.Enable=true
Stripe.Number=123
Paypal.Enable=false
Paypal.Merchant_No="A003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.env")
vp.SetConfigType("env")

實作 yml 使用的 config.yml 檔案如下

1
2
3
4
5
6
7
Version: "1.10.1.yml"
Stripe:
Enable: true
Number: 765
Paypal:
Enable: true
MerchantNo: "Yml003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.yml")
vp.SetConfigType("yml")

實作 json 使用的 config.json 檔案如下

1
2
3
4
5
6
7
8
9
10
11
{
"version": "1.10.1.json",
"stripe": {
"enable": true,
"number": 123
},
"paypal": {
"enable": true,
"merchantNo": "Json003"
}
}

修改 viper

1
2
3
// just for development environment
vp.SetConfigName("config.json")
vp.SetConfigType("json")

以上提供了幾種不同的 type

環境變數

我使用的環境變數如下

1
STRIPE__ENABLE=true;STRIPE__No=678;VERSION=8.88.8;PAYPAL__ENABLE=false;PAYPAL__MERCHANT_NO=ENV9999

首先,我們知道環境變數並不像 jsonyaml 檔一樣可以提供巢狀的結構,
這就與我們的需求有了衝突,好在 viper 提供了 BindEnv 的方法, 我們可以強制讓它建立出巢狀的結構,
如下:

1
2
3
4
5
vp.BindEnv("VERSION")
vp.BindEnv("Stripe.NUMBER", "STRIPE__No")
vp.BindEnv("Stripe.Enable", "STRIPE__ENABLE")
vp.BindEnv("Paypal.Enable", "PAYPAL__ENABLE")
vp.BindEnv("Paypal.MerchantNo", "PAYPAL__MERCHANT_NO")

寫在後面,在查找資料的過程中,可以發現 viper 提供了兩個功能強大的方法,
SetEnvPrefix 與 SetEnvKeyReplacer ;
SetEnvPrefix 可以自動加上前綴,SetEnvKeyReplacer 可以將分隔符置換。
可惜在我的情境尚且用不到,未來再作研究。

參考

(fin)

[實作筆記] Hexo Generate Error 處理

前情提要

我的 Blog 是透過 Hexo 這套框架建立出來的,
流程是:

  1. 撰寫文章
  2. 執行 hexo g 建立靜態檔
  3. 如果想在本地看 Blog 的效果,可以用 hexo s
  4. 執行 hexo d 部署到 Github

更多細節可以參考我以前寫得相關文章

問題

在上述的第 2 步驟,執行命令後,雖然可以產生靜態檔,
但會伴隨著以下的錯誤訊息,這就非常擾人了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FATAL {
err: [OperationalError: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
cause: [Error: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
},
isOperational: true,
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
}
} Something's wrong. Maybe you can find the solution here: %s https://hexo.io/docs/troubleshooting.html

這個錯誤訊息 OperationalError: EPERM: operation not permitted, unlink… .
是一個非常籠統的錯誤訊息,來自作業系統的底層,中間經過 hexo 與 node 的流程,
如果不深入鑽研(但是我沒有要深入),我們難以知道錯誤的細節。

好在,我發現當刪除了 public 資料夾之後,
再次執行 hexo g 就不會有錯誤訊息。

解決方法

簡單來說,我可以在每次 hexo -g 之前刪除 public 資料夾就可以了。
以下可以用一行指令替代。

1
rm -rf public | hexo -g

再進一步,我修改了 alias 指令如下

1
alias hxg="rm -rf public | hexo g"

如此一來,每次我執行 hxg 的時,就會依上述步驟先刪除再建立 public 靜態資料。
有關 alias 的設定,可以參考這篇文章

參考

(fin)