<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>隨筆 彙整 - 星君研究室</title>
	<atom:link href="https://abstreamace.com/sglab/category/essay/feed/" rel="self" type="application/rss+xml" />
	<link>https://abstreamace.com/sglab/category/essay/</link>
	<description>熱愛開源開發的全端工程師部落格</description>
	<lastBuildDate>Wed, 12 Nov 2025 09:52:52 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.3</generator>

<image>
	<url>https://abstreamace.com/sglab/wp-content/uploads/2020/08/cropped-shrewd-plain-32x32.png</url>
	<title>隨筆 彙整 - 星君研究室</title>
	<link>https://abstreamace.com/sglab/category/essay/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>GitHub 帳號被鎖經驗談</title>
		<link>https://abstreamace.com/sglab/2025/11/12/github/</link>
					<comments>https://abstreamace.com/sglab/2025/11/12/github/#comments</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Wed, 12 Nov 2025 09:45:09 +0000</pubDate>
				<category><![CDATA[初體驗]]></category>
		<category><![CDATA[踩坑記]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[GitHub]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=655</guid>

					<description><![CDATA[<p>網路上有很多人遇過的「毫無理由被鎖住 GitHub 帳號」今天被我遇到了。 我正在瀏覽 GitHub 到一半，突然就被登出，還在想說怎麼一回事，結果重新登入的時候就說我的帳號因為違反政策而被停權了。當... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2025/11/12/github/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/11/12/github/">GitHub 帳號被鎖經驗談</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2025/11/github.jpg" alt="" /></p>
<p>網路上有很多人遇過的「毫無理由被鎖住 GitHub 帳號」今天被我遇到了。</p>
<p>我正在瀏覽 GitHub 到一半，突然就被登出，還在想說怎麼一回事，結果重新登入的時候就說我的帳號因為違反政策而被停權了。當然我整個人超級傻眼，立刻按了上頭的申訴連結，結果下一個畫面是：</p>
<p>「請登入」</p>
<p>耶，搞笑嗎？我現在就是無法登入啊！還要我登入才能申訴？</p>
<p>幸好，仔細一開，那個頁面的最底下還有一個不起眼的連結「無法登入嗎？」，嗯，對，就是我。按了下去之後，通過了很繁瑣的驗證程序（email 驗證、簡訊驗證、人類識別測驗……）總算是可以來填寫申訴表單了。基本資料填了填，底下有一個最大的文字框，說要我「解釋一下我違反政策的部份」什麼的，耶，可是我根本就不知道我為什麼被停權啊！！我也想不到任何我有違反政策的事情啊！我看了一下網路上其他人分享的經驗，無非大概就是幾個原因：</p>
<ul>
<li>建立或者 fork 了一個侵權（違反 DMCA）的 repo。</li>
<li>repo 裡面放了違反政策的不當材料。</li>
<li>濫用 GitHub 服務，例如大量自動存取、浪費儲存空間、建立小號等等。</li>
<li>在 GitHub 上頭惹事、跟人起衝突、破壞社群和諧什麼的。</li>
<li>帳號疑似被盜。</li>
</ul>
<p>但是我完全沒有做這些事情啊！所以我就簡單寫「我真的不知道原因，但我樂意改正」傳了表單過去，隨後也收到了「已經接到申訴表單」的郵件通知。</p>
<p>之後我就繼續去找相關的網路文章，結果是越看越讓人擔憂，有些人隔了幾天要回帳號，有的過了一兩個月到數個月，甚至有些人始終拿不回來……不過有一件事似乎是成立的，就是「好好跟他們溝通」這點。這就不禁讓我覺得我稍早只寫了一句話是不是太混了？但問題是，很多人也指出，不要重複提交那個申訴表單。於是我只好在那個通知的郵件回覆，繼續補充一些我覺得該寫的事情。我是這樣寫的：</p>
<blockquote>
<p>Dear GitHub Support Team,</p>
<p>I would like to provide additional context regarding my account suspension. I did not receive any email or other form of notification from GitHub about any potential violation of the Terms of Service. Therefore, I am currently unaware of the reason behind this action.</p>
<p>To the best of my knowledge:</p>
<ul>
<li>I have not created or forked any repository that violates the DMCA.
</li>
<li>My projects do not contain inappropriate materials.
</li>
<li>I have never abused any GitHub services.
</li>
<li>My interactions with other community members in discussions and issues have always been respectful and appropriate.
</li>
<li>My account is secured with 2FA, and there have been no security incidents.
</li>
</ul>
<p>If any of my assumptions above are incorrect, I would sincerely appreciate specific information so I can make all necessary corrections.</p>
<p>I respectfully request that my account be reinstated. GitHub is essential for my open-source projects. I develop tools intended for public benefit — for example, my repositories “fen-tool” and “fontfreeze” — and I am unable to continue maintaining or improving them without access to my account. These projects are actively used by developers and enthusiasts, and I wish to continue contributing to the community through GitHub.</p>
<p>Thank you for your time and understanding.</p>
</blockquote>
<p>寄出去之後，當然還是掛念在心上，所以我繼續查看其餘的網路文章，滑著滑著看到了這個 Reddit 上的<a href="https://www.reddit.com/r/github/comments/1er6iwo/was_your_account_suspended_deleted_or/">討論串</a>，這個討論串名義上是說「在這邊貼文不會有任何幫助」，但是也有人指出，「搞不好會有 GitHub 的員工『剛好』看到你的貼文……」，且也有類似都會傳說的留言指出，好像在這邊留言之後不久就恢復的例子不少（但當然，該串裡面也有若干的人始終沒有恢復帳號）。我抱著姑且一試的心態、重新貼了我上面那些東西上去（附上我的帳號名跟申訴表的 ticket Id），結果——</p>
<p>——當然我很確定只是巧合啦——</p>
<p>——我貼文之後只過了一秒鐘，我就接到了 GitHub 的回信：</p>
<blockquote>
<p>Thanks for contacting GitHub Support!</p>
<p>Sometimes our abuse detecting systems highlight accounts that need to be manually reviewed.</p>
<p>We&#8217;ve cleared the restrictions from your account, so you have full access to GitHub again.</p>
<p>Please let me know if you need anything else.</p>
</blockquote>
<p>老樣子，沒講到底是什麼東西導致系統（八成是什麼半調子的 AI 吧）誤判的，但是總之是完全烏龍。不過能夠在一天之內就把帳號拿回來，我應該可以算是相當幸運的吧？</p>
<p>就在這邊留個經驗紀錄，給以後遇到此問題的人參考吧。</p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/11/12/github/">GitHub 帳號被鎖經驗談</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2025/11/12/github/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>BPS 開發分享之 24：Discord Webhook</title>
		<link>https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/</link>
					<comments>https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Fri, 09 May 2025 03:01:22 +0000</pubDate>
				<category><![CDATA[後端技術]]></category>
		<category><![CDATA[開發分享]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[Discord]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Webhook]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=642</guid>

					<description><![CDATA[<p>對於經營開源專案來說，用 Discord 來打造交流平台確實是我目前所見最佳的選項。本文來分享一些 BP Studio 使用 Discord 的心得。 簡介 最一開始，BP Studio 單純就只有 ... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/">BPS 開發分享之 24：Discord Webhook</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2025/05/Pokecut_1746691261363.jpg" alt="" /></p>
<p>對於經營開源專案來說，用 Discord 來打造交流平台確實是我目前所見最佳的選項。本文來分享一些 <a href="https://bp-studio.github.io/" target="_blank">BP Studio</a> 使用 Discord 的心得。</p>
<h2>簡介</h2>
<p>最一開始，BP Studio 單純就只有 <a href="https://github.com/bp-studio/box-pleating-studio/issues">GitHub Issues</a> 可以作為使用者回饋意見的管道，後來我又開啟了 <a href="https://github.com/bp-studio/box-pleating-studio/discussions">GitHub Discussions</a> 的功能，希望可以帶來多一點的討論，不過成效都不怎麼好。我想主要的原因在於，GitHub 終究是一個比較針對開發者的平台，一般的使用者大多甚至不會在那邊註冊帳號，更不用說貼文或討論了。</p>
<p>於是，我從 2022 年初開始開設了 BP Studio 專屬的 <a href="https://discord.gg/HkcdTDS4zZ">Discord 伺服器</a>。直到本文寫成為止已經過了三年多，這個伺服器上的討論雖然遠遠談不上熱絡、但是確實三不五時會有新手上來問一些問題、且也有熱心的使用者會願意幫我回答，光是這樣就已經幫上超大的忙了。</p>
<p>當然，想靠一個平台就把所有相關的討論都吸收進來、天底下是絕對沒有這麼神奇的事情的。也是有很多人沒在用 Discord 的、而會選擇到 Reddit 或其它平台的摺紙相關版面發問。又或者，即便是在 Discord 上，同樣也有很多人選擇在規模更大的摺紙社團伺服器發問、而不是到我這邊來。此外，雖然偶爾也會有人到我這邊來分享他們的作品，但大多數時候人們還是比較會選擇貼在自己的社群平台版面中，這也是可以理解的。</p>
<p>儘管如此，有專屬的 Discord 還是勝過沒有來得好很多。跟別的平台相比，Discord 的優點至少有：</p>
<ol>
<li>多層次的「伺服器 → 分類 → 頻道」架構，可以很好地組織版面與討論。</li>
<li>權限管理非常精細，對於委派其他人當管理者等等角色很方便。</li>
<li>非常適合即時的對話，同時剛好也在線上的人要加入也非常容易。</li>
<li>完整支援 <a href="https://www.markdownguide.org/" target="_blank">Markdown</a>，很適合跟程式有關的討論 <sup id="fnref1:latex"><a href="#fn:latex" class="footnote-ref">1</a></sup>。</li>
<li>擁有非常豐富的機器人生態與 Webhook 整合能力，可以玩出很多自訂的功能，這也尤其合開發者的品味。</li>
</ol>
<p>其中 Webhook 的部份就是本篇的主題，底下繼續深入說明。</p>
<h2>Discord Webhook</h2>
<p>簡單來說，Discord Webhook 就是一個可以允許其它應用程式對 Discord 伺服器上特定頻道進行操作（尤其是發送訊息）的一個 API 端點。一個 webhook 只會對應一個頻道，所以如果會需要自動發訊息到多個頻道的話，就必須建立多個 webhook。建立 webhook 的方法是到「伺服器設定 → 整合 → Webhook」當中點選「新 Webhook」、設定其名稱與對應的頻道即可。接著點選底下的「複製 Webhook 網址」，就會得到一個形如</p>
<pre><code>https://discord.com/api/webhooks/{webhook.id}/{webhook.token}</code></pre>
<p>這樣的網址端點，然後其它應用程式就可以透過這個端點來發訊息到指定的頻道中。發送訊息用的是 POST 方法，格式有兩種：直接發送 <code>application/json</code> 格式，或是使用 <code>multipart/form-data</code>（有要同時上傳附加檔案的話，就必須採用這種）。在前者的情況中，資料格式主要有：</p>
<pre><code class="language-json">{
    &quot;content&quot;: &quot;訊息內容&quot;,
    &quot;username&quot;: &quot;選填，發言者的顯示名稱；不指定的話即為 webhook 名稱&quot;,
    &quot;avatar_url&quot;: &quot;選填，發言者的顯示圖示；不指定的話即為 webhook 圖示&quot;
}</code></pre>
<p>其它一些比較少用的欄位可以參考<a href="https://discord.com/developers/docs/resources/webhook">官方文件</a>。而在後者的情況中，上面的這些資料必須轉換成 JSON 字串然後放在 <code>payload_json</code> 裡面，而要上傳的檔案則是用 <code>files[0]</code>、<code>files[1]</code>……這樣的欄位名稱（或者如果只有一個檔案，也可以叫 <code>file</code>）。以 JavaScript 來實作的話，就會是這樣：</p>
<pre><code class="language-js">const payload = { content: &quot;訊息內容&quot; };
const blob = new <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob" target="_blank">Blob</a>(...); // 在這邊產生要上傳的檔案
const formData = new FormData();
formData.append(&quot;payload_json&quot;, JSON.stringify(payload));
formData.append(&quot;file&quot;, blob, &quot;上傳的檔名&quot;);

fetch(&quot;https://discord.com/api/webhooks/.../...&quot;, {
    method: &quot;POST&quot;,
    body: formData,
});</code></pre>
<p>值得注意的是，webhook 網址是 id 和 token 兩者都明文寫出來的，也就是說拿到這個網址的人就有辦法進行一切 webhook 能做的操作（包括發送垃圾訊息、竄改 webhook 已經發送過的訊息、或甚至刪除掉 webhook 本身），因此 webhook 網址是只能讓信任的應用程式私藏著的、而不能例如公開地寫在 GitHub 上的程式碼當中的 <sup id="fnref1:github"><a href="#fn:github" class="footnote-ref">2</a></sup>。個人是希望他們未來可以改進一下設計、產生一個權限較小的可公開網址之類的，但是目前是還沒有這種功能。</p>
<p>底下介紹幾個 BP Studio 裡面的 webhook 應用。</p>
<h2>Checkly</h2>
<p>首先是我在<a href="https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/">另一篇文章</a>中介紹過的 <a href="https://www.checklyhq.com/">Checkly</a>。整合之後，就可以在 Checkly 進行自動測試遇到各種失敗情況的時候、傳送通知到指定的 Discord 頻道來（通常會是一個限定管理者觀看的頻道）。</p>
<p>要整合 Checkly 跟 Discord，只要在「Alerts」的頁面中點選「Add more channels」按鈕，選擇 Discord、隨便取個名稱、然後把 webhook 的網址貼上去儲存即可。美中不足的是，Checkly 並沒有提供一個簡便的功能、可以讓我們發送測試訊息來確定到底 webhook（或是其它的 alert channel）設定成功了沒有；唯一的測試方法就是故意寫一個一定會失敗的測試去給它跑。</p>
<h2>GitHub</h2>
<p>GitHub 也有提供 webhook 整合功能，但是它走的是它自己的格式，跟 Discord 的 webhook 格式完全不一樣。幸好 Discord 很貼心，在他們自己的 webhook 當中提供了一個 GitHub 專用的變種。要使用該端點，只要在通常的 webhook 網址後面再加上 <code>github</code> 就是了：</p>
<pre><code>https://discord.com/api/webhooks/{webhook.id}/{webhook.token}/github</code></pre>
<p>要設定整合，可以到 GitHub repo 的「Settings → Webhooks」點選「Add webhook」按鈕，然後把專用網址貼上，設定內容類型為 <code>application/json</code>，然後再底下如果選擇「Let me select individual events」就可以精確地指定發生哪些事情（例如推送、有人給星……）的時候要傳送通知過來。將這些通知送到一個公開的頻道當中，會很有助於讓使用者知道專案的持續維護進度、增進使用者黏著度。</p>
<h2>錯誤回報</h2>
<p>在本系列的<a href="https://abstreamace.com/sglab/2023/12/19/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-19%ef%bc%9a%e6%a0%b8%e5%bf%83%e9%8c%af%e8%aa%a4%e8%99%95%e7%90%86/">第 19 篇</a>中，我介紹過 BP Studio 攔截核心錯誤並且自動把錯誤 log 上傳的機制。當時我採用的是 <a href="https://www.filestack.com/">Filestack</a>，但尤其隨著 Filestack 在 2025 年五月底終止免費方案之後，我就更加覺得應該要遷移過來改用 Discord Webhook 會更方便，尤其是我即使是用手機也可以即時收到通知這一點。</p>
<p>可是對 BP Studio 來說，這邊卻有一個頭大之處。BP Studio 本身是一個純前端的程式，於是如果用它來直接呼叫 webhook 的話，無論如何都會把 webhook 的網址整個暴露出來：就算我用了各種混淆（obfuscate）的手法來讓原始碼難以閱讀，反正最終呼叫的網址到瀏覽器的 console 去一看就知道了，根本藏都藏不住。而這樣一來就跟前面提到的、webhook 網址不能曝光這一點有衝突了。關於這個問題，大抵就是兩個解法：</p>
<ol>
<li>盡可能混淆程式碼，然後原則上因為 webhook 的呼叫只有在發生核心錯誤的時候會發生，理論上此時面對的使用者應該是一個深度使用的忠實使用者才對，我<strong>應該</strong>可以信任他不會濫用 webhook 網址。</li>
<li>使用一個後端的 proxy 來把發送訊息的請求轉發給 Discord Webhook；或者用現成的、或者就是自己做一個。</li>
</ol>
<p>最後我還是決定採用第二種解法，比較不會心有不安。截至本文寫成（2025）為止，能夠作到這種請求轉發的現成免費服務有：</p>
<table>
<thead>
<tr>
<th>服務</th>
<th>免費方案額度</th>
<th>持續免費可能性 <sup id="fnref1:free"><a href="#fn:free" class="footnote-ref">3</a></sup></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://webhookrelay.com/">Webhook Relay</a></td>
<td>每月 150 次</td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><a href="https://hookdeck.com/">Hookdeck</a></td>
<td>每月 10000 次</td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><a href="https://www.svix.com/">Svix</a></td>
<td>每月 50000 次</td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td><a href="https://www.hookrelay.dev/">Hookrelay</a></td>
<td>每天 100 次</td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
</tbody>
</table>
<p>這些當然都可以試試看，但有了 Filestack 的經驗之後，天曉得這些服務什麼時候又會終止免費方案，這樣下去也還是沒有解決問題；況且自己寫一個這種轉發的 proxy 端點也沒有很困難（當然，前提是自己有一個伺服器可以用），所以我就選擇自己用 PHP 寫一個簡單的了。其程式碼如下：</p>
<pre><code class="language-php">&lt;?php
// 設定 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">CORS</a> 檔頭（因為我這支程式是放在另一個網域上）
header(&#039;Access-Control-Allow-Origin: https://bpstudio.abstreamace.com&#039;);
header(&#039;Access-Control-Allow-Methods: POST, OPTIONS&#039;);
header(&#039;Access-Control-Allow-Headers: Content-Type&#039;);

// 處理 OPTIONS 請求
if ($_SERVER[&#039;REQUEST_METHOD&#039;] === &#039;OPTIONS&#039;) {
    http_response_code(204); // No Content
    exit;
}

// 從伺服器的環境變數中讀取 Discord Webhook 網址，避免明文寫在程式碼裡
$webhook_url = $_SERVER[&#039;DISCORD_WEBHOOK&#039;];

// 檢查請求為 POST 方法
if ($_SERVER[&#039;REQUEST_METHOD&#039;] !== &#039;POST&#039;) {
    http_response_code(405);
    echo json_encode([&#039;error&#039; =&gt; &#039;Only POST requests are allowed.&#039;]);
    exit;
}

// 準備 multipart/form-data 的欄位
$post_fields = [];

// 寫入字串欄位
foreach ($_POST as $key =&gt; $value) {
    $post_fields[$key] = $value;
}

// 處理檔案上傳
if (!empty($_FILES)) {
    foreach ($_FILES as $key =&gt; $file) {
        if (is_array($file[&#039;tmp_name&#039;])) {
            foreach ($file[&#039;tmp_name&#039;] as $index =&gt; $tmp_name) {
                if (is_uploaded_file($tmp_name)) {
                    $post_fields[$key . &quot;[$index]&quot;] = new CURLFile(
                        $tmp_name,
                        $file[&#039;type&#039;][$index],
                        $file[&#039;name&#039;][$index]
                    );
                }
            }
        } else {
            if (is_uploaded_file($file[&#039;tmp_name&#039;])) {
                $post_fields[$key] = new CURLFile(
                    $file[&#039;tmp_name&#039;],
                    $file[&#039;type&#039;],
                    $file[&#039;name&#039;]
                );
            }
        }
    }
}

// 用 cURL 來發起請求
$ch = curl_init($webhook_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($response === false) {
    http_response_code(500);
    echo json_encode([
        &#039;error&#039; =&gt; &#039;Failed to forward the request.&#039;,
        &#039;details&#039; =&gt; curl_error($ch)
    ]);
} else {
    http_response_code($http_code);
    echo $response;
}

curl_close($ch);
?&gt;</code></pre>
<p>這只是最陽春的轉發，本身沒有加上任何的安全防護，所以還是有被濫用的可能，但是頂多只是垃圾訊息而已，是可以接受的風險；要是真的被攻擊再加強吧。</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:latex">
<p>但是相對的，Discord 目前並沒有內建 LaTeX 的支援，不過可以透過像 <a href="https://discord.bots.gg/bots/134073775925886976">MathBot</a> 或 <a href="https://bots.ondiscord.xyz/bots/510789298321096704">TeXit</a> 之類的機器人來達成這個功能。&#160;<a href="#fnref1:latex" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:github">
<p>如果你這麼做的話，不用多久你就會收到一個來自「Webhook Safety」的 Discord 伺服器的訊息，說你的這個 webhook 網址已經外洩了，除非你請他們把你的網址加入白名單當中、否則他們為了安全起見就會在一定的時間之後幫你把這個 webhook 刪除掉（就像我說的，任何拿到網址的人本來就能作到這一點）。我不確定這個 Webhook Safety 伺服器是不是 Discord 官方經營的（老實說，看起來有一點不像），但是至少他們確實顯示出了 webhook 網址洩漏的結果會如何。&#160;<a href="#fnref1:github" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:free">
<p>這是我用 <a href="https://chatgpt.com/" target="_blank">ChatGPT</a> 根據這些公司的成立時間、公司規模、企業用戶數與資金穩定度去分析的結果。&#160;<a href="#fnref1:free" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
</ol>
</div>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/">BPS 開發分享之 24：Discord Webhook</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2025/05/09/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-24%ef%bc%9adiscord-webhook/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Checkly 初體驗</title>
		<link>https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/</link>
					<comments>https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Tue, 01 Apr 2025 07:48:37 +0000</pubDate>
				<category><![CDATA[初體驗]]></category>
		<category><![CDATA[前端技術]]></category>
		<category><![CDATA[開發工具]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[Checkly]]></category>
		<category><![CDATA[Discord]]></category>
		<category><![CDATA[Playwright]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=622</guid>

					<description><![CDATA[<p>昨天晚上我偶然逛到我另一個有一陣子沒有更新的部落格網站，結果大吃一驚發現裡面的圖片都跑不出來了。我仔細調查，才發現原來是我在該網站使用的某個 WordPress 外掛程式自動更新到了新版，導致我另外自... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/">Checkly 初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.checklyhq.com/"><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2025/04/checkly.jpg" alt="" /></a></p>
<p>昨天晚上我偶然逛到我另一個有一陣子沒有更新的部落格網站，結果大吃一驚發現裡面的圖片都跑不出來了。我仔細調查，才發現原來是我在該網站使用的某個 <a href="https://tw.wordpress.org/" target="_blank">WordPress</a> 外掛程式自動更新到了新版，導致我另外自己寫的一段與之相輔的程式碼無法正常運作的緣故。</p>
<p>雖然這個問題本身不難修正，但天曉得這個情況持續了多久？幾天？幾週？雖然那個網站也不算人潮眾多，但是那陣子來我網站的訪客（尤其是新訪客）豈不很失望？這些念頭實在頗難讓我釋懷。於是我下定決心要解決這個問題，確保類似的問題不會再有下次。</p>
<p>當然，首先我一定是先把導致問題的外掛的自動更新關掉。但是這樣做，並沒有辦法本質上地杜絕其它外掛或者甚至 WordPress 自身更新導致的一些我未預期的問題。而我也不可能每天花時間親自查看網站是否運作正常；我會需要一個自動化的方案。說到這邊，就會想到 <a href="https://playwright.dev/" target="_blank">Playwright</a> 的 e2e 測試了，它可以讓我高度自訂地寫出一個檢查的腳本：到某個頁面、查看某個元素有沒有顯示、按下去看看互動對不對……等等。可是如果說要我在本地端排程去定時執行 Playwright 測試，我又覺得不是很理想。我希望的是一個功能上類似於 Playwright 的雲端服務、會在測試失敗的時候即時給我通知、而且當然最好免費。本來還在懷疑這種東西有沒有可能存在，結果問了一下 <a href="https://chatgpt.com/" target="_blank">ChatGPT</a> 竟然還真的有，就是今天要介紹的 <a href="https://www.checklyhq.com/">Checkly</a>。</p>
<p>事實上 Checkly 不只是功能上類似於 Playwright，而是它的瀏覽器測試根本就是跑 Playwright 沒錯，所以我連改寫腳本都不用了，稍早寫好的直接貼進去就行了（它也有提供一個編輯介面可以直接編寫 Playwright 程式碼，並且提供了很多範本可以參考）。Checkly 當然有給企業團隊用的付費方案，有更多進階功能，但是如果只是要定時跑跑測試、並且接收失敗通知這些基本功能的話，免費的 Hobby 方案就已經足夠了 <sup id="fnref1:hobby"><a href="#fn:hobby" class="footnote-ref">1</a></sup>。而且它接收通知的管道很多元，免費方案有電子郵件、Slack、自訂 Webhook、Discord 和微軟 Teams 這幾個方式可以設定（付費方案則尚有手機簡訊、GitLab alerts 等等許多）；像我就選擇了用 Discord 來接收通知（上面的圖示就是它們的 Discord Bot 專用圖示）。</p>
<p>它的瀏覽器測試最密集可以到每分鐘跑一次（這可以用來做伺服器的 heartbeat <sup id="fnref1:heartbeat"><a href="#fn:heartbeat" class="footnote-ref">2</a></sup> 檢查；當然，要是拿免費方案這樣跑，一天就會幾乎把每月額度都用完了），而像我是要檢查網站的功能是否正常，這不用跑得太頻繁，我設成每六小時跑一次就已經很足夠了。另外它還支援從世界各個不同的國家發起測試（這可以用來測試網站在不同地區的連線表現），設定維護空窗期（付費方案限定；免得網站進入排定的維護時一直被失敗通知騷擾），檢視與下載歷來的測試結果報表……超多實用功能的。</p>
<p>它也可以產生類似像這樣的 badge：<img decoding="async" src="https://api.checklyhq.com/v1/badges/checks/fb057457-b51e-478d-8c79-d78fb2ff90c5?style=flat&amp;theme=default" alt="" /></p>
<p>有了 Checkly，以後就不用再擔心網站發生意料之外的狀況而不自覺囉～可以安心了 <img src="https://s.w.org/images/core/emoji/16.0.1/72x72/1f600.png" alt="😀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>我對 Checkly 的初體驗評分如下：</p>
<p>實用程度：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
文件清楚：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
設置容易：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
概念易懂：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
客服回應：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:hobby">
<p>免費方案每個月可以跑一千五百次瀏覽器測試，非常夠了。更多關於免費與付費方案的功能比較，可以參考<a href="https://www.checklyhq.com/pricing">這裡</a>。另外，Checkly 的專員在客服過程中向我透露，他們近期中將會推出另一個介於 Hobby 和 Team 方案之間的新方案可以選擇。&#160;<a href="#fnref1:hobby" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:heartbeat">
<p>Checkly 也有個叫 heartbeat 付費功能，不過那個是用來監控我們設定的 Checkly 任務是否有正常在執行用的，意思不一樣。&#160;<a href="#fnref1:heartbeat" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
</ol>
</div>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/">Checkly 初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2025/04/01/checkly-%e5%88%9d%e9%ab%94%e9%a9%97/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Alpine.js 初體驗</title>
		<link>https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/</link>
					<comments>https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Mon, 08 Apr 2024 02:53:54 +0000</pubDate>
				<category><![CDATA[初體驗]]></category>
		<category><![CDATA[前端技術]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[Alpine.js]]></category>
		<category><![CDATA[petite-vue]]></category>
		<category><![CDATA[Vue]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=583</guid>

					<description><![CDATA[<p>之前我在開發 FontFreeze 以及 FEN Tool 兩個工具的時候，為了求簡便，我都使用了 petite-vue 這個精簡框架。但是 petite-vue 自從尤雨溪開發完 0.4.1 版之後... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/">Alpine.js 初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2024/04/alpine.jpg" alt="" /></p>
<p>之前我在開發 <a href="https://mutsuntsai.github.io/fontfreeze/">FontFreeze</a> 以及 <a href="https://mutsuntsai.github.io/fen-tool/">FEN Tool</a> 兩個工具的時候，為了求簡便，我都使用了 <a href="https://www.npmjs.com/package/petite-vue">petite-vue</a> 這個精簡框架。但是 petite-vue 自從尤雨溪開發完 0.4.1 版之後就不再維護了 <sup id="fnref1:rot"><a href="#fn:rot" class="footnote-ref">1</a></sup>，所以後來我都不敢繼續再用它來做新的專案，甚至後來 FEN Tool 也因為規模越來越大而整個用 <a href="https://vuejs.org" target="_blank">Vue</a> 重新改寫了。</p>
<p>最近我因為有實務上的需要，而想要寫一個簡單的<a href="https://github.com/mutsuntsai/lot-drawer/">抽籤程式</a> <sup id="fnref1:lot"><a href="#fn:lot" class="footnote-ref">2</a></sup>。像這種架構極為簡單的東西當然是不需要用上什麼大框架跟建置流程的，甚至就算要用 vanilla JS 來寫也一點都困難。但我想說既然有機會、就藉此來嘗試一下沒用過的東西好了。然而因為當時我有點急著要用那個程式，我也不打算採用需要花比較多時間才能摸索清楚的東西。</p>
<p>於是我就決定採用 <a href="https://alpinejs.dev/">Alpine.js</a>。它其實最初是從 Vue 發展出來的一個輕量框架，所以它在概念上跟 Vue 非常類似，甚至它後來也反過來啟發了 petite-vue，但跟後者不同地、它是一直都有持續在維護的，成熟度也夠高，是值得試試看的東西。</p>
<p>先說結論：如果你想快速寫一個功能單純的一頁式程式、且平常最擅長使用 Vue，那 Alpine.js 確實是不錯的選擇。不過它跟 Vue 之間的差異又比起 petite-vue 跟 Vue 之間的差異大了一些，未來假如要遷移到 Vue 會比較麻煩，所以如果預期之後程式的規模會持續長大的話，我就比較不推薦使用。然而，petite-vue 因為不再維護的緣故、我也同樣地不推薦。加起來的話也就是說，就我的經驗所及的範圍來說，如果專案預期的規模大到一定的程度 <sup id="fnref1:big"><a href="#fn:big" class="footnote-ref">3</a></sup>，那我就建議最好從一開始就選擇比較大一點的框架。反之，如果擺明就是要寫簡單的東西，那真的是可以試試 Alpine.js（特別是尤雨溪自己也在封筆 petite-vue 的時候這麼推薦了）。</p>
<p>對於慣用 Vue 的人來說，在使用 Alpine.js 的時候，除了像是「各種 <code>v-</code> 的指令都改名字變成 <code>x-</code> 開頭」以及「要用 <code>alpine:init</code> 事件來進行初始化設置」這種最基本的事情之外，大概只要記得兩個重點差異：</p>
<ol>
<li>Alpine.js 並沒有 <code>{{...}}</code> 這種模板語法，動態的文字都必須用標籤的 <code>x-text</code> 來達成。</li>
<li><code>x-if</code>、<code>x-for</code> 和 <code>x-teleport</code> 都只能用在 <code>&lt;template&gt;</code> 標籤上，而不能直接使用在一般的標籤之上。</li>
</ol>
<p>這兩個特性看似麻煩，但卻可以有效解決頁面載入的時候還要 cloak 模板語法的麻煩（雖然 Alpine.js 也是一樣有 <code>x-cloak</code> 的語法），倒也不失為是好的設計。</p>
<p>我對 Alpine.js 的初體驗評分如下：</p>
<ul>
<li>文件清楚：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>設置容易：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>概念易懂：<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ul>
<h3>2025.4.2 更新</h3>
<p>補充兩個從 Vue 跳槽過來的人容易忘記的事情：</p>
<ol>
<li><code>&lt;template&gt;</code> 裡面只能恰有一個根元件，亦即不能直接放文字，也不能裡面直接連放兩個 <code>&lt;template&gt;</code>（必須例如用一個 <code>&lt;div&gt;</code> 包起來）。</li>
<li>在 Alpine.js 裡面沒有 <code>x-else</code> 的語法（必須用 <code>x-if=&quot;(相反條件)&quot;</code> 來寫）。</li>
</ol>
<p>這兩個特性就真的有一點點煩人了。關於第二點當然有一些請求（甚至 <a href="https://github.com/alpinejs/alpine/pull/4353">PR</a>），但是目前還沒有加入此功能。</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:rot">
<p>儘管尤雨溪表示該專案已經「完成」了（<a href="https://discord.com/channels/325477692906536972/1050439284341096571/1054597576101474384">出處</a>），該專案仍舊確實累積了許多待解的 bug（在其討論區中，包括我自己很困擾的<a href="https://github.com/vuejs/petite-vue/discussions/169">一個</a>）以及沒人理會的 PR（也包括我自己的<a href="https://github.com/vuejs/petite-vue/pull/178">一個</a>在內），所以我說它沒有被維護、純粹是陳述事實。&#160;<a href="#fnref1:rot" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:lot">
<p>這個抽籤程式是為了一次聚會中我需要主持交換禮物的緣故才寫的。類似的程式也是有一些現成的，但是我想針對我自己的需求把做得更方便使用，所以就自己做了一個。&#160;<a href="#fnref1:lot" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:big">
<p>關於多大的規模才叫大到一定的程度，我想一個很好的指標就是、當專案開始需要進行元件化的時候就是夠大了，特別是 Alpine.js 本身並沒有內建元件化的功能（雖然有若干<a href="https://stackoverflow.com/questions/65710987/reusable-alpine-js-components">非官方解法</a>存在，但總之都脫離了 Alpine.js 設定的目標範疇），而 petite-vue 雖然算是有、但是其元件寫法卻跟 Vue 相差甚大，所以不管是哪一種情況，要之後遷移到 Vue 差不多都要把元件重寫一次。既然如此，那在打算進行元件化的時候就直接遷移就好了。&#160;<a href="#fnref1:big" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
</ol>
</div>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/">Alpine.js 初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2024/04/08/alpine-js-%e5%88%9d%e9%ab%94%e9%a9%97/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>網路 API 架構懶人包</title>
		<link>https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/</link>
					<comments>https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Fri, 08 Dec 2023 04:10:07 +0000</pubDate>
				<category><![CDATA[前端技術]]></category>
		<category><![CDATA[後端技術]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[GraphQL]]></category>
		<category><![CDATA[JSON]]></category>
		<category><![CDATA[REST]]></category>
		<category><![CDATA[RPC]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=569</guid>

					<description><![CDATA[<p>API（Application Programming Interface，應用程式介面）大家都在用，一些架構術語像是 RPC、REST、GraphQL 等等也常常聽過，但是我覺得網路上絕大部分的文章... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/">網路 API 架構懶人包</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>API（Application Programming Interface，應用程式介面）大家都在用，一些架構術語像是 RPC、REST、GraphQL 等等也常常聽過，但是我覺得網路上絕大部分的文章都沒有講到它們的重點在哪裡、都是一直在重複艱澀且不容易體會的定義。在這篇文章中，我想用我的視角來簡單解釋一下這些架構。</p>
<h2>RPC</h2>
<p>大部分在談 API 架構比較的文章都會放上這一張時間軸的圖，所以這邊我也稍微借來用一下：</p>
<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2023/12/image-1702000679927.png" alt="file" /></p>
<p>其中 CORBA（Common Object Request Broker Architecture）和 RDA（Remote Data Access）的概念跟我們現在心中想像的「API」概念差距比較大一點，所以不太需要多談。SOAP（Simple Object Access Protocol）並沒有太流行，所以各位也不用管。OData（Open Data Protocol）只是 REST 的一種實作，這邊也不多提細節。這些東西稍微知道一下有這麼一回事就好。</p>
<p>在我們今天會講到的幾種主要的 API 架構當中，最早出現的當屬 RPC（Remote Procedure Call，遠端程序呼叫），它甚至可以追溯到比上面這張圖畫出來的年代還更早。然而，從我這個數學背景來角度來看，網路上的資料對於 RPC 這則術語是很缺乏一個統一的定義的，有的時候人們很狹義地使用這個術語、但有時又很廣義，所以為了避免造成困惑，底下兩方面我都會講。</p>
<p>就字面上來說，RPC 的意思就是「想像我們直接呼叫遠端應用程式上面定義的函數或方法」。真正狹義而言，RPC 的 API 應該要有這樣的性質：</p>
<ol>
<li>基本上只有一個 API 端點（也就是 API 的網址）。</li>
<li>在送過去的請求（格式不一定，從而衍生出了 XML-RPC、JSON-RPC 和 gRPC 這些變種）當中，會明確指出我們要使用的遠端方法之名稱，藉此區分我們想要使用的功能。</li>
<li>然後在請求的後半段會是我們要傳入給遠端方法的參數或資料。</li>
</ol>
<p>耶，可是，也許是我見識少啦，在今天、這麼狹義的 RPC API 我還真的沒看過多少。取而代之地，比較多的 API 會是用不同的端點來區分不同的功能、然後請求的內容整個就是要傳入的參數或資料。像這樣的 API 我們可以說它是一種比較廣義的 RPC API；雖然其形式並非嚴格意義上的 JSON-RPC（例如啦），但是精神其實是差不多的，都是環繞著一個核心想法：</p>
<div class="card mb-3 text-white">
<div class="card-body">
    廣義的 RPC API，重點在於個別的「功能」是打包好的，也就是功能導向、是高階的 API。
    </div>
</div>
<p>而當網路上的文章在講「RPC vs REST」之間的對比時，通常那個作者是在指這種廣義的 RPC，而不僅限於狹義上的。</p>
<p>這個核心想法是一個兩面刃。一方面，功能導向意味著前端可以用最精簡的方式下達請求、並且收到完全符合需求的回應，不多也不少；但是另外一方面，功能導向也意味著前端跟後端是強耦合的。什麼意思呢？就是假如今天前端有新的功能需求是既有的 API 無法滿足的，那麼一定得找負責後端的團隊來修改、或者開出新的 API 來滿足新的需求。如此一來，前後端就沒辦法各自獨立開發、而需要密切配合；後端需要很清楚前端需要什麼功能，前端也需要很清楚後端開出來的 API 規格是什麼。</p>
<h2>REST</h2>
<p>而 REST（REpresentational State Transfer，表現層狀態轉換）的核心概念可以說就是為了解決這種耦合而誕生的。當然，如果去看網路上的定義，它還有一大堆更加根本的定義，像是利用 HTTP 方法（GET、POST、PUT、DELETE）來描述操作啦、通常是無狀態的（stateless）啦……但那些都不是重點！它相較於 RPC 的真正重點在於：</p>
<div class="card mb-3 text-white">
<div class="card-body">
    REST 著眼在「資源」的操作，也就是資源導向、是低階的 API。
    </div>
</div>
<p>呃，可是什麼是資源？這就有點抽象了，但具體一點來說的話，不妨理解成資料庫裡面的每一種資料表都是一種資源就好。於是 RESTful API（加上那個「ful」只是英文文法的問題而已，還是同一回事）就會用個別的端點來對應各種資源，然後前端就可以用 GET 等方法去操作對應資源的 CRUD（增查改刪）動作。</p>
<p>耶，等一下，那不就等於是直接把資料庫的操作曝光給前端了嗎？相信第一次學到 REST 的人應該很多都會有這個疑惑。嗯，是啦，很大的成份上來說就是這樣沒錯，但是也沒有到 100% 曝光資料庫的地步，因為後端還是會在中間做一道把關的動作，例如會去檢查 API 傳過來的 token、看看打 API 的人是否有對應資源的存取權，以及確保寫入的資料格式正確等等的。</p>
<p>資源導向的作法也是有其利弊：首先，它確實在很大的程度上解耦了前後端的開發，因為只要資源的種類不變，前端可以自己根據其需求去拼湊撈出來的資料等等，不會受限於後端提供的高階功能、而是可以自己用低階操作去組合出想要的高階功能。其次，雖然 REST 整體來說並沒有統一的實作方式，但是至少在同一個專案當中、在操作各種資源的時候是統一的，前端開發人員很容易舉一反三知道要怎麼使用、而不用多看後端團隊的文件。但是反過來，它的缺點則包括：</p>
<ol>
<li>比較沒效率：在用低階操作去組合高階功能的時候，可能會需要打很多次 API 才組得出想要的效果，而且其中可能會傳輸了很多前端其實並不需要的資料。</li>
<li>不適合牽涉到商業邏輯的交易：如果某一項高階操作必須正確地組合若干低階操作才能反應商業邏輯，用 REST 就不適合了，因為它比較難防範各種操作上的錯誤（包括使用者惡意自己打 API、前端的商業邏輯實作錯誤、或是低階操作到一半的時候網路斷線等等；這些要防不是不可能，但會比較麻煩）。REST 比較適合的情境是單純的內容，例如論壇或社交平台這種張貼的內容之間不需要存在什麼邏輯的東西。</li>
</ol>
<p>所以對於講究商業邏輯的功能來說，廣義的 RPC API 還是比較適合的。當網路上在對比「RPC vs REST」的時候，其實他們真的在講的是「功能導向 vs 資源導向」之間的差異，而不是狹義的 RPC 跟 REST。尤其，資源導向的 API 除了 REST 之外還有更新的 GraphQL。</p>
<h2>GraphQL</h2>
<p>GraphQL 簡單來說就是要解決 REST 最為人詬病的傳輸問題。它透過一套獨特的語法來定義「我到底想要什麼樣的資料」，然後讓後台根據這個語法來組合出前端想要的資料、或是類似地進行資料的修改。除此之外，它還多了一個「訂閱」的功能，可以讓前端持續訂閱資源的變動、並且在變動的時候接到通知（例如透過 WebSocket）。</p>
<p>它跟 REST 一樣是資源導向的（只是它可以比較靈活地存取多種資源），所以也同樣有規格統一的優點（而且比 REST 更加統一，因為語法都有規範，回傳格式也固定都是 JSON）、但也一樣有商業邏輯方面的弱點。而 GraphQL 通常是單一端點的，這部份它則比較像狹義的 RPC。相較於 REST，它適合的情境是當整個系統有比較複雜的資料結構、而且前端可能會有很多樣化的查找需求的時候。但是也要小心，如果 GraphQL 打包了太複雜的查找，它的效率可能反而會比 REST 要差。</p>
<h2>結語</h2>
<p>總而言之，在 RPC、REST 和 GraphQL 這三種最常被提到的 API 架構中，它們是各有各的長短處、也各有其適合的場域。這邊解釋了它們之間最重要的對比之處，希望對各位的理解會有所幫助。</p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/">網路 API 架構懶人包</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2023/12/08/%e7%b6%b2%e8%b7%af-api-%e6%9e%b6%e6%a7%8b%e6%87%b6%e4%ba%ba%e5%8c%85/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>網域轉移初體驗</title>
		<link>https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/</link>
					<comments>https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Sat, 10 Jun 2023 03:45:01 +0000</pubDate>
				<category><![CDATA[初體驗]]></category>
		<category><![CDATA[後端技術]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[DNS]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=540</guid>

					<description><![CDATA[<p>這個網域（abstreamace.com）從 2004 年創造至今已經快要 20 年了。在過去很長的一段時間裡面，我都是透過 Name.com 這家業者來維護網域的註冊，而直到今年我才很認真地思考改採... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/">網域轉移初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>這個網域（abstreamace.com）從 2004 年創造至今已經快要 20 年了。在過去很長的一段時間裡面，我都是透過 <a href="https://www.name.com/">Name.com</a> 這家業者來維護網域的註冊，而直到今年我才很認真地思考改採用其它服務的事情。這並不是說我對 Name.com 的服務有什麼不滿（也不過就是網域註冊而已，我連它們的 nameserver 都沒有使用到，還能有什麼服務差異？），而純粹就是他們如今的收費比較高；如果改採用別家的話，我每年還是能夠再省個幾塊錢美金的。</p>
<p>在所有可能的註冊業者選項當中，其實絕對最便宜的王者是：<a href="https://www.cloudflare.com/zh-tw/products/registrar/">CloudFlare</a>。對，就是這個大家平常不會跟網域註冊聯想在一起的網路安全公司，但是事實上它是有提供網域註冊服務的，而且它只收取支付給上游的必要費用、除此之外完全沒有額外抽成，所以在價格上是沒有人能跟它比的。可是，這裡頭其實有一個小麻煩在於，如果要使用它的這個最便宜方案，那麼 CloudFlare 規定就一定要使用它們的 nameserver 而無法自訂；反過來如果要自訂的話，就得採用它們的商用方案，而那反而不划算。由於我的 nameserver 一直都是使用主機供應商的，所有的 DNS 設定以及 SSL 驗證都是在上頭進行的，強迫要我改用 CloudFlare 的 nameserver 其實是有一點小麻煩的。</p>
<p>於是我退而求其次地看看有沒有可以提供跟 Name.com 類似性質的服務、然後價格落在兩者中間的業者，最後我找到的是 <a href="https://www.namesilo.com/">NameSilo</a> 這家。它搬過去的價錢是 $10、未來續約是 $11，就我的調查這對我的需求來說應該是沒有更便宜的了。</p>
<p>至於轉移過程，其實遠遠比我想像得還要簡單：</p>
<ol>
<li>先到舊業者的網站去把網域鎖定（lock）給解除，然後複製其 Auth code。</li>
<li>到新業者那邊去輸入要轉移的網域以及 Auth code，完成付費，新業者就會向舊業者提出轉移申請。</li>
<li>如果什麼都不做，轉移的動作會在 5 天左右自動完成，不過如果舊業者那邊有快速確認轉移功能的話，只要回到舊業者那邊按下確認按鈕，轉移動作就會馬上完成。</li>
</ol>
<p>就這樣，這些操作我在手機上就可以在十來分鐘內搞定了。尤其因為我並沒有使用舊業者提供的 nameserver 而是把網域指向主機供應商的 nameserver，我也完全不用煩惱 DNS 紀錄搬遷的問題，因為網域指向的 nameserver 在這個轉移過程當中是不會改變的。至於使用舊業者的 nameserver 的情況中，NameSilo 在做轉移的時候也有一個選項是可以把 DNS 紀錄複製到它們提供的 nameserver 上頭，所以真的是滿容易的。</p>
<p>網域因為真的是很單純的東西，所以能便宜就便宜吧！</p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/">網域轉移初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2023/06/10/%e7%b6%b2%e5%9f%9f%e8%bd%89%e7%a7%bb%e5%88%9d%e9%ab%94%e9%a9%97/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>關於程式碼註解之我見</title>
		<link>https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/</link>
					<comments>https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Mon, 15 May 2023 10:23:25 +0000</pubDate>
				<category><![CDATA[程式語言]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[Git]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=536</guid>

					<description><![CDATA[<p>本來以為程式碼註解這種事情應該是業界的共識，但是最近看到一些網路上的討論才發現，存在一派的人（例如這個影片）反而是明確地反對使用註解，他們的立論主要有幾點： 程式碼的本身的意義可以藉由好的命名規範、函... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/">關於程式碼註解之我見</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2023/05/g486.png" alt="" /></p>
<p>本來以為程式碼註解這種事情應該是業界的共識，但是最近看到一些網路上的討論才發現，存在一派的人（例如這個<a href="https://www.youtube.com/watch?v=Bf7vDBBOBUA">影片</a>）反而是明確地反對使用註解，他們的立論主要有幾點：</p>
<ol>
<li>程式碼的本身的意義可以藉由好的命名規範、函式抽離重構、型別宣告與模組化等方法來自我呈現出來；這些有做好的話，程式碼本身一看就知道是在幹嘛，並不需要另外使用文字加以解釋。</li>
<li>實務上，很多時候程式碼在修改了之後、註解並沒有也跟著更新，這使得維護的人反而需要花額外的成本去確定註解跟程式碼的一致性、否則就反而會被過時的註解誤導。</li>
<li>程式碼是可以有效地透過編譯器和 linting 工具來自動檢查的，但是註解卻暫時並沒有有效的手段可以自動檢查其正確性（也許 AI 發達的未來可以，但是現在還很不方便）。</li>
</ol>
<p>因此，他們覺得應該把重點放在撰寫出本身好閱讀的程式碼，然後根本不要使用註解。</p>
<p>不過我並不認同這樣的一種觀點。最主要地，好閱讀的程式碼或許可以呈現出程式碼的「what」（寫了什麼），但是卻仍然沒有辦法呈現出程式碼的「why」（為什麼要這樣寫），而後者在我看來才是註解的真正精髓所在。很多時候，程式寫了之後過了一段時間，我回去看過去寫的程式碼時常常會質疑「當初為什麼我要這樣寫？這必要嗎？是否可以刪掉這一段、或加以簡化？」，而如果當初我沒有把撰寫的理由註解下來的話，我就很容易浪費許多時間把當初的思考歷程重走一遍。所以現在只要是遇到一段程式碼是我覺得背後的理由並不顯然的，我不但會加以註解，還常常會在註解當中附上對應的參考資料連結等等以方便未來查閱。後者也是很重要的：因為程式碼背後的理由也是有可能會隨著框架套件等等的改版而過時的，附上那些參考資料有助於讓我未來需要重新評估某些程式碼的時候、可以快速地掌握之前的脈絡。這些都不是靠程式碼本身就能做到的。</p>
<p>再來，光靠程式碼本身是否就能有效傳達出程式碼的「what」，這點我也是大大存疑的。不管多麼好閱讀的程式碼，也還是不可能比得上自然語言要來得好閱讀；閱讀程式碼是需要大腦額外地進行理解和整理的功夫，但自然語言卻可以直接把那些整理過後的重點直接呈現出來，不但節省理解上的負擔、也可以免於閱讀一些次要的流水帳。此外 doc 類型的註解還可以在滑鼠移到識別項上面的時候直接顯示出來，使得我們不用切換檔案檢視就可以直接看到一些最重要的資訊，這即便是落實到極致的命名規範也不可能同樣程度地有效。</p>
<p>最後，「程式碼更新、但註解沒有跟著更新」這種事情透過 Git 是非常容易抓出來的，所以如果有這方面的問題，我會有點質疑這個團隊是否有好好落實 code review。如果連這麼容易抓出的問題都沒有在 code review 中被發現，跟我說他們的團隊就反而有辦法寫出好閱讀的高品質程式碼，這怎麼聽都很匪夷所思。即使有自動 linting，也只能診斷出最基本的可讀性問題，這跟「程式碼可以自明地呈現其邏輯」完全是兩個不同層次的事。我無法想像為什麼困難的後者做得到、簡單的更新註解反而就做不到。</p>
<p>真的要說的話，我覺得他們的觀點只有在一種情況下能成立，就是他們的程式碼實在是沒學問到了「就算註解也只是照本宣科」的那種地步、完全沒有更深入的洞見可以補充了。這種專案當然不是沒有，而且還多得很，但是如果一個軟體工程師做過的通通都是那種專案、乃至於他會形成這種反對註解的觀點，那他真的應該再多看看更廣大的世界才是。</p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/">關於程式碼註解之我見</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2023/05/15/%e9%97%9c%e6%96%bc%e7%a8%8b%e5%bc%8f%e7%a2%bc%e8%a8%bb%e8%a7%a3%e4%b9%8b%e6%88%91%e8%a6%8b/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>業界用程式工具與技能</title>
		<link>https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/</link>
					<comments>https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Mon, 14 Feb 2022 05:33:15 +0000</pubDate>
				<category><![CDATA[隨筆]]></category>
		<category><![CDATA[Git]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=134</guid>

					<description><![CDATA[<p>之前的文章「如何入門寫程式」中我分享了從完全沒學過程式一直到略懂的程度的建議法門，而這一篇就繼續在略懂寫程式了之後要繼續學哪些東西。 先聲明，如果你沒有打算靠寫程式吃飯，而純粹是把寫程式當作業餘興趣的... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/">業界用程式工具與技能</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>之前的文章「<a href="https://abstreamace.com/sglab/2020/07/03/%e5%a6%82%e4%bd%95%e5%85%a5%e9%96%80%e5%af%ab%e7%a8%8b%e5%bc%8f/">如何入門寫程式</a>」中我分享了從完全沒學過程式一直到略懂的程度的建議法門，而這一篇就繼續在略懂寫程式了之後要繼續學哪些東西。</p>
<p>先聲明，如果你沒有打算靠寫程式吃飯，而純粹是把寫程式當作業餘興趣的話，我這篇談的東西你未必需要會也沒關係；因為老實說，我自己在轉職做工程師之前，這些東西我也是一知半解或甚至完全沒聽過，但也還是寫了一堆我上一篇文章中提到過的那些小程式。所以單就自己寫一些給自己用的小程式的這個目的來說，基本功大抵就夠用了；繼續學本篇的東西的用意，主要就是為了要寫難度較高、不太可能用土法煉鋼的方式寫出來的程式。當然，本篇的東西會了之後，再回去寫小程式也是會寫得更好、更輕鬆。</p>
<p>要形容本篇的內容的重要性，我不禁想起了近年來很紅的一個 YouTube 頻道 <a href="https://www.youtube.com/channel/UC6vasuRFx3t3NTISG6iwUeA">Primitive Survival Tool</a>，主持的兩位柬埔寨神人完全不使用現代科技、純用土法和令人難以想像的毅力完成了一個又一個讓人瞠目結舌的建築專案（如果沒看過，你一定要點進去見識一下）。沒錯，用業餘的方法絕對也可以玩到神人級的境界，只是那種蓋房子的方法絕對沒辦法拿來當職業；首先實在太花時間，再來也不可能蓋得出高樓大廈。我這邊要講的，就是為了有效率地撰寫大型專案所必須要懂的知識。</p>
<p>此外，本篇的篇幅當然不可能把東西完整教會給各位，所以本篇的目的只是告訴大家有哪些東西是需要會的、以及為什麼你應該要會那些東西。而實際要學的話，就像我上一篇一開始就說過的「自學能力很重要」（笑）。</p>
<h2>框架（framework）</h2>
<p>既然剛才提到蓋房子，就用房子來比喻吧。如果我們去看街上大部分人住的房子，我們會發現很多共通的結構和元件，例如通常都會有「客廳、廚房、衛浴、臥室」這樣的格局分配，牆壁通常都是直角相交的，衛浴裡面基本上都會有馬桶跟洗手台，各個房間大多會有窗戶，房間內牆通常都是油漆或是貼壁紙，照明通常是裝在天花板上等等。這些共通點當然不是放諸四海皆準的，從來就沒有法律規定說房子一定要這樣蓋，世上的房子也一定找得到很多不是這樣蓋的的例子，但是長期下來，基於實用上的考量和人們的文化習慣等等，在建築圈裡面自然地就形成了前述的通則。當建築設計都依循著常見的通則在走的時候，施工的時候就有標準的流程可以走，建築工人的術語概念可以相通，而且很多材料零件都可以通用，蓋起來就有效率得多。然而，台灣民宅的通則跟日本建築的通則又明顯很不一樣；例如日本的房子很多都是木造的，但在台灣幾乎都是鋼筋水泥磚頭的。熟悉日本房子蓋法的建築工人，未必蓋台灣的房子也有同樣的熟練度。</p>
<p>寫程式的框架大抵來說也是這麼回事。雖然程式語言本身提供我們更多天馬行空的可能性可以寫出千奇百怪的程式出來，但是軟體工程演變至今，人們注意到一些需求和模式反覆地出現，於是發展出了一套套的框架來讓寫程式變得更容易。框架通常規範了整套程式是由哪些主要結構所組成、程式的脈絡應該如何運行、並且提供了各種工具來滿足一些常用的需求。熟悉一套框架之後，大部分的常見功能都只要照著框架設計的方式去做就可以很快地完成，而且其他也熟悉同樣框架的工程師要看懂或維護你寫的程式碼也會容易許多。你仍舊可以在框架的主體之上完全自由地寫一些額外的功能，但是框架會幫你在最短的時間之內把程式的大架構打穩。</p>
<p>舉一些具體的例子。以網頁來說，寫程式的時候常常會需要找出頁面上滿足特定條件的元件並對它們做一些操作，而歷史悠久的 <a href="https://jquery.com/">jQuery</a> 這個框架把前述的需求濃縮到很精簡的語法裡面，讓我們可以用較少的程式碼表達出我們想做的事情。近年來，我們又希望當記憶體中的資料改變的時候，頁面上的呈現可以自動也跟著改變，以省去我們寫一大堆程式去變更頁面來呈現資料的麻煩；因此就出現了幾套反應式框架像是 <a href="https://reactjs.org/">React</a> 和我個人偏好的 <a href="https://vuejs.org/">Vue</a>。在伺服器的部份，<a href="https://expressjs.com/">Express</a> 框架可以讓我們輕鬆在 <a href="https://nodejs.org/">Node</a> 環境上面撰寫伺服器程式。而在桌面應用程式方面，<a href="https://www.electronjs.org/">Electron</a> 這個框架讓我們可以直接用網頁來撰寫桌面應用程式，且產生的應用程式可以跨平台執行。這些不同的框架背後的程式語言都是 JavaScript，但是它們針對不同的需求、各自提供一整套完整的基礎功能來讓我們更容易地寫出對應的程式。</p>
<p>有些框架會相依於另一個框架，也就說它需要另一個框架先安裝了才能使用。例如在 C# 語言之中，所有的程式都是基於 <a href="https://dotnet.microsoft.com/">.NET</a> 這個框架在運行的，所以其它 C# 的框架，包括 <a href="https://zh.wikipedia.org/wiki/Entity_Framework">Entity Framework</a>、<a href="https://www.asp.net/mvc/">ASP.NET MVC</a>、<a href="https://blazor.net">Blazor</a> 等等，都是相依在 .NET 框架上的。除了相依性之外，應用程式也經常同時使用多個框架，例如你可以用 Electron 來打造應用程式的骨架、然後再用 Vue 來處理其互動性。</p>
<p>非程式語言的 CSS 也有例如 <a href="https://getbootstrap.com/">BootStrap</a> 這樣的框架，它針對了常見的排版需求、定義了一些版型讓設計者可以快速套用。當專案負責美工設計的人和負責製作頁面的人都同樣熟悉 BootStrap 的時候，傳遞畫面定義也變簡單了，因為就直接說例如「這邊放一個 card」就懂是什麼意思了。</p>
<p>不管你正在使用哪一種程式語言，都可以去了解一下該語言中有哪些時下流行的框架，並且至少熟悉其中一種，這樣可以讓你在進入職場的時候更容易快速開始工作並方便跟同事接軌。</p>
<h2>程式庫（library）</h2>
<p>如果說框架就好比是蓋房子的整體規範，那麼程式庫就有如是房子裡面的家電。程式庫是為了實現一組特定功能而預先寫好的程式（可能是原始碼或編譯過的檔案如 dll 檔），安裝到專案之中便能讓自己的程式獲得該程式庫提供的功能。框架通常著眼的是整個程式的大架構，而程式庫則是負責專門的功能，像是專做圖形處理、專做資料驗證、提供特定的互動元件、或是串接特定的服務等等。</p>
<p>要使用程式庫提供的功能，你不需要理解其內部程式碼的運作原理，你只需要知道如何正確呼叫它或者把它載入到你的程式中即可。這些程式庫的使用方法，我們稱之為是程式庫的 API <sup id="fnref1:2"><a href="#fn:2" class="footnote-ref">1</a></sup>。程式庫的 API 規格通常會詳細列在作者的網站上，或者有參考文件跟程式庫一起發行。</p>
<p>就如同框架有相依性，有些程式庫也會依賴特定的框架才能使用，像是我寫的 <a href="https://www.npmjs.com/package/gulp-workbox">gulp-workbox</a> 就是專門給 <a href="https://gulpjs.com/" target="_blank">Gulp</a> 用的，而 <a href="https://jqueryvalidation.org/">jQueryValidation</a> 顧名思義就是要有 jQuery 才能用。而就如同家電有眾多品牌，通常一種特定的功能也會有非常多不同的程式庫在試圖滿足這一項功能，因此寫程式的時候也需要稍微貨比三家一下來決定應該用哪一個。例如我的 <a href="https://www.npmjs.com/package/clickout-event" target="_blank">Clickout-Event</a> 是負責偵測「點擊在指定元件以外的範圍」的行為用的，做這件事情的程式庫至少有十幾個之多（不過既然我敢拿出來說，那就表示我有自信我的程式庫寫得比他們的都要更好用）。</p>
<h2>套件管理器（package manager）</h2>
<p>前面我們提到框架跟程式庫都有相依性的概念存在；除此之外，它們也都有版本的概念；框架或程式庫的作者常常會推出新的版本來提供新的功能或是修正一些錯誤。如果程式庫 A 依賴程式庫 B，然後 A 跟 B 各自推出了新的版本，且新版的 A 再次依賴於新版的 B，那你在更新的時候就必須要知道要同時更新 A 和 B 才行。數量不多的時候這還有可能手動進行，但是如果把所有的相依性整理起來，一個專案總共引用了上百個程式庫是很平常的事 <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref">2</a></sup>，這個要手動管理近乎是不可能的。</p>
<p>因此，各種程式語言現在都有它們的套件管理器。一個「套件」是指框架或程式庫打包之後的產物，裡面除了其程式本身之外，也會加上靜態資源、版本資訊、它所依賴的其它程式庫名稱、作者和版權資訊、簡短的使用說明與範例等等。而套件管理器則是幫助我們自動安裝和更新套件的工具程式。JavaScript 有 <a href="https://www.npmjs.com/">NPM</a> 和 <a href="https://yarnpkg.com/">Yarn</a>，Python 有 <a href="https://pip.pypa.io/">pip</a> 和 <a href="https://conda.io/">conda</a>，C# 有 <a href="https://www.nuget.org/">Nuget</a>，PHP 有 <a href="https://getcomposer.org/">Composer</a>，諸如此類的。你只要告訴套件管理器去安裝某個套件，如果它背後有相依於其它套件，那些也會同時自動下載並安裝最新版到你的專案之中；更新特定套件的時候也是如此，而且如果某個先前相依的套件如今不再需要，也會自動幫你移除以節省專案大小。</p>
<p>大部分的套件管理器都沒有內建 GUI 而需要在命令列介面當中操作，除了 Visual Studio 本身有內建一些 GUI 之外。<a href="https://code.visualstudio.com/" target="_blank">VS Code</a> 也有一些套件管理器的 GUI 外掛可以用。</p>
<p>隨著你寫的程式越來越多，有很大的機會是你也會需要製作至少是你自己要用的套件，因此很建議針對你最常寫的程式語言去了解一下如何開發對應的套件、以及發行到套件管理器網站上的流程。</p>
<h2>版本控制（version control）</h2>
<p>常玩電腦遊戲的人大概都聽過「謝夫羅德大法」：就是存檔跟讀取啦！玩到一個進度的時候就記得存檔一下，如果接下來玩到炸掉，那沒關係，讀取存檔從上次沒問題的地方繼續玩就好了。</p>
<p>有趣的是，寫程式也有這樣的需求。今天當我們把寫程式當成職業的時候，做出來的產品不再是由軟體工程師自己來設計，而是會有專案經理根據客戶的需求來規劃，軟體工程師變成是負責實現需求的人。然而，實務上我們經常會需求的變更；幾天前說要這樣弄，但是過了幾天突然又因為某些原因要更改設計了，這種狀況要完全避免是很難的。雖然有點挫折覺得這幾天的程式白寫了，但是也沒辦法，還是得先改回到之前的樣子然後再根據新的需求重新寫程式。要命的就在這裡：如果幾天前的版本沒有先另外儲存起來怎麼辦？我們怎麼能夠百分之百記得這幾天到底改了哪裡、然後正確無誤地還原到幾天前的樣子？</p>
<p>在沒有版本控制系統出現之前，要因應這種可能的狀況，唯一的辦法就是每隔一段時間把整個專案的檔案完整地備份到獨立的資料夾中。但是首先整個專案其實 90% 以上的程式碼都沒有改變卻要完整備份、這即便是做成壓縮檔也很浪費空間，再來我們也很難在必要的時候比較不同時間點的備份到底差異在哪裡，不容易確定哪一個備份才是我們要找的。</p>
<p>除此之外，大型的專案也往往不會只有一個工程師，而可能有好幾個人同時在寫不同部份的程式碼。當他們各自在修改的時候，暫時就會產生好幾個不同的分支版本，如此一來我們要追蹤正確的版本就又更困難了。而當我們最後要把不同工程師的成果統整起來的時候，我們還要很清楚每個人各自修改了那些部份，不然合併的時候一定會有遺漏。這些要手動管理都是很麻煩的。</p>
<p>版本控制系統就是為了解決諸如此類的版本管理需求才誕生的東西，而其中 <a href="https://git-scm.com/">Git</a> 是目前最多人在用的版本控制系統。當我們把一個專案資料夾變成 Git 存放庫之後，Git 會開始追蹤一切在這個資料夾裡面發生過的檔案變化，並且把各版本之間的改變以差異變更的形式儲存一個隱藏資料夾中，大幅節省了儲存空間，而且可以自動顯示出不同版本之間的差異在哪裡。當我們修改到一個差不多的地步時，我們可以「認可」變更，這個動作就好比遊戲中建立儲存點一樣，未來有必要的時候我們就可以自動還原整個資料夾的狀態到某次指定的認可之上。同時，Git 也可以讓我們創造專案的分支，即平行的版本，並且在必要的時候很輕易地合併不同的分支所作的修改。</p>
<p>最後，Git 也能夠讓我們把存放庫放到雲端空間，於是在不同的電腦上工作的各個工程師只要連線到雲端存放庫，就可以同步專案的狀態，大家都可以馬上看到別人做了哪些修改。著名的網站 <a href="https://github.com/">GitHub</a> 就是一個免費可以讓人上傳 Git 存放庫的空間。就算是業餘寫程式，把自己的專案上傳到 GitHub 上、也可以方便防範因為各種意外而導致本地檔案損壞的狀況，在雲端上永遠有備份。</p>
<p>Git 的預設操作也是在命令列中進行，不過除了 Git 本身內建有一套 GUI 之外，現在的 IDE 大多都有和 Git 整合，可以直接使用它們內建的 Git 功能操作認可、同步、比較編輯差異等等。</p>
<h2>偵錯（debug）</h2>
<p>對於夠複雜的專案來說，就算是寫程式的大神也很難完全避免程式寫錯。編譯階段的錯誤當然是容易抓到的，因為編譯器自然會告訴我們哪邊語法寫錯了、沒辦法編譯成功，麻煩的是執行階段的錯誤：程式能跑，可是跑出來的結果跟我們想要的卻不一樣，或是可能會當掉、閃退、跑太慢、出現錯誤訊息等等。要診斷執行階段的錯誤，只有靠自己的偵錯本領，所以這個是一定要鍛鍊的能力。</p>
<p>大部分的 IDE 都有提供偵錯工具，讓你可以在偵錯模式之下執行你的程式，透過中斷點、逐行執行、即時運算、變數內容查看等功能來逐漸縮小範圍、鎖定程式是從哪一個步驟開始跟你心中所想的執行結果有所出入，進而找出導致執行階段錯誤的原因並修正。如果程式執行得太慢，你也可以用同樣的方法找出是哪一段程式碼特別花時間，然後再設法加以改進。</p>
<p>不過，由於偵錯模式是一種特殊的直譯式執行模式，在一些少見的情況中其表現可能會跟實際執行的時候有一些差異，而偵錯工具在檢視計算屬性的時候可能也會產生一些副作用，這些差異可能導致實際執行中發生的錯誤無法在偵錯模式中復現，或是反過來在偵錯模式中出現的額外的錯誤。另外，也有一些情境是我們很難或甚至無法在偵錯模式之下執行程式的 <sup id="fnref1:3"><a href="#fn:3" class="footnote-ref">3</a></sup>、一定得正式發行才能執行。基於這一類的理由，我們也需要懂得如何在正式執行中進行偵錯，而最標準的辦法就是在程式執行的同時讓程式在一些關鍵的步驟中輸出記錄（log）。記錄可以輸出到主控台命令列、文字檔、或是我偏好的方式是輸出到資料庫中，而透過記錄內容的設計，我們一樣可以了解程式的執行步驟以及階段性的變數內容等等。</p>
<h2>測試（test）</h2>
<p>當我們要寫一個有點規模的程式的時候，我們基本上不太可能一口氣把整個程式從頭寫到尾，大抵都是基本架構先寫好，然後再逐漸地把需要的功能寫上去，而過程中常常又要回去修改之前寫過的部份以便可以加上更多功能。每個階段我們寫好一些功能之後，自然我們會稍微測試執行一下看看是否功能運作正確然後再繼續，不過經常會有一種情況是我們稍後做的修改不小心沒有兼顧到之前寫的功能、而使得之前的功能如果再次測試就會出問題的；這尤其在程式有大改版的時候很容易發生。</p>
<p>為了避免這種現象發生，但是又為了避免每次修改程式之後都得手動把所有的功能都測試一遍，我們就要會寫測試程式。測試程式是一個和你實際上在寫的主要應用程式平行存在的另一個小程式，它會用一些事先設定好的步驟和資料（稱為測試案例）去執行主要應用程式（可能是全部、也可能是僅其一小部份），然後看看執行的結果跟預期的結果是否相符。當測試程式發現主程式針對某項測試案例的執行結果不如同預期時，就會回報錯誤給我們知道。測試程式可以在很快的時間裡面把整個主程式的所有功能 <sup id="fnref1:cover"><a href="#fn:cover" class="footnote-ref">4</a></sup> 測試一遍，確保我們在開發的過程中一路上都沒有不小心動到之前寫過的功能。</p>
<p>測試程式可以完全自己寫，但在各種程式語言中大多也都有專用的測試框架（例如 JavaScript 中的 <a href="https://mochajs.org/" target="_blank">Mocha</a> 和 <a href="https://jestjs.io/" target="_blank">Jest</a>，Java 的 JUnit，C# 的 MSTest、NUnit 和 XUnit 等等）；這些測試框架除了幫我們打好基礎之外，另一個非常重要的好處在於它們常常跟 IDE 會有比較好的整合，例如會在某個地方顯示出當前專案中所有的測試項目，以便我們可以很方便地單獨執行其中的某些測試項目、而未必需要每次都把所有的測試全部跑一遍（對於大型專案來說，這樣做可能是會需要花一點時間的）。</p>
<p>如果一項測試牽涉到的程式碼非常大一串，就算測試結果沒通過，我們也難以立刻從那一大串程式碼裡面鎖定問題是出在哪個環節上。因此在寫測試的時候，我們會盡可能地把程式的功能切割成一小塊一小塊的，然後分別加以測試（因此，我們常會需要模擬每一個階段的資料輸入輸出）；這種把程式功能分割到最小單位再分別進行的測試就稱為單元測試（unit test）。要能夠寫單元測試，當然主程式本身的架構也必須要方便支援，例如把一大串很長的工作拆成若干個函數或方法，以便這些函數方法可以分開來被呼叫、進行測試。</p>
<p>除了單元測試，還有整合測試（integration test，測試單元之間整合起來的結果是否正確）和端對端測試（E2E test，指使用者真的去操作應用程式的功能，也就是測試應用程式的最大整體）。整合測試寫起來跟單元測試大同小異，只是覆蓋的程式碼更多；而端對端測試為了能夠在真實的執行環境中進行，也有專門的測試框架（例如 <a href="https://www.cypress.io/" target="_blank">Cypress</a>）可以撰寫自動測試。</p>
<h2>例外處理（exception handling）</h2>
<p>即便程式已經經過很多的測試，實務上還是很難完全避免發生一些開發者完全沒有預期到、測試的時候也沒有測出來的潛在例外可能性。所謂的「例外」，指的是程式在執行階段中遇到根本無法正確執行某一行指令、無法繼續的這種狀況（它同時也指具有例外處理機制的程式語言在遇到這種情況的時候會丟出的、包含有例外詳細資料的物件）。這種開發者沒有預期到的例外，尤其是在當我們把程式交給一個完全不會用的人亂用的時候很容易發生，因為此時使用者特別會去對程式做一些開發者沒想到的操作、或是提供奇怪的資料輸入。</p>
<p>最早年程式語言沒有內建例外處理機制的時候，如果程式執行過程發生例外，基本上要不是電腦直接當掉就是程式立刻閃退。雖然今天我們仍舊三不五時會遇到 App 閃退的情況，但是對於一個有充分應用例外處理機制的程式來說，這原則上是不應該發生的。對於那一類的程式，即使執行過程中發生了開發者沒有預期到的例外，程式也不會閃退，而能夠在提示使用者「操作有誤」或是「目前這項功能暫時無法使用」之後繼續讓使用者使用其它的功能，並且可能會在背景中把錯誤的細節回報給開發者，以便未來可能修正這項例外。</p>
<p>現在大部份的程式語言都有內建類似 <code>try</code>/<code>catch</code> 的語法可以接住程式丟出的例外並且加以處理；但是由於我們不太可能把我們寫的每一段程式碼都用 <code>try</code>/<code>catch</code> 包起來，有一些框架也提供了全域的例外處理機制設計，使得每當程式出現了漏接的例外的時候就自動進入到某一個地方來統一處理；此時我們就可以回覆一個預設的訊息給使用者，並且可能同時留下記錄以便開發人員可以檢視發生了什麼例外沒有被接住。</p>
<h2>持續整合/持續部署（Continuous Integration/Continuous Deployment）</h2>
<p>簡稱 CI/CD，這是一個對於大團隊開發的專案來說尤其很重要的概念。前面在介紹版本控制的時候提到，大團隊進行開發的時候經常會分出很多個不同的分支版本、以便團隊成員可以各自專注於自己負責開發的項目。這些不同分支的程式碼到最後都必須被合併到主要分支之上，而這個合併的動作如果拖得太久、各個分支的差異越來越大，就會變得非常難進行，所以一般都會盡可能縮短合併的週期（例如每天）、並且每次合併時都要確定整合之後的程式碼沒問題，這樣才能夠在有問題發生的時候及早發現並且方便修正；而修正了之後當然又要再次確認程式碼無誤……這個確認的過程我們當然也會希望可以自動化，而不是每次都需要手動進行，這就是所謂的持續整合（CI）。它通常會在每次進行完 Git 的推送之後自動執行如下的動作：</p>
<ol>
<li>建置：確定程式碼在一個標準的環境中是可以建置完成的；講白話一點就是起碼編譯要能過才行，這是最基本的。</li>
<li>測試：確保新的程式碼仍舊可以通過所有的測試案例，也就是既有的功能都沒有因為撰寫新功能而受到影響。</li>
<li>程式碼分析：這可能包括程式碼風格檢查（以確定團隊的成員都用統一的風格來撰寫程式）以及弱點掃描（是否採用了某種已知有安全性弱點的寫法）等等。</li>
</ol>
<p>在完成了 CI 之後，下一個步驟就是持續部署（CD，亦可指持續交付，即 Continuous Delivery），也就是自動把確認過沒問題、建置完成的應用程式發佈出去，且（特別是針對網路服務）持續監控應用程式是否正常執行。</p>
<p>常被提到的 CI/CD 工具包括 Jenkins、Drone、CircleCI、Travis CI、GitHub Action 等等。</p>
<h2>結語</h2>
<p>本篇中介紹的這些都是在職場上經常會被要求具備的技能，即使自己在練習寫小程式的過程中沒有感受到應用這些技能的必要性，也還是很建議有意轉正職的入門者來故意練習將它們引入到自己的專案之中，這對於未來參與大型專案會有很好的預備作用的。</p>
<p>當然還有太多的主題是這邊的篇幅無法一一介紹到的，像是重構、安全性、UI/UX 設計、演算法、軟體架構……這些也都是作為職業軟體工程師會需要多少了解的東西，要是有機會的話，未來會再分享在下對這些主題的心得。</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:2">
<p>你可能在串接網路服務的時候聽過 API（應用程式介面）這一則術語，不過 API 是泛指任何軟體跟軟體之間溝通的介面，所以這邊由於是你的程式主體要跟程式庫之間做溝通，因此其規格也是叫作 API。&#160;<a href="#fnref1:2" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:1">
<p>這其實也是因為套件管理器的發達才有這種現象；一旦套件管理變方便了之後，現在的程式庫有一種明顯的傾向是各自專注於非常特定的單一功能，然後再透過複雜的相依性把所有需要的功能像金字塔一樣堆積起來。程式庫越是專門化，也就越容易只引用那些我們需要的功能，也越容易分別確保各個程式庫的程式碼品質。&#160;<a href="#fnref1:1" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:3">
<p>例如我最常遇到的是這樣的情境：當我需要把站台佈署到公開網域以提供 callback 網址給第三方呼叫、但是我的網路環境又讓我無法架設反向代理來把網域指向偵錯程式的時候。&#160;<a href="#fnref1:3" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
<li id="fn:cover">
<p>一個相關的概念稱為程式碼覆蓋率（code coverage），也就是主程式的程式碼有多少被測試程式測試到了。理想狀況應該是覆蓋率達到 100%。&#160;<a href="#fnref1:cover" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
</ol>
</div>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/">業界用程式工具與技能</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2022/02/14/%e6%a5%ad%e7%95%8c%e7%94%a8%e7%a8%8b%e5%bc%8f%e5%b7%a5%e5%85%b7%e8%88%87%e6%8a%80%e8%83%bd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>VS Code 延伸模組撰寫初體驗</title>
		<link>https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/</link>
					<comments>https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Sun, 27 Jun 2021 13:03:24 +0000</pubDate>
				<category><![CDATA[初體驗]]></category>
		<category><![CDATA[開發工具]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[JSDoc]]></category>
		<category><![CDATA[VS Code]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=358</guid>

					<description><![CDATA[<p>最近 VS Code 在 1.57 版推出了一個新功能是內建對於 JSDoc 的 @link 語法的支援，其效果是可以將註解當中的符號與原始碼中真正的符號連結起來。如此一來，一方面我們可以在顯示出註解... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/">VS Code 延伸模組撰寫初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2021/06/image-1625230685319.png" alt="file" /></p>
<p>最近 <a href="https://code.visualstudio.com/" target="_blank">VS Code</a> 在 1.57 版推出了一個新功能是內建對於 <a href="https://jsdoc.app/" target="_blank">JSDoc</a> 的 <a href="https://jsdoc.app/tags-inline-link.html"><code>@link</code></a> 語法的支援，其效果是可以將註解當中的符號與原始碼中真正的符號連結起來。如此一來，一方面我們可以在顯示出註解的時候、直接按下上面的連結來前往對應的符號，另一方面如果我們對符號進行重新命名的重構、則被連結的符號也會跟著更新，這對於維護註解文件來說真的很方便。</p>
<p>不過這個功能雖然方便，用起來卻還是有一點讓人不禁強迫症發作。首先，假如我要參照的是一個類別的成員，那我就必須連同類別一起寫出來，即便我的 JSDoc 是寫在同一個類別裡面也一樣：</p>
<pre><code class="language-ts">class MyClass {
    public myProp: string;

    /** 這個方法跟 {@link MyClass.myProp} 有關 */
    public myMethod(): void { }
}</code></pre>
<p>除此之外，如果連結的對象是一個方法，那寫連結的時候不應該把 <code>()</code> 也加上去，否則就沒有辦法正確地連結：</p>
<pre><code class="language-ts">class MyClass {
    /** 這個屬性跟 {@link MyClass.myMethod} 有關 */
    public myProp: string;

    /** 這個方法跟 {@link MyClass.myProp} 有關 */
    public myMethod(): void { }
}</code></pre>
<p>問題是我們在閱讀上其實會比較希望：1. 不用多餘地呈現出當前所在的類別，2. 函數或方法可以加上括號、比較清楚。確實 JSDoc 也提供了這樣的語法，允許我們用任意的替換文字來決定呈現結果：</p>
<pre><code class="language-ts">class MyClass {
    /** 這個屬性跟 {@link MyClass.myMethod myMethod()} 有關 */
    public myProp: string;

    /** 這個方法跟 {@link MyClass.myProp myProp} 有關 */
    public myMethod(): void { }
}</code></pre>
<p>如此一來，在 VS Code 中的呈現效果就會像這樣：</p>
<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2021/06/image-1624927966690.png" alt="file" /></p>
<p>OK，確實當滑鼠移到符號上的時候看到的註解非常清楚了……但是註解的原始碼本身卻因此奇醜無比。難道沒辦法讓註解的原始碼也一樣跟著用好看一點的方式呈現嗎？</p>
<p>我是不知道會不會未來 VS Code 自己也加入了這個功能，但是在那之前，我想說就趁這個機會來自己寫一個 VS Code 的延伸模組（這是官方的用詞，底下簡稱外掛）來解決這項需求；我取名為 <a href="https://marketplace.visualstudio.com/items?itemName=MuTsunTsai.jsdoc-link">JSDoc&nbsp;Link</a>。本篇就來分享其中的心得。</p>
<h2>建立外掛專案</h2>
<p>官方提供了一個很好的範本來讓我們可以快速上手建立外掛專案，詳情可以參見官方的<a href="https://code.visualstudio.com/api/get-started/your-first-extension">教學文章</a>。簡單來說，首先必須安裝 <a href="https://yeoman.io/" target="_blank">Yeoman</a>（一個非常普及的鷹架工具，不過我個人可能更喜歡用 <a href="https://sao.vercel.app/">Sao</a> 一點點，這個改天有空再談）以及 <a href="https://www.npmjs.com/package/generator-code">generator-code</a>（哇靠，VS Code 是怎麼搶到「code」這個字的啊？簡直匪夷所思），然後執行 <code>yo code</code>、填一填問題就可以造出外掛專案了（會順便幫你執行 <code>npm install</code>，所以專案一打開馬上可以上路）。</p>
<p><strong>注意</strong>：稍後會提到的發布工具 vsce 目前並不支援 PNPM；這件事有一個積了很久的 <a href="https://github.com/microsoft/vscode-vsce/issues/421">issue</a> 但至今未有完美解法。所以暫時 VS Code 延伸模組的專案都只能用 NPM（或 Yarn 似乎也可以）來管理。</p>
<p>用 VS Code（當然）開啟專案之後，只要按下 F5 就可以進行偵錯執行；此時會打開一個新的 VS Code 視窗，你會看到標題列上面寫著「延伸模組開發主機」，這個特別的 VS Code 實體就是裝著我們正在開發中的外掛，於是就可以直接測試外掛的執行效果。</p>
<p><strong>小秘訣 1</strong>：開發主機預設會打開前一次執行時停留的工作區（若有的話）；如果按下 F5 之後發現狀態列一直在轉圈圈「正在建置…」卻沒有開啟 VS Code 視窗，這可能是因為你已經開啟了另一個 VS Code 視窗是跟「即將打開的開發主機」為相同工作區的。此時只要再按一次那個「正在建置…」，應該就會重新開啟一個空白的 VS Code 開發主機視窗了。</p>
<p><strong>小秘訣 2</strong>：如果之後已經發佈了開發的外掛並且安裝到了自己的 VS Code 上頭，那麼再次執行這個開發主機的時候會不會正式版外掛跟開發版外掛之間有衝突？我實驗的結果是不會，此時在開發主機視窗裡面會自動以開發版的外掛取代掉正式版外掛的功能，可以放心。</p>
<h2>撰寫外掛</h2>
<p>曾聽過有人評論「VS Code 不是一個編輯器，是一個框架」，這個說法並不誇張，因為 VS Code 的外掛能做的事情實在太多了，我這邊連要講個大概都不可能（況且我也不需要，官方<a href="https://code.visualstudio.com/api/get-started/extension-anatomy">教學文件</a>講得夠清楚了）。所以我這邊只針對我這個外掛中有用到的部份做簡略的解說就好。</p>
<p>這個外掛要做的事情是「找出當前編輯器中特定模式的字串、並且在呈現上加以取代」，而之前我在<a href="https://abstreamace.com/sglab/2021/06/13/bps-%e9%96%8b%e7%99%bc%e5%88%86%e4%ba%ab%e4%b9%8b-9%ef%bc%9a%e5%a4%9a%e5%9c%8b%e8%aa%9e%e7%b3%bb/">這篇文章</a>中曾經提到過的 <a href="https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally" target="_blank">i18n Ally</a> 外掛也做了非常類似的事情：它會把翻譯字串代碼替換成實際的翻譯文字（的摘要）呈現在畫面上，因此我這個外掛的程式碼很大的程度上參考了該外掛的技巧（這就是活在開源時代的福利，很多東西都直接參考現成的就馬上能學會，再也不用翻遍文件找答案了）。</p>
<p>底下我直接針對重點的地方講解；完整的程式碼在 <a href="https://github.com/MuTsunTsai/jsdoc-link">GitHub</a> 上頭，讀者有興趣可以自己參照。</p>
<h3>package.json</h3>
<p>主要的重點在於：</p>
<pre><code>    // 設定啟動條件為只要有跟 JS 或 TS 沾得上邊的東西被開啟的話，
    // 就啟動這個外掛。設成 &quot;*&quot; 當然也是一個可行的偷懶辦法，
    // 不過沒事最好還是不要隨便消耗裝這個外掛的使用者的裝置效能。
    &quot;activationEvents&quot;: [
        &quot;onLanguage:javascript&quot;,
        &quot;onLanguage:javascriptreact&quot;,
        &quot;onLanguage:typescript&quot;,
        &quot;onLanguage:typescriptreact&quot;,
        &quot;onLanguage:vue&quot;,
        &quot;onLanguage:svelte&quot;,
        &quot;workspaceContains:*.js&quot;,
        &quot;workspaceContains:*.jsx&quot;,
        &quot;workspaceContains:*.ts&quot;,
        &quot;workspaceContains:*.tsx&quot;,
        &quot;workspaceContains:*.vue&quot;,
        &quot;workspaceContains:*.svelte&quot;
    ],
    &quot;contributes&quot;: {
        // 這個外掛完全不需要提供選項、命令等等的東西、
        // 純粹在背景中跑就好了，因此這邊為空
    },</code></pre>
<h3>extension.ts</h3>
<p>extension.ts 是外掛的主體程式碼；這個外掛做的事情很精簡，因此一個檔案就搞定了，不用太複雜。</p>
<h4>註冊事件</h4>
<p>extension.ts 輸出了一個函數為 <code>activate()</code>，這個函數顧名思義就是在外掛啟動的時候會被執行，而在這裡面我們將來註冊我們要監聽的事件：</p>
<pre><code class="language-ts">import * as vs from &#039;vscode&#039;;

const THROTTLE_DELAY = 800;

export function activate(context: vs.ExtensionContext): void {

    ...

    // 把等一下會看到的 process 函數做一個 throttle，以增進效能
    // 這個 throttle 是模仿 lodash 的作法簡化寫成的；
    // 原本我是直接引用 lodash，但是被嫌打包的檔案太多，就想說自己刻一個算了
    const throttledProcess = throttle(process, THROTTLE_DELAY);

    // 當切換了編輯器的時候當然要更新畫面。
    vs.window.onDidChangeActiveTextEditor(throttledProcess, null, context.subscriptions);

    // 只要選取範圍改變也都要更新；
    // 這包括輸入游標的移動、以及當然文字輸入也會觸發。
    // 我固然是要改變顯示效果，但我也一樣必須讓使用者仍然能編輯文字，
    // 因此只要使用者的選取範圍有包括到 @link 所在的那一列，
    // 就不要對那一列進行顯示轉換
    vs.window.onDidChangeTextEditorSelection(throttledProcess, null, context.subscriptions);

    // 改變當前檔案的時候也要更新畫面。
    vs.workspace.onDidChangeTextDocument(event =&gt; {
        const editor = vs.window.activeTextEditor;
        if(event.document == editor?.document) throttledProcess();
    }, null, context.subscriptions);

    // 啟動的時候總之先執行一次再說
    throttledProcess();
}</code></pre>
<h4>替換顯示文字的原理</h4>
<p>VS Code 允許外掛在特定的文字上面加上裝飾（decoration），例如可以改變文字顏色、設置粗體斜體、加上底線外框等等，也可以在要裝是的文字的前面後面指定要插入某種東西（例如常見的應用是一個小圖示或色塊）。無論是要進行什麼樣的裝飾，首先要作的都是利用 <code>window.createTextEditorDecorationType()</code> 方法來產生一個「裝飾類別」。之後當我們呼叫 <code>TextEditor.setDecorations()</code> 方法的時候，編輯器會一口氣地把所有傳入的位置都加上指定類別的裝飾（且上次呼叫時加入的相同類別裝飾如果沒有再次列在傳入的位置之中，則那些裝飾會被移除，這個設計很方便，使得我們不用擔心裝飾會被重複加上去）。</p>
<p>然而，內建的 API 裡面並沒有「完全把特定的文字改顯示成另外的一些文字」這樣的機制存在。那 i18n Ally 是怎麼做到這個效果的？原來它使用了一個有點 hack 的技巧。關鍵在於宣告類似這樣的裝飾類別：</p>
<pre><code>const hiddenDecorationType = vs.window.createTextEditorDecorationType({
    textDecoration: &#039;none; display:none;&#039;,
});</code></pre>
<p>這邊利用了 VS Code 會把 <code>textDecoration</code> 屬性的內容完全照抄地寫進 CSS 裡頭的「漏洞」來「注入」任意的 CSS 進去。所以替換文字的真面目就是：先本原本的文字完全隱藏起來，待會再利用 <code>DecorationOptions.after</code> 去插入要替換的文字即可。</p>
<h4>找出要替換的文字</h4>
<p>底下是 <code>process()</code> 函數的開頭部份：</p>
<pre><code class="language-ts">const supportedLang = [&#039;javascript&#039;, &#039;typescript&#039;,
    &#039;javascriptreact&#039;, &#039;typescriptreact&#039;, &#039;svelte&#039;, &#039;vue&#039;];

function process(): void {
    const editor = vs.window.activeTextEditor;
    const document = editor?.document;
    if(!editor || !document) return;

    const lang = document.languageId;
    if(!supportedLang.includes(lang)) return;

    ...</code></pre>
<p>這邊做的事情是確定當前開啟的檔案是支援的其中一種程式語言，不然就不要繼續處理。接下來 <code>process()</code> 做的事情就是先用 <code>document.getText()</code> 方法取得當前編輯器中的全部文字，找出裡面所有型如 <code>/** ... */</code> 的部份，然後再去看這裡面有沒有型如 <code>{@link text alt}</code> 這樣的東西。找到了的話，我們就去產生一個 <code>DecorationOptions</code> 物件，其結構類似這樣：</p>
<pre><code class="language-js">{
    range: new vs.Range(start, end),
    renderOptions: {
        after: {
            color: linkColor,
            contentText: alt,
            fontStyle: &#039;normal&#039;
        }
    }
}</code></pre>
<p>其中 linkColor 是用內建方法去抓取當前主題中的連結文字顏色，以便顯示出來的效果跟當前使用者的佈景主題設定一致。</p>
<pre><code class="language-ts">linkColor = new vs.ThemeColor(&#039;textLink.foreground&#039;);</code></pre>
<p>另外注意到我設定了 <code>fontStyle: &#039;normal&#039;</code>，這是因為有些人會在佈景主題中設定註解要用斜體字顯示，但即便是那樣 @link 應該也還是要用一般字體，所以這邊我就自作主張地強制設定字體為一般字體了。</p>
<h4>完成替換</h4>
<p>找出全部要替換的目標之後，如前述地這邊再多做一項檢查，就是每一個替換目標有沒有包含在選取範圍所在的列上，沒有的話才真的要進行替換。</p>
<pre><code class="language-ts">const selection = editor.selection;
const sets = decoratorSets.filter(d =&gt;
    (selection.start.line &gt; d.start.line || d.start.line &gt; selection.end.line) &amp;&amp;
    (selection.start.line &gt; d.end.line || d.start.line &gt; selection.end.line)
);</code></pre>
<p>在外掛裡面我定義了兩種裝飾類別，除了前述的 hiddenDecorationType 之外，還有另外一個：</p>
<pre><code class="language-ts">const hoverEnableDecorationType = vs.window.createTextEditorDecorationType({
    textDecoration: &#039;none; display:inline-block; width:0; height:0; overflow:hidden;&#039;,
});</code></pre>
<p>之所以要用這個是因為我實驗之後發現要用這樣的方式隱藏才能夠正確地把 hover 訊息的效果保留下來。於是 <code>process()</code> 函數所做的最後一件事就是把找到的替換位置依照性質分組成這兩種類別，然後再呼叫 <code>editor.setDecorations()</code> 方法完成替換。</p>
<p>這就是這個外掛所做的全部的事情。</p>
<h2>上架外掛</h2>
<p>上架外掛的部份官方<a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension">教學文件</a>也是講得滿仔細的，且官方工具 vsce 也算是滿好用的。跟 np 很類似，vsce 也是可以選擇 <code>major</code>、<code>minor</code>、<code>patch</code> 三種發佈等級，會自動增加 package.json 的版本號、認可並且上傳至 VS Code Marketplace。當然在發佈之前必須先註冊 Marketplace 和 Azure DevOps 的帳號，不過這些文件中都示範得很清楚，不贅述。</p>
<p><strong>小秘訣 1</strong>：我的印象中，如果執行的是 <code>vsce publish minor</code> 的話（合理推測 <code>major</code> 應該也一樣），在發佈成功之後會順便自動開啟 GitHub 頁面來讓你填寫 Release 文案，但如果是 <code>vsce publish patch</code> 的話似乎就不會自動執行這個動作。你還是可以自己去 GitHub 填寫、如果想要的話，不過請千萬記得要先把認可 push 到 GitHub 上頭再做這個動作，否則到時候你真的要 push 的時候可能會遇到一堆問題（我曾經搞砸過一次，那後續的修正還有一點小麻煩，總之記得就對了）。</p>
<p><strong>小秘訣 2</strong>：執行 vsce 之後會在 git 上面增加 tag，但是預設設定中 VS Code 並不會把 git tag 在 sync 的時候推送到遠端；如果要自動執行 tag 的推送，必須設定 <code>git.followTagsWhenSync</code> 為 <code>true</code>。</p>
<p><strong>小秘訣 3</strong>：可以的話，盡量不要讓自己的外掛依賴別的套件以便縮小打包的大小；而如果非得要相依，那最好在建立專案的時候啟用「<a href="https://webpack.js.org/" target="_blank">webpack</a> 打包」的選項，如此一來就會幫你設定好打包機制、將相依性都包成一個檔案以方便發佈。</p>
<p><strong>小秘訣 4</strong>：Azure DevOps 的 access token（設定的地方在大頭照左邊那個有齒輪的圖示點進去）預設的期限只有 30 天，雖然可以自訂一個超長的期限、但預設不建議這麼做。因此對於像我這種不用經常維護的人來說，大概每次更新都得來產生一次新的。</p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/">VS Code 延伸模組撰寫初體驗</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2021/06/27/vs-code-%e5%bb%b6%e4%bc%b8%e6%a8%a1%e7%b5%84%e6%92%b0%e5%af%ab%e5%88%9d%e9%ab%94%e9%a9%97/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>TypeScript 中的 any 和 unknown</title>
		<link>https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/</link>
					<comments>https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/#respond</comments>
		
		<dc:creator><![CDATA[村哥]]></dc:creator>
		<pubDate>Thu, 06 May 2021 04:09:18 +0000</pubDate>
				<category><![CDATA[程式語言]]></category>
		<category><![CDATA[隨筆]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://abstreamace.com/sglab/?p=268</guid>

					<description><![CDATA[<p>TypeScript 當中有 unknown 這個型別已經是好一陣子的事情了，網路上也有很多文章在解釋它跟 any 的差別，然而那些文章大多都只是從定義層面上在探討，並沒有真的回答到「為什麼我們應該要... &#187; <a class="read-more-link" href="https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/">閱讀全文</a></p>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/">TypeScript 中的 any 和 unknown</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" src="https://abstreamace.com/sglab/wp-content/uploads/2021/05/image-1625231442991.png" alt="file" /></p>
<p><a href="https://www.typescriptlang.org/" target="_blank">TypeScript</a> 當中有 <code>unknown</code> 這個型別已經是好一陣子的事情了，網路上也有很多文章在解釋它跟 <code>any</code> 的差別，然而那些文章大多都只是從定義層面上在探討，並沒有真的回答到「為什麼我們應該要用 <code>unknown</code>」的這個關鍵問題，因此這邊我想稍微寫一點心得。</p>
<p>一開始，我先用我自己的方式來定義一下兩者：</p>
<ul>
<li><code>any</code> 型別是指「這個東西同時為任何的東西」。</li>
<li><code>unknown</code> 型別是指「我們不知道這個東西是什麼東西」。</li>
</ul>
<p>注意到我的用詞，這導致了兩者決定性的差異：</p>
<ul>
<li>不管你要對 <code>any</code> 型別的變數進行任何操作都可以，因為它同時是任何的東西！</li>
<li>不管你要對 <code>unknown</code> 型別的變數進行任何操作（除了純粹的讀寫和比較之外）都<strong>不行</strong>，因為你根本不知道它是什麼東西。</li>
</ul>
<p>這樣聽起來，<code>unknown</code> 好像非常難用，所以很多新手在當他遇到「不知道是什麼東西的東西」的時候，他會直覺選擇使用 <code>any</code> 而非 <code>unknown</code>。可是從軟體架構的角度來看，這樣做卻是危險的，而唯有理解為什麼，我們才會知道 <code>unknown</code> 其實是個很好用的東西；我們甚至還會得到一個結論是：<strong>一個健全的 TypeScript 程式，應該要幾乎沒有使用到 <code>any</code> 型別才對</strong>。</p>
<h2>功能一：保證變數的值不會被不小心地操作到</h2>
<p>我們來想像一個寫底層架構的時候常常遇到的情境。我們在寫一個輔助函數，比方說這個函數是用來比較兩個陣列是否相等用的。然而，我們並沒去對「誰會來用這個輔助函數」多作假定，所以我們只知道傳入的參數一定是陣列，但是陣列裡面有可能是任何的東西。</p>
<p>問題來了，當我們在宣告參數的類別的時候，我們不能只說「反正這個參數是一個陣列」，因為陣列在 TypeScript 中是一個泛型，TypeScript 要求我們在使用陣列型別的時候一定要指定泛型參數。但是我們不知道陣列裡頭有什麼，怎麼辦呢？我們可能會這樣寫這個輔助函數：</p>
<pre><code class="language-ts">function compareArray(arr1: any[], arr2: any[]): boolean {
    // 如果連陣列大小都不一樣那肯定就不一樣，直接傳回結果
    if(arr1.length != arr2.length) return false;

    for(let i = 0; i &lt; arr1.length; i++) {
        if(arr1[i] !== arr2[i]) return false;
    }
    return true;
}</code></pre>
<p>可能有人會問，為什麼不改用泛型的宣告，例如下面這樣：</p>
<pre><code class="language-ts">function compareArray&lt;T&gt;(arr1: T[], arr2: T[]) {
    ...
}</code></pre>
<p>原因有兩點：一、我們並沒有規定說陣列裡面放的一定是同一種型別的東西，有可能是各種不同型別的東西一起來的；二、這種泛型的宣告會使得被比較的兩個陣列一定要打從一開始就是同一種陣列、才能夠呼叫這個函數，否則在編譯階段就會被 TypeScript 擋下來，但是實務上我們可能會真的想要在執行階段中拿一個數字陣列和一個字串陣列來作比較。</p>
<p>總結起來，兩點原因都是基於同樣的軟體架構哲學：我們不希望對「呼叫這個函數的程式碼」作太多的假定，而希望這個函數可以是以最靈活的方式被呼叫。</p>
<p>那這樣一來，看起來把參數宣告成 <code>any[]</code> 就是必然的了。可是我們來稍微發揮一下想像力吧：假如今天我們突然想要寫另外一個函數，它的功能是只會去比較陣列的偶數索引。因為程式碼很類似，所以接到這個任務的菜鳥工程師就直接複製貼上，然後稍微改幾個字，但是他卻手殘了：</p>
<pre><code class="language-ts">function compareArrayAtEven(arr1: any[], arr2: any[]): boolean {
    // 如果只要比較偶數索引，那兩個陣列的大小最多可以差一
    let diff = arr1.length - arr2.length;
    if(diff &gt; 1 || diff &lt; -1) return false;

    for(let i = 0; i &lt; arr1.length &amp;&amp; i &lt; arr2.length; i++) { // 錯了
        if(arr1[i] !== arr2[i] + 2) return false; // 錯了
    }
    return true;
}</code></pre>
<p>原本他應該是要把 <code>i++</code> 修改成 <code>i += 2</code> 才對，但是他當時剛好在跟公司新來的可愛女生聊天，一時不注意把 <code>+ 2</code> 的事情寫到下一行去了。</p>
<p>此時 <code>any</code> 的一個大缺陷就暴露了出來：對於 TypeScript 來說，因為 <code>arr2[i]</code> 是一個 <code>any</code>，不管要幹嘛都可以，所以當然我們也可以拿它來作加法或其它操作，因此這個程式就這樣通過了編譯而沒有報出任何錯誤，以至於這個錯誤得一直等到不知道過了多久、在執行時發現比較的結果不正確、才會來檢查程式碼而發現菜鳥的疏忽。而我們知道，程式碼的錯誤越晚被發現、代價往往也越高；說不定這項錯誤會一直等到程式都已經上線了才被發現，而搞不好已經因為這個錯誤引發了許多嚴重的交易問題。</p>
<p>然而，好的型別利用卻可以幫助在一開始就避免這個問題。如果我們原本在寫 <code>compareArray</code> 的時候改用 <code>unknown</code> 型別，那麼菜鳥在手殘的時候就會馬上發現：</p>
<pre><code class="language-ts">function compareArrayAtEven(arr1: unknown[], arr2: unknown[]): boolean {
    let diff = arr1.length - arr2.length;
    if(diff &gt; 1 || diff &lt; -1) return false;
    for(let i = 0; i &lt; arr1.length &amp;&amp; i &lt; arr2.length; i++) {
        if(arr1[i] !== arr2[i] + 2) return false; // TypeScript 會在這邊報錯
    }
    return true;
}</code></pre>
<p>可以參見這個 <a href="https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABBOBbADgQwE4FMCC22mAnvlAKIBuuYAFDtgIwBci4A1mHAO5gDaAXQA0iRgCY2nbnyEBKNgCM4cADa5MSAN4AoRPsTqoiACYxgwRAF4xRJgDp1YAOZQAFogC0t7OMe1XNwBuPQNzOjMLRAA+RCZEAB8E03NLAB4vJjlEPCgQbCRgTFUAZ1wQg0RgOGw6I0QYa0QABiCGxAzGBydAxAAyPvbOoj8e9zaYAGpJ7N1KyvCu-hhBRABCKxsJZdXJxHFs3PzC4rK2gHpzxAAVEnRcAGUIbBh0Y0Bgc0AKdUBMBMAohMBGDUA9MmhfQAXxBOVweQKiCg2BA5R0oKAA">Playground</a> 來看報錯的效果。</p>
<p>因為我們不能夠對 <code>unknown</code> 型別的東西進行任何除了讀寫和比較以外的操作，所以拿 <code>arr2[i]</code> 來進行加法、或者呼叫它的成員等等都是不行的，我們只能原封不動地把它跟另外一個變數做比較、或是把它的值原封不動地儲存到另一個變數之中等等。使用了 <code>unknown</code>，就可以避免一切這種不小心的手殘，因為這個型別可以保證變數的值絕對不會被動到——而在實務上，幾乎所有「我們不知道這個變數的型別是什麼」的底層情境中，我們確實就是希望能保證這一點。這很合理：既然我們不知道它是什麼，那就表示我們不知道它有哪些操作可用，而如此一來我們又怎麼會想要去操作它呢？</p>
<p>反過來，用了 <code>any</code> 其實在軟體架構觀點上，簡直就是在自己的程式碼裡面放了一個不定時炸彈，隨便一個不小心都可能導致對變數的值做了我們本來沒有要做的操作。</p>
<p>所以這就是 <code>unknown</code> 的第一個重要功能：在很多底層的程式碼中，我們不知道傳入的值是什麼型別，但是我們本來也就沒有要對它做任何操作、而只是要暫時存放或比較它，此時用 <code>unknown</code> 就能保證它絕對不會不小心被動到。</p>
<h2>功能二：負責把關來自外界的輸入</h2>
<p>假設我們的 TypeScript 程式有一些對外的公開 API 可以讓別的程式來呼叫，那其實我們對於它們傳進來的參數是沒有任何掌控的，尤其如果那些「別的程式」是由不同的團隊來撰寫的時候更是如此。我們當然可以在 API 文件上面寫清楚我們預期傳入的參數是什麼型別，但是他們會不會照做（或甚至文件有沒有看清楚）就又是另外一回事了。</p>
<p>因此，其實在內部程式碼當中，對於任何對外的 API 介面，不管我們實際上預期的參數型別為何，我們一開始都應該要把其參數宣告成 <code>unknown</code> 型別才比較安全 <sup id="fnref1:1"><a href="#fn:1" class="footnote-ref">1</a></sup>，然後之後再來在程式碼中做該有的型別檢查以限縮參數的型別。例如下面的 API 是一個預期一個字串參數的例子：</p>
<pre><code class="language-ts">export function splitString(arg: unknown): string[] {
    if(typeof arg == &#039;string&#039;) {
        // 檢查完了之後才做我們真正要做的事情，例如：
        return arg.split(&#039;,&#039;);
        // 此處 TypeScript 會正確地知道 arg 的型別為字串，
        // 因為剛才我們用 typeof 檢查過了。
        // 對於更複雜的型別檢查，可以用 instanceof 關鍵字、
        // 或是使用 TypeScript 的 TypeGuard 來達成，
        // 這是題外話，這邊先不深入討論。
    } else {
        // 否則看要怎麼進行錯誤處理；這邊的方法是傳回預設值
        return [];
        // 或者也可以丟出一個 Error 包含了我們自訂的錯誤訊息
    }
}</code></pre>
<p>養成把對外 API 的參數宣告成 <code>unknown</code> 的好習慣，並且避免使用 TypeScript 中的 <code>as</code> 關鍵字來作型別斷言（取而代之地，應該使用 <code>instanceof</code> 或自訂的 TypeGuard 來確認型別），將可以提醒寫程式的人永遠要對傳入值做必要的檢查（否則在型別限縮之前，我們將根本不能對參數進行操作），如此一來便可以預防各種執行階段中的不可預期現象發生。反之如果是宣告成 <code>any</code>，那就有可能不小心對參數值做出原本我們沒有要做的事情。</p>
<h2>那 <code>any</code> 是否永遠都不該使用？</h2>
<p>很大的程度上來說，是的；我個人現在是「TypeScript 程式碼中應該要幾乎沒有 <code>any</code> 才對」主張的擁護者。對於很多我接手的程式碼，我都會先搜尋出所有使用到 <code>any</code> 的地方，而它們大多都可以直接換成 <code>unknown</code> 而不用作額外的修改——而如果換成 <code>unknown</code> 之後某個地方因此就出現了編譯錯誤，那幾乎在所有的情況中，那都是突顯出了程式碼具有潛在的異味（bad smell），而釐清為什麼改成 <code>unknown</code> 之後有編譯錯誤、往往能讓程式碼變得更加健全。</p>
<p>只有一種情況是我會勉強接受 <code>any</code> 的使用的，那就是當我們引入了一個第三方的型別，我們很清楚其規格、但是我們偏偏又沒有該型別的完整定義檔、而我們自己去寫定義檔又很浪費時間的時候。然而，這樣的使用有幾個前提是應該要遵守的：</p>
<ol>
<li>使用了 <code>any</code> 型別的物件應該要充分地被封裝起來，使得使用它的程式碼都非常清楚該變數要怎麼操作。如果有很多程式碼依賴於該物件，那應該要提供一個有良好定義型別的介面來讓其它程式碼間接操作該物件，而不是讓所有程式碼直接取用它。</li>
<li>對於該物件傳回的值，應該馬上用型別斷言或型別檢查來確定其型別，而不是繼續讓傳回值維持 <code>any</code> 的狀態。</li>
</ol>
<p>如果沒辦法做到這兩點，那最好還是花一點時間自己把該型別當中會用到的東西宣告一下，這不僅能讓程式碼更有條理，也可以避免很多潛在的手殘可能性。</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:1">
<p>至於定義檔（<code>.d.ts</code> 檔案）則應該要寫清楚預期的是什麼型別，這是兩回事。&#160;<a href="#fnref1:1" rev="footnote" class="footnote-backref">&#8617;</a></p>
</li>
</ol>
</div>
<p>這篇文章 <a href="https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/">TypeScript 中的 any 和 unknown</a> 最早出現於 <a href="https://abstreamace.com/sglab">星君研究室</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://abstreamace.com/sglab/2021/05/06/typescript-%e4%b8%ad%e7%9a%84-any-%e5%92%8c-unknown/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
