跳到內容
關於我 數位花園

部落格

使用 NPM 腳本建立 Hugo 文章

  • 基礎知識:NPM, Hugo, JavaScript, shell script
  • 預先安裝 VS Code, NPM CLI, Hugo CLI

使用 Hugo CLI 建立文章對我而言是一項繁瑣的工作。因為我總是使用文章樣板(archetype)建立文章,並將其放置在巢狀階層的資料匣中。例如,在建立本篇文章時,必須在終端機中輸入以下指令:

Terminal window
hugo new --kind develop posts/_developer/create-hugo-post-with-npm-script

問題在於,我總是忘記擁有多少種文章樣本,以及資料匣結構目前長什麼樣子。資料匣結構可能是動態的,可能非常頻繁地調整。此外,我非常喜歡 VSCode 在側邊選單「Explorer」中提供的 NPM SCRIPTS 功能,如下所示的截圖:

npm script in side menu

這個功能,我個人稱之為「點擊執行腳本」,若使用者無法記住或忘記腳本怎麼寫,此時就非常方便。但據我所知,它似乎只支援 Node 套件管理,也就是 NPM。為了將「點擊執行腳本」功能與 Hugo CLI 結合使用,需要使用 NPM 作為中間件(middleware),縱然 Hugo 靜態網頁在任何時候都不需要 NPM 或任何 Node 套件。讓我們開始吧。

首先使用 npm init 初始化 NPM。

然後,在將此腳本添加到您的 package.json 後,讓我們嘗試透過 NPM 啟動 Hugo 本地伺服器:

package.json
"scripts": {
"dev": "hugo serve -D",
}

在終端機中,輸入 npm run dev,或者只需在側邊選單點擊 NPM 腳本來執行:

npm run dev

成功執行 Hugo CLI!✨

因此,NPM 腳本完美地呼叫了 Hugo CLI。然後,讓我們嘗試實現最終目標:建立一篇文章。

首先,我們需要安裝兩個套件:

  1. @inquirer/prompts:利用 JavaScript 腳本在終端機裡,打造容易操作的介面
  2. inquirer-directory:在終端機裡也能夠輕鬆操作資料匣路徑

接下來,在根目錄中建立了一個 JavaScript 檔案 createPost.js,以下是程式碼:

"use strict";
const inquirer = require("inquirer");
const { input, select, Separator, confirm } = require("@inquirer/prompts");
const { execSync } = require("child_process");
const inquirerDirectory = require("inquirer-directory");
const BASE_PATH = "./content";
inquirer.registerPrompt("directory", inquirerDirectory);
const exec = (commands) => {
execSync(commands, { stdio: "inherit", shell: true });
};
/**
* Create post script
*
* @see https://github.com/SBoudrias/Inquirer.js
* @see https://github.com/nicksrandall/inquirer-directory
*/
(async function () {
const archeType = await select({
message: "Select a archetype",
choices: [
{
name: "Basic",
value: "basic",
description: "Basic post",
},
{
name: "Dev",
value: "dev",
description: "Post for developer.",
},
new Separator(),
{
name: "Garden",
value: "garden",
description: "Note for digital garden.",
},
],
});
const title = await input({ message: "Enter your post title" });
const directory = await inquirer.prompt({
type: "directory",
name: "path",
message: "Please choose post directory.",
basePath: BASE_PATH,
});
const answer = await confirm({ message: "Confirm create the post?", default: false });
if (answer) {
exec(`hugo new --kind ${archeType} ${directory.path}/${title}`);
exec(`open ${BASE_PATH}/${directory.path}/${title}/index.md`);
}
})();

在腳本中,我提供了 3 個問題,以及一些操作:

  1. 選擇一個文章樣板。
  2. 輸入文章標題。
  3. 選擇一個目錄。
  4. 確認建立。
  5. 執行 Hugo 文章建立腳本。
  6. 最後,打開我們建立的文件。

完成精心打造的腳本後,然後將其加入到我們的 package.json 中:

package.json
"scripts": {
"create": "node createPost.js"
}

執行 npm run create,以下是執行結果:

npm run create

以上,開發愉快。

[筆記] 以狀態機實作多階段表單

基礎知識: VueTypeScriptXStatevee-validatezod

時常看到「多階段」的表單流程,也就是會將一個本來很長的表單拆分成好幾個部分來填寫,相較冗長的一整頁表單,使用者在填寫時比較沒有心理上的負擔。

跟表單相關的開源套件很多,而我選用以下幾種來處理跟表單相關的任務:

  • 表單狀態管理:這裡因為範例使用 Vue,所以以 vee-validate 管理表單所有 inputselect 等元件的渲染狀態
  • 表單驗證:表單驗證的部分,搭配 vee-validate 官方推薦的 zod 來處理驗證邏輯

以上是表單的部分,再來的重點是,要如何設計這個「多階段」的架構?

若將所階段裡面的欄位都合在一個表單,如下圖:

chart 01

在切換階段的時候決定顯示哪些欄位,然而在切換下一階段時,又要只驗證當下的那幾個欄位,這樣會造成複雜的驗證邏輯。

於是我想到將每個階段獨立成自己的表單,所以所有驗證都不再是局部驗證,而是對表單的所有欄位(例如階段一表單的所有欄位)來做驗證:

chart 02

由於拆分成多個表單,簡化了表單的驗證邏輯,不用再做多餘的判斷(局部欄位驗證)了。每個表單元件的職責,如下:

  1. 欄位狀態管理
  2. 所有欄位驗證

將上述任務委派給表單元件處理之後,剩下待處理的邏輯是:

  1. 顯示現在是何種階段表單元件的狀態
  2. 送出表單資料,執行非同步請求

由於「第一階段」的下一步只能到「第二階段」,而不行到「第三階段」或是「確認階段(Step Confirm)」,因此我想到利用有限狀態機來解決上述的兩個問題,而嘗試使用以實作有限狀態機很有名的 XState 套件來處理這些任務。

因此,職責分配圖如下:

chart 03

前一段大致描述了初步的想法,這裡整理一下職責分配的規劃,分為兩個部分:

  1. 階段流程控制(要顯示哪一階段的表單)
  2. 儲存所有表單資料
  3. 送出資料,執行非同步請求
  4. 非同步請求的等待狀態(loading)、錯誤狀態

以上皆由 XState 實作

  1. 表單欄位狀態管理(由 vee-validate 實作)
  2. 表單欄位驗證(由 zod 實作)
  3. 表單送出(submit)事件與表單資料(由 vee-validate 實作)

表單元件,以第一階段 Form1.vue 舉例,大致如下:

<template>
<div>
<h2 class="formTitle">Choose channels you like</h2>
<form class="form" @submit="onSubmit">
<div>
<input type="checkbox" id="discovery" :value="1" v-model="channels" />
<label class="label" for="discovery">Discovery</label>
</div>
{/* other inputs... */}
<div v-if="errors.channels" class="error">{{ errors.channels }}</div>
<div class="buttonGroup">
<button class="button" type="submit">next step</button>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import type { Form1Model } from "@/types";
import { toTypedSchema } from "@vee-validate/zod";
import { useForm, useField } from "vee-validate";
import z from "zod";
interface Props {
initialValues: Form1Model;
}
interface Emits {
(event: "next", values: Form1Model): void;
}
const props = withDefaults(defineProps<Props>(), {
class: "",
});
const emits = defineEmits<Emits>();
const validationSchema = toTypedSchema(
z.object({
channels: z.number().array().nonempty("Please choose at least one channel."),
})
);
const { handleSubmit, errors, values } = useForm<Form1Model>({
initialValues: props.initialValues,
validationSchema,
});
const { value: channels } = useField<number[]>("channels");
const onSubmit = handleSubmit((values) => emits("next", values));
</script>

表單的欄位初始值(initial values)由狀態機提供,經由 props 傳進來;然後表單在送出的時候,再發事件出去讓狀態機處理,一個人只負責一件事,可謂「單一職責原則」的精神。

各階段的表單的顯示控制,在外層 MultiStepForm.vue 實作,導入狀態機並決定顯示邏輯。各表單會發出各種事件(下一步、上一步、送出等),然後交付給狀態機來執行。

<template>
<div :class="`h-full w-full ${props.class}`">
<h1 class="title">Multi Step Form Example</h1>
<div class="grid grid-cols-2 gap-x-6">
<div>
<h2 class="subTitle">Form Component</h2>
<div class="p-4 border border-slate-700 rounded-lg">
<Form1
v-if="state.matches('step1')"
@next="send('NEXT_TO_STEP_2', { formValues: $event })"
@prev="send('PREV')"
:initial-values="state.context.form1Values"
/>
<Form2
v-if="state.matches('step2')"
@next="send('NEXT_TO_STEP_3', { formValues: $event })"
@prev="send('PREV')"
:initial-values="state.context.form2Values"
/>
<Form3
v-if="state.matches('step3')"
@next="send('NEXT_TO_STEP_CONFIRM', { formValues: $event })"
@prev="send('PREV')"
:initial-values="state.context.form3Values"
/>
<FormConfirm
v-if="state.matches('stepConfirm')"
@prev="send('PREV')"
@submit="send('SUBMIT')"
:is-submitting="state.matches('stepConfirm.submitting')"
:error="state.context.error"
:machine-context="state.context"
:payload="state.context.payload"
/>
<FormComplete v-if="state.matches('complete')" @restart="send('RESTART')" />
</div>
</div>
<div>
<p class="subTitle">Current Machine Context</p>
<pre class="preBlock">{{ state.context }}</pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useMachine } from "@xstate/vue";
import Form1 from "@/components/Form1.vue";
import Form2 from "@/components/Form2.vue";
import Form3 from "@/components/Form3.vue";
import FormConfirm from "@/components/FormConfirm.vue";
import FormComplete from "@/components/FormComplete.vue";
import { multiStepFormMachine } from "@/multiStepFormMachine";
interface Props {
class?: string;
}
interface Emits {
(event: "click"): void;
}
const props = withDefaults(defineProps<Props>(), {
class: "",
});
const emits = defineEmits<Emits>();
const { state, send } = useMachine(multiStepFormMachine);
</script>

接著是狀態機,我將狀態機獨立成一個檔案 multiStepFormMachine.ts,以方便管理:

import { assign, createMachine } from "xstate";
import type { Form1Model, Form2Model, Form3Model, SubmitData } from "./types";
import { FORM_1_INITIAL_VALUES, FORM_2_INITIAL_VALUES, FORM_3_INITIAL_VALUES } from "./default";
import { sendFormData } from "./utils";
type MachineEvent =
| { type: "NEXT_TO_STEP_2"; formValues: Form1Model }
| { type: "NEXT_TO_STEP_3"; formValues: Form2Model }
| { type: "NEXT_TO_STEP_CONFIRM"; formValues: Form3Model }
| { type: "PREV" }
| { type: "SUBMIT" }
| { type: "RESTART" };
export type MachineContext = {
form1Values: Form1Model;
form2Values: Form2Model;
form3Values: Form3Model;
payload: SubmitData | null;
error: string | null;
};
const INITIAL_MACHINE_CONTEXT: MachineContext = {
form1Values: FORM_1_INITIAL_VALUES,
form2Values: FORM_2_INITIAL_VALUES,
form3Values: FORM_3_INITIAL_VALUES,
payload: null,
error: null,
};
type MachineState =
| { context: MachineContext; value: "step1" }
| { context: MachineContext; value: "step2" }
| { context: MachineContext; value: "step3" }
| { context: MachineContext; value: "stepConfirm" }
| { context: MachineContext; value: "stepConfirm.submitting" }
| { context: MachineContext; value: "complete" };
export const multiStepFormMachine = createMachine<MachineContext, MachineEvent, MachineState>(
{
id: "multiStepForm",
initial: "step1",
context: INITIAL_MACHINE_CONTEXT,
states: {
step1: {
on: {
NEXT_TO_STEP_2: {
target: "step2",
actions: assign({
form1Values: (context, event) => event.formValues,
}),
},
},
},
step2: {
on: {
NEXT_TO_STEP_3: {
target: "step3",
actions: assign({
form2Values: (context, event) => event.formValues,
}),
},
PREV: {
target: "step1",
},
},
},
step3: {
on: {
NEXT_TO_STEP_CONFIRM: {
target: "stepConfirm",
actions: assign({
form3Values: (context, event) => event.formValues,
}),
},
PREV: {
target: "step2",
},
},
},
stepConfirm: {
initial: "preSubmit",
states: {
preSubmit: {
entry: assign({
payload: (context, event) => ({
...context.form1Values,
...context.form2Values,
...context.form3Values,
}),
}),
on: {
SUBMIT: {
target: "submitting",
},
},
},
submitting: {
invoke: {
src: "formSubmit",
onDone: {
target: "#multiStepForm.complete",
actions: "resetContext",
},
onError: {
target: "errored",
actions: assign({
error: (context, event) => event.data.error,
}),
},
},
},
errored: {
on: {
SUBMIT: {
target: "submitting",
},
},
},
},
on: {
PREV: {
target: "step3",
},
},
},
complete: {
entry: "resetContext",
on: {
RESTART: {
target: "step1",
},
},
},
},
},
{
actions: {
resetContext: assign(INITIAL_MACHINE_CONTEXT),
},
services: {
formSubmit: async (context, event) => {
if (context.payload) {
return await sendFormData(context.payload);
} else {
return await new Promise((resolve, reject) => reject("Context cannot be null."));
}
},
},
}
);

狀態機的流程操作,可以參考這個可視化頁面:multi-step-form | Stately

以上是狀態機的程式碼,有點冗長還請見諒 🙏

主要職責是:

  1. Form1 會發出 NEXT_TO_STEP_2 事件進到 Form2,或 Form2 發出 PREV 事件回到 Form1,以此類推。
  2. FormConfirm 會發 SUBMIT 事件告訴狀態機要執行非同步請求,將表單的資料送出。
  3. 狀態機的 context 中,存放 Form1 及其他表單元件的欄位資料,以及最後要送出請求的酬載(payload),此外還有送出非同步請求的狀態(loading、error)

以下是最後實作出來的結果,左邊是表單元件,右邊則顯示目前狀態機 context 的狀態,能清楚知道何時會更新 context 的資料

畫面

所有程式碼請參考這裡

以上是多階段表單的一些構想,利用狀態機將表單的職責單一化,也清楚將邏輯切割並分派給各部分處理。

這是第一次真正自己認真寫狀態機,也還在摸索學習的階段,若有任何問題歡迎留言告訴我 😎

感謝收看 🙏

漢語拼音輸入法介紹與實用技巧

對於使用繁體中文的人來說(尤其是在台灣),漢語拼音或許是一個熟悉又陌生的存在。

我跟大部分的台灣人一樣,從小學的是注音,電腦打字自然也是用注音。直到 2014 那一年(可能吃錯藥,或是頭撞到),開始了學習拼音的道路。然後就一路使用到現在。正好公司月會要介紹這個主題,所以正好順便記錄一下自己的使用體驗及心路歷程。

我想要查詢一下近年台灣各輸入法使用的比例,卻沒什麼相關的統計數據,只有找到 2011 年的一篇統計報告

usage percentage

來源:Pollster 波仕特線上市調網

我想各輸入法在市場上使用率的消長,也不影響注音輸入法的地位,市佔率大幅領先其他各家。無論是老牌的倉頡、或曾經紅極一時的嘸蝦米輸入法,大概也無法追趕上注音的優勢地位。

中文輸入法大致分為兩類:以字音為基礎的輸入法,稱為「拼音輸入法」;以字形為基礎的則稱為「字形輸入法」。

以大家比較熟悉的注音與倉頡來比較,其實可以很明確地分別兩者的差異。注音使用注音符號來拼寫文字(對,聽起來是廢話),也就是以中文字的發音來組合文字;倉頡利用各種字根去拼寫文字,以字形來組合文字。

字形輸入法,是依據字形拼寫的輸入法,簡單來說,會寫就拼得出來。

  • 倉頡輸入法
  • 嘸蝦米輸入法
  • 大易輸入法
  • 行列輸入法
  • 速成輸入法

拼音輸入法,則以字音拼寫的輸入,會唸就拼得出來。

  • (漢語)拼音輸入法
  • 注音輸入法

其實常見的注音輸入法也是拼音輸入法的其中一種,而在中國、新加坡等地最普及的「漢語拼音輸入法」,就是平常所稱呼的「拼音輸入法」。兩者的差異很明顯,一個是用注音符號來拼音,另一個則是用羅馬拼音(英文字)來拼音。

以下整理了依漢語拼音輸入法的使用體驗,整理出的優缺點:

優點缺點
詞彙首碼略拼使用者不普及
拆字模式軟體優化不足
筆畫輸入模式在地化不足
可同時訓練英打、日打
轉換成本低
有聲調拼音
英文拼字檢查
輸入數字無須切換模式
九宮格模式(行動裝置)

這是一個拼音輸入法很普遍的功能。「首碼略拼」是我自己創造的名詞。也有人稱為「字根首碼拼音」。

直接看範例會比較好理解:

assemble-mode

上面這個詞彙,完整的羅馬拼音會是:

xin shi ji fu yin zhan shi

然而,我們有更快的輸入方式,也就是只取每個字拼音開頭的第一個英文字,變成:

x s j f y z s

以 macOS 拼音為例,以下是輸出結果:

assemble-mode

只要是一個常見的詞彙,都很適合用這種首碼略拼的方式輸入,藉此加速打字速度:

assemble-mode

assemble-mode

或許上面的範例詞彙實用性不高(應該也沒有人要手打整首長恨歌吧 😅),然而一些常見人名、地名、常見名詞等,也適用此方式,例如:馬英九(m y j)、蔡英文(c y w)、民進黨(m j d)、拉斯維加斯(l s w j s)⋯⋯

以 macOS 的拼音來說,它具有學習模式,也就是會記憶平常輸入的使用者自訂詞彙,學起來之後,也同樣能夠用首碼略拼的方式打出來。

下圖是關於生難字的一則新聞報導

hard-character

來源:這些生難字你都會嗎? 網友慚愧:除了吃飯什麼也不會 | ETtoday 生活新聞 | ETtoday 新聞雲 (2016)

這些字到底該怎麼唸呢?

i-say-ikea

來源:#轉 話說我都唸 IKEA - 梗圖板 | Dcard

我們常說「有邊讀邊、沒邊讀中間」。但太複雜太難的生難字大概也很難猜出發音。

打出不會唸的生難字,則是字形輸入法的強項,因為不需要知道發音,只需要知道字形結構。同時,拼音輸入法則無用武之地,而這也是漢語拼音輸入法厲害的地方。因為它的「拆字」模式,擁有在拼音輸入架構下,同時又保有拼字輸入特性的一種模式,文字表達有點難理解,就讓我們直接來一探究竟吧!

我們拿上面報導的其中兩個詞來做範例。拆字模式顧名思義,就是將一個字拆成很多區塊來看,拆字方式沒有固定(非倉頡輸入法那種固定拆解方式)。(有錯誤請不吝指教)

hard words

以「覿氅」為例,「覿」字可拆成左右兩個區塊:「賣」與「見」;「氅」字可以拆成上下,「敞」與「毛」。

hard words

因此我們輸入「chang」(敞)與「mao」(毛)之後,按下進入拆字模式的快捷鍵:shift + 空白鍵(以 macOS 為例),會看到選單裡有我們要難字,而且還很貼心附上發音,「氅」字音同「廠」。

我們再來練習這個詞,「曩磲」:

hard words

「曩」字分成「日」、「襄」兩部份;「磲」分成「石」、「渠」兩部份,結果如下:

hard words

除了中文生難字外,某些日文漢字也可以打出來,有些日文漢字源自中文古字,所以古代就存在了,只是現在中文語圈沒在使用。

例如下面這個日文漢字:「雫」。

hard words

會日文的同學,可以馬上切換日文輸入法打出來。「雫」字在日文唸作「しずく」(shi zu ku),意思是眼淚。經過上面兩個範例,相信各位同學應該知道要怎麼拆解了,也就是拆解成「雨」與「下」:

hard words

原來這個字中文發音同「哪」。

我們再來挑戰另一個很常見的日文漢字吧!

hard words

日文漢字:「峠」,日文唸作「とうげ」(to u ge),是棧道的意思。以這個字的結構來說,左邊是「山」,至於右邊 ⋯⋯ 看起來像是「卡」,但卻不是,於是再將右半邊拆成上下兩部份(也就是「上」、「下」二字),所以拆解成「山」、「上」、「下」,來看看結果:

hard words

原來這個字的中文發音同「股」啊 🤔

我認為拆字模式是拼音輸入架構下的一個很不錯備案,彌補了拼音拼寫時,生難字打不出來的缺點,若是注音只能舉雙手投降了

另外值得注意的是,拆字模式只適合輸入單一漢字,而無法連續輸入很多漢字(畢竟不會一整句話都是生難字吧)

「拆字模式」通常可以涵蓋大部分生難字的情境,但難免還是會有拆解過後的,部分字仍不會唸的情況。此時「筆畫輸入法模式」就派上用場了。

漢語拼音輸入法除了「拆字模式」之外,還整合了手機上會搭載的「筆畫輸入法」,可以透過切換「筆畫輸入模式」開啟使用「筆畫輸入法」,並進行輸入。

以 macOS 為例,輸入 u 則進入「筆畫輸入法模式」:

stroke-mode

進入模式之後,就可以開始利用 hspnzx 開始拼寫文字。下面是簡單的範例:

stroke-mode

在我的認知裡,遇到生難字時,「拆字模式」是拼音的備案,那麼「筆畫輸入模式」就是備案中的備案了 💪

mind-explosion

或許有同學會問,這幾個符號是什麼意思?我之後打算寫一篇關於「筆畫輸入法」的文章,就先讓我賣個關子吧 😉

這裡只要知道拼音輸入法,同時自帶筆畫輸入法模式就行了

這裡指的轉換成本低,是針對注音輸入法的使用者,畢竟注音輸入法的使用者佔了大多數,如果要跳槽拼音輸入法,轉換成本(學習成本)就是一個很重要的考量因素。(像是我曾經要學習倉頡最後卻放棄了,因為學習門檻太高 😅)

注音轉換漢語拼音,或是漢語拼音轉換注音,其實只要記憶一張對照表就行了,下面兩張是來自網路的對照表:

pinyin-zhuyin-1

來源:漢語拼音輸入法:學習篇 - Hiraku Dev

pinyin-zhuyin-2

來源:Just Old, So Record.: 注音-漢語拼音 對照表

我在學習拼音輸入法的時候,就是以這個對照表為起點的,簡單的背起來之後,就是大量的實戰練習,不懂的或忘記的,再回去對照表查詢就行了。

對照表中有一些特別標註顏色的拼音,那是身為學習注音的用戶比較不直觀的拼音方式,需要多留意與背誦。

例如:「女」的注音是「ㄋㄩ ˇ」,拼音是「nv」;「呂」的注音是「ㄌㄩ ˇ」,拼音是「lv」。「熊」的注音是「ㄒㄩㄥ ˊ」,拼音是「xiong」。

此外,如果本身注音就不太好,「ㄣ」、「ㄥ」分不清楚,「應該」寫成「因該」,那我只能說 ⋯⋯ 學拼音一樣會寫錯字,因為同為拼音輸入法,會唸錯音,打出來的字就還是錯的,請務必補救一下小學國文:

help-chinese

來源:升大學搶救國文大作戰 (新書、二手書、電子書) - 讀冊生活

好啦,其實這本是高中國文,我只是想借用一下書名而已。

其實漢語拼音輸入法也有跟注音輸入法一樣的聲調系統,例如以下的字,在輸入完 mi 之後,按下 tab 標上聲調「一聲」,讓待選字更少更精準。按第二次 tab 則切換成「二聲」,以此類推。

tone

同拼字模式,聲調切換只限定輸入一個字的時候,不適用連續輸入,所以我個人平常比較少使用到。

由於漢語拼音輸入法是使用英文 26 個字母來拼寫中文字,所以只使用了英文字母的鍵位,不需要像注音輸入法佔用到第二列(數字那一列)的鍵盤,因此鍵位就跟英打是共用的,同樣地,日文的羅馬拼音輸入法也是共用這 26 個英文鍵位。

因此,只要學習一種鍵位(也就是 QWERTY 美式鍵盤排列,如下圖),不需要額外記憶第二種鍵位(例如注音或倉頡鍵位),這對盲打的練習是事半功倍的。試想一下,無論你現在是在打英文、中文或是日文,都是在加強熟悉這個美式鍵盤排列的鍵位,可說是一箭三鵰啊!

us-keyboard-layout

此外,由於沒有佔用到第二列,也讓拼音輸入法可以不用像注音輸入法,需要先切換英數模式才能輸入數字。除了可以直接輸入數字之外,在輸入中英文夾雜的字句時,也是非常地好用,而且還具備英文單字拼音檢查的功能!

例如我常忘記「維護」的英文,到底是 maintenance 還是 maintanance,這時候拼音輸入法就會告訴我答案:

en-spell-check

手機上通常也會有標準 qwerty 全尺寸鍵盤排列的拼音輸入法,以 iOS 為例:

pinyin-qwerty

同時也可以設定成手機上經典九宮格的模式:

t9-pinyin

九宮格輸入法的好處有:

  • 適合小螢幕的行動裝置
  • 適合單手操作

如果有胖手指困擾的同學們,九宮格式的輸入法可說是一大福音啊!

九宮格式的輸入法還包括中文筆畫輸入法(T9 筆畫輸入)、日文假名輸入法、韓文輸入法:

t9-stroke

t9-kana

t9-korean

相信常看日劇、韓劇的同學們,應該對這些鍵盤並不陌生才是 😎

方便與學習中文的外國人溝通,這個是附帶的優點。學習拼音輸入法之後,就等於學會了整套的漢語拼音。

現行世界上學習中文的人,幾乎都是用拼音在學中文的。

可能有同學會反駁:「那是因為外國人學簡體中文的比較多啊!」

但以我的經驗,我有一位來台北的師大華語中心學習繁體中文的日本朋友,他學的也是拼音,而不是注音。

其實想想也是很合理,學中文字前,要先學一套標音符號系統,成本是很高的,但英文字母大家都認得,所以學習成本相對低很多。

在我的認知裡,「注音」與「日文五十音」的地位並不一樣,注音不會出現在正式書信文章裡(注音文不算正式書信,兒童書籍算是特定讀者取向),但五十音會,日文可以沒有漢字,但不能沒有五十音。因此學日文五十音是必要,然而注音則不是。

若有很多世界各地學習中文的外國朋友,學習世界通用的漢語拼音可說是必須的,不然當外國朋友問:「這個字怎麼唸?」的時候,端出注音他們也看不懂啊 ⋯⋯ 這時候拿出漢語拼音,讓身旁的台灣人投以驚訝的眼光,享受當下的優越感

使用族群少的話,就很難推廣出去,然後也沒有機會擠進作業系統預設輸入法的成員裡,像是曾經紅極一時的嘸蝦米輸入法就是如此,因為不是系統內建、屬於第三方輸入法之外,光是需要付費這點,就讓它不會出現在公用電腦(例如圖書館的電腦)上了。

其實跟前一點的不普及因素也有關係,若沒有大量的使用者,開發者就不會去做很多的優化(雖然身為市占率最高的注音輸入法,在微軟上的選字智慧實在是 ⋯⋯ 嗯你懂的)

問問各位在座的各位(沒學過拼音也沒關係),「垃圾」兩字你們會怎麼拼?

  1. le se
  2. la ji

基本上以台灣用語而言,不用說一定是第一個。但以 Google 拼音輸入法為例,必須輸入「la ji」才能打出「垃圾」兩字。而 macOS 內建的繁體拼音輸入法則是輸入正確的「le se」。這就是所謂有無在地化的差異。因為以往的 Google 拼音輸入法,是「繁體模式」,所以整個系統大部分只是簡體字翻譯成繁體字而已,用語是否有針對台灣用語做調整,就是另一回事了。

而這方面,蘋果(包含 iOS 與 macOS)的在地化就做得十分好,這也包含前面所提首碼略拼的例子(台灣政治人物、政黨名字略拼)。

結論是:我都唸垃圾,而不是唸垃圾。

meme-garbage

目前來說,品質最好的漢語繁體拼音輸入法就是蘋果家了(也是我正在使用的),第三方的話,應該就屬 RIME 中州韻輸入法了,畢竟它同時可以安裝在 macOS 與 Windows 上,其他的只是列出來而已,不太推薦使用。

  • macOS 繁體拼音輸入法
  • iOS 繁體拼音輸入法
  • RIME | 中州韻輸入法引擎(Windows, Mac)
  • 微軟拼音輸入法(需添加簡體中文語言,並設定繁體模式,在地化不足)
  • 微軟注音輸入法:拼音模式(不好用,不推薦)
  • Google 拼音輸入法(以停止更新,不建議使用)

[開箱] 無印良品隨身風扇

2023 年春季,無印良品出了一款隨身風扇,準備好迎接酷暑的到來。

台灣的一年四季,大致可分為:夏、夏、夏、冬。嗯,你沒有眼花,確實就是三個夏天。

對於怕熱、易流汗的人來說(像是肥宅如我),風扇可是生活不可或缺的必要裝備。

身為無印良品的愛用者,幾乎所有無印出的風扇都有了,也就是:

就只差高貴的DC 馬達風扇沒有入手而已,稱為風扇大戶也不為過(喂

在入手這款之前,手邊擁有一支尺寸很迷你的素樂隨身風扇(右邊那支):

fan-comparison

尺寸比無印的稍小,三段的風量控制,非常輕巧好攜帶

使用起來十分愉快,直到發現了 MUJI 的隨身風扇 ⋯⋯

其實我也考慮了一陣子之後才入手的(畢竟手邊也有相似的產品啊)

既然入手了,那它就叫做 ⋯⋯ 「迷你白」吧!(取名好辛苦)

MUJI 隨身風扇最大的魅力,在於這個可折疊式擺放的機構:

foldable structure

平常坐捷運或是走路逛街,用手拿著倒是沒什麼問題,然而在吃飯的時候,雙手可是挺忙碌的,若能擺在桌上吹的話,使用體驗也會大大提升,所以最後還是敗下去了 😎

這個隨身風扇有四段的風量調整,第一段風速很特別,是自然風,也就是會轉一下又停下來的那種間歇模式,後面依序是小、中、大三種風量,調節風量的按鈕在後方:

back

開機狀態長按按鈕大約 2 秒,就可以關機

實際在吃東西的時候使用,涼風吹在臉上,那種尊榮感真的不可言喻

MUJI 隨身風扇使用起來的確不錯,但也不是沒有缺點,這裡來整理一下我自己的看法,做簡單的比較:

MUJI素樂
體積14.8(L) x 6.8(W) x 3.3(H)13.5(L) x 6(W) x 3.5(H)cm
重量93g87g
風量段數自然風 / 小 / 中 / 大小 / 中 / 大
噪音比較較小較大
充電介面Micro USBUSB-C
放置桌面OX

其實兩個都是不錯的產品。

MUJI 風扇缺點在於,折疊的機構部分有點擔心會容易損壞,另外充電介面是使用有點過時的 Micro USB。

素樂的風扇雖然只能手持,然而充電孔則是使用目前最通用的 Type-C 款式,算是不錯的優點。

兩者體積很相近,重量都在 100g 內,都是很輕巧好攜帶的尺寸;兩者的三段風速應該都很夠用,自然風實不實用,就因人而異了。

續航力的部分,我就懶得實測了 🙃

MUJI 這次出品的這款隨身風扇,算是一款實用、價格親民的消暑道具,而且還有保固一年。

在結帳的當下,可愛的 MUJI 店員很貼心地提醒,不要邊充電邊使用,還有第一段是間歇自然風,不要以為是壞掉了 😆

希望各位同學今年的酷暑也能過得舒適愜意

世界和平

以上

{/reference link/}

[Hugo] 用 GitHub Actions 將 Hugo 網站部署至 Vercel

必備知識:Hugoshellnpmgit

原本部落格是部署在 GitHub 自家提供的靜態網頁 GitHub Page 上,但因為 GitHub 對於免費帳號的限制(給你免費用還嫌東嫌西),若要使用 GitHub Page 部署網頁,程式庫(repository)就要維持開源(public)的狀態,但是我希望將自己部落格的程式庫設定為私人的(private),畢竟尚未完成的草稿也在上面,並不想給別人看到。(是說這麼冷清的部落格,沒人會沒事去翻你的原始碼)於是,就來研究有哪些可以部署靜態網頁的平台,其中兩個是我比較感興趣的,分別是:DenoVercel。想記錄一下設定部署的過程及遇到的問題。

Vercel 是一個很有名的前端部署平台,也是 Next.js 官方推薦的部署平台。它提供了「一鍵部署」的懶人功能,但由於我想用 GitHub Actions 自行寫腳本執行部署的動作,所以就不用一鍵部署了。

我參考網路上一位來自烏克蘭的工程師 Oleh Andrushko 的文章,其實他的描述已經很詳細清楚了,但我又撞到了一些奇怪的問題,因此卡了好一段時間。

  • 在本機安裝 Vercel CLI
  • 以 GitHub 帳號登入 Vercel CLI
  • 準備部署 Vercel 所需的三把鑰匙(Vercel 帳號憑證、Vercel 組織 Id、Vercel 專案 Id)

首先,我們必須在本機以 npm 全域安裝 Vercel CLI:

Terminal window
npm i -g vercel

安裝完成後,開啟終端機(MacOS 的 terminal 或是 Windows 的 cmd),輸入 vercel,第一次安裝完通常會出現要求登入的訊息:

Terminal window
vercel
Vercel CLI 28.10.1
> > No existing credentials found. Please log in:
> Log in to Vercel (Use arrow keys)
Continue with GitHub
Continue with GitLab
Continue with Bitbucket
Continue with Email
Continue with SAML Single Sign-On
─────────────────────────────────
Cancel

這裡選擇以 GitHub 登入 Vercel:

Terminal window
> Log in to Vercel github
> Please visit the following URL in your web browser:
> Success! GitHub authentication complete for "<你 GitHub 登入所使用的 email>"
? You are deploying your home directory. Do you want to continue? [y/N] n

最後會問你是否要部署 home 目錄,我們沒有要部署 home 目錄,所以選擇 n

進入要部署的專案資料匣位置,再下一次 vercel 指令:

Terminal window
vercel
Vercel CLI 28.10.1
? Set up and deploy "<你的 repo 的本地位置>"? [Y/n] y
? Which scope do you want to deploy to? "<你的 GitHub 名字>"
? Link to existing project? [y/N] n
? What’s your project’s name? "<你的 repo 名稱>"
? In which directory is your code located? ./
Auto-detected Project Settings (Hugo):
- Build Command: hugo -D --gc
- Development Command: hugo server -D -w -p $PORT
- Install Command: None
- Output Directory: `public` or `publishDir` from the `config` file
? Want to modify these settings? [y/N] n
Deploying "<你的 Vercel 帳號名稱>/<部署專案名稱>"
🔗 Linked to "<你的 Vercel 帳號名稱>/<部署專案名稱>" (created .vercel and added it to .gitignore)
🔍 Inspect: "<Vercel 部署專案的頁面網址>" [2s]
Preview: "<Vercel 部署後預覽的網址>" [10s]
📝 To deploy to production (hugo-test-navy.vercel.app), run `vercel --prod`

執行 vercel 指令後,會在根目錄自動產生 .vercel 資料匣,裡面會有兩支檔案,分別是 project.jsonREADME.txt,而我們要關注的是 project.json,其內容如下:

{
"projectId": "<你的 Vercel 專案 Id>",
"orgId": "<你的 Vercel 組織 Id>"
}

在此我們取得了「Vercel 組織 Id」、「Vercel 專案 Id」兩把鑰匙。

切記這兩組 Id 是必須保密的,所以方才跑 vercel 指令的時候,有加入 .gitignore,脫離版本控制。詳細描述在 README.txt 裡面。

第三把鑰匙「Vercel 帳號憑證」則要前往 Tokens – Account – Dashboard – Vercel,手動建立一個 GitHub 與 Vercel 建立連線的憑證。

前往 GitHub 的 repo,進入 Actions secrets 的設定頁面:repo > Settings > Secrets > Actions(如果找不到,網址會是:https://github.com/<你的 GitHub 帳號>/<repo 名稱>/settings/secrets/actions),然後按 New repository secret 將這三個密鑰加進去。

在本機的 repo 裡建立 GitHub Actions 的部署腳本:/.github/workflows/vercel-prod.yaml(註:檔案名稱可以任意取)

內容則參考 Oleh 的範例去修改,內容請參考以下連結:GitHub Actions for Vercel Deployment

複寫 Vercel 自動建置(build)設定

Section titled “複寫 Vercel 自動建置(build)設定”

因為我們要使用自己的 GitHub Actions 腳本進行建置的動作,所以要複寫掉 Vercel 的自動建置。

參考 vercel-action 部署工具的文件,前往 Vercel 專案的設定頁面 repo > Settings > General,在 Build & Development Settings 區塊中,Framework Preset 會自動偵測,自動帶入「Hugo」,在此選取「Other」,然後將「BUILD COMMAND」右方的 Override 按鈕開啟,複寫的值為「」,若 Vercel 阻擋空值儲存,則加一個「空白」可以解決問題,設定結果請參考下圖:

GitHub Actions 的錯誤訊息:

Terminal window
Error: No Output Directory named "public" found after the Build completed. You can configure the Output Directory in your Project Settings.

依據 Oleh 的部署腳本,它的部署路徑是 public

- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
id: vercel-action
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
github-comment: false
vercel-args: --prod
working-directory: public # 這裡產生錯誤

我將 working-directory: public 這行移除,就能正常部署了

GitHub Actions 的錯誤訊息:

Terminal window
Error: Input required and not supplied: env
at getInput

加入 env 參數:

- name: Update Deployment Status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
env: ${{ steps.deployment.outputs.env }} # 這裡加入 env 參數
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env_url: ${{ steps.vercel-action.outputs.preview-url }}

錯誤訊息:

Terminal window
vercel-production
Node.js 12 actions are deprecated. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/. Please update the following actions to use Node.js 16: amondnet/vercel-action@v20

Node 12 版本即將不支援這類提示性訊息,並不會造成部署的錯誤,只是在提醒而已。根據訊息,將使用的 GitHub Actions 工具升級到最新版(前往該工具的開源專案,就可以得知最新的版號),就可以消除提示訊息了。

以上是紀錄我使用 GitHub Actions 腳本自動部署到 Vercel 的過程、設定以及碰到的一些小問題。

一開始也提到,在經過了一番的調查之後,部署平台的口袋名單有 Vercel 與 Deno。最後採用 Vercel 的原因是,它提供了兩個部署環境:Preview 與 Production,於是我們可以寫兩份 GitHub Actions 腳本,分別部署 developmain 分支,雖然最後我並沒有使用到 Vercel 預覽環境。然而因為我對於部落格的 Git flow 流程 1 是直接在 main 分支寫稿上並推上遠端(反正在 markdown 裡的 front matter 可以設定為草稿,在建置的時候就會略過尚未完成的文章),此外設定除了分支名稱、一些設定不太一樣之外,都大同小異,有如此 Git flow 需求的同學,可以參考烏克蘭老兄的文章

此外,估計也會記錄一下部署 Deno 的流程,形式上也會跟本篇差不多。

  1. 關於部落格的 Git flow 流程,我有寫一篇短文描述這件事:GitFlow & Blog Version Control