0%

前言

好久沒更新了,這次在工作上遇到一點問題,也採了不少的坑,所以來記錄一下。

問題

為了追求 Performance,如果有使用到 SSR 框架(例如 Next 或 Nuxt),有時會把獲取資料這個步驟放在 Server Componet 來做,如此一來資料會在 Client 端請求頁面當下就發送,而不是等到開始渲染頁面時才發送(可以參考這篇文章)。

不過這樣會有個問題,如果我們今天發送的請求是需要做身分驗證的,不論是透過 Session 還是 Token,我們都需要把 Cookie 帶上,但是在 Server Component 中,我們沒辦法直接使用 document.cookie[註] 來取得 Cookie,因為這是在 Server 端執行的,所以我們需要找到一個方法來取得 Cookie。

[註] 有時候會看到沒有設定 cookie 還是可以直接發送請求,那是因為瀏覽器自動幫我們在發送請求時也把 cookie 帶上,同樣 server 端也沒有這個功能。

解法

在 next 中,可能會看到某些文章表示可以使用 getServerSideProps 來取得 cookie,但是這個方法只適用12以下的版本,從13開始的版本已經不支援了,所以我們需要找到其他方法。

根據官方文件,他們已經把原生的 fetch 做擴充,所以不再需要 getServerSideProps 來取得資料,而是直接使用 fetch 就可以了。同時官方也提供了 cookie 以及 header 可以讓我們在 server component 也能自定義 cookie 以及 header。

但是具體到底要怎麼攜帶 cookie ? 使用 TS 你就會發現 cookie() 給的資料根本不是字串而是 ReadonlyRequestCookies 這個型別,我參考了 stackoverflow 上的這篇文章才知道要怎麼使用。cookie() 可以直接使用 toString(),所以在 header 中設定 cookie 時,可以直接使用 cookie().toString()。

例外處理

這邊也有一個小小的坑,就是 response 的錯誤處理,如果 server 掛了或是使用者給的資料不對導致 response 是 4xx 或 5xx 怎麼辦?

官方的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.

if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}

return res.json()
}

export default async function Page() {
const data = await getData()

return <main></main>
}

我們要另外設定一個 error.tsx 來處理,官方文件把他放在 Error handling的章節 (小抱怨: Fetch Data 章節沒有提供超連結也沒有額外說明和 error.ts 的關係,甚至在 ts 範例中的註解也沒有將 error.js 改成 error.ts 或 error.tsx。像我這種有問題才會翻文件的人很難去注意 Error handling 章節會和這裡有關。)

範例程式碼中的 throw new Error('Failed to fetch data') 會去觸發最靠近當前頁面的 error 文件,並且去渲染

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
'use client' // Error components must be Client Components

import { useEffect } from 'react'

export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])

return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}

額外補充

這裡補充一個和前面比較無關,但同樣也是我在解決這個問題的過程中有遇到的狀況。

在使用 fetch 時,如果你的網址是使用相對路徑,例如 /api/...,那麼在 server component 中就會變成 http://localhost:3000/api/...,這樣就會導致跨域的問題,所以要改成使用絕對路徑,例如 https://inthuang.tw/api/...。當然在開發種比較常用到的是把網域設定成環境變數,例如 process.env.NEXT_PUBLIC_DOMAIN,這樣就可以在不同環境中使用不同的網域。

小結

這次的問題其實不算很大,但是在解決的過程中還是花了不少時間,也遇到了各種狀況糾纏在一起,所以就來記錄一下。

不知道大家有沒有看過我之前寫的一篇心得文 「The Clean Coder」閱讀筆記,作為 The Clean Coder 的姊妹書,The Clean Code 應該是大家更耳熟能詳的,而本篇文將會紀錄我閱讀這本書的心得筆記。

因為章節很多,心得文也會分成幾篇,這篇文將會紀錄第一章的內容,我會盡量以一章一篇的形式來記錄,不過要是內容太多也會拆開來寫,那麼就讓我們開始吧!

無暇的程式碼

第一章的重點不外乎於介紹什麼是無瑕的程式碼以及我們為何需要,作者提出了許多知名人士的話來佐證這個觀點,例如:Bjarne Stroustrup (C++ 的發明人)、Gray Booch (Object Oriented Analysis and Design with Application 一書作者)、Dave Thomas (OIT 的創立者)等等,他們都有各自的觀點,但大致上都在說明 clean code 的重要性。

作者也有提出自己的看法,他也強調本書所提出的方法都只是實踐 clean code 的其中一種方法,並非絕對。

童子軍原則

這一小段大概就是第一章的精華了,前面洋洋灑灑寫了一大堆,不外乎就是要引出 clean code 的重要性,而童子軍原則代表的意義是: 在離開前讓你的營地比你到來前更乾淨。套用於軟體開發上,我們應該要在每次寫完一個功能後,讓程式碼比我們寫之前更乾淨

小結

沒錯,這一章就是這麼短,不過真正精采的在後頭,下一篇文將會紀錄第二章的內容,敬請期待!

這篇文章會用一個簡單的範例來說明 Graphwalker 的使用方法,並且會用到 Selenium 來驅動瀏覽器。

關於 Graphwalker

Graph Walker 是一個 Model Base Testing 的工具,它可以讓你用一個 Graph 的方式來描述你的測試案例,他提供一個 UI 介面讓你繪製 Model (以 Graph 的方式呈現),並且可以讓你用 Java 或是 C# 來撰寫測試程式。

範例

本篇將會介紹一個簡單的範例以及我痛苦的踩雷歷程來帶大家入門 Graph Walker,程式碼在我的 GitHub 上可以找到。

環境

  • Windows 10
  • Java JDK 19.0.2 (依照官網說法Java 8 以上都可以)
  • Maven 3.9.0
  • Graphwalker 4.3.0
  • Selenium 4.8.0
    關於 JavaMaven 的安裝可以參考我提供的連結,這邊就不再贅述。

Graphwalker 的安裝

Graphwalker 有兩個工具,一個是 Graphwalker Studio,一個是 Graphwalker CLI,要建模型的話這邊我們只會用到 Studio,所以我們就先安裝 Graphwalker Studio,後面會用到 Graphwalker CLI 的時候再來安裝。

按連結下載後,把它放到你想要的地方,接著打開命令提示字元,輸入以下指令來執行 Graphwalker Studio。

1
java -jar graphwalker-studio-4.3.2.jar

或是直接對他點兩下也可以。

之後打開瀏覽器,輸入 http://localhost:9090/studio.html,就可以看到 Graphwalker Studio 的畫面了。

建立 Model

這邊 Graph Walker的官網文件就寫得蠻清楚的,可以直接參考他首頁的 gif 建立新的 Model 檔案。

新建好空檔案後記得要改名稱。

模型有兩個元素

  • v: Vertex,代表一個狀態或是頁面,是靜態的
  • e: Edge,代表一個動作或事件,是動態的

在 Graphwalker Studio 中

  • 按住鍵盤的 v 並點擊畫面,可以建立一個 Vertex
  • 按住鍵盤的 e 並點擊起點 Vertex 後按住不放,拖曳到目標 Vertex,可以建立一個 Edge

除了使用 Graphwalker Studio 來建立模型,你也可以使用其他工具來建立,例如 yEd,他產出的 graphml 檔案也可以直接拿來使用。

我的模型

我的模型是用來測試我自己之前的 Side Project,是一個電商網站。我想要測試從首頁一路點擊到商品細節頁面的流程,所以我建立了以下的模型。

  • e_init: 模型的起點,在左側設定欄下方 Start Element 可以打開這個設定
  • v_StartBrowser: 啟動瀏覽器
  • e_GetUrl: 進入首頁
  • v_HomePage: 首頁
  • e_ClickProductLink: 點擊商品列表連結
  • e_ClickHomeLink: 點擊首頁連結
  • v_ProductPage: 商品頁面
  • e_ClickProductCard: 點擊商品卡片
  • v_ProductDetailPage: 商品細節頁面

完成後按左方的儲存按鈕,就可以把這個模型儲存下來了。它會自動幫你下載一個叫做 test.json 的檔案,記得重新命名成你想要的名稱,這邊我就叫做 WineWorld.json, 之後你的 JAVA Interface 會用這個命名。

寫測試程式

首先我們要建立一個 Maven 專案,並且加入 Graphwalker 的 Dependency,這邊官方文件就寫的很不清不楚了,好多該加的都沒寫。

  1. 建立 Maven 專案
    1
    mvn archetype:generate

這邊官網本來有給一串指令,意思是說可以直接建立一個包含 graphwalker 的 Maven 專案,但是我試了好幾次都失敗,所以我就直接用上面的指令建立一個空的專案,再加入 graphwalker 的 dependency。

設定
GroupId: tw.inthuang 這個可以自己設定
ArtifactId: WineWorld 這個也可以自己設定
Version: 4.3.2 這個請不要動,會影響到後面的 graphwalker 的版本
Package: WineWorld 這個也可以自己設定

  1. pom.xml 加入 Graphwalker 的 Dependency
1
2
3
4
5
<dependency>
<groupId>org.graphwalker</groupId>
<artifactId>graphwalker-cli</artifactId>
<version>${project_version}</version>
</dependency>
1
2
3
4
5
<dependency>
<groupId>org.graphwalker</groupId>
<artifactId>graphwalker-java</artifactId>
<version>${project_version}</version>
</dependency>
  1. 重要pom.xml 加入 Maven-graphwalker-plugin 的 Plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>org.graphwalker</groupId>
<artifactId>maven-graphwalker-plugin</artifactId>
<version>${project_version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>generate-sources</goal>
</goals>
</execution>
</executions>
</plugin>

上面你可以看到不管是 Graphwalker 的 Dependency 還是 Maven-graphwalker-plugin 的 Plugin,都有一個 ${project_version},這個是我們在第一步驟設定的 Graphwalker 的版本號,如果你的版本號不是 4.3.2,請記得要改成你的版本號。

  1. 加入 Selenium 的 Dependency
1
2
3
4
5
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.8.0</version>
</dependency>
  1. 把 Graphwalker 的 Model 檔案放到 src/main/resources/tw/inthuang/ 資料夾下
    注意,resources 資料夾和 java 資料夾是平行的,不要放錯了,裡面還要包含 GroupId 的資料夾,你的 java 資料夾裡面有幾個資料夾才到自動生成的 App.java,這裡就要有幾個。
1
2
3
src|-main-java-tw-inthuang-App.java
|
|-main-resources-tw-inthuang-WineWorld.json
  1. 建立 Graphwalker 的 Interface
    使用 graphwalker:generate-sources 這個指令,會自動幫你建立 Graphwalker 的 Interface,這個 Interface 會自動幫你實作 Model 的所有 Vertex 和 Edge。
1
mvn graphwalker:generate-sources

完成後你會看到多出一個 target 資料夾,裡面有很多東西,按照 targert -> generated-sources -> graphwalker -> tw -> inthuang -> WineWorld.java 這個路徑,就可以找到一個名為 WineWorld.java 的檔案 (這個檔案的名稱取決於你的 model file 名稱)。不要客氣點開來看看會發現有許多用你剛剛模型的 Vertex 和 Edge 命名的 Method,這些 Method 就是 Graphwalker 會自動幫你實作的。

  1. 實作 Test.java
    打開你的 main 資料夾,最裡面應該有一個預設的 App.java,把它刪掉,然後建立一個名為 WineWorldTest.java 的檔案,裡面寫入以下程式碼。
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package WineWorld;

import java.time.Duration;

import org.graphwalker.core.machine.ExecutionContext;
import org.graphwalker.java.annotation.AfterExecution;
import org.graphwalker.java.annotation.BeforeExecution;
import org.graphwalker.java.annotation.GraphWalker;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.WebDriver;
import org.junit.Assert;

/**
* Hello world!
*
*/
@GraphWalker()
public class WineWorldTest extends ExecutionContext implements WineWorld {

public static FirefoxDriver driver = new FirefoxDriver();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

@BeforeExecution
public void setup() {
System.out.println("Setup happens here");
}

@AfterExecution
public void cleanup() {
System.out.println("Cleanup happens here");
driver.quit();
}

public void e_Init() {
System.out.println("Init");
};

public void e_ClickHomeLink() {
System.out.println("Click Home Link");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.partialLinkText("WineWorld")));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("loading")));
driver.findElement(By.partialLinkText("WineWorld")).click();
};

public void e_ClickProductLink() {
System.out.println("Click Product Link");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.linkText("商品列表")));
driver.findElement(By.linkText("商品列表")).click();
};

public void e_ClickProductCard() {
System.out.println("Click Product Card");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("card")));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("loading")));
driver.findElement(By.className("img-overlay")).click();
};

public void e_GetUrl() {
System.out.println("Get URL");
driver.get("https://inthuang.tw/WineWorld/");
};

public void v_StartBorwser() {
System.out.println("Start Browser");
};

public void v_HomePage() {
System.out.println("Home Page");
wait.until(ExpectedConditions.titleContains("Wine World | 你要找的酒,都在這裡"));
Assert.assertEquals("Wine World | 你要找的酒,都在這裡", driver.getTitle());
};

public void v_ProductPage() {
System.out.println("Product Page");
wait.until(ExpectedConditions.urlContains("#/product"));
Assert.assertEquals("product", driver.getCurrentUrl().substring(32));
};

public void v_ProductDetailPage() {
System.out.println("Product Detail Page");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.tagName("h3")));
Assert.assertEquals("商品介紹", driver.findElement(By.tagName("h3")).getText());
};

public void e_NewEdge() {
System.out.println("New Edge");
};
}

Source

總之就是實現剛剛 Graphwalker 產生的 Interface,然後實作裡面的 Method。

簡單介紹一下大致在做什麼:

1
2
public static FirefoxDriver driver = new FirefoxDriver();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

設定瀏覽器和等待時間

1
2
3
4
5
6
7
8
9
10
@BeforeExecution
public void setup() {
System.out.println("Setup happens here");
}

@AfterExecution
public void cleanup() {
System.out.println("Cleanup happens here");
driver.quit();
}

設定測試開始前和結束後的動作,這邊我們只是印出一些訊息,然後關閉瀏覽器。

1
2
3
public void e_Init() {
System.out.println("Init");
};

這個 Method 就是 Graphwalker 產生的 Interface 裡面的 e_Init,這個 Method 就是在測試開始前會執行的動作,我們只是印出一些提示訊息。

1
2
3
4
5
6
public void e_ClickHomeLink() {
System.out.println("Click Home Link");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.partialLinkText("WineWorld")));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("loading")));
driver.findElement(By.partialLinkText("WineWorld")).click();
};

等待網頁上的 WineWorld 這個 Link 出現,然後點擊它,不過因為我有些頁面有蓋一個 loading 的 overlay,所以又多一行等待他消失的程式。

1
2
3
4
5
public void e_ClickProductLink() {
System.out.println("Click Product Link");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.linkText("商品列表")));
driver.findElement(By.linkText("商品列表")).click();
};

等待網頁上的 商品列表 這個 Link 出現,然後點擊它。

1
2
3
4
5
6
public void e_ClickProductCard() {
System.out.println("Click Product Card");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("card")));
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("loading")));
driver.findElement(By.className("img-overlay")).click();
};

等待網頁上的 card 這個 Class 出現,然後點擊它,同樣也是因為 overlay 的原因,我們又多一行等待他消失的程式。

1
2
3
4
public void e_GetUrl() {
System.out.println("Get URL");
driver.get("https://inthuang.tw/WineWorld/");
};

對網址 https://inthuang.tw/WineWorld/ 發出 get 請求。

1
2
3
public void v_StartBorwser() {
System.out.println("Start Browser");
};

啟動瀏覽器。

1
2
3
4
5
public void v_HomePage() {
System.out.println("Home Page");
wait.until(ExpectedConditions.titleContains("Wine World | 你要找的酒,都在這裡"));
Assert.assertEquals("Wine World | 你要找的酒,都在這裡", driver.getTitle());
};

等待網頁的 title 包含 Wine World | 你要找的酒,都在這裡,然後驗證網頁的 title 是否等於 Wine World | 你要找的酒,都在這裡

1
2
3
4
5
public void v_ProductPage() {
System.out.println("Product Page");
wait.until(ExpectedConditions.urlContains("#/product"));
Assert.assertEquals("product", driver.getCurrentUrl().substring(32));
};

等待網頁的 url 包含 #/product,然後驗證網頁的 url 是否等於 #/product

1
2
3
4
5
public void v_ProductDetailPage() {
System.out.println("Product Detail Page");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.tagName("h3")));
Assert.assertEquals("商品介紹", driver.findElement(By.tagName("h3")).getText());
};

等待網頁上的 h3 這個 Tag 出現,然後驗證網頁上的 h3 這個 Tag 的文字是否等於 商品介紹

1
2
3
public void e_NewEdge() {
System.out.println("New Edge");
};

這個東西其實我不知道是哪來的,但是 Graphwalker 產生的 Interface 裡面有這個東西,所以我也不得不實作它,猜測可能是當初建立的模型有多按到一條 Edge 導致的,但我沒看到他在哪,真尷尬,總之你的模型要是沒問題就別管他。

執行測試

接下來就是執行測試了,我們只要在終端機輸入以下指令就可以了:

1
mvn clean graphwalker:test

clean 是清除之前的編譯結果,graphwalker:test 是執行 Graphwalker 的測試。

執行完後,就會看到噴出一堆運行結果,可以看到 graphwalker 的 logo 出現在畫面上,還記得當初看到有多感動,試了好多次才看見他。

然後就是一些測試過程中會印出的訊息

最後會看到執行結果以及 Build Success

大功告成!

Debug

這邊教你怎麼看測試後的報告,執行 mvn clean graphwalker:test 後,會在專案的根目錄下產生一個 target 的資料夾,裡面會有一個 graphwalker-reports 的資料夾,資料夾裡有一個名字很長的 xml,這就是我們要的報告,打開後拉到最底下會看見錯誤訊息以及錯誤的行數。

結論

這篇文章分享了這兩天踩雷的結果,畢竟官方的文檔給了好多方法但是前後說詞不一阿… 像是根本沒提到 model 可以是 json 也可以是 graphml,又或者是沒說要加入 graphwalker 的 plugin,這些都是我花了很多時間才找到的,希望這篇文章能幫助到大家,如果有任何問題歡迎敲我 DC 或 推特討論。

繼上一次的2022切版直播班心得分享後,我又參加了2023六角 Vue 作品實戰班,這次的課程內容是以 Vue 為主,並提供了 API 讓我們能夠實作一個完整的電商網站作品,多麼吸引人阿!一個屬於自己的完整作品,能夠學 Vue 又能擁有自己的完整作品,正好是我需要的。

課程內容

課前影音

跟切版直播班一樣,一開始就有提供很多課前影音,說真的有了這些課前影音,要跟上直播正課基本上是輕而易舉。我一直很喜歡六角這種方式,每個影片都短短的看起來也不會有太大壓力。

直播課程

每週五晚上固定會有直播課程,基本上是影音課程的延伸,有些內容會在影音課程中沒有提到,但是在直播課程中會有更詳細的說明,這樣的方式讓我們可以更深入的了解課程內容。除了每週五的正課,後來週三也有加課,是關於一些樣式設計和套件的介紹,這些內容也是很實用的,讓我們可以更快的完成作品。以及每週日也會有每週作業講解,讓我們有個作業的方向。

作業

每週都有一個作業,每次都有三個等級可以挑戰,老實說跟切版直播班相比, Vue 的作業對我來說簡單很多,我每週都能做到最高等級,可能也是因為我以前有學過 Vue,所以對於 Vue 的語法比較熟悉,所以作業做起來比較快。

專題班

雖然一開始就說過會帶著我們做出電商作品,不過專題班比較特別,會由另一位帶著我們做,可以選擇就著主線的電商下去,也可以自訂題目。我這次的目的是練習 Vue 所以依舊選擇了主線電商。專題的部分我們需要自己構想網站架構、自己畫線稿圖,首頁部分可以請設計師設計也可以自行設計,我沒什麼設計天分所以當然是請了設計師幫我設計美美的首頁。之後會有專題發表會可以參加,學員們可以簡報自己的作品給公司看,並且可以提供履歷給公司參考。

分組

和切版直播班相同,這次也可以進行分組,並且每週有額外的小組任務,這次有別於切版班的組員,大家都很積極說話,互動也很高,很開心可以分到這樣的團隊,老實說這才是我想參加分組的原因。

心得

最大收穫

果然還是電商作品,一直都很喜歡做 Side Project 帶給我的成就感,這麼完整的作品能給我成就感也是相當之大,不只如此,我還能拿著這份作品去投遞實習,這是我參加這個課程的最大收穫。

最喜歡的活動

這個非小組討論時間莫屬,我們的組員會固定約時間討論,每次討論都能多學一點東西,這都是自己一個人自學所得不到的寶貴經驗。

課程建議

有優點就有缺點,這堂課我覺得問題最大的應該是六角提供的 API 文件,很多部分沒有寫清楚的感覺,常常要花很多時間嘗試才能找到答案,例如說 cart api 回傳的資料包在 data 裡的 data 裡,挺迂迴而且不直覺,又例如 admin-product-all 這支 api 好像不能用,我本來想自己修改每頁呈現的 product 數量 (因為原本給的get admin product 的 api 每頁給的是寫死的,所以我得全部抓過來再自己寫函式),但發現拉過來都是空的,customer 的 product api 就沒有這問題。這些都是我在做作業時遇到的問題,希望以後有機會改善。

給想參加的人

你要是很想要一個作品,參加準沒錯,要是你也有求職需求這絕對是一個很好的機會,六角提供的專題班絕對是個難得的機會,像我也拿著作品去投了實習。此外也有提供職涯輔導的面談,這個倒是讓我很意外,你可以和老師約時間討論專題進度規畫以及未來學習和生活規劃,不確定有哪些線上課程也有提供這種服務,真的很酷。

結語

這次的課程我覺得很棒,我很喜歡這種有作品的課程,雖然可能要花上不少時間但我認為很值得,我也很期待以後的課程,以後有機會我也會再參加其他課程,希望能繼續學習到更多東西。

作品連結

https://inthuang.tw/WineWorld/

我猜,你之所以拿起這本書,因為你是程式設計師,”專業主義”這個說法吸引了你。你就該如此。我們這種專業人士迫切渴求的,正是,”專業主義”。

這段話是出自 「The Clean Coder」 這本書序章的一段引文。沒錯,我拿起這本書正是因為我希望自己成為一個專業的程式設計師。

這本書作為 「The Clean Code」 的姊妹書,我想大多數人應該都是從 「The Clean Code」 這本書開始看起,不過我並沒有這樣做,單純只是我對於這本書的重點「專業主義」以及身為專業程式設計師該有何態度感到興趣。實際上這兩本書也沒有所謂的觀看順序,只要你有興趣,就可以從任何一本書開始閱讀。

專業主義

這章節的重點在於身為專業人士該清楚知道自己想要什麼,並對自己的專業有所負責。最讓我印象深刻的是職業道德的部分,裡面提到「你應該計畫每週工作60小時,前40小時是給雇主的,後20小時是給自己的。」書中也提到這20小時該拿來學習新知,增加自己的專業能力。很常會聽見一些前輩說每當投入職場往往沒有多餘的時間學習新知,但依照書中算法,一天工作8小時,扣掉睡眠時間與學習時間,一週甚至還有多餘的52小時可以去做學習以外的事。我猜想這也跟台灣職場的加班文化也有關係。

說「是」與說「不」

對於做不到的事情必須堅決說不,即便上頭給了時間壓力,也不要因而退讓說出「試試看」這種話,因為這樣往往會導致後面你將花上更多心力去彌補在不足的時間內趕出來的程式。開發上的時程只有你自己最清楚,所以你必須要對自己的時間有所掌控,並且要能夠說出「我需要多久時間完成這件事情」。

對我來說這件事真的很難以想像,畢竟我還沒有真正的工作經驗,而且我認為我是個容易妥協的人,但我想這也是一個很重要的能力,因為這樣才能夠讓你在工作上有所作為,而不是被動的接受工作的安排。現在開始我必須時時刻刻告訴自己,做不到就是做不到,不要因為時間壓力而妥協。

相對於說「不」,對專業人士來說,說「是」是一種承諾,同樣的要避免說出「我們應該試試…」之類的話,直接說出「我將在…之前完成這件事情」,這樣才能夠讓你的上司知道你的承諾,並且讓你的上司也能夠更好的安排工作。

懂得說「是」與「不」才能讓你的話聽起來更讓人信服,也使得你表現得更為專業。

寫程式

這章節提到了在寫程式時該使用什麼樣的態度,避免讓自己處於精神不佳的狀態下工作,熬夜加班趕工只會使得寫出來的程式碼一團糟,這裡也呼應了專業主義中提到的時間安排,以及透過說是與說不來自己不會陷入需要加班趕工的重要性。

避免進入流態區(Tne Flow Zone)

這段其實讓我很意外,所謂的流態區指的是寫程式時偶爾會進入的狀態,那時候的我會寫得渾然忘我,專注力與生產率提高,覺得自己無所不能,我相信很多人都有這樣的經驗,一直以來我都認為進入這種狀態是件好事,但書中提到這種狀態會讓你的理性下降,並且被打斷時可能會變得暴躁易怒,所以應該避免進入這種狀態。

程式設計師大多自負、固執、內向

這句話真是說到我心坎裡了,除了不夠自負,我承認我固執又內向,所以我也可能因為被別人打斷而感到不開心,畢竟我也沒有特別喜愛與人交流,不過被打斷時千萬記得對於過來請求你幫助的人隨時都要有禮貌,因為你有天可能也會需要求助於他們,你也不會希望你向他們求助時他們以糟糕的態度回應你。

阻塞與保持節奏

總是會有那種死活都寫不出程式的時候,這時候需要的是轉換心情,起來動一動,或是去喝杯咖啡,這樣才能夠讓你的大腦重新清醒,並且重新思考問題,或是找個夥伴一起做 Pair Programming。(老實說 The Clean Coder 這本書中並沒有詳細介紹 Pair Programming 該如何實現,但卻不斷地提到他的重要性,並且強調專業的程式設計師該時時刻刻進行 Pair Programming,我想等我開始閱讀 The Clean Code 就能一探究竟了)。也要適時停止工作,回家洗個澡休個息避免超時加班。平時也可以看一些無關的書籍,讓自己的大腦有所休息,書中稱這為創意輸入,觀看某些特定書籍或影片反而更能激發自身的創意。我想對我來說的創意輸入應該會是漫畫和小說,特別是奇幻小說,他們確實能夠讓我想像力更加豐富。

測試驅動開發(TDD)

TDD是專業人士的選擇
這章節提到了測試驅動開發的重要性,這是一種開發方法,也是一種開發態度,這種開發方法的核心是先寫測試,再寫程式,這樣才能夠讓你的程式碼更加乾淨,也能夠讓你的程式碼更加容易維護。必須說雖然這不是我第一次聽過TDD,但是這是我第一次真正的了解到TDD的重要性,這也是我第一次真正的了解到TDD的核心。

TDD的三大法則

  1. 在撰寫任何程式碼之前,先撰寫一個失敗的測試。
  2. 只撰寫剛好無法通過的單元測試,不能編譯也算無法通過。
  3. 只撰寫剛好能通過當前測試的產品程式碼。

遵循這三個法則大概就能以每30秒一次的速度寫出一個測試,不但速度快也使得你的程式碼更容易維護,為了符合測試情境,你就會盡可能找出最好的設計。

練習

這章節提到了練習的重要性,練習可以讓你的技能更加精進,不管是開發速度還是思考速度都可以透過練習來提升。

程式柔道設計場(Coding Dojo)

很有趣的是,許多用於程式練習的專有名詞都是武術名詞,Coding Dojo是一個由志願者主導的全球性社區,為年輕人提供免費的程式研討會,作者當時也參與了這種活動,裡面有許多練習程式的方式

  1. Kata
    Kata一詞出自日文的「型」,意思是「形式」,指的是單獨練習的武術動作的詳細的動作編排模式。在程式設計中,Kata 是對於一個題目的反覆練習,而不是針對如何去解題來做訓練,作者提到他最喜歡的 Kata 是 Bowling Game Kata,這是一個計算保齡球遊戲分數的題目,這個題目的難度不高,但是卻能夠讓你練習到許多程式設計的技巧,例如:重構、設計模式、TDD等等。我也因為這次的閱讀,才知道了這個題目,並且我的第一次TDD體驗就獻給了Bowling Game Kata,我使用了Javascript + Mocha.js + Chai.js來實作這個題目,也有照著作者提供的步驟透過Java來實作,非常有趣,歡迎大家可以試試看。
  2. Wasa
    Wasa基本上就是兩個人的Kata,一個人寫測試,一個人寫產品,並且兩個人必須同時進行,完成後再交換角色,這樣可以讓你更加了解對方的程式碼,並且可以讓你更加了解對方的思考方式,這樣也能夠讓你的程式碼更加乾淨。
  3. 自由練習(Randori)
    自由練習就是兩個人以上的Kata,每個人都可以寫測試或是產品,並且要一個接續一個,你寫的產品通過了測試,就輪到下一個人寫產品。

驗收測試

驗收測試的重要性在於確保產品有符合顧客、業務、開發方與測試方的需求,避免出現因為溝通上的問題而導致的產品錯誤。
溝通一直是個很大的成本,即便我還沒正式有工作經驗,也能從一些學校的分組報告中感受到,當你有個組員只會說好或是什麼都不說,很可能他做出來的東西和你預期的完全不相同,時刻確保對方真的有了解自己的想法很重要。

自動化測試

使用人工來進行測試的成本太過昂貴,所以必須進行自動化測試,而自動化測試產生的文件也能用來當作評估是否有達到需求的文件。直到今天我才了解為何常常會在網路上看見自動化測試的詞了,因為他就是如此重要。

驗收測試的人員與時間點

理想狀態下必須由業務與QA共同進行測試,這也讓我很意外,我以前以為測試就是QA的工作,與業務無關,不過實際上業務通常也不會有時間來進行測試,所以多數還是只交給QA來進行。而測試的重點在於寫測試與開發產品的人必須不同。

持續整合(CI)

持續整合是一種開發模式,它的目的是讓開發者能夠在每次提交程式碼後,就能夠立即知道是否有破壞程式碼,這樣就能夠讓開發者能夠在破壞程式碼之前就能夠修正,這樣就能夠減少破壞程式碼的機會,也能夠讓開發者能夠更加快速的開發。我第一次體驗是在 Linux 核心實作這堂被我拋棄的課上,只要提交一次程式碼到 github 上,git action 就會自動進行測試,並將測試結果 email 到我的信箱,只能說 Linux 核心實作的課程真的很棒,短短一週就體驗到完全沒體驗過的開發經驗,只可惜我目前沒有多餘的時間來處理他的作業,只能說是沒有緣分。

測試策略

QA有幾個要點:

  1. QA 應該要找不到任何錯誤
    即便 QA 可能屬於另一個獨立部門,開發部門依舊要以 「QA 應該要找不到任何錯誤」為目標。
  2. QA 也是團隊的一部份
    看起來 QA 好像與開發團隊是對立面,但實際上 QA 也是團隊的一部份,QA 與開發團隊的目標是一樣的,都是要讓產品能夠正常運作,所以 QA 也是團隊的一部份。
  3. QA 是需求規約定義者
    QA 做的是向業務收集需求,並且將需求轉換成測試案例,一般來說業務所撰寫的是針對正常路徑(happy path)所做的測試,而 QA 是針對極端情況、邊界值與異常路徑(unhappy path)所做的測試。
  4. QA 是 特性描述者
    QA 回報時會描述系統運行的真實情況,所以並不是在翻譯顧客或是業務的需求,只是在鑑別系統是否有達到需求。

自動化測試金字塔


這張圖說明了自動化測試的幾個階段,從下到上分別是:

  1. 單元測試
    單元測試是由與開發產品相同的語言撰寫的測試,並且測試的範圍是單一的函式,這樣的測試可以讓開發者能夠快速的知道自己的產品是否有錯誤,並且能夠快速的修正。TDD 中先寫測試再寫程式碼的概念就是來自於單元測試。
  2. 元件測試
    元件測試是驗收測試的一種,由業務與 QA 共同進行,並由開發人員進行補助。通常需要在 FITNESS、JBehave、Cucumber 等元件測試環境下來進行測試。目的是要讓「不具備編寫測試能力的業務人員也能理解這些測試」。
  3. 整合測試
    這種測試只適合元件很多的產品,將元件組裝後測試他們彼此之間是否能正常通訊,一般由系統架構師與首席設計師來編寫。整合測試不會作為持續整合的一部份,因為持續整合區要週期性的執行,而整合測試的時間成本較高,所以不適合作為持續整合的一部份。
  4. 系統測試
    用來測試系統是否已經組裝完畢,通常由系統架構師與技術負責人來編寫,一般使用與UI整合測試相同的語言和環境來進行測試。
  5. 人工探索式測試
    這個階段就是需要人工介入,不寫任何腳本,目的是為了模擬使用者的操作,並盡可能找出無法預期的錯誤。

時間管理

時間管理對於任何人來說都是個重要的議題,如何在有限時間內完成任務,書中也提了幾個令我感到驚訝的觀點:

會議

會議的兩條真理

  1. 會議是必要的
  2. 會議會浪費大量的時間

所以只參加必要的會議,有些會議邀約要懂得適時拒絕。這讓我很訝異,因為我一直以為會議不能拒絕,畢竟有時候會議的邀請是來自於上司,感覺自己沒有那個立場可以拒絕,書中甚至提到可以禮貌的在會議中提出自己似乎與本場會議無關希望提早離席,至少現在我不認為我做得到,但我也意識到這麼想的自己是不是就是不夠專業,能夠有效的掌握自己的時間並且懂得拒絕不必要的會議才是專業人士該有的作為,我想這也只能之後慢慢自己摸索了。

爭論

如果一件事無法在5~30分鐘內解決,那爭論就是沒有意義的,不如花時間去蒐集資料讓資料來說話。

番茄鐘工作法

以一個番茄鐘為單位,每個番茄鐘為25分鐘,每個番茄鐘後休息5分鐘,每四個番茄鐘後休息30分鐘。
也是一個我早有耳聞卻從來沒有實踐過的東西,直到這次我才開始嘗試,本來以為能夠長時間專注的我不需要番茄鐘工作法,但這方法意外的很適合我這種不愛休息的人,因為適度的休息能夠回復專注力,我很常在一整天的長時間專注後感到很疲倦,自從使用了番茄鐘工作法我強烈感受到每個工作週期我的效率與專注度都沒有太嚴重的下滑。

死胡同與泥潭

開發陷入死胡同很容易發覺也很容易讓人知道要放棄,但在泥潭中你還能前進,實際上卻是你花了更多心力在泥潭中前進,其實從頭來過會是更快的方法。這點我也有感受,有時候在開發時會覺得某些方法不是很好,但至少能用,但最後發現直接重寫遠比用這種不好的方法來的快,但我也不是很能夠在開發中就能夠判斷出我是否陷入了泥潭中,所以我想這也是我需要多多練習的地方。

預估

最簡單也最困難的東西,你身為專業人士自然能知道自己大概要花多少時間進行開發,但同時你給出的時間也是一個承諾,如果開發到一半發現自己預估錯誤一定要馬上和團隊成員反映,以減少對整個團隊的影響。我也清楚知道這有多困難,我當然會想在自己承諾的時間完成工作,而我又是個極度守承諾的人,我想這也是我該學習的地方: 勇於面對自己的預估錯誤。

協作、團隊與專案

即便多數身為程式設計師的人都是不愛與人打交道才會成於此行的,但在團隊中你還是需要與人打交道,不管是與團隊成員、業務、客戶或是上司,你都需要學會如何與他們溝通,這也是我覺得這本書最有價值的地方。

結對程式設計(Pair Programming)

前面也提過這本書不斷強調結對程式設計,也認為專業的程式設計師平時就該用結對程式設計,與別人一起思考問題可以提升效率。我想我在閱讀 The Clean Code 時會認真地了解這個概念(畢竟本書沒有細提)。

工具

本篇列舉了一些開發上必備的工具

  1. git - 版本控制
    git 的重要性我想不用多說,作為現在主流的版本控制工具,git基本上是所有程式設計師該學的工具之一。
  2. 編輯器
    裡面提到了vi、emacs等等,但我最愛用的還是 VSCode ,雖然書中沒有提到,但這已經是現今主流的編輯器,社群龐大,插件豐富,而且還有很多大公司的開發者維護的插件,這是我最愛的編輯器(而且個人覺得 emacs 超難用)。
  3. 問題追蹤
    Private Tracker 是作者使用的工具,不過這些我都沒有使用過,也是第一次聽說。
  4. 持續建置
    Jenkins,而我自己也只用過 git 的 actions
  5. 單元測試工具
    JUnit、NUnit、Midje 等等,視你使用的語言而定,像我開發 Javascript 就有用到 Mocha、Chai 等等。
  6. 元件測試工具
    FitNesse,這是作者開發的軟體,在書中可以看到作者一直提到。

結語

寫了這麼長終於是到了尾聲,這本書的內容對於我來說啟發真的很大,不論是帶我進入 TDD,還是開始使用番茄鐘,甚至我以前也不會寫閱讀筆記,我也會繼續努力的學習,希望能夠成為一個更好的程式設計師。

AOS (Animation-on-Scroll)套件是一個在網頁開發上很常使用的動畫套件,不過在使用到「fade-left」與「fade-right」時很常會發生網頁出現x軸的情形。

以上面的例子來說,在我頁面滾動到灰色區塊前都會有一個x軸存在,而造成這個原因正是「fade-left」與「fade-right」這兩個效果並用所導致;因為這兩個效果是利區塊位置改變(從負軸到正軸的位移),所以就會產生x軸。(此情況也會發生在自己寫的animation-on-scroll上,道理是相同的。)

解決方法

知道原因之後要解決這件事其實很簡單,產生x軸的原因是元素超出畫面,那個幫他上個overflow-hidden把超出去的元素藏起來就好了。

為了一勞永逸,直接在body上面加上overflow-x: hidden;的style就可以了。

地雷

但這邊還有個小小地雷,在做RWD時你會發現手機版的overflow-x: hidden;上不去,這是因為手機版瀏覽器會自己加上overflow: auto;還是加在inline-style上,根本概不掉,所以這時候要用一個div把你的整個網頁包起來,overflow-x: hidden就寫在上面。

總結

這個問題困擾我好久了,害我一直沒辦法做左右淡入的動畫,還好有找到解決方法,希望這篇文章可以幫助跟我遇到相同問題的人。

這幾天AI繪圖的討論度很高,加上我這幾天剛裝了Copilot,所以就來聊聊這兩個東西吧。

我相信有在接觸程式的人沒聽過Copilot也聽過AI幫你寫程式的插件,沒錯Copilot就是這個神奇的AI插件。為什麼我會把Copilot和AI繪圖擺在一起討論? 簡單來說我覺得他們本質上是相同的東西,有趣的是AI繪圖的討論度非常之高,甚至又再度出現AI是否會取代繪師的聲音,而Copilot卻沒有造成這麼大的討論度(不能說沒有,他當初出來時也是有人在討論搞不好會取代我們這些工程師,不過基本上沒有AI繪圖那麼熱烈)。

關於Copilot

先來聊聊Copilot好了,這一兩天使用下來我覺得Copilot根本是神一般的發明,我只能說超級好用,他不只可以幫我寫程式還可以幫我寫文件跟文章,你現在看到的這篇文章就是在Copilot的輔助下完成的。Copilot會在你輸入幾個字之後自動給出他覺得你接下來會想輸入的內容,給出的內容基本上跟我想寫的八九不離十了,還能幫你通順文句。不過使用完也很清楚一點,他根本沒辦法取代工程師,他只是一個輔助工具,他只能幫你寫一些重複性的東西,或是給你一個寫作方向;他也沒辦法很精準的寫出完全跟你所想的相同的內容,多數時候我還是要手動去修改並對他給出的內容做取捨。

關於AI繪圖

聊完Copilot再來聊聊本篇重點AI繪圖,先說明我自己其實沒有用過AI繪圖,不過倒是有不少朋友有用,也有親眼看他們操作給我看。加上我認為自己關注了不少繪師,也有朋友是繪師,所以我是綜合他們的說法加上我自己的看法來討論的。

許多人使用的都是Novel AI裡的付費AI繪圖工具,操作方式就是下關鍵字並讓AI透過關鍵字去生成圖片,又或者是餵一些現有的圖片給AI去生成新圖,經過幾次不斷的修改,最後就可以得到一張不錯的圖片,有些圖片在我看來是真的神,我想一般人可能無法分辨是不是AI繪製的。這也是根本上造成這次話題的原因:如果AI繪圖這麼神繪師是不是要沒飯吃了!?這答案跟我對Copilot的看法一樣,不會,因為他根本上只能算是輔助工具,多看幾張就知道很多部分他還是沒辦法完美達到我們想要的結果,很多時候還是需要人工改圖,這跟Copilot的情況很相似,以及他也是有一些弱點,例如他的弱項是手部繪製,手都畫得奇形怪狀,又或者是風格很統一幾乎清一色都是日系畫風又很容易畫出大奶妹子,我想這也跟當初餵進去訓練模型的資料有關;加上每位繪師都有自己獨特的風格,以上都是目前AI無法取代的,就算有些成功案例又如何,他還是有很多地方無法達成們人類的需求,但作為不會繪畫的人來說AI繪圖算是一大福音吧,跟委託一位昂貴的繪師相比這個可能便宜些。

至於為什麼說他是輔助工具?許多繪師都說他的光影、服裝設計都是非常好的參考對象,這也跟我對Copilot的看法相同,他所給我的建議是個方向,但基本上我沒辦法照抄,因為那就不完全是我要的。

除了取代繪師的部分有不少討論度,真正討論最兇的其實是餵進去的資料的版權問題,不過我這篇想著重在AI本身而不是智財權,所以不會有任何相關討論請見諒。

小結

綜上所述,我認為AI繪圖跟Copilot都是輔助工具,他們的存在並不會讓繪師或工程師失業,他們只能讓我們更快更好的完成工作罷了。比較有趣的點是AI繪圖討論度大於Copilot,或許是工程師們比較了解AI的原理也說不定?我自己是認為AI要成長到能完全取代還要好長一段時間,一來是AI這東西本質上就是大量機率與統計學的應用,這些東西都只能無限接近答案,無法得到完完全全一模一樣的成果,現階段很多部分都還是有待加強,或許有天他能夠無限接近到我們幾乎分辨不出來,但他們終究是不相同的東西,本質來說就不同了,不過這部分討論比較哲學一點,就不多贅述了;二來是說不准以後又會出現有別於AI的技術而將它比下去也說不定。以上就是我的淺談,只是記錄下最近遇到這兩個相似討論度又高的東西以及我產生的小小心得,個人淺見。

前言

因為HEXO不支援Latex語法,所以我把文章放在Hackmd上,請移動至Hackmd觀看。

Source code

如果你不想看那麼多字想直接看程式碼,可以直接點上面的連結,程式碼都有註解,應該不難懂。

最近在godaddy上購買了一個網域名稱,經過幾番波折終於幫自己的github page設定好自訂的domain name了,這篇會來介紹怎麼設定。

購買網域名稱

第一步當然是要去買一個網域,除了godaddy也有其他服務可以用,這邊就只介紹godaddy,基本上大同小異。

  • 輸入自己想要的網域名稱看看有沒有被使用,沒有就可以繼續

  • 我這裡就隨便打幾個字,他會給你推薦方案,我自己是買了inthuang.tw這個名稱

godaddy設定

  • 進到設定裡面,找到自己購買的網域

  • 點選新增

  • 把內建的 CNAME www domianname 刪掉(很重要!!一定要做!!)

  • 輸入以下網域

    1. 自己的github.io的網址

    2. github主機ip

      * github總共提供四個ip,我是任選兩個設定
    

github page設定

  • 到自己hexo的repo設定裡,如果不知道怎麼部屬hexo到自己的git上面可以參考這篇

  • 在設定裡找到pages

  • 往下拉有個custom domain,輸入你購買的網域名稱

  • 等他跑好就行,記得跑好後要勾選下面的啟用https

  • 拉到最上面就可以看到自己網頁的link成功變成自訂的domain name

結語

我一開始在設定時cname www username.github.io都無法設定,一直說請輸入有效的值,一開始我以為是www有錯,後來才發現我要把預設的 cname www domainname刪掉才能新增,真是搞死我…這邊新增後github那邊也能順利驗證TLS了,可喜可賀。

如此一來其他的專案的domain name也能變成我自訂的名稱,不用特別設定,因為他們都是在github.io底下的網站,相當方便。

有一種版型稱為瀑布流,特點是像瀑布一樣會垂直落下,每個元素的高度會不同,並且第二排第一個元素會接在第一排長度最短的元素底下。

  • 瀑布流排版

  • masonry 套件

    這個套件提供了瀑布流排版的js與css,有了這個套件就不用自己手寫一套瀑布流。

  • bs5官網也推薦使用這個套件,因為bs5自己沒有瀑布流的函式庫。