跳到內容
關於我 數位花園

Inori

43 篇文章由 Inori 撰寫

使用 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

[開箱] Boox Poke 4 Lite 電子閱讀器:開放與封閉的抉擇

最近入手了 Boox 文石的電子閱讀器 Poke 4 Lite,今天就來聊一下關於電子閱讀器的各種事吧!😎

說到電子書,亞馬遜可以說是很早就在佈局電子書市場,深耕多年。

在偶然的情況下,從友人那裡獲得了一台 Kindle Paperwhite 第二代,開啟了閱讀電子書的旅程。

一開始使用 Kindle 的印象,沒有 LED 螢幕的背光、沒有惱人的 APP 訊息通知、因為很省電,只需要好幾周充一次電,各種會打擾分心的因素幾乎都摒除了,打造了可以靜心讀書的環境。

使用 Kindle 閱讀器,除了在 Kindle 書城買書(我是使用日本亞馬遜),也會自己在網路上下載電子書來看。由於 Kindle 可接受的檔案格式是 MOBI、AWZ,較為通行的電子書格式 EPUB 卻無法相容,因此下載下來的電子書檔案都要經過轉檔。另外,若是下載了簡體書,也要轉換成繁體的版本,增加了不少麻煩,畢竟 Kindle 是封閉系統的閱讀器。但所幸轉檔、置入檔案的流程不是非常頻繁,書也不是很快就看完,所以也不算是太大的困擾。

有好一陣子,當時(約莫是 2014 年)的日本亞馬遜書城裡沒有繁體中文書(大概有簡體書),而且不想換到中國亞馬遜看簡體書,也沒有很積極地要閱讀英、日文的書籍(想練習外語閱讀力,但是懶惰 🙈),反而都去圖書館借中文書來看,不用錢又有看不完的書,實在是很大的吸引力,因此 Kindle 閱讀器就晾在一旁,而且一放就是好幾年。2016 年,正是台灣電子書剛起步的時候,我也開始在用樂天 kobo 買電子書,通勤的時候,在手機(四吋的 iPhone SE)上閱讀,後來也在讀墨 Readmoo 上也買了一些電子書。總之,用手機看電子書的機會增加了許多,畢竟出門的時候一定會帶手機,然而閱讀器卻未必。「想要看卻發現沒帶身上,帶出門卻沒有用」的情況也不在少數(墨菲定律啊)。由於使用的頻率越來越少了,於是在某一次整理東西的時候,毅然決定將 Kindle 給脫手了。

去圖書館借書,的確十分方便的資源,而且也會不斷有新書可以借閱。以台北市的圖書館為例,以往個人借閱證的借閱額度是 10 本,現在好像增加到 25 本的樣子,利用網頁或 APP 預約借書,可以在預約書都抵達的時候一次借回來,然後看完之後,再換其他本。比較熱門的書通常預約的人比較多,會等久一點,不過除非是閱讀迅速的人,不然應該不受影響,畢竟大部分的書是不需要排隊等待借書的。而且,圖書館就是滿滿的書山,根本看不完哪~

我聽說有些人比較潔癖,因為嫌圖書館的書太髒,所以從不借圖書館的書。圖書館的書畢竟是很多人使用過的,確實不免會比較髒,我雖然沒有非常在意,但有時候也會遇到很不好的情況,例如:紙上有原子筆、鉛筆的畫記,黏著鼻屎、或將整頁插畫用美工刀割下來的情況,實在是挺困擾的。有免費的書讓很多人可以閱讀,卻做如此缺德的事,實在是不可取啊 😔

此外,有些熱門書,就算要排隊預約,也要排幾個月,看到自己排一、兩千號,也是挺灰心的。像是前陣子很熱門的「被討厭的勇氣」、「情緒勒索」、「原子習慣」,都是大排長龍的熱門書。但想一想,一本書也沒多少錢,買了就可以馬上享受了,就算可以免費看,卻要等足足半年之久,感覺也不划算啊~而這也是我會直接買電子書的一大動機。

近年,日本亞馬遜書城上開始有繁體中文書了。雖然書量跟台灣樂天 kobo 與讀墨 Readmoo 兩大巨頭比起來還差很遠就是了。然而日本亞馬遜開始上架繁體中文書這個趨勢,著實令人期待。因為以電子書的 APP 整體使用體驗而言,我認為 Kindle 還是最好的。

首先,先來談一下書城的平台,也就是軟體的部分。

電子書綁定書城的平台及其帳號,若我們在各家都買了書,就會形成很分散的情況:想像一下,家中必須放很多個書櫃,大本的書必須放大的書櫃、一般的書籍放在一般的書櫃、文庫本的書籍則放在小型的書櫃、而漫畫尺寸也放在漫畫尺寸的書櫃。

若我們在各個平台都買了幾本書(通常都是因為想讀的書在 A 書城正好特價,或是只有 B 書城有買電子書的版權),就會發生這樣的情況,手機裡安裝了各家的 APP,我現在就是處於這樣的混亂狀態。若要集中管理,只能在固定在某一家買書了。電子書商城的經營模式畢竟不一樣,無法像紙本書一樣,想買皇冠出版的哈利波特時,誠品書店有賣、金石堂書店也有賣、在博客來網路書店也買得到。

或許有人會說:「電子書的優點就是輕便啊,多裝一個 APP 在手機裡也不會變重,沒有這麼嚴重吧」

我可以說,或許這是身為極簡主義者的潔癖情節 😝

除了書籍綁定書城平台之外,再來談談硬體:閱讀器的部分。

由於各家的閱讀器幾乎都是封閉式系統,意思就是:看書只能用自己家的書城。若我們買樂天 kobo 的閱讀器,就只能看 kobo 的書。當然自己放電子書檔案進去通常也是允許的,但畢竟那不是花錢買授權書的流程,而不在討論的範圍內。所以,若有使用好幾個書城,且都要使用閱讀器的話,豈不是要買好幾個閱讀器嗎?!這實在與電子書環保的理念背道而馳,節省了紙張的消費,反而買了更多電子產品,我想大部分人也不會這麼做。因此,開放系統的閱讀器正可以解決這個問題。

Boox 文石的閱讀器在代理進台灣之後,我一直有在關注。他們主打的一大賣點就是:Android 系統的閱讀器,所以可在 Play 商店下載任何 APP,就跟使用智慧型手機一樣。開放系統的優點,解決了封閉系統閱讀器的最大痛點,也就是單裝置多平台的終極目標。然而,文石的閱讀器也不是沒有缺點。

電子墨水螢幕跟手機螢幕的差別在於,基於目前(2022 年)電子墨水的技術,它的刷新速度沒有 LED(或 OLED)螢幕這麼快速,因此一般的手機 APP 也不會特別為了電子墨水螢幕這個小眾硬體去做軟體上的優化或客製化的設定。而這些正是封閉系統閱讀器的優點所在。封閉系統的閱讀器針對電子墨水螢幕的特性去做軟體上的調校,所以在使用體驗會比手機 APP 直接在電子墨水螢幕上好很多。例如 APP 通常會加上翻頁的特效,因為這樣的特效在 LED 上很有翻實體書的感覺,LED 螢幕上看起來演示得很好,但在刷新速度較低的電子墨水螢幕上卻慘不忍睹。有些 APP 可以在設定裡面,將一些漸變動畫給關閉,再翻頁切換的體驗就會近似於封閉系統的閱讀器,但有些則不行,例如說亞馬遜的 Kindle APP ⋯⋯

於是,最近因為有開始閱讀電子書,手機閱讀縱然方便,但因為工作時都是使用電腦,所以希望下班後可以減少看 LED 螢幕的時間,於是又興起了買電子閱讀器的念頭。

雖然看到亞馬遜可以買繁體中文書有點心動,很想購入最新的 Kindle 閱讀器,但礙於手邊還有其他平台的中文書籍要閱讀,因而頓時陷入了兩難。最後的抉擇是買了文石 Boox Poke 4 Lite,在上面閱讀 Kindle、kobo、Readmoo 的電子書。

標題雖然稱之為開箱文,但沒有常見開箱文該有的樣子,請見諒 😅

這裡就簡單列一下 Boox Poke 4 Lite 的規格,並與低、中階最新的 Kindle、還有我的手機做比較:

Poke 4 LiteKindle 11Kindle Paperwhite 5iPhone 12 mini
尺寸153 x 107 x 7.1 mm158 x 109 x 8.0 mm180 x 120 x 8.1 mm131.5 x 64.2 x 7.4 mm
重量150g158g205g135g
顯示器6 吋6 吋6.8 吋5.4 吋
解析度758 x 1024
(212 ppi)
1072 × 1448
(300ppi)
1236 x 1648
(300ppi)
1080 x 2340
(476ppi)

一開始對 Poke 212ppi 的解析度有點擔心,但使用幾天下來,發現也看不出什麼顆粒感(說到底沒有寫輪眼),150 克的重量拿起來真的沒什麼負擔。若說要以集中使用亞馬遜買電子書為目標來說(念念不忘 Kindle 😝),Boox 算是一個不錯的過渡期選擇,當然或許就此習慣開放系統的閱讀器而回不去了也說不定。總之,比起用手機閱讀,電子墨水的螢幕總是舒服許多,重點是毫無分心、毫無干擾。

或許有人會問:「既然是開放式系統,會不會因此把手機常用的 APP 都安裝,然後就跟手機一樣充滿著推播訊息?」

我有試著裝過 Spotify,操作體驗真的不好。前面有提過,電子墨水目前的更新速度並不快速,就算裝了社群類、通訊類 APP,我們也不會想要在上面使用,因為體驗實在是太糟了 😆

既然說到這裡,來分享一下我目前有裝的 APP 吧:

  • Kindle
  • Notion
  • Readmoo 看書
  • Wikipedia
  • 樂天 Kobo

雖然本篇主要是想聊聊電子閱讀器,畢竟電子閱讀器的出現,跟電子書密切相關。若讀者是多年的蘋果用戶使用者(無論是 iPhone 或 iPad),應該都知道一個叫做 iBooks(2018 年改名為 Apple Books)的 APP,我查了一下維基百科,發現它在 2010 年 iOS4,也就是 iPhone 4 的年代就已經存在了。而亞馬遜的 Kindle 則在更早的 2007 年發表了第一代的 Kindle 電子閱讀器。然而蘋果公司則從未聽到要研發電子閱讀的任何消息 ⋯⋯

紙本書及電子書的優缺點,我想網路上很多人都曾討論過這個議題,我自己的感想也是大同小異,如果不介意的話就繼續讀下去吧 😆

就先從優點開始講吧

偏好紙本書的人,通常不外乎就是因為:

  • 喜歡翻書的感覺、紙的觸感
  • 新書的紙好香,油墨味好香
  • 看實體書好有氣質(虛榮感氾濫 )
  • 整面書櫃一眼望去,收藏慾的大滿足
  • 特典彩繪書衣、或是作者親筆簽名(腦粉大爆發)
  • 來回查閱方便直覺
  • 可以用筆做筆記
  • 可以借給別人
  • 可以二手脫售

再來說說電子書的優點

  • 輕薄輕量,一間圖書館隨手帶著走(古人無法想像飛機在天上飛的概念)
  • 通常價格比實體書便宜,而且購買方便(就跟蒐集 Steam 遊戲一樣簡單。什麼?我剛說蒐集嗎?)
  • 若是看很多外語書,那購買也更方便
  • 容易利用零碎時間閱讀(包括雨天濕漉漉的通勤日)
  • 可以全域搜尋、詞彙翻譯、生詞查詢、維基百科條目查詢
  • 可以在不同裝置閱讀,而且同步閱讀進度
  • 重量很重,也佔空間(搬過家的人應該都知道書有多重)
  • 紙張需要保養及維護(溫度與濕度的控制很重要,還要防蟲害)
  • 地震的時候很可怕(聽說日本 311 大地震時,很多人被倒下的書櫃壓到)
  • 二手書不一定好賣
  • 藍光傷眼的問題(若使用手機或平板等裝置)
  • 推播訊息,其他的 APP 會使人容易分心(若使用手機或平板等裝置)
  • 來回查閱比較困難,而且不直覺
  • 少了翻書的感覺、書本的味道
  • 人家不知道你/妳是在滑手機,還是在看文學巨著(又是莫名的虛榮心)
  • 不能墊泡麵(誤)

從上面我整理出的優缺點看來,紙本書與電子書都沒有壓倒性的優勢,可以說是各有各的好處。

有些人因為電子書沒有看書的感覺、沒有紙的觸感,所以不想看電子書。有位朋友曾經說:「我買書來收藏,才不要花錢買一個檔案。」收藏家的浪漫是可以理解的。但試想一下,現在的作家應該大部分都是用電腦寫書了吧,所以在出版社尚未發行之前,基本上書的本體就是一個電子檔案,作家在向出版社交稿的時候,也是用電子檔的形式吧?

仔細想想,人類文明裡,書籍的載體變動了很多次,古埃及的紙莎草、石碑、美索不達米亞的泥板、古中國的竹簡、中古歐洲的羊皮紙 ⋯⋯ 等等。而以紙本為載體的形式也存在了很長的時間,人們自然也將紙張跟書本聯想在一起,對於電子書這件事當然會有很多的不習慣。然而再想想,現在電子郵件幾乎取代了以往的紙本書信,電子書要全面取代紙本書也並非不可能,雖然也沒有期待它發生,我還是很喜歡紙本書的。對很多人來說,紙本書或許也象徵著文明的基礎、文化的底蘊,如果都電子化或許就沒意思了。

只能說,兩者的優點在於使用情境的不同,才能夠發揮所長。工具書為了查找方便,所以使用紙本書;娛樂性質的小說,買電子書存放在手機或閱讀器裡,省去看完很難處理的困擾;收藏一整套火影忍者的漫畫,光是擺在架上就賞心悅目 ⋯⋯。當然,如果收藏一整套電子版的火影忍者,隨時隨地都可以翻閱享受,那又是另一種收藏的浪漫。

有時候,書是工具、是資訊的載體;但在某些情況下,書本身更是擁有者的收藏。

於是我可以這樣子歸納:

若一本書記載的資訊遠大於書本身,就買電子書。

若一本書的收藏意義重大(內容固然也很棒),就買紙本書。

牙醫與臭豆腐

晚餐後有預約看牙醫。

雖然是預約看牙醫,但實際上牙齒並沒有什麼不適,只是做定期的洗牙維護罷了。在吃晚餐之前,突然想到要看牙醫這件事。不是我忘記今天要看牙醫,而是突然想到:「看牙醫之前,吃東西也沒有禁忌這件事?」不過聽到禁忌不要誤會了,不是指迷信那回事。我在思考的問題是,打個比方好了:如果看牙醫之前去吃臭豆腐的話,牙醫會不會在你嘴巴張開的那一刻,皺起眉頭呢?

我以前也想過這些事,各科的醫生有各種辛苦。皮膚科醫生可能每天看著滿臉痘子的病人、臭腳伸得高高有長雞眼的病人。外科醫生時常看到病人身體裡的各種內臟、還有滿滿的紅色。牙醫每天要聞著從病人的口中飄出來的各種味道,一邊辛苦地工作。每次這樣想像著,都會不禁感嘆:「啊,這種工作我還真做不來,跟生命有關,尤其跟人命有關的工作,我真的沒辦法。」倒不是因為看到血會昏倒這麼簡單的理由。

回到臭豆腐的話題,如果換成吃麻辣鍋,不知道牙醫師會是什麼心情?如果工作忙到餓著肚子還沒吃飯,卻又從病人口中聞到各種美食的味道,不是很令人沮喪嗎?不過畢竟飯還是要吃的,最後我吃了清淡的飯菜、配上了味噌湯,或許會有味噌湯的味道。不過不要吃臭豆腐的話,應該在可接受的範圍吧?我暗自認為牙醫師會這麼想。各位去看牙醫之前,會特別做什麼事嗎?例如:吃個涼糖、刷個牙、用漱口水漱個口,還是不會特別做什麼?

[CSS] Tailwind 主題色的切換

必備知識:CSSHTMLJSReact

Tailwind 提供了深色模式(Dark Mode),可以自行設定深色模式時的樣式,只要在 CSS class 前面加上 dark: 關鍵字:

<div class="text-black dark:text-white">
{/* ... */}
</div>

假如我們要做「主題色切換」的功能,dark: 這個用法也許可行,但也只限於兩個主題色的切換,另一個缺點就是到處都要加上 dark: 的切換樣式,這聽起來不是一個好的做法。

想跳過本文廢話、直接看最終實作結果的同學,請走傳送門

在 Tailwind config 的自定義主題色盤

Section titled “在 Tailwind config 的自定義主題色盤”

在 Tailwind 的 config 當中,其中一個客制化的選項,就是可以自定義顔色的 class,例如:

module.exports = {
theme: {
colors: {
primary: "#0d6efd",
secondary: "#6c757d",
danger: "#dc3545",
warning: "#ffc107",
},
},
};

PS. 以上的名稱及色碼取自 Bootstrap 的主題色盤

若我們有兩個主題色,config 就必須寫成這樣:

module.exports = {
theme: {
colors: {
"theme-1-primary": "#0d6efd",
"theme-1-secondary": "#6c757d",
"theme-1-danger": "#dc3545",
"theme-1-warning": "#ffc107",
"theme-2-primary": "#0d6efd",
"theme-2-secondary": "#6c757d",
"theme-2-danger": "#dc3545",
"theme-2-warning": "#ffc107",
},
},
};

如此一來,就要在 HTML 上去抽換不同主題的 class:

function Button() {
const buttonTheme = isTheme1 ? 'bg-theme-1-primary' : 'bg-theme-2-primary';
return <button className={buttonTheme}>;
}

到處都要有這樣的判斷,看來也不是明智的做法。此外,若我們有四、五個主題要切換,就不是三元運算可以簡單解決的事。

CSS 提供了變數的功能,可分爲區域變數與全域變數。

定義 CSS 區域變數,下面這個 --main-bg-color 只能在這個 scope({} 刮號)裡面使用:

element {
--main-bg-color: brown;
}

比較常見使用 CSS 變數的方式,是使用 CSS 全域變數,建立一個稱作 :root 的 pseudo-class,裡面定義的變數在整個 HTML document 底下都可以使用(其實就是全域變數了):

:root {
--main-bg-color: brown;
}

使用 CSS 變數時,用 var() 這個函式,將要使用的變數作爲參數傳進去:

element {
background-color: var(--main-bg-color);
}

我們結合 Tailwind 及 CSS 變數的特性,就可以達到主題色盤抽換的目的,我們將方才 config 中寫死的色碼,用 CSS 變數取代:

module.exports = {
theme: {
colors: {
'color-one': 'var(--color-one)',
'color-two': 'var(--color-two)',
'color-three': 'var(--color-three)',
'color-four': 'var(--color-four)',
'color-five': 'var(--color-five)',
},
}
}

設定好 Tailwind config 之後,接著就是定義各主題色盤變數的時候了

const theme_ayanami = `
:root {
--color-one: #1d446c;
--color-two: #f1f1f1;
--color-three: #571a1a;
--color-four: #000000;
--color-five: #525252;
}
`;
const theme_ikari = `
:root {
--color-one: #3f6d4e;
--color-two: #8bd450;
--color-three: #1d1a2f;
--color-four: #965fd4;
--color-five: #734f9a;
}
`;

然後將變數包在 <style> 後,安插在 <head> 裡面:

function App() {
const [currentTheme, setCurrentTheme] = useState("ayanami");
const getThemeVariables = (_theme) => {
switch (_theme) {
case "ayanami":
return theme_ayanami;
break;
case "ikari":
return theme_ikari;
break;
}
};
useEffect(() => {
if (!document.getElementById("customThemeId")) {
const head = document.head;
const newStyleElement = document.createElement("style");
head.appendChild(newStyleElement);
newStyleElement.id = "customThemeId";
newStyleElement.innerHTML = getThemeVariables(currentTheme);
} else {
const styleElement = document.getElementById("customThemeId");
if (styleElement) {
// 更新 CSS 全域變數
styleElement.innerHTML = getThemeVariables(currentTheme);
}
}
}, [currentTheme]);
return (
<div>
<button onClick={() => setCurrentTheme("ayanami")}>凌波零</button>
<button onClick={() => setCurrentTheme("ikari")}>碇真嗣</button>
</div>
);
}

在這裡定義了兩個主題,分別是「零號機」與「初號機」的主題色,此後,就可以在 HTML 上使用了,例如:

<div class="bg-color-one"></div>

藉由抽換 <style> 的内容,將新切換的主題變數複寫進去,便可以主題切換了!

從瀏覽器的 Dev Tools 中,可以看到我們只抽換了 CSS 的全域變數,元件的部分都沒有異動

同場加映!「初音未來」跟「涼宮春日」的主題色!😘

若想看整個範例的結果,請前往這裡

範例原始碼,請參考這裡

我們利用 CSS 全域變數的特性,並搭配 Tailwind 的客制化顔色設定,實現了主題色切換的目的。優點非常顯著,我們在切換主題的時候,不用動用任何一行原本的 HTML、不用改動任何的 CSS class,也不用在元件裡面寫切換主題的邏輯,就可以快速達成主題切換的效果。

再來是 CSS 全域變數的部分,各主題的變數其實就是一個字串,上面的範例直接放在 JS 裡面,因此若遇到要更新主題顔色、或新增新主題色時,仍必須重新打包(build)一次前端的專案。若要避免重新打包,就要看 CI/CD 的架構策略,將這些字串移到別的地方,然而這不在我目前擅長的領域,也非本次討論的重點,就交由各位同學去發揮吧!

以上,Tailwind 主題切換分享到這裡,祝各位開發愉快 😎

初探多執行緒實作模式

本篇是略讀「Multithreaded JavaScript」這本書第六章:「多執行緒實作模式」(Multithreaded Patterns),整理的一些筆記

本章節介紹了一些多執行緒常見的實作模式,有以下:

  1. 執行緒池(Thread Pool)
  2. 互斥鎖(Mutex)
  3. 環形緩衝(Ring Buffers)
  4. 演員模型(Actor Model)
  • 多執行緒應用程式很常使用的一個實作方式
  • 執行緒池是一個集合,裡面含有同質性(homogeneous)的 worker 執行緒,每個 worker 都可用來處理重負載、複雜運算的工作
  • Node.js 的 libuv 函式庫提供了執行緒池的功能,預設為四個執行緒,可以處理底層 I/O 的一些操作
  • 概念很類似分散式系統
  • 分為兩部分討論:執行緒池的大小(Pool Size)與委派策略(Dispatch Strategies)
  • 一般而言不會動態改變執行緒池的大小
  • 通常在作業系統中,處理器核心跟執行緒並沒有直接的關聯
  • 當執行緒的數量遠超過處理器核心數量時,效能反而會下降
  • 以 Node.js 的 libuv 函式庫為例,裡面包含三個執行緒,分別是:主執行緒、worker 執行緒、垃圾回收執行緒(Garbage Collection Thread)
Node.js
// browser
cores = navigator.hardwareConcurrency;
cores = require("os").cpus().length;
  • 別忘了還有主執行緒,所以是 n + 1
  • 依據不同的目的來決定執行緒的數量
    • 對於挖礦而言,99.9% 的工作在於 worker 上所執行繁重的運算,幾乎沒有 I/O,主執行緒也沒有特別的事情,因此可以開與核心數量相同的 worker 執行緒
    • 對串流影音或轉檔而言,則有大量的 CPU 運算及 I/O 操作,因此必須預留兩個核心給這兩個程序,剩下再分配給 worker 執行緒
  • 如果不確定要如何分配,那麼留一個核心給主執行緒是一個安全的作法

我們將高成本、繁重的運算收集整理起來,然後分配個 worker 執行緒去執行這些任務,這裡介紹三個常見的委派策略:

  • 按照順序指派工作,指派到最後一位,下一次就回到第一位 worker 身上
  • 可以確保每位 worker 都有事做
  • 可能造成每位 worker 的負擔不均
  • HAProxy 稱此委派策略為 roundrobin
  • 就如字面的意思,隨機選取一個 worker 來處理工作
  • 可能造成每位 worker 的負擔不均
  • 將新的任務指派給負載最低的 worker
  • 當有最低負載的 worker 有兩個的時候,隨機選取一個指派
  • HAProxy 稱此委派策略為 leastconn
  • Mutex 全名為 mutually exclusive lock.
  • 互斥鎖是一個存取共享資料的控制機制
  • 此機制確保共享資料在同一時間,只允許一項任務執行
  • 互斥鎖在有人存取共享資料的時候上鎖,並在結束後解鎖
  • 在上鎖及釋放鎖定之間,稱之為「臨界區段」(Critical Section)

串流資料與環形緩衝(Ring Buffers)

Section titled “串流資料與環形緩衝(Ring Buffers)”
  • 環形緩衝是「先進先出」(first-in-first-out, aka FIFO)佇列的典型實作,利用一組「配對指標」指向當下的記憶體位址
  • 當佇列的指標在陣列末位,下一步會移動到陣列首位,形成環形結構的概念
  • 在北美餐廳中常被使用的點餐圓盤(order wheel)則是類比世界中,同樣的概念實踐
  • head 指標:指向下一個寫入佇列的位置
  • tail 指標:指向下一個讀取佇列的位置
  • 佇列長度:我們想要建立多大的佇列,一個陣列長度,head 指標、tail 指標會在上面移動

借用一下書中的示意圖:

ring buffer

  • 當寫入佇列時,head 指標移動的下一個位置
  • 當讀取佇列時,tail 指標移動的下一個位置
  • 當指標位在佇列末位,下一次就會移到首位
  • 因為是環形,所以沒有頭尾之分,因此指標在哪個位置並不重要
  • tail 指標最多只會跟 head 指標在同一個位置,不能超過 head 指標
  • 緩衝區滿載的時候,若要再寫入佇列,則有兩種策略:
    • 覆蓋最舊的佇列:相較於未處理的舊資料,新資料比較重要
    • 拋出異常,且不寫入佇列:資料順序性很重要的時候
  • 必須正確讀出最舊佇列
  • 儲存在環形緩衝裡的每個元素不會做移動,只有加入與移除,適合實作「先進先出」(first-in-first-out, aka FIFO)佇列
  • 「非環形緩衝」若要實作 FIFO,則必須在處理完一個任務之後,移動佇列上所有的元素
  • 「非環形緩衝」較適合實作「後進先出」(last-in-first-out, aka LIFO)佇列
  • JavaScript 的堆疊(stack)就是 LIFO 的實作
  • 動態改變佇列的大小,意味著必須重新賦予記憶體,會影響效能。若要實作動態佇列,鍊表(linked list)則較為適合
  • 演員模型是實踐同步運算的一種程式設計模式
  • 一個 actor 代表一個執行程式碼的容器
  • 在 Erlang 程式語言中,actor 是一級公民,但在 JavaScript 中也可以模擬其實作
  • 每個 actor 皆具有三種功能:執行運算、建立新的 actor、actor 之間的相互傳遞訊息
  • 每個 actor 擁有自己的訊息佇列(message queue),以 FIFO 佇列順序處理每個任務
  • 因為 actor 都無法操作共享記憶體(shared memory),因此避免了多執行緒容易發生的情況:race condition 及 deadlock
  • actor 是單執行緒,一次執行一件事
  • 使用 actor 的系統必須能夠接受延遲或順序不一致的現象
  • 每個 actor 可以擁有一個位址,例如:tcp://127.0.0.1:1234/3 代表著在 1234 port 中,第三個 actor

借用一下書中的示意圖:

actor model

鴨川的各種風貌

曾經一段時間住在京都車站的南側,大概是十條車站那一帶。

因為鄰近鴨川,所以很常去那裡散步或跑步。説到鴨川這條河流,想必去過京都旅遊的人都會有印象。鴨川由南邊一路向北,貫穿京都市的各個著名的觀光景點。京都市内東西向的幾條大路,我想很多人並不陌生。從北邊的「二條」一直到靠近京都車站南邊的「十條」。如果對路名不太有概念也沒什麽關係,因爲我也不是非常地熟門熟路。我自創一個口訣來瞭解全貌:

古城二條、祇園四條、清水五條、前站七條、後站九條

二條最有名的景點大概就是二條城;四條則是祇園、八坂神社、鴨川納涼床,還有商店街、咖啡店、旅館與酒吧;五條非清水寺、產寧坂莫屬;七條則是京都車站的前站,有京都電視塔與 Yodobashi Camera;京都後站大概就是東寺、東福寺、Aeon Mall

或許你好奇問:「那十條有什麼?」

十條是個很平凡的地方,硬要說有什麼地標的話,任天堂總部在這裡,像是一塊龐大的白色石碑佇立在那裡,直到晚上九點多仍是燈火通明。

起初,鴨川給我的印象是一個充滿濃濃京都風、河邊充滿著閒散旅客的地方,或是夏天的「鴨川納涼床」(雖然我從來沒進去坐過)。但實際上那卻只是其中的一小部分而已。或者該說,大部分的景象其實沒有那麼地唯美。

沿著主要幹道往東邊走,就會遇到鴨川。因為過橋之後就要上交流道,路上的車快速疾駛,空氣中充斥著引擎聲。橋的前方有一整區的集合住宅大樓,一整排站開來望著鴨川,我從那個社區旁一個狹窄走道溜進去,迎面而來就是鼎鼎大名的鴨川了。陽光佈滿這個河床,更顯得綠意盎然,河邊的步道緊鄰著水域,也沒什麼欄杆之類的人造物,感覺接近了大自然一點。步道上幾乎沒有人,散步兼散心,想說就這樣一路往北走,能走多遠就走多遠。

潺潺流水聲、映入眼簾的各種鳥類、植物、河床上滿地的礫石,說真的真是另一種享受,像是「我正存在著」的那種感覺。有時會看到零星幾個孩子在河裡戲水、或是大叔們坐在那靜靜地釣魚。平凡無奇的景色,也會來一點不一樣的驚喜。偶爾在河床上看到一堆堆的石碓,各種不同的型態,依然故我地佇立在那裡。起初以為是因爲超自然力量都市傳説之類的緣故造成的(做什麼白日夢),後來查了一下,是稱之為「賽之河原」的傳說故事。

賽之河原 1 的傳説是出自於日本的中世紀時代,但也有種説法是出自於佛教的「法華經」。人們相信比父母親早逝的孩子們,會在陰間 2 的河床 3 ——也就是西院(齋院)的河床上遭遇苦難,他們會在此堆起石頭,然而惡鬼會出現,撞倒那些石碓,並責罵那些孩子們,此時地藏菩薩就會出現拯救那些孩子。「賽」一字,一說是出自於京都的佐比川(「佐比」音同「賽」)或是奈良的狹井川(「狹井」音同「賽」)。

原來是一個帶點悲傷的故事,而不是什麼都市傳說。我不知道做那些石碓的人們是不是都發生這樣的事。每次經過時,雖然沒有認真數過,總感覺有增加新的石碓,卻從來沒有看過正在堆疊石頭的人影。我不禁思索,他們是在何時來這裏堆石頭呢?又是帶著如何的心情?這個故事帶給我一種悲傷但沉靜的感覺,卻隱隱有股强大的意志力。

每從一座橋下穿越,就代表著又往前了一點。有些橋是鐵路專用,有些則是一般道路,給行人、汽車使用。尤其是站在鐵路專用橋的下方,距離大概只有兩公尺左右,往上可以看到陽光從鐵軌之間灑下,與其説是木漏日4,應該稱作是漏日才是。不過原本柔幻似水的詞,用在此確實少了那種意境,反帶了點陽剛的氣息。當電車經過的時候,壓過鐵軌發出的聲響可真是震撼,心想著自己正站在沉重巨大疾駛電車的正下方,那種感覺真是難以言喻。

越往北邊,河邊的步道逐漸變得寬敞,也越來越多人影。有像是音樂系的學生(我猜的),面著河吹著小號或是薩克斯風,我不太懂音樂,但伴隨著鴨川的流水聲與充滿綠意的河床,確實是視覺與聽覺上的享受。還有人帶了小型烤肉架,圍坐著吃肉、喝啤酒。以往我都是用橋來辨認位置的(例如:「啊,三條大橋到了」)但其實從人們在河邊休閒活動,就可以大略知道目前正位於鴨川的哪一段。繼續往北走,就會看到有名的「鴨川納涼床」,上面塞滿了旅客,納涼床附近一帶的河床非常寬廣,但是同樣也擠滿了人,而且一眼望去,不只日本人,外國人的面孔也不在少數。你可以想像是在大安森林公園的草地上,擠滿了人圍坐成一個個圈子那種光景。離開河床,走上去看看旁邊的街道。四條烏丸附近交通特別地繁忙,烏黑得閃閃發亮的計程車到處都是,還有滿滿的人。這裡大概是最熱鬧的地方吧,有各種餐廳、商店街、咖啡店、酒吧,還有百貨公司及旅館,外國人密集度最高的地方,總像是走在大阪梅田車站附近的感覺。

其實從十條走到四條、三條這一帶,腳已經很酸了,雖然只要走回街道上搭個電車就可以輕鬆回家,但我實在不想這麼做。充滿吵雜與人群的商店街會讓我更身心更疲憊。於是我選擇沿著原路回去,我還想要多聽聽鴨川的聲音。天色漸暗,四條烏丸一帶的人卻仍然很多,京都的夜生活才剛開始呢,這裡大概是京都少有的夜生活吧。托觀光客的福,店家才願意營業到比較晚的時間。三條若再繼續走下去,直到出町柳站,那裡是鴨川與今出川的匯流處,因為是交匯處所以水域非常寬廣,有很多觀光客、學生們,還有有名的跳石。

台灣有些地方的河濱公園規劃得很好,非常適合晚間散步或運動,所以我想說也可以在鴨川試試,沒想到出乎意料。吃完晚餐後,我走向通往交流道的橋邊,看到橋的對面,小鋼珠店閃亮的招牌,讓我聯想到美國拉斯維加斯的霓虹招牌,雖然我沒去過拉斯維加斯。一樣走過河邊社區旁狹窄的走道,卻馬上感到異樣。完全沒有路燈,白天走的時候完全沒有注意到,晚上卻異常得明顯,河邊一盞路燈都沒有,幾乎只有微弱的月光,聽起來很浪漫?一點也不。白天都沒什麼人了,更別提晚上,此時也就不逞強說自己不怕黑了,走路的時候總是瞻前顧後、提心吊膽,沒有人還好,看到疑似人影就嚇得半死。鴨川景色依舊,只是籠罩在黑夜裡,整個氛圍就不太一樣了。心中不斷拉扯:「再走一小段?還是掉頭直接回家?」最後在前方一個小出口,還有一盞路燈(當下心裡則稱之為明燈)的位置離開了鴨川。

那次真是有驚無險的經驗,雖然我什麼也沒看到、沒聽到、更沒遇到什麼事,但心裡總有種說不出的不安。當下還冒出「就算在這裡被殺也不會馬上被發現」的想法。才晚上八點多而已,河邊只剩一片漆黑 ⋯⋯ 我想納涼床那邊應該還是車水馬龍、燈火通明吧,彷彿完全不同的世界,若問哪一個比較接近日常的鴨川,我應該會選差點把我嚇死的那一種吧,但是僅限於白天就是了。只要走一次,就能享受鴨川的不同風情、不同的魅力。不過若邀我夜遊鴨川,恕我敬謝不敏。

{/* Ref:

  1. 賽の河原(さいのかわら)

  2. 冥土(めいど)

  3. 河原(かわら)

  4. 木漏れ日:日文中非常有詩意、卻難以翻譯的詞彙之一,字面意思是「從樹葉空隙灑進來的陽光」

E2E 測試導向的開發流程

某天,我們從設計師那裡拿到了設計圖

於是我們開始切版,跟往常一樣,主要注重在畫面的樣式與互動上,此時並不會考慮到 E2E 測試

若開發期間,很幸運地拿到了 QA 的測試案例(或任何寫測試案例的人),我們是否可以在切版的當下,也將未來寫測試案例的情況也加入考慮?

此外,這裡提到的測試案例是指 E2E 測試(自動化測試),而非人工的整合測試

為了讓寫測試的過程能更順利,依據測試案例上的各種 action,例如:點擊某個 button、在 input 輸入文字,或是從某個 div 元素上獲取文字。在這些計畫做事的 DOM 元件上安插屬性(attribute)作為 querySelector 的查詢指標

若手上沒有測試案例又該如何呢?沒有測試案例,就無法準確知道哪些元素會被查詢,只能用推測的。此時腦中閃過一個想法:

如果制定一些規則,然後按照這個規則去添加屬性,是否就可以維持一定的覆蓋率?

因此我根據自己寫測試的經驗,嘗試制定了一些規則。我將會分成以下幾個部分說明:

  1. 元件取向
  2. 位置取向
  3. 狀態取向
  4. 無形資料取向
  5. 結構取向

以下所有程式,我會以 React 元件來解釋。實際上無論框架,最終都會回到 HTML 元素身上

插入 data-test-page 屬性於最外層的 HTML 元素:data-test-page='<頁面元件名稱>'

有了 data-test-page 屬性,我們可以很快地辨認出這個頁面元件的範圍(我更喜歡稱之為「邊界」)

註:屬性名稱全看個人喜好,這裡展示的是我自己覺得不錯的命名方式 🙃

例如我們有一個 home 元件:

function HomePage() {
return <div data-test-page="home">{/* ... */}</div>;
}

插入 data-test-comp 屬性於最外層的 HTML 元素:data-test-comp='<元件名稱>'

有了 data-test-comp 屬性,我們可以很快地辨認出這個基本元件的範圍

function Button() {
return <div data-test-comp="button">{/* ... */}</div>;
}

元件會在不同地方重複使用(這也是我們要抽成元件的初衷,DRY 原則),因此,data-test-comp 就會出現重複的情況,而且如果在相近的地方有多個重複的元件,就不太好快速辨識目標元件

比如說我們在一個頁面上,用了 5 個 button 元件,而測試案例的動作是:「選取其中一個 button,然後點擊它。」此時,我們必須先找出所有 button 的元素,然後在裡面尋找我們要做點擊的那個 button(比對 button 的文字;或是直接指定第幾個 button 元素 ⋯⋯ 等)

這個過程不困難,但我認為這個過程很煩,而且不斷重複在做這個查找的動作。於是我開始思考,是否有更精確、更快速的方式。如果有一個唯一值(或接近唯一的值,亦即很少重複),這個元素尋找的過程將會簡單許多

因此,像是這樣的情境,我們需要另一個表示功能的特徵1 屬性(這個特徵屬性最好是唯一值)

使用 data-test-feat 屬性,用來辨識這個元件在這裡所提供的功能(或目的)

我認為這個屬性比較偏向是非強制性的。倘若只有在元件上標示屬性,仍然可以拿到我們想要的元件。例如:在導覽列(navbar 元件)上的登入 button 及選單裡(menu 元件)的登入 button,分別包含在不同元件裡面。此外,過度使用可能會造成最後很多重複的屬性,唯一性的特徵消失了,精準打擊(aka 準確查詢)的初衷也會隨之化為泡影。因此,要在這之間取得平衡

所以,data-test-feat 屬性大概有兩種加入方式:

有時候,基於元件的樣式設計(有外層決定寬高、背景色 ⋯⋯ 等),我們會在元件的外面包一層容器元素

function HomePage() {
return (
<>
{/* ... */}
<div data-test-feat="confirmBtn">
<Button />
</div>
<div data-test-feat="cancelBtn">
<Button />
</div>
{/* ... */}
</>
);
}

上面的範例,可以很清楚地知道,在 home 頁面裡,有兩個 button:確認 button 及取消 button

如果在元件外面包一層容器元素不是你的菜,可以透過 props 的方式穿進去元件裡:

若沒有外層的容器元素,透過 props 傳進去是另一個選擇:

function HomePage() {
return (
<>
{/* ... */}
<Button dataTestFeat="confirmBtn" />
<Button dataTestFeat="cancelBtn" />
{/* ... */}
</>
);
}

在 button 元件裡:

function Button({ dataTestFeat, btnText, labelText }) {
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-feat={dataTestFeat}>{btnText}</button>
</label>
</div>
);
}

有時候我們想知道某個元件的狀態,假如我們有一個 toggle button 元件:

toggle button

測試案例中,我們想要在點擊 toggle button 之後,確認它會確實從 on 狀態轉變成 off 的狀態

我們當然可以透過辨認 style 的變化得知它的狀態。如果是用 SASS 的話,或許可以很簡單地從 class 上辨認出(端看 class 怎麼設計了)。然而我最近比較常用的是 tailwindcss,此時就會變得不太直覺。以下是我的 toggle button 元件:

function ToggleButton() {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<button
type="button"
className="rounded-full overflow-hidden relative w-16 h-7 shrink-0 text-white font-semibold text-sm uppercase ml-2.5 bg-neutral-500"
onClick={handleChange}
>
{/* here is what the difference by state */}
<div className={`absolute transition left-1.5 top-1/2 -translate-y-1/2 ${isOn && "translate-x-9"}`}>
<div className="rounded-full bg-white w-4 h-4 relative">
<div className="absolute right-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">On</div>
<div className="absolute left-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">Off</div>
</div>
</div>
</button>
);
}

這個元件裡,translate-x-9 是 on 與 off 狀態上差異的 class。我們仍然可以辨識,但會造成修改樣式時後容易抓不到元素的情況。Utility first 樣式庫的優點,在此時卻成了缺點

像這種情況,建議加入表示狀態的屬性:

function Button({ btnText, labelText }) {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-state={isOn} onClick={handleChange}>
{btnText}
</button>
</label>
</div>
);
}

另一個常見的情境是載入狀態(loading),假如有一個列表與搜尋列元件:

function SearchPage() {
// ...some state here
return (
<div data-test-page="searchPage">
<div data-test-comp="searchBar">
<input value={searchValue} data-test-feat="searchInput" />
<button onClick={handleSearch} data-test-feat="searchBtn">
Search
</button>
</div>
<div data-test-comp="list" data-test-loading={isLoading}>
{/* list data here */}
</div>
</div>
);
}

註:基於可讀性,我直接用 HTML 表示

點擊搜尋 button 之後,頁面就會開始 loading 打 API 拿資料,並在結束後,結束 loading 狀態,並更新資料至底下的列表

假如有一個 card 元件要顯示商品資訊,如在元素上存在 product id 的話,就能快速地找到目標商品。然而商品資訊卡片通常不會顯示 product id,此時就必須將這個值加到屬性當中:

function ProductCard({ productId, ...restProps }) {
return (
<div data-test-comp="productCard" data-test-product-id={productId}>
{/* product card content */}
</div>
);
}

至於命名原則,我認為只要夠語意化就行了

在一些特殊情境下,我們會需要用 div 元素去模擬其他元素

這裡舉個例子,用 div 元素去模擬 table 元素的結構。加入 data-test-el 屬性:data-test-el='<模擬的元素名稱>'

function Table() {
return (
<div data-test-el="table">
<div data-test-el="tbody">{/* ... */}</div>
</div>
);
}

或許會有人不太懂為何要這樣做,但我還是可以簡單說明一下

在開發的時候,遇到某些樣式在與 table 有關的元素上(也就是<table><thead><tbody>⋯⋯ 等),在 Safari 瀏覽器下的顯示會不如預期(Safari 的 bug ⋯⋯),因此必須用 div 元素重建整個 table 的結構,因此在加入屬性之後,可以很快地了解整個 table 的結構,藉此增加可讀性

誠如模擬元素,為了增加可讀性,我們也可以在其他地方加入屬性,可以更快地了解整個元件結構。例如一個 dialog 元件:

function Dialog() {
return (
<div data-test-comp="dialog">
<div data-test-el="dialogHeader">this is dialog header</div>
<div data-test-el="dialogBody">this is dialog body</div>
<div data-test-el="dialogFooter">this is dialog footer</div>
</div>
);
}

加入 data-test-el 屬性之後,可以有效率地一眼看出整個元件的結構(在這裡,dialog 分為 dialog header、dialog body、dialog footer 三個區塊)

開發時,加入屬性將會是一個漫長的過程。在開發的過程中,測試案例並非我們的優先考量。制定規則並遵守它,可以讓我們無腦地加入屬性,並將專注力放在實作樣式外觀、操作互動以及資料上。當開發結束後,撰寫測試案例將會更為順利,並感謝以前的自己

最後,說到底這些規則畢竟只是自己的想法,並非什麼業界標準那麼偉大的東西 😎

開發愉快

  1. Feature,我翻譯成「特徵」

機動性與獨立傢俱

我對擁有機動性特質的東西有種莫名地偏好

比起裝潢時就固定位置的系統傢俱,我比較喜歡獨立傢俱

可以隨時改變整個空間的擺設,是一個很大的優點

空間的擺設只要有稍微地變化,就會帶來煥然一新的感覺

雖說沒有要「先查看當日運勢,再依照幸運方位做擺設」如此般地狂熱

但也有像是「今天的陽光特別璀璨,很適合將矮桌挪到窗邊看點書」的雅緻

⋯⋯開玩笑的

一本正經八百地講起了系統傢俱與獨立傢俱之間的差別

但對於租屋族來說,好像也沒有什麼選擇可言

基本上就看房東的屋子是如何就是如何

硬是要塞進大型傢俱,對居住的當事人而言,恐怕也是很困擾的

所幸現在的租屋處,在搬進來前,是幾近空屋的狀態

說是得來不易,我覺得一點也不誇張

然而,我也沒有因此要大顯身手、打造成 Ikea 樣品屋的北歐風,或無印良品風

只有少數幾樣必要的傢俱就足夠了

這讓我想到賈伯斯的一張照片:

他席地而坐在空蕩蕩的房間裡,只有一盞落地燈與黑膠唱片機相伴

steve jobs in living room

身為極簡主義者,適當的空白是很重要的

這句台詞頗適合,但他本人並沒有說過這種話就是了

不過無需將這概念想得太過崇高

其實概念很簡單的:

i'm minimalist

或許,把身為凡人的我與老賈相比,本來就不是個明智的作為

學習摩斯電碼

最近在重溫一款以前很喜歡的遊戲:絕地戰兵(HELLDIVERS

遊戲中有一個叫做「真實傳播器」(Truth Transmitter)的裝置,在任務完成後,它會發出摩斯電碼訊號,其傳輸的訊號是:

··· ··- ·—· · ·-· -····- · ·- ·-· - ···· -··· · ·- -·-· --- -· ·- -·-· - ·· ···- · ·-·-·-

意思是:

SUPER-EARTHBEACONACTIVE.

↓ 真實傳播器 ↓ truth-transmitter

這讓我對摩斯電碼產生興趣

於是就開始在網路上尋找摩斯電碼的學習資源

Google 提供了一個線上的摩斯密碼學習工具,可以在手機或是電腦上開啟,可以立即開始學習,有興趣的同學可以點選連結前往:Morse - Learn

這個學習工具利用圖像學習記憶法,去記憶每一個字幕以及對應的符號,感覺非常地有用

雖然在學習的當中,感覺有點像是在學海軍常用的「北約音標字母NATO phonetic alphabet)」(也就是所謂的 Alpha、Bravo、Charlie、……), 但那些北約音標字母對應的單詞,都是世界通用的,而非只是方便記憶

事實上,我也有找到其他的記憶學習法

學習的過程中,每個字母對應一個單字,然後利用這個單字將其意義圖像化之後,聯想對應的密碼組合,感覺滿有趣的

Google 的這套學習方式,其字母對照表如下:

hello morse

當然,除了英文字母外,還有數字的圖像記憶,這裡就不列出來了,待有興趣的同學自行探索

從前面的對照表中,我們知道基本上是由短訊號、長訊號兩種元素組成

但基本組成元素大致可分為分為五種:

  1. 點(或稱「滴」):短訊號,符號表示為 ·,1 個單位時間(點決定基本單位時間)
  2. 劃(或稱「答」):長訊號,符號表示為 -,時間長度為 3個單位時間
  3. 點劃間隔:在一個字母裡,···--- 之間的間隔,為 1 個單位時間
  4. 字符間隔:3 個單位時間
  5. 單詞間隔:7 個單位時間

借用一下維基百科的範例,「morse code」寫成摩斯電碼,如下:

−− −−− ·−· ··· · −·−· −−− −·· ·
M O R S E C O D E

再來就是三種間隔的差異

從下表可以看出,第一行是時間軸,每個數字代表單位時間

第二行是每個字母的開始與結束範圍

第三行是實際打訊號的動作,= 代表 signal on,. 代表 signal off

1 2 3 4 5 6 7 8
12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
M------ O---------- R------ S---- E C---------- O---------- D------ E
===.===...===.===.===...=.===.=...=.=.=...=.......===.=.===.=...===.===.===...===.=.=...=
^ ^ ^ ^ ^
| dah dit | |
symbol space letter space word space

電碼需要傳輸媒介,才能將要表達的訊號傳出去,接收者才能收到訊息並加以解讀,傳統的摩斯電碼是透過電纜傳輸電子訊號,此外還有其他的傳輸媒介:

  • 有線電路:傳統的摩斯電碼透過叫做「電鍵」(Telegraph Key)的裝置、經由有線電路傳輸電子訊號
  • 無線電波:無線電波透過無線電波傳送兩種類的訊號
  • 可見訊號 1 媒介:利用阿爾迪斯燈(Aldis lamp)日光儀(Heliograph)或是手電筒發送可見光訊號
  • 可聽訊號 2 媒介:例如用車用喇叭發送聲波訊號

Prosign 是 Procedural Sign 的縮寫,是摩斯電碼為了簡化及標準化通訊方式的一種方式

主要會是透過一些大家共識的縮寫來代表個意義,藉此增進通訊效率與準確性

一個簡單的範例是:用 K 代表 “okay, hear you, continue” 的意思

另一個常見的 prosign 的範例是廣為人知的 SOS,摩斯電碼寫成:···---···,因為 prosign 是一個組合代表特定意義(例如這裡是求救訊號),所以字母之間就不會用間隔(··· --- ···

不是所有的 prosign 都是通用的,不同的 prosign 由不同的機構定義

Google 除了有一個學習網頁推廣摩斯電碼之外,還提供了一個摩斯電碼的輸入法供大家使用

gboard

只要在手機上安裝 Gboard 輸入法(App StoreGoogle Play),就可以在裡面新增摩斯電碼輸入法

打字的時候,還會有電報的聲音呢~真的很酷 😆

缺點就是只能打英文

可以趁跟別人聊天的時候多多練習摩斯電碼

雖然一開始應該會奇慢無比吧🤣

其實寫這篇文的當下,我還沒有把所有的摩斯電碼符號學完

只完成了英文字母及 0~9 的數字,剩下的符號部分則還沒開始

其實摩斯電碼不是只有英文,歐洲的一些語言也有各自的版本,像是希臘文、俄文

亞洲的部分,像是阿拉伯文、日文、甚至中文也都有自己的一套摩斯電碼

感覺滿有趣,之後想來研究一下日文的摩斯電碼

PS. 據維基百科,日文摩斯電碼稱之為和文符號Wabun Code), 日文稱為「和文モールス符号

  1. Visual signal

  2. Audible

行動裝置上 100vh 的奇怪行爲

最近遇到一個手機瀏覽器上,在某個特定排版會發生高度有誤的問題

進而發現手機瀏覽器的特殊行爲

查了一下資料,發現這個問題已經存在許久,也有不少的解法

大致分為用 JS 及 CSS 去解決

CSS 的解決方式比較簡單明瞭

想要直接看結果,可以走傳送門:JS 解決方案CSS 解決方案

這個頁面是滿版的,所以 container 的部分佔滿整個 document.body

container 裡面,由上而下包含三個區域:header、body、footer

header 與 footer 分別固定在 container 上緣與下緣,body 撐滿剩下的空間,如果内容超過就會有捲軸滾動

layout 的示意圖如下:

layout

外層的 container 寬高分別是 100vw、100vh,在電腦上看起來沒什麼問題,然而在手機上就不是那麼回事:

100vh-issue-1

可以看到 container 高度是 100vh,卻仍然可以 scroll 的現象,照理來說應該要剛好撐滿才是

備註:可以在手機上點開連結,看一下真實情況


因此,若將 container 裡面的東西加進來,就會造成內外都有 scroll 的情況:

100vh-issue-2


看來 CSS 所抓到的 100vh,跟實際上的 window 高度不一樣,window 比較小,所以造成了 scroll

於是,試著把 container 元素的高度與 window 的高度印出來

var containerElement = document.getElementById("container");
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + window.innerHeight + "px";

在電腦瀏覽器上,這兩個數字會是一樣的,在手機上卻不同:

different-height

查了一下網路上的資料,大概有兩種方式可以解

一個是用 JS,一個是用 CSS

於是我加上一個 resize 事件,監聽 window 的高度變化,並顯示在畫面上:

var containerElement = document.getElementById("container");
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
function updateWindowHeight() {
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + window.innerHeight + "px";
}
updateWindowHeight();
window.addEventListener("resize", updateWindowHeight);

在 iOS Chrome 上,可以看出兩者的高度並不一致:

100vh-issue-3-ios-chrome

iOS 的 Safari,除了 container 與 window 高度不同之外,還發現了另一個現象

100vh-issue-3

在最頂的時候往下滑動,window 的高度是動態的

mind-explosion

在 Android Chrome 上,也出現 window 變化的狀況,但還是有點差異

100vh-issue-3-android-chrome

原來是彩蛋啊!!🥚

既然 100vh 與 window 的高度會不一致,而且在 iOS Safari 與 Android Chrome 上還會不斷改變, 那麼就在監聽 windowresize 事件時, 將 container 元素的高度複寫成當下的 window 高度就行了:

(function () {
function updateWindowHeight() {
var currentWindowHeight = window.innerHeight;
var windowHeightEl = document.getElementById("window-height");
windowHeightEl.innerHTML = "window innerHeight: " + currentWindowHeight + "px";
var containerElement = document.getElementById("container");
containerElement.style.height = currentWindowHeight + "px";
var containerHeight = containerElement.clientHeight;
var heightElement = document.getElementById("container-height");
heightElement.innerText = "container clientHeight: " + containerHeight + "px";
}
updateWindowHeight();
window.addEventListener("resize", updateWindowHeight);
})();

100vh-issue-4

我們可以看到,container 高度與 window 高度,始終保持同步,也就避免出現外層 scroll 的情況發生了

100vh-issue-rotate

旋轉螢幕也不是問題~

既然 container 的高度解決了,再將 container 裡面的 header、body、footer 加進來:

100vh-issue-final

一切看來都非常美好 😎

至於 resize 事件是否要加入 debounce 增進效能,那就看個人選擇了

CSS 的方法有分幾種:

.container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
max-height: -webkit-fill-available; // 加上 fill-available
background: lavender;
}

但如同前綴 -webkit- 的字面意義,在 Android Chrome 上仍然有問題

但 Windows 及 macOS 上的 Chrome 卻沒事

ChromeSafari
macOSOO
WindowsONA
iOSOO
AndroidXNA

將 html 與 body 的高度設為 100%,底下的 container 就放心地用 100%

完全將在 mobile 上有爭議的 vh 單位移除了,是一個簡潔的方式

html,
body {
margin: 0;
padding: 0;
height: 100%; // 用 100%
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%; // 這裡也用 100%
background: lavender;
}

所有平台及裝置的瀏覽器都通過了考驗 💯

ChromeSafari
macOSOO
WindowsONA
iOSOO
AndroidONA

有一個人分享了利用新 CSS 單位的解法,也是一個不錯的選擇:

.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100dvh; // 用單位 dvh
background: lavender;
}

關於 dvh 的示意圖如下:

new unit

PS. 本圖來自 Google IO

但這個似乎還很新,看來支援度沒有太好

簡單測試了一下,在 Safari 可以正常運作,但是 Chrome 卻不認識這個單位

ChromeSafari
macOSXO
WindowsXNA
iOSOO
AndroidXNA

支援度可以參考:“dvh” | Can I use… Support tables for HTML5, CSS3, etc

之後想要來研究一下關於 dvh 這一類給 mobile 專用的單位

ChromeSafari
MacOSOO
WindowsONA
iOSXX
AndroidXNA
  • O:代表 100vh 與 window 高度一致
  • X:代表 100vh 與 window 高度不同
  • NA:此平台無此瀏覽器
ChromeSafari
iOSXO
AndroidONA
  • O:代表 window 高度會隨 scroll 變化
  • X:代表 window 高度固定
  • NA:此平台無此瀏覽器
OSChromeSafari
iOS15.15103.0.5060.6315.15
Android11103.0.5060.71NA

這次因為使用了滿版 layout 搭配 100vh,而發現了這個水很深的問題

iOS Safari 在往下滑動時,window 會不斷改變的現象,確實是大開眼界

總結下來,JS 的解法需要去監聽 resize 事件, 此外大概不可避免要搭配 debounce,會需要寫很多的程式碼, 還需要在適當的時機取消訂閱 resize 事件,避免記憶體洩漏問題, 簡單來說為了效能及資源,都要額外再做其他事

CSS 的三種解法都相對簡單,沒有太多的程式碼

但唯有第二個解法(html、body 設為 100%)通過了所有瀏覽器的考驗

第三個解法(新單位 dvh)雖然現在有相容性問題,但未來的可用性令人期待

[小說] 我只想好好說話

注意:內文有大量劇透,請自行斟酌

或許有時候

我們認為自己有悲慘的遭遇,認為別人過得幸福,又怎麼會理解

然而這不一定是真實

隱藏在美好外表下的,不一定也如此美好


本書的主角柏崎悠太,是剛就讀國中的男生

他入學第一天最大的煩惱,就是自我介紹

因為他有嚴重的口吃,只要跟別人說話,總是會口吃

就算在家裡,與家人對話也是一樣,這個問題一直困擾著他

他在學校刻意避開與其他人交流,以免被別人發現他有口吃

在校門口收到學校社團的傳單時,卻有一件事讓他在意

本來就不打算參加社團的他,無意間看到了廣播社的傳單:

十分歡迎對於說話感到棘手的人,社團活動會仔細教你發聲方法等。 只要透過練習,你一定也能夠好好發出完美又清晰的聲音!

這段話使他更積極、正向了一點,但也只維持了幾秒鐘

「還是不行吧!那是說給一般人聽的,而我連一句話都無法好好說出來,根本不是一般人」

於是,他在舒適圈的邊界之間,猶豫徘徊了一陣子


在故事的後半段,他從古部的自白中得知她的經歷

其實比悠太還慘

她不但被同學霸凌、失去好友、還被父母唾棄,甚至暴力相向

聽了這番經歷之後,悠太自責自己的不懂得珍惜

他的周遭,縱然仍有些人會嘲笑他,卻也有很多溫柔、體貼的人

想要理解他、出手幫助他

廣播社的社長,一位很和善的學長

身為考生的他,出場的機會並沒有很多

然而我認為卻是一個很重要的中介者角色

因為如果沒有他,悠太永遠不會知道姊姊在社團裡受到孤立

悠太也不會因此拿到古部手上的那本劇本,也不會知道古部想幫助他的心情

而且也不會知道椎名老師所做出非常不易察覺的改變

在我看來,立花所做的事,串連所有的角色

也讓主角悠太看到了這整個全貌

雖然他之後應該會因為把悠太姊姊的事情都抖出來,而被臭罵一頓吧


他跟悠太姊姊的關係,作者並沒有太多著墨

雖然我覺得他們之間的互動,會有一些有趣的故事

覺得有點可惜

坐隔壁的古部同學,是一名看來清純可人的女生

但相對於外表,她對任何人都很冷淡,沒有太多的表情,也不多話

總給人拒人於外的感覺(美少女就是這樣才有魅力啊

也是因為她的原因,悠太才半推半就地加入了廣播社

她始終對悠太的說話方式,沒有表現出任何特別的反應

有別於以往一般人會有的反應,例如:感到怪異、訕笑、皺起眉頭、或以尷尬的微笑表示同情

而她總是面無表情、耐心、平靜地聽他把話說完

我腦中浮現的,是不帶有任何的情緒、直視對方的表情

她拿了自己喜歡的動畫劇本,半強迫地邀請悠太一起做劇本的台詞練習

古部在最後的自白中,道出她過往不好的遭遇

她曾經歷過與悠太相同的痛苦,而且還遭遇身旁人們不友善地對待及霸凌

這其中還包括父母以及曾經的好友

因此,她認為唯有像悠太這樣同樣遭遇的人,才能夠了解她

也不惜被討厭的風險,強迫悠太陪她做劇本的台詞練習,希望能夠治好口吃的問題

她好幾次製造了與悠太獨處的機會,只是私心希望他不要跟其他人成為朋友

雖然不是「美少女看到男主角就一見鍾情、瘋狂追求」,那種輕小說才會出現的劇情

但是也有點病嬌的味道(抱歉歪樓了

正是因為被曾經唯一的好友背叛、同儕的霸凌,而對身邊的人們缺乏安全感與信任吧

班級的導師是一名隨性又古怪的老師

悠太去辦公室跟椎名老師領取社辦鑰匙的時候,瞄到老師的電腦螢幕上是跟工作無關的頁面

然而據立花學長的說法

椎名因為班上有特殊狀況的學生,所以也在教學方式上做了些的調整

她不再課堂上點學生回答問題,這也讓悠太鬆了一口氣

也因此她必須花更多的時間在備課

但可惜的是,椎名只能改變她自己負責的英文課,其他老師的教課時段,仍然是悠太的夢魘 ⋯⋯

在與悠太的通話過程中,也表示自己也是頭一次,遇到有口吃困擾的學生,頓時也不知道要怎麼做

她除了在教學方式上改變之外,也沒有特別做什麼了

我想你也不喜歡在眾人面前有特殊待遇吧?

或許就如同椎名所說,如果給悠太特殊待遇,說不定會引來霸凌之類的事情

這或許是老師沒有改變太多的原因之一

身為悠太的姊姊,也是身邊最關心他的人之一

她不時地關心、鼓勵他

雖然有點強勢、多管閒事(悠太的說法

但她曾經跟悠太一起在網路上查詢口吃的相關資料,希望能找到解決辦法

以悠太的視角來看,她是一個「過著幸福人生」的普通人

然而他不知道姊姊為了弟弟,在學校社團裡遭到嚴重的排擠

但她隱瞞著家人,表現出不讓人操心的孩子

理由是她「不希望把自己的問題帶回家」

由此可見她是一個十分堅強的孩子

在還未閱讀之前,我期待會有很多關於廣播社活動的描述

但是事實上卻沒有(笑

古部與悠太在廣播室裡,練習著劇本的對話

連放學後的校內廣播,也是輕描淡寫地帶過

很多時候,主要都是描述著練習時刻,悠太唸台詞時難受心情與心裡百般的掙扎

看了都不禁緊張起來,想要大喊:「夠了!可以不用再勉強自己了!」

古部很堅持要繼續做劇本的對話練習,試圖說服他相信這樣持續努力做,可以治好口吃

悠太不願相信,覺得在做沒有用的努力,於是抗拒練習

他們起了爭執,悠太無法接受古部如此強硬地堅持

在強烈的情緒下,他更無法說出自己的想法,於是他用了「筆談」的方式,道出自己的想法:

我已經不想再跟妳說話了

然後留下錯愕的古部同學,逃離了廣播室

看到筆談的內容,我感受到非常強烈的情緒,內心實在是百般交雜

我想當下看到這段話的古部,她應該很受傷吧

參賽的名額是兩人,原本是指派立花與古部

但是經過來一番波折,悠太鼓氣勇氣跟椎名老師說自己要參賽

所以最後由悠太與古部上場

至於比賽的結果,也算是 happy ending 啦

其實在故事的前期,我就隱約懷疑古部同學也曾有口吃的困擾

後來聽古部自己的自白之後,才確認我猜中了

至於為什麼會這麼猜測?

因為讓我聯想到另一個故事,那就是手塚治虫的一部很有名的漫畫「怪醫黑傑克」

我在看黑傑克漫畫的時候,有一段故事令我印象深刻

注意:以下有「怪醫黑傑克」劇透,請自行斟酌


故事的劇情如下:

有一位男孩,因小兒麻痺不便於行,平時非常努力地做復健

他看了一位醫生:本間丈太郎的書,紀錄他治療病患的故事

其中有一位患者,雖然不是小兒麻痺,但同樣也不良於行

書中記載著這位病患雖然有行走上的困難,仍然做了長途徒步旅行

男孩深受啟發,也效仿他,從廣島徒步走到大阪

途中遇到開車尾隨、臭名遠播的黑傑克醫生(因為時常收取高額醫療費用、還沒有醫師執照

男孩企圖要趕走黑傑克,覺得他心懷不軌

但黑傑克總是在艱困的路段給予有用的建議

男孩頓時恍然大悟,明白書中描述的病患,就是黑傑克本人

但是他疑惑問到:「但是你看起來很正常,不像是殘障者 ⋯⋯」

此時,黑傑克什麼也沒說,只是拉起他的褲管,秀出滿是縫合線的雙腿

幼時遭遇重大意外的黑傑克,身體支離破碎

他很努力在復健上,徒步旅行則是其中之一


古部同學就如同黑傑克一樣,而悠太則像是那個男孩

正因為經歷過同樣的痛苦,所以才能夠理解悠太的處境

也正因為自己克服了困難的障礙,才會不斷說服對方「一定可以治好的」

每個人的外表下,或許藏著不為人知的煩惱,表面上的幸福並非真實,有可能正好相反

悠太的姊姊不希望將自身的問題,帶給家人困擾

但並不代表她沒有任何煩惱與痛苦、過著幸福無憂的人生

大家都有不願讓人知道的事情或煩惱,但無人知曉不代表沒有

看來冷淡的人,也並非漠不關心

椎名老師沒有像悠太的姊姊有非常多積極地作為,相較之下,感覺是比較消極、散漫的類型(沒有貶義

我想要表達的是,有些人用自己的方式做出關心與貢獻

縱然當事人(主角)的感受可能微乎其微,但也不容忽視它

只是如果立花學長沒有告訴悠太的話,他大概永遠不會發現吧

即使沒親身經歷,也能夠有同理與諒解

在古部的自白中,曾說自己討厭所有人,這裡所有人指的是普通人,也就是那些沒有口吃困擾的人

這跟她的經歷有關,確實可以理解

她提到對和善的立花學長仍有一絲敵意,只因為他是普通人

還刻意支開他,讓她跟悠太可以在廣播社社辦獨處(這舉動莫名覺得可愛

但我希望她之後也能夠嘗試敞開心胸,接納那些善良的普通人

並非所有普通人都會對用嘲笑的態度去面對有困擾的人

像是立花學長、悠太的姊姊、椎名老師他們

雖然他們無法完全理解口吃的困擾,畢竟沒有真實經歷、只是個旁觀者

但不代表他們都是壞人,他們也願意去幫助人

無意間在圖書館找到這本書

當初不知道在哪裡看到這本書的廣告海報

頓時被標題及封面的繪圖深深吸引

poster

此圖來源:妞新聞

我原本預期想看到的是廣播社會從事的活動、期待有一些有趣的故事(也不是說本書的故事不有趣

然而卻跟想像的不太一樣

我很喜歡封面的繪圖,個人一種很安靜祥和的感覺(但故事的發展卻非如此

tw version book cover

查了一下,發現這是中文版才有的封面

日文原文版的封面長這樣:

jp version book cover

風格看起來陰沉許多,而且 ⋯⋯

沒有可愛的古部同學(抱歉又歪樓了

書中悠太的大部分台詞,都是結巴的狀態

雖然還沒有讀過日文版的內容,但是覺得譯者應該花了不少時間

也很敬佩譯者,台詞也充分傳達了

我很好奇,把結巴的台詞從日文翻譯成中文的過程,會是什麼樣的感受

前面提到黑傑克的故事,因為是很久很久以前看的

劇情的內容非常地模糊

只依稀記得大概而已

所以還特別去翻閱了一下漫畫,重新複習這段故事

結果是 ⋯⋯ 嗯,跟記憶中的劇情差異有點大 😂😂😂

果然記憶力不太可靠啊~(大概是年紀大了

用 JavaScript 偵測裝置的方位 - 直向及橫向

在 CSS 中,我們可以用 Media Queries 的方式,分別定義橫向與直向的樣式:

.box {
width: 300px;
height: 300px;
display: none;
}
@media (orientation: landscape) {
.landscapeBox {
display: block;
background: lightblue;
}
}
@media (orientation: portrait) {
.portraitBox {
display: block;
background: lightcoral;
}
}

但是,雖然可以隨著直、橫向定義不同的樣式

但卻無法讓元件知道現在的狀態,JS 並不知道目前的裝置方位為何

也就無法將這個值納入元件的 state 去做其他的判斷了

我原本打算使用 react-hook-screen-orientation 這個套件來判斷行動裝置的方位

於是我翻了一下這個套件的原始碼,發現它監聽的事件是 orientationchange,以下是部份原始碼:

window.addEventListener("orientationchange", updateOrientation);

於是,我查了一下 MDN 的文件,發現這個事件即將要捨棄了:

Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

雖說要棄用,但目前這個事件在手機上還是可以正常作用

於是我又在原始碼發現了這一段:

const getOrientation = () => window.screen?.orientation?.type;

在 macOS Safari 的 console 裡輸入 window,發現 window.screen 底下沒有 orientation 這個屬性

所以使用套件,會抓到 undefined 也是很合理的

因此我推測所有使用 Webkit 的瀏覽器一樣沒有 orientation 屬性

測試了四個作業系統(Windows、macOS、iOS、Android)、三種瀏覽器(Chrome、Firefox、Safari)之後,得出以下結論:

ChromeFirefoxSafari
MacOSOOX
WindowsOONA
iOSXXX
AndroidOONA

Windows 與 Android 系統上沒有 Safari,所以就不在討論範圍內

果然不出所料,Webkit 系的瀏覽器,全部都拿不到 orientation 屬性

而 MDN 也列出 orientation 屬性對各瀏覽器的支援度,如下表:

browser compatibility

iOS 上的 Chrome、Firefox 應該也屬於 Safari on iOS 那一類

令人驚訝的是,IE11 居然有支援!

雖然行動裝置上也沒有 IE 可以測試,而 Windows 的 IE 整個打不開 CodeSandbox

所以就不實測了

至於 Safari 的結果就 ⋯⋯ 🤦‍♂️

不知道 Safari 什麼時候會支援啊 ⋯⋯

以下是測試結果中使用的裝置,其作業系統及各瀏覽器版本,有需要可以參考一下:

OSChromeFirefoxSafari
MacOSMonterey 12.3.1101.0.4951.64100.015.14
WindowsWin10 21H2100.0.4896.12787.0NA
iOS15.14.1101.0.4951.58100.115.14
Android11101.0.4951.6191.1.0NA

因此,就算 orientationchange 事件仍然可以使用

但只要有裝置上的 window.screen 物件沒有 orientation 屬性

就沒辦法用此方法來判斷裝置的方位

於是我查了一下看是否有其他替代方案,在 stack overflow 找到了一些解決方案

而用 window.matchMedia("(orientation: portrait)") 去判斷當前 window 的方位,似乎是可行的方案

並加上監聽 change 事件,即可在當 orientation 改變的時候,得到最新的方位

於是,我把它寫成一個 hook,以便之後方便使用:

import { useCallback, useEffect, useState } from "react";
const getPortraitFromMediaQuery = () => window.matchMedia("(orientation: portrait)");
function UseIsPortrait(): boolean {
const initialIsPortrait = getPortraitFromMediaQuery().matches;
const [isPortrait, setIsPortrait] = useState<boolean>(initialIsPortrait);
const updateOrientation = useCallback((event: MediaQueryListEvent) => {
console.log("event emitted!!");
if (event.matches) {
// Portrait mode
setIsPortrait(true);
} else {
// Landscape
setIsPortrait(false);
}
}, []);
useEffect(() => {
const portrait = getPortraitFromMediaQuery();
// start listening event
portrait.addEventListener("change", updateOrientation);
return () => portrait.removeEventListener("change", updateOrientation);
}, [updateOrientation]);
useEffect(() => {
console.log("isPortrait: ", isPortrait);
}, [isPortrait]);
return isPortrait;
}
export default UseIsPortrait;

若將 matchMedia 改寫成:window.matchMedia("(orientation: landscape)")

matches 屬性拿到的布林值就會是相反的,就看比較偏好用哪種方式判斷了

window.screen.orientation 確實是一個不錯的判斷方式

只可惜 WebKit 系的瀏覽器不支援,希望以後可以跟進(對 Safari 的成長速度頗為擔心就是了

window.matchMedia("(orientation: landscape)") 目前看來可以取代的方案

美中不足的是,它只能判斷「直」(portrait)與「橫」(landscape)

而無法像 orientation 屬性可以判斷主要橫向(landscape-primary)、次要橫向(landscape-secondary)、主要直向(portrait-primary)、次要橫向(portrait-secondary)四種不同的值(型別為 string

但其實基本的直、橫向判斷,就足以應付大部分的情況了

以上的程式碼全貌,可以參考以下的 CodeSandbox

可以試著在手機、平板等裝置上打開,看看不同瀏覽器的差異

最下面的有顏色的方塊是用純 CSS 的方式寫的,方便跟元件的 state 做比較

遞迴元件與巢狀列表

工作上遇到一個頁面,是要呈現一個巢狀的 list

每個 list 底下,還會有自己的屬於自己的 list

而每個 list item 都有一個 checkbox 在前面

HTML 會長得像以下結構:

<ul>
<li>
<input type="checkbox" />
<label>1</label>
<ul>
<li>
<input type="checkbox" />
<label>1-1</label>
</li>
<li>
<input type="checkbox" />
<label>1-2</label>
<ul>
<li>
<input type="checkbox" />
<label>1-2-1</label>
</li>
<li>
<input type="checkbox" />
<label>1-2-2</label>
</li>
{/* 繼續往下長.... */}
</ul>
</li>
</ul>
</li>
<li>
<input type="checkbox" />
<label>2</label>
</li>
</ul>

目前只有固定的三層,可以直接寫死三層的 list 結構

但這不是一個靈活的結構,因爲未來某一天若要添加第四層,就得在 HTML 再添加下一層的 <li>

我希望的是用 data-driven 的方式去做渲染

也就是 data 有幾層巢狀,HTML 的部分就會依據 data 長出幾層

巢狀的資料結構,我第一個想到的就是「遞迴」

某些場合我們會使用遞迴函式

函式可以遞迴,也就是在自己裡面呼叫自己,那元件是否也可行呢?

於是我查了一下關於 recursive component 的文章

發現 Vue 與 React 雖然寫法不太一樣,但都有遞迴元件的方式達成不斷巢狀渲染的目的

這裡就用 Vue 來示範

以下分三個方向來討論,分別是:

  • 資料面
  • 顯示面
  • 互動面

首先,先定義一下 data 的型別:

type Item = {
id: string;
isCheck: boolean;
children: Item[]; // <-- 這裡又是 type Item,不斷往下長⋯⋯
};
type List = Item[];

從 children 的型別可以看出,type Item 也是一個遞迴的結構

因此,我們的 data 的結構就會是 type List 的樣子:

var nestedList: List = [
{ id: "1", isCheck: true, children: [] },
{
id: "2",
children: [
{ id: "2-1", isCheck: true, children: [] },
{
id: "2-2",
isCheck: true,
children: [{ id: "2-2-1", isCheck: true, children: [] }],
},
],
isCheck: true,
},
];

我們在這裡只寫三層的 list 結構就好,三層若可以正常顯示,四層、五層、⋯⋯、n 層也不會有問題

接下來就是 HTML 的部分,我們定義一個元件,就叫做RecursiveList吧:

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" />
<label>{{ item.id }}</label>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "RecursiveList",
props: {
list: {
type: Array,
default: () => [],
},
},
};
</script>

這裡可以渲染第一層的 item 了,因此我們著手進行第二層以下

如果每個 <li> 有 children,就要再跑一次 v-for 去渲染

遞迴函式呼叫自己,就是爲了避免寫重複的邏輯、做重複的工作,遞迴元件也是同樣的道理

因此,我們在元件裡建立遞迴的結構:

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" v-bind:checked="item.isCheck" />
<label>{{ item.id }}</label>
{/* ⬇️ 這裡使用遞迴 ⬇️ */}
<RecursiveList v-bind:list="item.children" v-if="item.children" />
</li>
</ul>
</div>
</template>
<script>
// ...
</script>

從此,無論是第三層、第四層、……、第 n 層,都可以渲染出來了~

nested template

到這裡爲止,僅只是顯示方面,別忘了每一個 item 裡面還有一個 checkbox

接下來,我們要為這個 checkbox 增加互動行爲

我們做出的巢狀的 <ul><li> 結構之後,每個 item 的 checkbox 互動行爲又是如何呢?

這裡有兩個條件要符合:

  1. 當任一 checkbox 被 check/uncheck 時,該 checkbox 底下的所有 checkbox 都要被 check/uncheck
  2. 每個 checkbox 的下一層有任一 checkbox 為 check 狀態,它自身就要被 check

當 checkbox 點擊的當下,以這個 list item 的元件為主體(以下簡稱當事人),分為兩個方向:

  1. 向下的操作:改變所有子層(及子層的子層 ⋯⋯ 等) checkbox 的值
  2. 向上的檢查:確認父層是否要變動

首先,我們來做第一個條件的互動:

當事人若爲 check,底下所有子孫都要 check,也就是全選;反之,則全不選

所以我們新增一個 handleChildren 的方法

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" v-bind:checked="item.isCheck" v-on:change="handleChildren($event, item)" />
<label>{{ item.id }}</label>
<RecursiveList v-bind:list="item.children" v-if="item.children" />
</li>
</ul>
</div>
</template>
<script>
export default {
// ...
methods: {
handleChildren(event, item) {
const newValue = event.target.checked;
function changeAll(item, value) {
item.children.forEach((child) => {
child.isCheck = value;
changeAll(child, value);
});
}
changeAll(item, newValue);
item.isCheck = newValue; // 更新自身的值
},
},
};
</script>

handleChildren 方法裡面,changeAll 這個函式也是做遞迴呼叫

handleChildren 傳進來的 item 底下所有的 children 都掃過一遍

最後也別忘了更新自己本身的值

完成了向下的操作,接下來看向上的檢查

當事人觸發 change 事件後,若為 check,則父層不用做任何檢查

但若為 uncheck,他的父層(parent)要判定其子層,也就是當事人的平輩層(sibling)是否都沒有 check,若都沒有,就要 uncheck

這個父層若 uncheck 了,那又要執行一次這個父層的父層(aka 當事人的祖父層、grandparent)的檢查 ⋯⋯

也就是會一路往上每一層都做檢查,直到最上層

在當事人的視野裡,我們只從 props 接收到 list,也就是父層的 children 這個陣列

壓根不知道自己的父層是誰

於是,他必須通知自己的父層,也就是遞迴元件的上一層去做 children 的檢查

此時,Vue 的 $emit 就派上用場了

還記得前些年,前高雄市長韓國瑜的口號嗎?

沒錯,那就是「貨出去,人進來,高雄發大財」

來改編一下這句話:

event 出去,props 進來,前端發大財

woz-in-us-festival

利用 $emit 的特性,來通知上層:

你底下的其中一個子層 uncheck 了,請檢查自己該不該繼續 check

於是,新增一個 handleParent 方法

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input
type="checkbox"
v-bind:checked="item.isCheck"
v-on:change="handleChildren($event, item), handleParent()"
/>
<label>{{ item.id }}</label>
<RecursiveList
v-bind:list="item.children"
v-if="item.children"
v-on:emit-parent="handleParent(item)"
/>
</li>
</ul>
</div>
</template>
<script>
export default {
// ...
methods: {
// ...
handleChildren() {
// ...
},
handleParent(item) {
this.$emit("emit-parent");
if (!item) return; // 沒有帶參數,表示是當事人,故不檢查
const childrenHasSomeChecked = item.children.some((item) => item.isCheck);
item.isCheck = childrenHasSomeChecked;
},
},
};
</script>

有別於一般 $emit 的寫法,這個 $emit 不帶任何 payload 出去,只是發送 emit-parent 事件

遞迴到上一層之後,emit-parent 事件觸發後執行的函式,依然是 handleParent 方法

此外,還從參數帶了父層的 item 進來,以便做 check 的檢查

至此就大功告成了


當事人父層的父層檢查、父層祖父層檢查,也都是同樣的邏輯

重複的工作,就交給遞迴去做了

emit-parent 事件會一路 $emit 上去

在最上層,也就是第一次出現 RecursiveList 的 App 元件裡面

handleEmitTop 方法去接 emit-parent 事件,並用 console.log 紀錄下來:

<template>
<div id="app">
<RecursiveList v-bind:list="nestedList" v-on:emit-parent="handleEmitTop($event)" />
</div>
</template>
<script>
import RecursiveList from "./components/RecursiveList.vue";
export default {
name: "App",
components: {
RecursiveList,
},
data() {
return {
nestedList: [
// ...
],
};
},
methods: {
handleEmitTop() {
console.log("do nothing");
},
},
};
</script>

果然會跟預想的一樣,每次的點擊,無論哪個 checkbox

每次都會一路 $emit 到最上層,然後印出 do nothing

但最上層沒有 checkbox 了,所以無需檢查,因此不用做任何事

上面的所有程式碼,請參考以下 CodeSandbox:

這次是因工作上的需要,而研究了關於遞迴的寫法

雖然這個不確定會巢狀幾層的 data 結構,會造成後端定義 model 的困擾而沒有採用

但也是一個不錯的嘗試

希望未來能有機會用到這種簡潔的寫法

Vue 因為框架的特性,直接對 data 的操作比較簡單,而無需在意是哪一層、哪一個 children

雖然過程中還挺手忙腳亂的,不知道哪一層是哪一層,然後誤打誤撞的寫出可以動的東西

it works

然而其實只需要專注在當事人身上就好了

話說回來,隨意修改 data 而不透過 props 去傳遞新的 data ,確實不是個好作法,但也確實能達到目的

若換作是 React,想必寫法會很不一樣

如果你好奇:文中那張照片、站在沙灘高舉雙手的人是誰?

他是蘋果電腦的共同創辦人:史蒂夫·沃茲尼克(Steve Wozniak)

那是 1982 年沃茲尼克所舉辦的音樂節 US Festival 現場

在他的自傳中提到自己夢想著籌辦音樂節

雖然音樂節圓滿舉辦,但不僅沒賺錢,反而虧損

可是他本人還是很開心,非常享受音樂節的每一刻

這張照片的肢體語言令我印象深刻

也充分表現出他的心情

就算沒有發大財,每天還是要過得開心

勞動節的放空提案

其實一直有勞動節的實驗計畫想要做

去年因為不小心有點小意外所以沒有做,所以今年就不能再錯過

雖然稱為計畫,也不是什麼非常不得了的事就是了

勞動節的本意是為了體恤勞工平常的辛勞,所以在這天讓身為勞工的大家可以休息一天

然而就廣義的角度而言

我們身為自己身體的 CEO,是不是也該讓自己的身體器官好好放一天假?

畢竟它們平常也沒有在週末休假啊

於是我開始有了「讓身體好好休息一整天」的初步想法

具體上來說要如何做呢?

大致的方向是這樣的

我制訂了 5/1 這天可以做的三件事:

  1. 吃飯
  2. 閱讀
  3. 睡覺

就這樣。

也是可以一整天不吃飯,但我沒嘗試過斷食一天,抱歉了腸胃,你們還是得工作

雖說是休息,總不能一整天都在發呆吧

於是我選擇了加入閱讀這一項,但僅限於紙本的書籍(而且是小說類,低耗腦力的類別)

若眼睛也要休息的話,大概只能睡覺了,抱歉了眼睛

至於睡覺 ⋯⋯ 稍微累的時候就去休息,在現代社會是多麼奢侈的一件事 🙃

基於生理構造的關係,大腦、心臟、肺大概也無法休息了,抱歉了三位

我只能在能力範圍內,執行放鬆的計畫

  • 任何 3C 產品、LCD 螢幕
  • 運動
  • 外出

因此,平時總擺在桌子中央的 Mac 就暫時收進櫃子裡了

手機作為通訊用及聽一點輕音樂,只有最低限度的使用

減少從螢幕接收的藍光,所以相關設備都先收起來

或許你會問:「為何運動不行?」

沒為什麼,我只是想說既然都要休息一天,那不如走節能路線

逛街、喝咖啡都挺耗費能量的,所以外出僅限吃飯

嗯,規則制訂完成

於是就依照上面的規則去執行了

悠哉地在早餐店吃著厚牛總匯三明治

感謝早餐店這天還上班,不然我就沒有早餐吃了

然後翻開圖書館借來的書

東野圭吾的「解憂雜貨店」

一直很想看的一本書

這本也不是什麼新書

然而一忙就忘記了

於是就過了這麼多年 ⋯⋯

雖進入了五月,但卻是一個涼爽的天氣

外面不時下著雨,雨聲也在耳邊迴響

雨聲真是療癒啊~

心裡不禁感嘆了起來

追隨著書中的文字,躍身小說的世界裡

很神奇的是,做的事情變少了,時間也有走得比較慢的錯覺

正好跟小說裡浪矢雜貨店的情節不謀而合

偶爾起身走動,回想了今日的計畫

嗯⋯⋯好像沒有什麼其他可做的事情

頓時驚覺:

習慣性在閒下來的時候找點事做,似乎成了現代人的行為模式

也就是說,時間上若有空白,就想要找個東西填進去

手機、平板、電腦、電視、網路都被去除之後,我們的生活還剩什麼呢?

我悠悠回想起那個沒有手機、電腦、網路不發達的年代,到底是怎麼生活的?

同樣的一天 24 小時,我們都做了些什麼活動?

我不禁思忖著

然而怎麼樣也回想不起來

只好再次回到小說裡

看書看累了,就爬上床休息

休息夠了就繼續看書

不知不覺,也來到了晚餐時間

心想著:吃個拉麵作為完美的 ending 吧!

窗外仍下著雨,如此奢侈的天氣

於是搭了捷運,去附近的一間拉麵店

感謝捷運的員工與司機們沒有休息,不然我就要走路了

走在路上,邊想著如果拉麵店今天公休,要吃什麼的備案

從不遠處望去,店面的落地窗透出搖曳昏黃的亮光

彷若像我招著手:「快進來坐啊~」

感謝拉麵店今天也很認真的營業,才有美味的拉麵

我不禁在心裡雙手合十

感謝款待,雖然我久久才來一次

回到家,伴隨著窗外不曾間斷的雨聲

繼續把手邊的書看完

以上就是這次的「什麼都不做勞動節計畫」

雖然字面上說是什麼都不做,但還是有做一些事就是了(笑

太過於認真的過每一天,最後變成忙得閒不下來

身心都沒有得到真正的休息

這大概已經是現代人的文明病了吧

這次的勞動節連假,因為台鐵的罷工停駛,新聞不斷在報導這件事

雖然我知道他們本來就是輪班制

但我其實有一個想法:

既然是勞動節,那就所有勞工、各行各業都休息一天如何?

雖然這種事如果發生了一定會引起大亂

可是,當我們放連假休息、出去玩的時候

如果百貨公司的員工沒有上班,我們如何逛街購物?

如果餐廳老闆不營業,我們也無法大啖美食

所以我才會對今天還在工作的人獻上由衷的感謝

此外,也再次感謝我的身體不辭辛勞的為我工作,雖然我不是對員工很好的 CEO(笑

而我又試著想像各行各業(包括便利商店)都休息一天的情況

整個台灣應該會街上空蕩蕩的,像是空城一樣吧

初探 C# 的 Array 與 List

C# 的 List 與 Array 有很多類似之處

在看 LINQ 介紹的時候,出現了兩個 LINQ 函式:ToList()ToArray()

看到這兩個詞之後我不禁好奇,List 與 Array 又有何不同呢?

C# 中的 Collections 資料型別有很多種,包含:

  • ArrayList
  • List
  • SortedList
  • Dictionary
  • Hashtable
  • Stack
  • Queue

而想討論的是 Array、List 與 ArrayList 這三種

Array 的定義如下:

An array is the data structure that stores a fixed number of literal values (elements) of the same data type. Array elements are stored contiguously in the memory.

由定義我們知道,C# 使用連續的記憶體空間存放 Array,因此在初始化的當下,就決定了這個 Array 的長度

我們可以替換 Array 裡面成員的值,但無法新增或是刪除裡面的成員

因此,若要增加陣列的長度,就要再建立一個更大長度的新陣列,然後將就原陣列的所有成員 copy 過去,再將原陣列刪除

宣告一個 Array,可以這樣寫:

int[] numbers;
string[] colors;

或在宣告的時候直接初始化:

int[] numbers = new int[3] { 1, 2, 3 };
string[] colors = new string[3] { "red", "green", "blue" };

若用 var 初始化陣列,則可以省去陣列長度,讓編譯器從成員去自行推斷陣列的長度:

var numbers = new int[] { 1, 2, 3 };
var colors = new string[] { "red", "green", "blue" };

更簡短的寫法:

int[] numbers = { 1, 2, 3 };
string[] colors = { "red", "green", "blue" };

在初始化的時候,必須指定 Array 的長度

此外,成員的數量也要與長度相同,不然就會報錯

ArrayList 的定義如下:

In C#, the ArrayList is a non-generic collection of objects whose size increases dynamically. It is the same as Array except that its size increases dynamically.

因為 ArrayList 在宣告的時候,並不用定義其內部成員的型別,意即每個成員可以是不一樣的型別

List 跟 ArrayList 都不使用連續的記憶體來儲存資料,因此它的長度是動態的,會隨著成員的數量而增減

宣告一個 ArrayList 有兩種方法:

ArrayList list = new ArrayList();
var list = new ArrayList();

可以用 Add() 方法,隨時加入新的成員至 ArrayList:

var list = new ArrayList();
list.Add(1);
list.Add("hello");
list.Add(false);
list.Add(null);

或用物件初始子(object initializer syntax)的寫法:

var list = new ArrayList()
{
1, "hello", false, null
};

由此可見,ArrayList 中的成員不一定是同型別的,這跟 JavaScript 的陣列很相似

ArrayList 提供一些方法,來操作內部的成員,剛才範例中的 Add() 就是其中之一

除此之外,還有 AddRange()Insert()Remove()Sort()IndexOf⋯⋯ 等

比較常見的屬性有:CapacityCount

Count 很好理解,也就是 ArrayList 的成員數,相當於 JavaScript 的 Array.prototype.length

Capacity 則代表這個 ArrayList 可以裝載多少成員,這其實跟記憶體的配置容量有關

List 的定義如下:

The List<T> is a collection of strongly typed objects that can be accessed by index and having methods for sorting, searching, and modifying list. It is the generic version of the ArrayList that comes under System.Collections.Generic namespace.

List 其實是泛型版本的 ArrayList,在 List 裡面的每個成員,其型別都要符合傳入的 T 泛用型別

List 來自於 System.Collections.Generic namespace,而 ArrayList 來自於 System.Collections

宣告、初始化、對成員的操作方法,基本上都跟 ArrayList 一樣

List 同 ArrayList 有 Count 這個屬性,表成員的數量

List 有些方法與 ArrayList 一樣,詳細的範例可以參考 TutorialsTeacher 網站

List 與 Array 將資料儲存至記憶體的方式不太一樣,在這篇文章分析了兩者的相異之處

以 List 而言,在建立一個 List 的時候,會先定義長度為 4 的陣列

當裡面的 item 數量大於 4 時,則再創建一個新的陣列,其長度為原本的兩倍(也就是 8),並將舊的陣列從記憶體中釋放(dereference, garbage collection 機制)

再下一次遇到 item 數量超越陣列長度的時候,就再做一次(長度增為 16)

可以從下面的範例印證:

可以看到,Capacity 確實是依照兩倍的方式增長

最後面,用 TrimExcess() 這個方法將 Capacity 多餘未使用的記憶體移除,達成 Count 跟 Capacity 相等。然後呼叫 Clear() 清空所有成員,Count 歸零,Capacity 則維持原狀

ArrayArrayListList
記憶體使用靜態且連續的記憶體使用動態且不連續的記憶體使用動態且不連續的記憶體
成員同型別資料異型別資料同型別資料
命名空間System.ArraySystem.CollectionsSystem.Collections.Generic
成員修改只能夠變動成員,無法增減可增減成員可增減成員

研究過 Array 及 List 之後,才知道最重要的差異就在於記憶體使用

然而,我在專案裡幾乎只有看到 List<T>,因為我們處理的資料大部分都是集合物件,也大量使用 LINQ 函式來處理資料

因此相較於 Array,List 的靈活度也就顯而易見了

遞迴函式與夢境

遞迴函式是一個很有趣的東西

也常在一些特殊的資料結構上會使用到

而遞迴最主要的構成因素就是在函式裡面呼叫自己,大概長得像是下面的結構:

function recursion(param) {
recursion(param);
}
// initial call
recursion(initialParam);

但若像上面的結構,不斷地呼叫自己,沒有終止的機制,就會像無窮迴圈一樣,讓電腦記憶體爆掉

因此,遞迴函式裡面必須有一個刹車的機制

流程控制:繼續走?還是回頭?

Section titled “流程控制:繼續走?還是回頭?”

在流程控制(control flow)上,可以分成兩條路:

  • 符合終止條件:停止
  • 不符合終止條件:呼叫自己

流程圖大概如下:

graph LR
A(Initial call) --> B{Continue?}
B -->|No| C(Finish)
B -->|Yes| D(Call again)
D --> E{Continue?}
E -->|No| F(Finish)
E -->|Yes| H(Call again)
H --> I(Go on...)

我很喜歡用一個比喻去詮釋遞迴的概念,那就是:全面啟動

電影中的主角柯伯如何進入夢境的?

沒錯,睡著就行了!

而在夢中的他,有兩個選擇:

  1. 醒來,回到現實世界
  2. 再睡著一次,進入「夢中夢」,也就是第二層夢境

但不幸的是,隊員的其中一人在出任務的時候受傷而進入昏迷狀態,柯伯必須去下一層救他

原本計畫順利的話,應該就此打道回府才是

然而面對這樣情況,他也無法執行了

任務達成的條件尚未成立

於是他說:「我們必須前往更深處」

go deeper

他只好再睡著一次,去下一層夢境了

然後他終於找回隊員了

該怎麼回到現實世界呢?

沒錯!就是 kick!

would be a kick

在理解遞迴的同時,也需要先了解這兩個關鍵字作為基礎知識

一個是執行環境(execution context),另一個則是堆疊(stack)

這裡就直接用英文稱呼,比較直覺一點

在 JavaScript 的世界裡,其實有兩種 execution context:

  • 全域執行環境(Global Execution Context)
  • 函式執行環境(Function Execution Context)

而我們這裡討論的,主要是函式執行環境,而所有函式執行環境的最外面,即是所謂的全域執行環境

context 就是記住當下的時空背景:當下的有哪些變數、其值又是什麼

每一次的函式呼叫,就會創造一個新的 context,以便記憶此次呼叫的環境狀態

創造出來 context 則會存放在稱為 stack 的地方

新的 context 建立了之後,就丟進去這個 stack,

因此,stack 裡的順序就會是:舊的在下,新的在上

下一次要去拿 context 的時候,就會先拿到比較新的 context

大致了解 context 與 stack 的概念之後,我們來看一個簡單的範例:階乘的計算

同時,我們把全面啟動的故事也放在手邊(到底是有多愛全面啟動

階乘的數學定義如下:

$$ n! = n \times (n - 1) \times (n - 2) \times \ldots \times 3 \times 2 \times 1 $$

我們要拿指定的初始值 n 去做階乘計算

function factorial(n) {
// 終止條件
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1); // 呼叫自己
}
}
factorial(4); // result: 24
factorial(2); // result: 2
factorial(0); // result: 1

終止條件是,當 n = 0 時,return 1

因為階乘的定義上:

$$ 0! = 1 $$

假設我們的初始值 n = 3,於是我們執行第一次的呼叫:

factorial(3);

柯伯坐在飛機頭等艙舒適的座椅上睡著了,進入了夢境

第一次的呼叫,建立的一個新的 context,裡面有我們帶進去的 n = 3

所以我們可以這樣表示:

Context { n: 3 }

並且將這個 context 丟進 stack 中儲存,可以這樣表示:

Stack = [
{ n: 3 }
];

然而我們的 n 沒有符合終止條件,於是,進行了第二次的呼叫 factorial

柯伯為了解任務,再次睡著進入了夢中夢

此時,我們帶入 factorial 的參數是 n - 1,所以是 3 - 1 = 2,帶入 2

第二次的呼叫,再次建立一個屬於它的 context:

Context { n: 2 }

於是 stack 變成:

Stack = [
{ n: 2 },
{ n: 3 }
];

我們又回到了函式的第二行,再次判斷終止與否,但仍未滿足條件,於是進行第三次的呼叫:

柯伯達成任務了,不料,隊員昏迷不醒,於是他躍身進入下一層夢境

然後再重複了一次遞迴

OK,這次 n = 0,所以要 return 1 回去,至此,不再遞迴下去

柯伯找回遺失的隊員要打道回府了

return 代表結束這個函式,並回傳值

但別忘了,我們現在在第幾層夢境?

柯伯說:「我是誰?我為什麼在這裡?」

啊,我是指在哪一個 context 裡?

最後那一個對吧,也就是 stack 最上面那個:Context: { n: 0 }

所以當我們 return 之後,回到上一次層是:Context: { n: 1 } 這個 context,而不是現實世界(aka Global 的 context)

柯伯醒來一次,回到上一層夢境

然後我們看到上一層這樣寫:return n * factorial(n - 1)

於是,1 跟上一層的 n 相乘之後,又繼續 return 更上一層,一連串的 return

柯伯一行人,藉由一連串的 kick,一路回到現實世界

整個過程像是這樣:

factorial(3);
3 * factorial(2);
2 * factorial(1);
1 * factorial(0);
return 1;
return 1 * 1;
return 2 * 1;
return 3 * 2;

於是最後我們得到結果就是:

$$ 3! = 6 $$

我們總共呼叫了 factorial 這個函式四次,稱之為遞迴的深度(depth)

如果我們想知道每一次呼叫時的 depth,可以這樣寫:

function factorial(n, depth) {
console.log("depth: ", depth); // 現在的遞迴深度
console.log("context: n =", n); // 當前的 n 值,也就是目前的 context
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1, depth + 1);
}
}
factorial(2, 1); // 第一次呼叫,所以深度從 1 開始

這樣就可以看到每一個 context 及對應的 depth 了:

depth: 1
context: n = 2
depth: 2
context: n = 1
depth: 3
context: n = 0

於是柯伯轉動手上的陀螺,以避免迷失自我

而這裡的 console.log 則成為各個 context 裡的指標(電影裡稱之為「圖騰」totem)

spinner

但柯伯的陀螺只能判斷是在夢境(aka Function Execution Context)或是現實(aka Global Execution Context

還是不太方便呢(重點錯

由上述的範例可知,每一次的呼叫,就會創造一個 context 並存到 stack 之中

所以遞迴的深度越深,context 也會越多,電腦需要花更多的記憶體去存放這個龐大的 stack

到達極限的時候,就會造成「堆疊溢位」(stack overflow)

遞迴函式自有它簡潔優雅的優點,但優點是用吃重記憶體這個缺點換來的

就像我們以前會說:「Chrome 瀏覽器的速度好快啊!」但它是吃記憶體的大怪獸啊

基於效能的考量,就會衍生出迭代(Iterate)與遞迴(Recursion)的優缺點比較了

有機會再聊這塊

其實一直想要寫關於遞迴的文章

但又不知道從何寫起

而每當在用遞迴函式的時候,總想到全面啟動這部電影

為了寫這篇,我還重新看了一次電影呢 🙈

參考網路上很多關於遞迴的範例

其他常見的遞迴範例還有,像是費式數列、巴斯卡三角、巢狀資料結構的處理……等

他們都寫得很好,我也把連結列在參考資料

或許我並不是想要寫遞迴,而是全面啟動吧 🙃

[小說] 便利店人間 - 我們都是組成世界的小齒輪

book cover

博客來-便利店人間

惠子在便利店打工,已經過了 18 年了

從這間便利店開店以來,換了好幾任店長、當初的同事們都已經不在這裡了

只有惠子,是這裡唯一不變的因素

提示:以下有劇透,請自行斟酌


從小的時候,她就是個古怪的孩子

一些言語或是舉動,時常換來大人們的驚訝或無語

甚至她的家人們也不斷地在討論該如何治好

或許,跟其他人不一樣,就顯得不太正常,而是異常,是故障的,必須修理才行

然而,她還是不懂自己到底哪裏故障了,哪裏又需要修理

隨著年紀的增長雖然沒有解決她的疑惑

倒是讓她學會一件事:

只要看起來像是普通人,就不會讓家人擔心了

大學的時候開始在這間便利店打工的時候,家人們都很高興

對惠子而言,她不懂怎麼做才能成為普通人

自從進入便利店打工以來,各式各樣的人來這裡,

當大家穿上便利店店員的制服、照著員工手冊去收錢、補貨、各種店內雜務

就宛如變身成稱為「便利店店員」的生物

只要穿上制服、依循員工手冊去行動,就好像變成了普通人一樣

曾經一任店長對她說:

時薪裡面也包括做好自我管理,帶著健康的身體去上班 1

也就是說,健康管理,也是對工作的很重要的一環

於是,她的整個生活,都是以便利店為中心著想的

然而,整整 18 個年頭都在便利店打工,又讓家人、朋友感到疑惑

大學的幾個時常相聚的同學幾乎都結婚或是生了小孩

而她今年 36 歲,還是一個單身打工族

這樣的標簽難免不引起別人的好奇

她跟多年保持聯絡的同學們聚餐

聚餐上,結婚的同學們帶來她們的老公

也理所當然地分享彼此的近況

然而,惠子卻因此成了焦點

雖然借用了妹妹建議她的說詞:

因為身體不好,所以在便利店打工

還是無法制止大家的好奇心,尤其是那些初次見面的同學的老公

大家突然像是記者似地詢問,或該說是審問

接二連三的身家調查,開始出主意

本人並未尋求建議,大家已經開始幫她想好人生的劇本、下一步該怎麼做、有沒有對象可以介紹 ⋯⋯ 等等

在這樣光景,讓她回想起小學的時候:

眾人都有些疏遠我,把身子轉開,眼睛卻帶著好奇,就像觀察某種可怕生物似地望著這裡

於是她想著:「啊,我變成異物了

某日,新來的打工男子,叫做白羽

他是一個很厭世、工作不認真、還會騷擾女客人、女同事的人

店裡的人都很討厭他

不到兩週的時間就被火了

某日惠子撞見他又要跟蹤女客人

為了避免她又去騷擾別人

所以將他拉去家庭餐廳,開始聽他悲憤地發表言論:

世界無法容忍異物,大家都被要求叫不要一致

年紀大還在打工會被質疑、為何沒有戀愛經驗

現代其實還是像遠古時代的部落

無法參加打獵或是對部落沒什麼貢獻的人,都會被部落的人瞧不起

然後成為部落的累贅

⋯⋯⋯⋯

於是,無業的他,為了不被世界排擠

想要透過結婚,隱身在這個社會當中

藉此貼上稱為「普通人」的標籤

於是惠子提出了結婚的提議 ⋯⋯

雖然最後結婚這件事不了了之

但惠子收留了因為沒繳房租被趕出來、居無定所的白羽

從外界看來,他們成了同居人

但也只是同時存在在便宜小公寓的兩人

白羽藏匿在此,可以躲避外界的眼光

一開始雖然不太情願,但後來也認為這個提議不賴

而惠子打電話跟妹妹只說了一句:「有男人在我家裡」

引來了妹妹自行腦補了很多情節,而且很為她高興

此時,她成了正常人

惠子也因此免除了一些煩惱

因為在短短近兩週的期間,被問了「為何還沒結婚?」14 次、「為何還在打工?」12 次

繼上次打電話給妹妹之後,惠子又在大學同學的聚會做了一次試探

然後她們興高采烈地自行腦補了一個劇本

就算對她們來說,是一個除了名字之外、完全素未謀面的陌生人

她感受到自己好像真正加入了她們的圈子

不禁感嘆:原來我一直以來都是圈外人

某天,惠子辭去了做 18 年的便利店打工

大家都以為是白羽有工作了,很高興地歡送她離開

而事實上卻相反

那個無所事事的男人很認真得逛著求職網站

但卻不是自己要去面試,而是要惠子養她

從便利店離職之後,她的整個作息就變得亂七八糟

吃飯也是有一餐沒一餐的

起床時都不知道是現在是幾點

因為沒有要去便利店工作,所以開始失去生活目標

變得渾渾噩噩

某日,終於收到了一家公司的面試通知

白羽則跟著她一起前往面試地點

在面試前,她們路過一間便利店借用廁所

惠子感受著便利店的各種聲音、架上的商品、不熟稔的新進店員 ⋯⋯

她順手幫忙整理商品,引來新店員的稱讚

此時的她,終於想通了一件事

她終究是稱為「便利店店員」的一種生物

便利店的「聲音」不停地流入我的體內,我是為了聆聽這聲音而生的 2

她知道自己接下來不該去面試的

我醒悟了。我不僅僅是個人,更是個便利店店員。 即便我是一個扭曲的人,即便養不活自己而路倒街頭, 我還是無法逃離這個事實。 我所有的細胞都是為了便利店而存在的。3

白羽很氣憤地離開了

於是兩人從此分道揚鑣

而惠子心裡想著:

得趕快調養好身體,以便快速地行動、更迅速地補充飲料、拖好地板、更完美地聽從便利店的聲音 4

於是,接下來要開始準備應徵新的便利店工作了

我不經意地望向倒映在剛離開便利店玻璃上的自己。 一想到這雙手、這雙腳,全是為了便利店而存在, 我覺得倒影中的自己頭一次成了一個有意義的生物 5

故事就到這邊為止了

相信往後的幾年,她還是會繼續在便利店工作吧

書中不斷地描述社會上大部分人的想法及他們看待怪人的反應

大夥每天埋頭苦幹的目的,就是為了跟上群體的腳步

照著一樣的劇本,演一樣的戲碼

或者說,依照工廠的生產手冊,製作出一樣的產品

就如白羽所說:「對部落沒有貢獻的人,就會被視為異類、被唾棄、或等著被淘汰

日本的社會群體意識之高,是眾所皆知的

也在本書中,用不同對話不斷印證這一點

或許隨著時間推移,現在也有所改變也說不定

而在漫長的人生當中,我們又到底在追求什麼

又是為了什麼而活著呢?

或許惠子會在往後的日子裡,再次遇到同樣的事情

接受各種質疑、議論紛紛、狐疑的眼光對待

在她之後的人生裡,究竟還會發生什麼樣的事呢?

她還能在便利店工作多少年、老年又會過著怎樣的生活?

作者並沒有提到

說不定就如她自己所說的:養不活自己而路倒街頭

我還真不忍想像。。

小說人物的幸福 6 在於:不用擔心接下來會發生什麼事

因為故事結束了,然後就沒有然後

然而值得高興的是,她終於找到了屬於她的地方

而且是可以奉獻她整個人生的地方

大概就就如同白羽所謂的「為村落帶來貢獻」吧

這樣說起來,也不是什麼壞事吧

這本書沒有很長,很快的就看完了

然而因為要加入故事的節錄,而翻覆翻閱其中比較重要的對話與劇情

所以花了額外一些時間

甚至也看了日文版的內容

白羽用日文說話的時候,聽起來倒是滿客氣的,不像中文那樣不屑

整個人的印象不太一樣

無業厭世男的角色印象沒有變就是了

另一點值得一提的就是

每看一次其中的對話時,就有一種莫名的沈重感

像是那種潮濕悶熱的空氣般,使呼吸有點不太順暢的那種沈重感

時常為深陷困境或尷尬場面的惠子捏一把冷汗

但又想不出更好的方式為她解圍

也因此讓我低落了一陣子

反倒是她本人雖然感到困擾,但也表現出怡然自得的樣子

或許她本來就是這樣我行我素的人吧

  1. 原文:体調管理をして健康な体をお店に持っていくことも時給の内だ

  2. 原文:身体の中にコンビニの『声』が流れてきて、止まらないんです。私はこの声を聴くために生まれてきたんです。

  3. 原文:気がついたんです。私は人間である以上にコンビニ店員なんです。人間としていびつでも、たとえ食べていけなくてのたれ死んでも、そのことから逃れないんです。私の細胞全部が、コンビニのために存在しているんです。

  4. 原文:コンビニのために、また身体を整えないといけない。もっと早く正確に動かして、ドリンクの補充も床の掃除ももっと早くできるように、コンビニの『声』にもっと完璧に従えるように、肉体のすべてを改造しかなくてはいけないのだ。

  5. 原文:私はふと、さっき出てきたコンビニの窓ガラスに映る自分の姿を眺めた。この手も足も、コンビニのため存在していると思うと、ガラスの中の自分が、初めて、意味のある生き物に思えた。

  6. 該說是幸福,還是不幸呢?

重訓名詞中英日對照(持續更新)

鑒於前陣子看了「流汗吧!健身少女」這部動畫,再加上去健身房做訓練, 開始認識一些健身相關的名詞, 所以想要整理出中、英、日三國語言的名詞對照表, 如果有錯誤還麻煩不吝指教 🙏

中文English日本語
自身自重訓練Bodyweight Training自重(じじゅう)トレーニング
器械重量訓練Machines Trainingマシントレニング
自由重量訓練Free Weightsフリーウエイト
體幹訓練体幹(たいかん)トレーニング
彈力帶訓練チューブトレーニング
バランスボール(きん)トレ
中文English日本語
槓鈴臥推Bench Pressベンチプレス
啞鈴臥推Dumbbell Pressダンベルプレス
啞鈴飛鳥Dumbbell Flyダンベルフライ
啞鈴彎舉Dumbbell Curlダンベルカール
啞鈴錘式彎舉Hammer Curlハンマーカール
傾斜啞鈴彎舉Incline Dumbbell Curl
佐特曼彎舉Zottman Curl
啞鈴斜托彎舉Decline Dumbbell Curl
W 型彎曲槓彎舉EZ Bar Curl
Kick Backキックバック
法式彎舉
仰卧臂屈伸
頭顱粉碎者
French Pressフレンチプレス
Front Pressフロントレイズ
Side Pressサイドレイズ
Dumbbell Rear Raiseダンベルリアレイズ
硬舉Dead Liftデッドリフト
捲腹Crunchクランチ
Abs Rollerアブローラー
深蹲Squatスクワット
中文English日本語
紅肌
慢縮肌
Red Muscle
Slow Twitch
遅筋(ちきん)(持久筋(じきゅうきん)・SO 筋・赤筋(せっきん))
白肌
快縮肌
White Muscle
Fast Twitch
速筋(そっきん)(短瞬発筋・FG 筋・白筋(はっきん))
速筋(そっきん)(長瞬発筋・FO 筋・ピンク(きん))
中文English日本語拉丁語
胸大肌pectoralis major大胸筋(だいきょうきん)musculus pectoralis major
胸小肌pectoralis minor小胸筋(しょうきょうきん)musculus pectoralis minor
前鋸肌Serratus anterior前鋸筋(ぜんきょきん)
ボクサー(きん)
musculus serratus anterior, serratus lateralis
背闊肌Latissimus dorsi muscle広背筋(こうはいきん)musculus latissimus dorsi
斜方肌Trapezius僧帽筋(そうぼうきん)musculus trapezius
三角肌Deltoid muscle三角筋(さんかくきん)Musculus deltoideus
回旋筋腱板
ローテーターカフ
肱二頭肌biceps brachii muscle上腕二頭筋(じょうわんにとうきん)musculus biceps brachii
肱肌Brachialis muscle上腕筋(じょうわんきん)musculus brachialis
肱三頭肌Triceps brachii muscle上腕三頭筋(じょうわんさんとうきん)Musculus triceps brachii
muscles of the forearm前腕筋群(ぜんわんきんぐん)musculi antebrachii
腹直肌Rectus abdominis muscle腹直筋(ふくちょくきん)musculus rectus abdominis
腹外斜肌Abdominal external oblique muscle外腹斜筋(がいふくしゃきん)musculus obliquus externus abdominis
腹內斜肌Abdominal internal oblique muscle内腹斜筋(ないふくしゃきん)musculus obliquus internus abdominis
Transversus abdominis muscle腹横筋(ふくおうきん)musculus transversus abdominis
長背筋群
Iliopsoas muscle腸腰筋(ちょうようきん)musculus iliopsoas
股四頭肌quadriceps femoris muscles大腿四頭筋(だいたいしとうきん)musculus quadriceps femoris
大腿後肌Hamstring大腿屈筋群
ハムストリングス
Triceps surae muscle下腿三頭筋(かたいさんとうきん)musculus triceps surae
中文English日本語
跑步機Treadmillトレッドミル
ランニングマシン
スピンバイク
滑步機
橢圓機
Ellipticals
飛輪Stationary bike
划船機Rowing machineローイングマシン
胸推機Chest Pressチェストプレス
蝴蝶機Butterfly Machineバタフライマシン
器械肩推Machine Shoulder Pressマシンショルダープレス
滑輪下拉Lat pulldownラットプルダウン
ラットマシン
坐姿划船Seated Rowingシーテッドローイング
Back Extensionバックエクステンション
Abdominal Crunchアブドミナルクランチ
Cable Crunchケーブルクランチ
大腿推蹬機Leg pressレッグプレス
雙腿伸屈Leg Extensionレッグエクステンション
坐姿腿部外彎機Hip Abductionヒップアブダクション
マルチヒップアブダクション
引體向上Pull-up
中文English日本語
啞鈴Dumbbellダンベル
槓鈴Barbellバーベル
懸垂マシン
チンニングマシン
長凳Benchベンチ
彈力帶Stretch Strapチューブ
ゴム
プッシュアップバー
バランスボール
腹筋ローラー
アブローラー
壺鈴Kettlebellケトルベル
ハンドグリップ
メディシンボール
アブマット

html ruby 標籤與亞洲語系標音

因為想要在 markdown 上面寫日文,希望在漢字上面標上假名,所以來研究一下如何實作

查了一下,發現了平常很少用到的 <ruby> 標籤

根據 MDN 的文件:

HTML <ruby> 元素的意思是旁註標記。旁註標記用於標示東亞文字的發音。

包在 <ruby> 標籤內的字,就可以加上標音,以日文為例:

とある<ruby><rb></rb><rt></rt><rb></rb><rt>じゅつ</rt></ruby><ruby><rb>禁書目録</rb><rt>インデックス</rt></ruby>
とある<ruby><rb></rb><rt></rt><rb></rb><rt>がく</rt></ruby><ruby><rb>超電磁砲</rb><rt>レールガン</rt></ruby>

渲染出來的結果:


とあるじゅつ禁書目録インデックス

とあるがく超電磁砲レールガン


<ruby> 裡面包含兩種元素:<rb><rt>

rb 是 ruby base 的意思,也就是要被標音的文字,像是日文漢字或中文字

rt 是 ruby text,也就是標音文字,像是平假名、片假名、注音等

一個 <rb>,後面接的 <rt> 就會標註在前面的 <rb> 上面

當然,中文的注音也可以:

<ruby><rb></rb><rt>ㄅㄧㄢˋ</rt><rb></rb><rt>ㄊㄞˋ</rt></ruby>
<ruby><rb></rb><rt>ㄐ一ㄢˋ</rt><rb></rb><rt>ㄆㄢˊ</rt><rb></rb><rt>ㄗㄨㄟˇ</rt><rb></rb><rb></rb><rt>ㄆㄠˋ</rt></ruby>

結果:


ㄅㄧㄢ ˋㄊㄞ ˋ

ㄐ一ㄢ ˋㄆㄢ ˊㄗㄨㄟ ˇㄆㄠ ˋ


也來試試韓文:1

<ruby><rb></rb><rt>저우쯔위</rt><rb>子瑜</rb><rt>조쯔위</rt></ruby>

結果:


저우쯔위子瑜조쯔위


我們可以把整段要標音的文字都放在 <ruby> 裡面,也可以以詞彙為單位分段:

<ruby><rb></rb><rt></rt><rb></rb><rt>ばく</rt></ruby>
<ruby><rb></rb><rt>もく</rt><rb></rb><rt></rt></ruby>
<ruby><rb></rb><rt>ろく</rt></ruby>
カイジ

結果:


ばく もく ろく カイジ


基本上沒有特別規定,如何整理依個人喜好而異

Hugo 支援直接在 markdown 寫 html,所以上面的那些範例都可以正常地被渲染出來。但問題是寫法不友善,也不好閱讀

然後在網路上找到一位日本人 Spiegel 分享了一段關於 ruby 的 shortcode 之後,就新增進我的 shortcode 裡面:

layouts/shortcodes/ruby.html

<ruby><rb>{{ .Inner }}</rb><rp>(</rp><rt>{{ index .Params 0 }}</rt><rp>)</rp></ruby>

原本我們要這樣寫:

<ruby><rb>再起不能</rb><rt>リタイヤ</rt></ruby>

往後可以這樣寫:

{{</*ruby "リタイヤ">}}再起不能{{</ruby */>}}

結果:


再起不能(リタイヤ)


只需要寫最外層的 <ruby> ,而不用寫裡面的 <rb><rt> 了~

但用這個 shortcode 的寫法,就變成以一個 <ruby> 為標音單位了:

とある{{</*ruby "ま">}}魔{{</>}}{{<ruby "じゅつ">}}術{{</ruby>}}の{{<ruby "インデックス">}}禁書目録{{</ruby */>}}

結果:


とある()(じゅつ)禁書目録(インデックス)


科技始終來自於惰性啊 😎

在 google 的時候,其實不太好搜尋,一方面資料本來就少,另一方面,跟 ruby 語言同名的關係,非常容易查到 ruby 語言的相關資訊 😂

標音功能在我網頁開發近兩年的時間裡(菜逼八),完全沒有用到過,多國語言就算有日文,八成不會有標音吧。如果我是在出版社或教育界工作的話,那還比較有可能呢

  1. 我不懂韓文,所以這是參考子瑜的維基百科,若有標音斷句錯誤再麻煩糾正我,謝謝 🙏

[開箱] Nomad 無線充電盤與皮革錶帶

觀望 Nomad 這個品牌有一陣子了,很喜歡他們低調沉穩的色調及產品設計。尤其是對無線充電盤 Base Station 系列與 Apple Watch 皮革錶帶特別有興趣

考慮買無線充電盤卻遲遲沒有入手,是因爲手邊支援無線充電的裝置,只有 iPhone 與 AirPods Pro,因此覺得沒有改成無線充電的絕對必要性。直到前陣子入手了 Apple Watch 之後,就有購買花錢的理由,三個裝置都支援了無線充電,實現了所謂的三位一體方便且可同時為所有裝置充電的方案

Nomad 的無線充電盤大概分爲以下幾種:Base Station Pro、Base Station、Base Station Stand、Base Station Mini

Base Station Pro 要價 US$199.95(約 NT$5,516),是尊爵不凡的最高級款。最大特色就是「全表面充電」(Full-Surface Charging),有別於一般的無線充電必須對準線圈的中心才能充電的小缺點,它的黑科技(官方稱為 FreePower 演算法)可以自動判斷手機的位置並進行充電,最多可支援三個裝置隨手丟在充電盤上充電,某方面是實現了蘋果公司當時沒有上市的 AirPower 無線充電盤

Base Station 就是一般的無線充電盤,擁有三個充電線圈,最多同時為兩個裝置充電。有三種版本:Hub 版、Watch 版、Watch Mount 版。後面兩者的差異在於後者只有手錶架,必須加裝 Apple Watch 的磁吸充電器才行;前者則有内附 MFI 原廠認證的磁吸充電器

此款是直立式無線充電盤,價格 US$99.95(約 NT$2,756),只能為一支 iPhone 充電。此外,直立式的充電盤不在我的考慮範圍内,所以這裡就不做太多介紹了

同樣也是單一裝置的無線充電盤,可以算是 Nomad 入門無線充電盤了,價格 US$69.95(約 NT$1,929)。體積小巧,如果沒有太多充電設備的話,這款也滿不錯的選擇。也因爲功率較低,所以只需要 20W 的變壓器就可以驅動。擁有磁吸功能,只要是 iPhone 12 以後的機種,放上去就可以輕鬆地對應到可以充電的位置

美國官網的整新品:價格很香,但看得到買不到

Section titled “美國官網的整新品:價格很香,但看得到買不到”

去年(2021 年)11 月錯過了黑色星期五(Black Friday)官網的大折扣活動。想説聖誕節應該還有優惠活動吧?但什麽都沒有……

某天看到了官網的 Outlet Sales 上架了 Base Station 的整新機,價格最多有到原價的 5 折!當下看了很心動,很開心地加入購物車,要填寫國際寄送的時候就卡住了……上面顯示無法運送至該地址。我試了一下一般的產品是可以運送到台灣的。於是推測應該是整新機不給送海外。想花錢卻買不到啊~

直到聖誕節的隔日,12/26 看到全站 8 折的活動:Treat Your Self ⋯⋯ 😭

嗯?你說這表情?這叫做內牛 ⋯⋯ 啊不是,是淚流滿面

原本有考慮 Base Station Apple Watch Edition,但因為已經有原廠附的磁吸充電器了,若買了 Apple Watch Edition,充電器就會晾在一旁,覺得這樣有點浪費。所以最後買的是磁吸版本的 Base Station Hub Station | Magnetic 搭配 Apple Watch Charging Mount

base station box

因為最新的磁吸版本目前只有 Hub 版,所以要加手錶架,再裝上去:

watch mount box

其實是用 3M 膠黏上去的,有安裝說明書、適應各種尺寸手錶的橡膠墊片:

watch mount content

充電盤後面還有兩個充電孔,依序是 USB-A、USB-C、電源孔、光源感應器:

base station back

另外還有附兩個國際插頭可以替換,一個歐規(右)、一個英規(左):

base station plug

Nomad 目前的錶帶材質,分成皮革、橡膠與金屬(鈦金屬與不鏽鋼)

但說到 Nomad,第一個聯想到的材質當然是皮革啦

以往都習慣配戴橡膠的錶帶,基於防水及耐用性的考量,但一直有想要嘗試皮革錶帶的想法

Nomad 的 Modern BandTraditional Band 皮革錶帶看起來都不錯,後來選擇了 Modern Band,我選的是棕色、黑色錶扣:

leather band

發現皮革材質不太防水,洗手的時候難免會有噴濺到皮革的表面,皮革就會有很明顯的痕跡,乾燥之後,痕跡才會慢慢消失,對於用習慣橡膠錶帶,洗手時有時候會順便沖洗手錶的我來說,算是有點小不習慣。如果要防水又想要皮革錶帶的話,就要選擇 Active Band Pro 皮革橡膠複合材質的錶帶了,但是就少了皮革的味道

另外,皮革錶帶是用幾次後,就會有明顯的皺紋,尤其是內側:

leather band front

leather band back

防水的部分,目前簡單用皮革保養油做簡單防水,未來考慮用防水噴霧做防水塗層,彌補原本不防水的皮革表面

Nomad 錶帶用的皮革就是 Horween 的皮革,對皮革沒什麼太多研究的我,不禁好奇:Horween 皮革又是何方神聖?

查了一下別人的介紹文章,發現這家發跡於美國芝加哥的皮革公司,他們生產的皮革被用在很多知名品牌上,例如:Alden、Timberland、Cole Haan、Carmina、Allen Edmonds、Chippewa⋯⋯ 等,無論是平價或是昂貴的品牌,聽起來就滿厲害。(雖然我只聽過 Timberland 🙈)此外,美國三大運動聯盟:NFL(美式足球)、NBA(籃球)、MLB(棒球)都使用 Horween 皮革製作比賽用具,像是 Wilson 的美式足球、Spalding 的籃球、Rawlings 的棒球手套

用手機多年來都是使用有線的方式充電,不但穩定快速、發熱量低也對電池比較的壽命。但如果很多裝置每天都要頻繁地充電,充電的便利性就相對重要,因此才決定改用無線的方式。至於電池壽命嘛 ⋯⋯ 我用手機平均是 2 ~ 3 年,而且從未換過電池(全部有線充電的情況)就換新手機了,若真的感受到電池嚴重老化,就送原廠換一顆新電池吧!畢竟為了極度保護電池健康而無法隨心所欲地充電,對我而言只是本末倒置罷了,花錢買來的東西就是要開心地使用,為生活帶來價值,而不是用得提心吊膽,各位同學認為呢?

[開箱] Apple Watch s7

想要直接看使用心得的同學,可以往傳送門 👉

近幾年一直有在觀望這個產品線,但說起來智慧手錶沒有非常吸引我的功能,再加上手邊已經有手錶了,所以也沒有說服自己買的理由

直到最近,陪友人 Enoch 去買 Garmin Venu SQ,為了監測他的睡眠狀況。看了他的睡眠分析數據之後,覺得滿有趣的。而自己是睡眠時好時壞的人,非常在意自己的睡眠品質,也嘗試各種改善睡眠的方式。因此才興起試試看智慧手錶的念頭。看了幾個 YouTube 的介紹影片,又因為自己使用 iPhone,基於系統整合度考量,所以決定入手最新的 Apple Watch s7

watch s7 box

我選的 Nike 45mm GPS 版、黑色 1 的鋁金屬錶殼,搭配軍綠色 2 的橡膠洞洞錶帶 3

watch nike sports band

個人覺得 Nike 的洞洞運動錶帶很好看,在黑色 4、白色 5 與軍綠色間做選擇,因為三個搭配黑色錶殼都很好看,因此小孩才做選擇,就都買了最後決定是軍綠色了,因為我還是最喜歡軍綠色 😆

官方稱做「橄欖灰色配軍褲卡其色」2橄欖灰是外側的顏色、軍褲卡其色是內側的顏色

錶殼的材質,除鋁金屬之外,還有不鏽鋼及鈦金屬可供選擇。除了材質不一樣之外,內部硬體是沒有差別的,但價格上就有滿大的差距,這是需要特別留意的地方。另外,特殊版本會有專屬的錶面主題,例如:Nike 版有 Nike 錶面主題、Hermēs 版有 Hermēs 的特殊錶面主題 ⋯⋯ 等

我其實是因為喜歡 Nike 的洞洞錶帶才選擇 Nike 版本的,Nike 專屬錶面主題倒是其次 😂

以下各機種有提供的錶殼材質表:

一般版Nike 版Watch EditionHermēs 版
鋁金屬
不鏽鋼
鈦金屬

各版本都有不同的錶殼材質可以選擇,但有些有限制,像是我買的 Nike 版本只有鋁金屬材質,顏色也只有午夜(黑)、星光(銀)兩色;一般版的鋁金屬材質則有很多顏色可以選擇(藍、黑、銀、綠、金);Watch edition 則只有鈦金屬。

所有版本都有 41mm 跟 45mm 可以選,就看自己的手腕適合什麼尺寸。唯有必須要注意的是,41mm 與 45mm 錶帶是不共用的

鋁金屬最輕、材質最軟;不鏽鋼在重量方面最重、但也最堅固;鈦金屬的硬度與宗重量則介於兩者之間

41mm45mm
鋁金屬32.0g38.8g
不鏽鋼42.3g51.5g
鈦金屬37.0g45.1g

錶帶的部分材質大致上分為橡膠、金屬、皮革三種材質,而且種類眾多,要實際去 Apple 店裡試戴才知道箇中差異

我本身比較喜好橡膠材質的錶帶,比較耐用又兼顧防水,觸感也比較親膚。然而橡膠親不親膚,這點就比較見仁見智了

不鏽鋼錶帶則從來不是我考慮的材質,第一是因為重量太重,第二是因為戴起來很冰冷,尤其是冬天的時候 🥶,第三是金屬錶帶的扣環式設計,長度是固定的,所以無法隨心情調整鬆緊度

皮革錶帶會想要嘗試,但 Apple 原廠的皮革錶帶因為設計的關係,裡面裝滿了強力磁鐵來固定錶帶的位置,所以看起來像是佈滿膠囊的鉸鏈 ⋯⋯ 外觀上沒有很吸引我,所以也不在考慮內了

不過未來會考慮 Nomad 的皮革錶帶,屆時再做介紹

價格因為版本、規格及搭配錶帶不同而有多種組合的價格,所以這裡以我買的 44m Nike GPS 版,及友人 Ben 買的 44m Watch Edition LTE 鈦金屬版(搭配「編織單圈錶環」)來做比較

NikeWatch Edition
41mm GPS$11,900
41mm LTE$14,900$24,400
45mm GPS$12,900
45mm LTE$15,900$25,900

價格上的差距差不多落在 1.5 ~ 2 倍,若是 Hermēs 版的話,那差距就又會再拉大了

GPS 與 LTE 版本的差別就在於 LTE 版本可以在不跟手機連線的狀況下打電話。不過若要使用到 LTE 行動網路及通話功能的話,還要每月再進貢付一筆錢給電信公司

若手機會隨時帶在身邊的同學,其實 GPS 版本就很夠用了,就算出門運動不帶手機,跑步的路程、心率等資訊也會記錄在手錶裡,等到跟手機連線之後,就會自動同步資料至手機的健康 APP 裡。若有手機不在身邊還需要利用 Watch 打電話的同學,LTE 版就比較適合你

我之前配戴的是使用了 4 年多的 Casio 登山錶(PRW-3510),主要功能有電波自動對時、三大感測器(數位羅盤、氣壓、氣溫偵測)、太陽能充電。雖然是登山錶,但本人卻沒有在爬那種很高的山,僅只是遠足的程度而已 🙈

電子錶本身就很省電(望向 Casio 10 年電力系列的電子錶 ⋯⋯),再加上太陽能電池,所以從來不用煩惱充電的問題。另外,Casio 的電子錶外殼都很堅固,曾經很多次在經過門口的時候,不小心讓手錶一頭撞上金屬門框,每次的嚇一跳,但發現外表都沒什麼明顯損傷,液晶螢幕是下凹的,當然毫髮無傷,這點讓我滿驚訝的。雖然 Apple Watch s7 的玻璃螢幕官方特別強調這一代顯示器的玻璃更堅固耐用,但再怎麼堅固,它終究還是玻璃,所以下次經過門口的時候就要特別小心 😂

網路上最推薦的兩個睡眠分析 App,AutoSleepPillow。可以監測睡眠的各種數據。例如:淺層睡眠時間、深層睡眠時間、快速動眼時間(REM)、心率、血氧濃度等數據。每天起床後就看一下昨晚的睡眠報告,還滿有趣的,數據可以當作參考用。之後有機會,再來分享一下這兩個 App 的使用心得及比較

内建的「體能訓練」APP 提供了很多的運動選項可以選擇,借一下官方的圖片:

workout app

我目前最常使用運動是:健行、划船機、瑜珈、室外步行、傳統肌力訓練、羽球、桌球

或許對現代人來說,每天幫電子產品充電已經是家常便飯的事。但對我而言,多一個裝置,就要多一分需要充電的煩惱

所幸 s7 除了螢幕比上一代更大之外,也加快了充電速度,所以在充電的便利性上有所提升,可是還是必須每天充一次電,即使一次只充 8 分鐘 6

另外就是內附的磁吸充電線(俗稱的「聽診器」),是新版的 Type-C 版本,而非前幾代所附的 Type-A 接頭,由於手邊沒有任何 Type-C 充電器,還特別為了 Watch 充電買了一個 Apple 原廠的 20W 充電器。

type-c charger

看 LINE 訊息、行事曆、提醒事項等,提供了閱讀通知訊息的便利性,當時如果要做回覆的話,只能用手寫的, s7 這代加大了螢幕所新增的全尺寸 qwerty 鍵盤僅支援英文鍵盤及簡體中文。不過若要回覆很長的簡訊,還是使用手機比較方便啦~

handwashing timer

手錶會偵測洗手的動作及聲音(八成是水聲),判斷使用者正在洗手,然後開始做洗手的計時,規定是洗手長度 15 秒的時間

Apple 好貼心,時時刻刻在關心監聽何時在洗手

microphone is muted

我看網路上有些人認為洗手計時功能很雞肋,但我自己覺得還滿不錯的。有時候會因爲趕時間就隨便洗手,這時手錶就會提醒你還需要再洗幾秒鐘,在後疫情時代,洗手真的不能馬虎啊~

另外無意間發現很有趣的事:就是戴著刷牙的時候,手錶竟誤認爲是在洗手,然後開始倒數計時 😂

不過這也可以提醒自己不要隨便刷刷就去睡覺了。除了手部衛生,口腔衛生也很重要啊~

「活動記錄」分為「活動」(紅圈圈)、「運動」(綠圈圈)、「站立」(藍圈圈)

watch face activity

活動的部分,如果是通勤族,上下班需要搭捷運轉車、轉公車的人,轉車走路的路程就已經完成一部分了,只要在上班時間不時起身走走到處閒晃,基本上都可以完成紅圈圈

運動的部分就真的要靠運動才能比較容易完成了,為了健康,這部分就不能偷懶啦~

站立的部分是最好達成的吧,上班時間一定會起身去廁所、裝水或稍微散步打忙之類的,所以藍圈圈也是滿容易達成的目標

我曾經遇到比較有趣的是,站在站立工作桌前用電腦,突然手錶跳出要我起身站立活動的提示,可是我一直站著啊~ 😂

錶面主題真的太多選擇了,我就介紹幾個我經常使用的錶面主題吧:

去健身房的時候,就用這個錶面。將運動的時候需要的所有功能都放在錶面上了:碼表、倒數計時器、心率偵測、體能訓練 APP

infograph modular

以往戴電子錶,比較習慣看數字的時間,而且一定要 24 小時制 😎

nike hybrid

睡前的 downtime 時間(睡前的半小時~一小時),將不必要的資訊都移除,所以就用這個錶面

numerals duo

有時候如果想要看更多資訊,就會使用這個主題:

infograph

身為通勤族,通勤時常需要 Spotify 或 Podcast 的陪伴。戴了 Apple Watch 之後,手機就算放在口袋,也可以用手錶來控制音量、或是轉下一首歌

在 Apple A13 信義店取貨的時候,就順便問了店員保固的價格,以 Watch s7 Nike 版而言,AppleCare+ 的價格是 $2590,保固為期兩年。每年有兩次損壞免費換新的機會,也就是不小心摔壞了,可以免費換新的

正好友人 Ben 最近購入 Watch Edition 不鏽鋼版,也加購了 AppleCare+,所以就拿這兩個機種來比較:

Nike 版(45mm GPS)Watch Edition 版(45mm LTE)
保固時間兩年三年
價格$2590$4990

AppleCare+ 如同跟購買的價格一樣,大約貴了快一倍,也是滿合理的,在保護時間上也有差異,分別是 2 年與 3 年

這也是意外從 Apple 店員那裡得到的資訊。AppleCare+ 其實是可以退款的,而且隨時都可以, 但前提是沒有使用過任何一次 AppleCare+ 做維修或更換才行。退款的金額會依照剩下保固時間的比例去計算

先講缺點吧,我這個人並不提倡衝動購物的 😎

  • 需要每天充電
  • 會不小心花錢買一堆錶帶來替換(個人造業個人擔 🙈
  • 受限螢幕尺寸,回覆訊息還是不太方便
  • 玻璃錶面比較脆弱
  • 藍光
  • 睡眠、運動監測
  • 洗手計時、刷牙計時
  • 錶帶及錶面主題可以隨心情、場合更換
  • 推播通知(看訊息很方便)
  • 地圖導航
  • 戴口罩也可以解鎖 iPhone
  • 音樂播放控制

以上就是簡單的開箱介紹,列出了我自己使用上的感想及優缺點分析

規格的比較都是參考蘋果官網的資訊,因為 Apple Watch 的種類實在太多了,若有發現錯誤的地方,也請不吝指教,我會儘快修正,所有資訊還是以官網的數據為準喔~

題外話是,跨年在看 101 煙火的時候,無意間看了一下手錶,赫然發現 Apple Watch 居然也在放煙火耶~~ 🎆

new year hanabi

new year hanabi video

真的很酷!還玩了好幾次 😆

查了一下,才知道這不是新功能,在幾年前就有這個有趣的彩蛋了,聽說生日那天也會有彩蛋呢!

若想要看更詳細的解說,可參考幾位 YouTuber 的開箱介紹:

想清楚?Apple Watch 7 買前必看 TOP 6 問題!到底該怎麼選?GPS 行動網路版, 鋁殼不鏽鋼鈦金屬材質, 音樂, Nike 版差異

阿康這部影片針對各機種的差異比較,算是解決了我的選擇障礙

他還有一系列的 Apple Watch s7 影片介紹,包括他住院時的使用心得,感受到他真的很愛這產品 😂

Apple Watch 的超詳細使用指南|帶你解鎖隱藏功能!還有選購建議和錶帶推薦|大狸子切切裡

切裡除了詳細的產品介紹與使用情境分析之外,並不會有任何推坑的感覺。她說的一句話我很喜歡:

消費本身不會讓你進步,只有腳踏實地去付出才可以

在說的是衝動購物並不會讓身體更健康,而是要確實地持續地行動

而且,這麼可愛的妹子不看一下嗎 🙃

Apple Watch Series 7

蘋果爹講解的也很詳細,也解了我很多的疑問,像是在 s7、s6 或 SE 之間做選擇這樣的問題

他也分別對擁有舊款 Apple Watch 及第一次準備入手的人(像是我)提出建議,算是滿有幫助的

另外有一部影片甚至解釋為什麼他反戴手錶的原因

好用到翻 🔥Apple Watch 使用半年心得!對女生來說最好用的地方是…?|愛莉莎莎 Alisasa

艾莉莎莎分享的是她覺得好用的 10 個優點及一些缺點,標題説是女生最好用的地方,大概是指自拍功能吧?其他優點的部分就男女都適用喔~

如果身為莎粉的話怎能錯過呢 😂

不過她其中有部分講解錯誤,GPS 版本在不連接手機的狀況下,也是可以使用 Apple Pay 的 7


內容更新紀錄:

  • [2022/01/02] 修改錯字
  1. 官方名稱:「午夜色」/「midnight」,Apple 都偏好用聽起來很炫砲的顏色名字

  2. 官方名稱:「橄欖灰色配軍褲卡其色」/「Olive Gray/Cargo Khaki」 2

  3. 官方名稱:「Nike 運動型錶帶」

  4. 官方名稱:「Anthracite 配黑色」/「Anthracite/Black」。查了一下字典,Anthracite 是「無煙煤炭」的意思

  5. 官方名稱:「Pure Platinum 配黑色」/「Pure Platinum/Black」

  6. 根據官方數據,充電 8 分鐘可以使用 8 小時,前提是要用 type-c 版的磁吸充電線與 20w 充電器

  7. Everything the Apple Watch GPS-only can do without an iPhone

[開箱] 漂浮與迴旋 - Balmuda The Cleaner 無線吸塵器

等待已久 Balmuda The Cleaner 無線吸塵器首發預購終於到貨啦~

官網請參考這裡:BALMUDA The Cleaner

開箱前要先聲明:這是個人開箱,非業配 😂

迫不及待來開箱囉~

想直接看使用體驗分享的人,請走傳送門

它有黑、白兩款可以選擇,我當然是二話不說選擇白色啦~

這款吸塵器的外型真的太美了,難怪官方文案標榜著:

給視覺也潔癖的你

不太確定我有沒有視覺潔癖,但身為極簡主義者,多少應該有一點吧

當然啦,空有美型怎麼行呢?這款的另一個特點是:

雙刷頭懸浮式的吸塵器

懸浮式的設計(當然不是真的懸浮)達到完全自由解放的操縱手感,後面會做進一步地解說

盒裝的紙箱也是走極簡風格,只有印上品牌 logo 跟產品名稱

box

打開之後,在紙箱的上蓋,印有簡易說明書:「嘿!就是這麼簡單!」的概念

quick-guide

如果是購買基本款的話,配件包含: 吸塵器本體、長柄握把、手提式握把、刷毛吸頭、縫隙吸頭、充電座、變壓器、保養刷及說明書

basic-parts

而我選的是豪華組合,會多一個專用吸頭組,包含: 延長軟管、小型吸頭、平板吸頭、縫隙刷毛吸頭、織品吸頭、專用收納包及說明書

extension-pack

簡單說就是掃帚桿子的概念

上面的還有 logo 及機型的刻印,挺有質感的

normal-hand-holder-1

頂部有電源按鈕,長按可以切換普通及強力模式

normal-hand-holder-2

刷毛吸頭是這吸塵器的最大賣點,兩個主動式刷毛實現 360 度的自由迴旋

吸頭上面也有低調的刻印文字:

Dual Brush Head & 360 Swipe

刷頭上方的兩個旋轉軸,是實現 360 度迴旋的一大功臣

dual-brush-nozzle-1

翻面之後,來看一下刷頭懸浮的秘密:底下有三個萬象滾輪,搭配兩個刷毛。刷毛在啟動的時候,會向內旋轉。

dual-brush-nozzle-2

刷毛往內滾之後,從中間的吸孔收集灰塵,另一側也有一個吸孔

dual-brush-nozzle-3

此外,主吸頭的四個角落,各配上一個小滾輪,為了增進沿牆面移動時的滑順感

dual-brush-nozzle-4

接下來是這台吸塵器的心臟:主機

main-machine-1

這個是背面,黑色的部分是集塵盒,拆開來之後的樣子

main-machine-2

正面則是放電池的地方,電池的部分因為有鎖螺絲,就懶得拆下來了

main-machine-3

可替換長柄握把,改成用手提式的握把

hand-holder-1

尾部也有電源鍵,一樣是長按切換普通模式/強力模式

hand-holder-2

slot-sucker

charger-set-1

底部有整線的功能,有左右兩個出線孔可以選擇, 但因為電源孔偏向左側,所以選擇右邊出線的話會短一點

charger-set-2

加購豪華組合,會多出這些吸頭組

extra-parts

另外還有一個專用的收納包

extra-parts

可用於縫隙的吸頭,前方的吸嘴是橡膠的軟性材質

small-nozzle

用於桌面、電視櫃等平面清潔用的平板吸頭,前端的部分是具彈性的橡膠材質。 但說明書有特別提到:

若吸入水分、液體,可能導致吸塵器故障

因此不能拿來當窗戶玻璃刮刀刮水使用,縱然它不是使用紙質集塵袋

看來這台是無法直接吸入液體的,要特別留意

flat-nozzle

縫隙刷毛吸頭可以用來清潔紗窗或是窗框之類的地方

crack-nozzle-1

刷毛的長度可以做三段式的調整

crack-nozzle-1

crack-nozzle-1

根據說明書,織品吸頭可以用於棉被、沙發、窗簾等布製品。

fabric-nozzle-1

fabric-nozzle-2

至於是否可以除塵蟎就不確定了,畢竟這吸頭沒有主動式滾輪、此款也不是非常強力馬達的機型,或許不行吧

這是一般的使用模式,外型讓人聯想到掃帚

normal-mode

手持模式時,將上部的長柄握把換成手提式握把,再換上縫隙吸頭:

hand-mode-1

單手手持是有點沈重的,而且像是一把遊戲中會出現的槍

hand-mode-2

可以像一般的吸塵器一樣,前後來回運行:

vertical-drift

也可以橫向操作,就像在使用掃把或拖把一樣

horizontal-drift

刷毛吸頭可以在任何的角度下,往 360 度任何一個方位移動,而且非常地省力:

free-drifting

如同前面提到的吸頭構造,三個萬向輪加上刷毛滾輪的滾動, 達成如此滑順的操作手感

所以也不是真的懸浮啦~只是滑順的另一種說法而已。這可不是氣墊船呢

但是切記手要抓穩,因為真的很~滑~,小心就這樣飛出去了

slipping

根據官方的數據

充電時間標準模式強力模式
4 小時30 分鐘10 分鐘

以我自己目前的狀況:

每天都會拿來使用,目前都是用標準模式,每次最長連續時間不會超過 5 分鐘(畢竟我不是住豪宅 ⋯⋯)

使用完就放回去充電,所以也感受不到續航力的問題

畢竟以官方數據 30 分鐘來看,我也只用了 1/6 的時間而已

若之後有用到沒電的情況,會再更新實際使用的時間


以上是簡單的使用體驗介紹

如果要看更詳細的使用情境, 可以參考官網的使用情境介紹, 裡面的示範影片應該比我的好多了 😂

這比較偏個人喜好了,比起 Dyson 那種充滿科技感鮮豔的配色, 這種沈穩內斂的色調比較適合融入室內的擺設(也推薦給是 HSP1 的同學 🙃 )

其實原本有打算購入隔壁棚「正負零」的 XJC-C030 無線吸塵器, 價格上也相對便宜許多。

看了官網懸浮功能的介紹之後,我的荷包就失守惹 😂(這人腦波真弱

滑順的操縱手感可說是吸引我敗下去的主因

The Cleaner 可以走任何路線,正著走、倒著走、橫著走、L 型、S 型都難不倒它!

moonwalking

原來是麥可啊,我以為是吸塵器呢

集塵盒的容量只有 0.13L,可能需要經常清理集塵盒

容量跟 Dyson 的幾款無線吸塵器來做比較:(數據來自官網)2

The CleanerDyson v12Dyson v11Dyson Omni-glide
0.13L0.35L0.54L0.2L

跟 Dyson 的這幾款吸塵器相比,容量確實少了很多

總重量是 3.1kg,同樣拿三台 Dyson 來做比較:

The CleanerDyson v12Dyson v11Dyson Omni-glide
3.1kg2.2kg2.97kg1.9kg

重量表現完全是輸慘了,Dyson 的 Omni-glide 吸塵器 也同樣主打具備靈活旋轉的雙刷吸頭,重量卻少了將近 1/3

另外就是,雖然重心偏低,但在拖動的時候因為滑順,再加上重量所造成的慣性,很容易有拉不回來的感覺,在單手操作上並不是非常地順手。 用雙手的話,操縱性會比較好。

我是搭上官網的早鳥預購優惠,搭配擴充吸頭組合的價格是 $16,388

官網的原價是 $23,990

最近3 有在特價的樣子,是 $19,880

再一次拿 Dyson 家的產品價格做簡單比較

The CleanerDyson v12Dyson v11Dyson Omni-glide
$16,388$25,900$31,900 ($22,900)$16,900

以上就是 Balmuda The Cleaner 的簡單開箱介紹

優缺點的比較部分,只有拿 Dyson 家的產品來做對照(其他品牌的抱歉了),畢竟是大家比較熟悉的品牌 (幾乎每個台灣人家裡都有一台 Dyson??)

也再次深刻體會 Dyson 之所以受歡迎的原因

比較方面也只在規格的數據上,沒有實際使用的比較(YouTube 上面一堆分析影片輪不到我啊~~ 🙈 )

而是把重點放在是 Balmuda 這款吸塵器的設計及細節

感謝各位同學的收看~ 👻


以下是閒聊廢話可以跳過。🙈

 

 

 

 

 

 

 

Balmuda 目前的產品命名大部分都是加上定冠詞「The」為開頭,非常地好記。 像是蒸氣烤麵包機 The Toaster、手沖壺 The Pot、藍芽音箱 The Speaker ⋯⋯ 等。

以下超連結可能有劇透,請自行斟酌

如果未來有出新產品,例如:GPS 導航,可以叫做 The World(ザ・ワールド)

the-world

電子閱讀器,叫做 The Paper(ザ・ペーパー)

the-paper

Balmuda 的產品經理不用謝了,我都幫你們想好惹

 

 

 

 

 

 

 


內容更新紀錄:

  • [2021/11/21] 新增價格
  1. 高敏感族群,HSP = Highly Sensitive Person, What Is a Highly Sensitive Person (HSP)?

  2. 手邊沒有 Dyson 可以試用,所以就拿官網的數據來做比較囉~ 🙇

  3. 當前時間 - 2021/11/21

雷吉歐斯 - 簡易說明書

Mode = モード

分爲「普通模式」及「覺醒模式」,只有君主卡擁有這兩種模式

一般模式下的君主,有使用卡牌的限制。覺醒之後,才能使用這些被限制使用的卡牌。請參考覺醒模式

以下是君主角色覺醒的條件:

當君主身上的魔法核都移動到魔法核放置區的那回合,即啟動「覺醒模式」

普通模式下,只能解鎖小兵卡、效果卡 而覺醒模式下,可以獲得以下能力:

  • 可解鎖所有的卡牌(包含領主卡、Arts 卡)
  • 我方回合下【疲勞】,可選擇(從場外)獲得一枚魔法核,並放置在君主身上
  • 我方回合下【疲勞】,選擇場上一隻小兵或領主,(在本回合內)賦予強襲能力

Category = カテゴリー

卡片共分爲五種類,分別爲:

Color Symbol = カラーシンボル

卡牌屬性有四種,分別是:

  • 黑(闇)
  • 紅(火)
  • 白(光)
  • 綠(草)
  • 藍(水)

Class = クラス

角色卡牌有一個屬性,叫做「職業」(Class)

Cost = コスト

解鎖手牌所需要花費的成本(需要消耗魔法核

Core Cost, CC = コアコスト

領主跟君主是同盟關係,爲了維持友好關係,需要一點給她一些禮物(進貢)

只有領主角色放置在場上的時候,會多一項「維護成本」。除了解鎖當下需要耗費魔法核之外,放置在場上的時候,需要耗費的「維護成本」(從解鎖的魔法核中拿出對應的維護成本支付)

ATK/HP

ATK 為角色攻擊力,HP 為角色血量

WT, Wait Turn

小兵卡、效果卡等會有等待回合的數值,其數值是等待回合的意思,也就是當這張卡使用完、被擊倒之後,會放置在相對應的等待區

以上介紹的卡面位置資訊,可以參考以下圖示:(截圖來自原始文件)

Master = マスターカード

原文叫做 Master Card,就是要你掏出魔法小卡的意思。

君主即是玩家的化身。君主是一個角色,但有兩種模式(位於卡牌的兩面):一般模式及覺醒模式。覺醒模式下,可以發動特殊能力。

Minion = ミニオンカード

任由君主差遣的戰鬥用角色,可以對敵方的小兵、領主或是君主造成傷害。

Lord = ロードカード

與君主併肩作戰的同盟角色,只有在君主覺醒之後(覺醒模式)下,才能解鎖領主角色。

Skill = スキルカード

君主角色使用的效果卡,而君主只能使用跟自己有同樣屬性的技能卡。

另外,有些技能卡會同時擁有兩種屬性。

Arts = アーツカード

跟技能卡類似,同樣是效果卡,但唯一不一樣的點是,君主角色要在覺醒之後,才能使用 Arts 卡。

卡牌數量為 40 張,加上 1 張君主卡。同種卡牌最多只能持有三張。

備註:新手入門牌組由 25 張卡牌及 1 張君主卡組成,稱為「迷你牌組」,也可以用來對戰。

只要達成以下條件其之一即獲勝:

  • 敵方的君主生命值歸零
  • 牌庫的卡牌抽完
  • 手上擁有的 12 魔法核(包含領主的維護魔法核)

遊戲的盤面如以下圖示:

遊戲盤面上,各種狀態的卡牌放置規則如下:

Leader Zone = リーダーゾーン

放置君主牌與領主牌區域,君主牌在遊戲期間常置右側,而左側則放置領主牌,場上只能放置一張領主牌。

Minion Zone = ミニオンゾーン

最多可以放置三張小兵卡,分為左、中、右戰線,分別可以放置一張小兵卡。

Wait Zone = WT、ウェイトゾーン

等待區分為:WT-IVWT-IIIWT-IIWT-I 區。

等待區的卡牌在自己的回合結束前,會將所有等待區的卡牌往前移動一格(也就是 WT - 1)。 舉例來說,位在 WT-III 的卡牌,回合結束時,全部移動至 WT-II。 然而,在 WT-I 的卡牌,則會移動到「板凳區」。

Standby Zone = スタンバイゾーン

原文是 standby zone(スタンバイゾーン),是待命區的意思,我譯作「板凳區」

板凳區只有放置兩張的空間, 也就是當「客滿」的時候,必須選擇要保留的兩張卡, 剩下的卡牌則移至「卡牌移除區

板凳區的牌組在每回合都可以使用,而且無需再一次支付魔法核

Life = ライフ

代表玩家的目前生命值,最大值為 20 點,歸零的瞬間就輸了

手牌就是拿在手上,就不用多做解釋了

除外ゾーン

移除卡牌的放置區域,已移除的卡牌在本局遊戲將無法再使用這些

山札ゾーン

牌庫區,每回合抽牌從這裏抽。

Core Zone = コアゾーン

魔法核的放置區。 此區域分又分為兩區,下半部是「活性區」,放置當回合「未使用的魔法核」; 上半部是「疲勞區」,放置當回合「使用過的魔法核」

Core = コア

魔法核是君主魔力的象徵。也代表他/她口袋裡有多少錢。 君主持有的魔法核放置在魔法核放置區

卡牌的擺放狀態分為:

  • 活性狀態(活性状態):縱向擺放的卡牌為活性狀態
  • 疲勞狀態(疲労状態):橫向擺放的卡牌為疲勞狀態
  • 覆蓋狀態(リバース状態):卡牌覆蓋
  • 公開狀態(オープン状態):卡牌正面朝上

Unlock = アンロック

欲解鎖手牌,必須支付活性區的魔法核,支付數量寫在該卡牌的左上角。 解鎖後的卡牌,則可以放置在場上(小兵、領主)、或是發動能力(效果卡)。

欲解鎖的卡牌必須與君主有相同屬性(顏色)

欲解鎖與君主完全不同屬性的卡牌:

  • 若為單屬性卡牌:需再多支付 1 點的魔法核,方能解鎖
  • 若為雙屬性卡牌:雙屬性卡牌是職業專用的卡牌,因此要與君主卡牌擁有的兩個屬性完全相符,才能解鎖

解鎖的卡牌可以立即使用,然而,若為效果卡、小兵卡,解鎖後也可不放置場上或發動效果,放在板凳區(有空位的話)

  1. 玩家雙方將君主牌於各自首領放置區的右側區塊,以普通模式那面朝上放置
  2. 玩家雙方將各自的牌組洗牌、覆蓋放置於牌庫區
  3. 用猜拳等方式決定先攻方
  4. 將生命值標示放置於生命刻度 20 點的位置
  5. 玩家雙方各取 5 個魔法核,放置在君主卡身上
  6. 後攻玩家從君主卡身上,將 1 個魔法核移動至WT-III的位置
  7. 雙方從各自的牌庫抽 5 張牌(不得換牌)
  8. 先攻者開始第一回合

每個玩家回合,分為五個階段:

  1. 起始階段
  2. 魔法核階段
  3. 抽牌階段
  4. 主要階段
  5. 收尾階段

Start Phase = スタートフェイズ

  • 將場上所有疲勞狀態的卡牌轉為活性狀態
  • 將場上所有覆蓋狀態的卡牌轉為公開狀態

Core Phase = コアフェイズ

  • 若君主卡上有魔法核,則移動 1 個魔法核至活性區
  • 君主卡上沒有任何魔法核的瞬間,則翻面轉為覺醒模式

`Draw Phase = ドローフェイズ

  • 從牌庫抽一張牌加入手牌,抽牌後,牌庫若無牌可抽,則敗北
  • 抽牌後,可在手牌中選一張牌,以覆蓋狀態放置於卡牌移除區,並再從牌庫區抽一張牌(每回合僅允許一次棄一抽一)

主要階段:此階段可進行以下動作,而且不限順序及次數

Section titled “主要階段:此階段可進行以下動作,而且不限順序及次數”

Main Phase = メインフェイズ

  • 手牌的解鎖:支付相對應的魔法核,解鎖卡牌。解鎖後的卡牌可直接使用。(然而,若是效果卡、小兵卡,也可選擇放置於板凳區)
  • 使用板凳區的卡牌:可以使用板凳區的卡牌。板凳區的卡牌因為已經解鎖過,所以不用再支付魔法核。
  • 卡牌攻擊:活性狀態的小兵或領主可以進行攻擊,攻擊之後,則轉為疲勞狀態。(卡牌放置在場上的回合,視為「無法行動」狀態,故無法進行攻擊)
  • 小兵的移動:活性狀態的小兵可以移動至隔壁戰線(移動後即變成疲勞狀態)
  • 使用卡牌自身的效果:場上的任何角色都可以支付需要的成本後,使用其特殊技能
  • 將場上的卡牌移除遊戲:將場上活性狀態的角色移除遊戲(包含覆蓋且活性狀態的角色)
  • 將板凳區的卡牌移除遊戲:將板凳區的卡牌移除遊戲

End Phase = エンドフェイズ

ウェイトターン経過処理の流れ

  • 放置在 WT-I 的卡牌,在收尾階段,可以全數移動至板凳區的任意位置。但若超過板凳區的放置空位數量,則將多餘的卡牌移動至卡牌移除區。放置在 WT-I 魔法核在移動回合,則直接移動到魔法核的活性區
  • 放置在 WT-IIWT-IIIWT-IV 的卡牌及魔法核,於移動回合各自移動至 WT-IWT-IIWT-III

某些卡牌有「WT+1」、「WT-1」等效果。這些效果可以針對指定卡牌做等待區的位置移動。例如:WT-I的卡牌,在WT+1之後,就會移至 WT-II;反之,WT-1 則移至板凳區。

ユニットによるアタック処理の流れ

Unit = ユニット

放置在場上的卡牌:君主、小兵、領主被稱作是「戰鬥單位」。而我方單位可以向對方單位發動攻擊。

攻擊單位可選擇要攻擊的對象(小兵或領主)進行攻擊宣告,並在攻擊之後轉成「疲勞狀態」。(當回合才放置的卡牌,放置當下會是「無法行動狀態」,所以無法進行攻擊宣告。或是,如果持有「可以對敵方君主造成傷害」能力的效果卡或 Arts 卡,此時我方君主則以戰鬥單位的身份對敵方君主進行攻擊宣告。此外,君主攻擊後,不用轉成疲勞狀態。

Target Unit = ターゲットユニット

符合以下條件的對象才能攻擊:

  • 宣告攻擊的小兵之正前方斜前方的地方小兵
  • 同一列戰線若沒有阻擋者(Blocker = ブロッカー,也就是前方的敵方小兵),則可以直接對敵方君主、領主發動攻擊

戰鬥單位進行攻擊的時候,雙方的戰鬥單位分別承受對方攻擊力 ATK 相等的傷害。造成傷害的點數,從生命值 HP 中扣除(小兵、領主);君主受到傷害的情況,就從玩家生命狀態中扣除。

承受攻擊結束之後,若此戰鬥單位還有剩餘 HP,則可以繼續留在場上。若 HP 歸零,則被擊破

Block = ブロック

當小兵或領主在 HP 歸零的瞬間,則被擊破(Block)。 被擊破的小兵會移動至指定的等待區; 而領主則將上面放置的魔法核移動到君主身上之後,移至卡牌移除區。 也就是說,被擊破的領主在此局遊戲無法再次使用。


  • v1.0 (2021/11/02): 初版中文翻譯
  • v1.1 (2021/11/03): 更新解鎖規則、class 中譯、修錯字

在 VSCode 上「直接」瀏覽 remote repo

想要直接看介紹 跳過廢話 的可以走 👉 傳送門

今天要介紹一個 VSCode 好用的插件(或許有些人已經知道了)

叫做 GitHub Repositories

其實前陣子就已經知道有這個插件, 但礙於公司 VSCode 版本太舊不支援此插件而作罷, 只能乾瞪眼流口水 ⋯⋯ 而前陣子公司開放 VSCode 升級之後, 我就可以裝這個插件啦~(撒花

因此藉這個機會來分享一下

當我們在開發的時候,總會用到第三方套件。 當文件看不懂的時候,通常會直接到那個套件的 GitHub repo 直接去看它的原始碼是怎麼寫的。 而在 GitHub 網頁上瀏覽一個 repo,無論是資料匣的來回查找、或是做全域搜尋, 都不是一個 user friendly 的事情。 此時我們會直接 clone 那個 repo 下來,在自己本機用 IDE 開啟, 以便做上述那些動作。

然而日子一久就會發現,電腦裡多了一堆 clone 別人的 repo。😂

看了真是心煩哪~

而這個插件強大的地方,就在於不用將 repo clone 下來,也可以直接在 VSCode 上面瀏覽

安裝好插件之後,點選右下角的雙箭頭圖示:

double-arrow-icon

然後在 VSCode 的正上方,會跳出視窗:

open-repo

選取 Open Repository from GitHub, 在這裡輸入想要尋找的 repo 名稱、或是貼上 repo 的網址(這裡以 jQuery 為例) ,輸入 jQuery 之後:

search-repo

Boom!! 它直接在 GitHub 上搜尋跟 jQuery 相關名稱的 repo!太神啦~

按下 enter 鍵之後,就可以在本機瀏覽 remote 端的原始碼囉~

從此以後不用再將原始碼 clone 下來,也省去了每次都要 git pull 更新原始碼的動作(到底是有多懶)

當然這不限於別人的 repo,如果是自己的 repo 的話,就如同直接在 GitHub 頁面上修改檔案, 修改完之後 commit,但不用再做 push 的動作了~

切換 branch 仍可保存未 commit 的 code

Section titled “切換 branch 仍可保存未 commit 的 code”

官方文件有提到,切換 branch 可以保留未 commit 的更動,所以我就實際來測試一下。

我先在 develop branch 的 index.html 加入一行 h1

add-code-on-develop

儲存後 checkoutmaster

checkout-master

會跳出這個提示視窗,主要是提醒沒有 commit 不會同步更新到 remote 端,直接按 continue:

add-code-on-master

master branch 的 index.html 也加入一行 h1 並儲存,切回去 develop 之後會發現,剛才的更動還存在! 同樣地,再切回去 master,剛才在 master 加上去的那一行也還在!

add-code-on-develop PS. 其實是同一張截圖

it-just-works

It just works. - Steve Jobs

這邊特別點出一些小缺點

無法啟 node server,也無法執行 npm 的任何指令。 不過也能理解啦~ 畢竟檔案都不在本機, 無法在本機啟 server 也是合情合理的。

平常開發前端的專案,eslint 跟 prettier 是很重要的兩大工具。 fix on save 是開發時不可或缺的好東西, 但是 remote repo 通常不會把 node_modulespush 上去, 用這個插件自然也不會看到node_modules, 於是 eslint 跟 prettier 組合的 fix on save 功能也理當無法使用了

這個插件真的很推薦!

這個插件目前是 v0.16.0 版本,而且還在 Preview 階段, 目前的功能已經很好用了, 希望未來可以增加更多不錯的功能~

感謝收看

關於 React 傳送門 (Portal)

React portal 是一個神奇的魔法。 官網的描述是這樣:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

簡單來說,它可以跨越 JSX 結構層級,依附在指定的地方。

以下是這個 portal 的 API 及所需參數:

ReactDOM.createPortal(child, container);

第一個參數 child:可以是 dom 元素、字串或是 fragment。(官網稱之為 renderable React child

第二個參數 container:要依附的目標(dom 元素)

最常使用的時機,就是在使用 modal(dialog)元件的時候,因為它要凌駕所有的 dom 元素之上,才能覆蓋它們。 或父層元件擁有 overflow: hiddenz-index css 屬性,但是子元件想要不受父元件屬性限制的時候。

首先,我們將會有三個元件,分別是:Target、Parent、Child。 Child 元件是 Parent 元件的子元件,而 Target 元件則是我們要利用 React Portal 依附的目標元件。

Child 元件:設定寬、高皆為 400px

src/components/Child.jsx

export default function Child() {
const STYLE = { width: 400, height: 400, background: "lightgreen" };
return <div style={STYLE}>Child element</div>;
}

接著是 Parent 元件:設定寬、高為 300px,還有 overflow: hidden。 並將 Target 元件的參照,傳進 createPortal 的第二個參數裡,也就是依附對象。

src/components/Parent.jsx

import React from "react";
import ReactDOM from "react-dom";
import Child from "./Child";
function Parent(props) {
const { forwardRef, targetEl } = props; // 將 target element 傳進來,供 createPortal 使用
const STYLE = { width: 300, height: 300, background: "lightblue", overflow: "hidden" };
return (
<div ref={forwardRef} style={STYLE} onClick={handleClick}>
Parent element
{targetEl && ReactDOM.createPortal(<Child />, targetEl)}
</div>
);
}
export default React.forwardRef((props, ref) => <Parent forwardRef={ref} {...props} />);

另外,為了要取得該元件的元素參照,所以使用了 forwardRef

最後是 Target 元件:

src/components/Target.jsx

import React from "react";
function Target(props) {
const { forwardRef } = props;
const STYLE = { width: 500, height: 500, background: "lightgrey" };
return (
<div ref={forwardRef} style={STYLE}>
Target element
</div>
);
}
export default React.forwardRef((props, ref) => <Target forwardRef={ref} {...props} />);

在 App 元件引用 Target、Parent 元件:

src/App.jsx

import { useState, useRef, useEffect } from "react";
import Target from "./components/Target";
import Parent from "./components/Parent";
function App() {
const targetElRef = useRef();
const parentElRef = useRef();
const [currentTargetEl, setCurrentTargetEl] = useState(null);
const STYLE = { display: "flex" };
useEffect(() => {
if (targetElRef.current) setCurrentTargetEl(targetElRef.current);
}, []);
return (
<div>
<div style={STYLE}>
<Target forwardRef={targetElRef} />
<Parent forwardRef={parentElRef} targetEl={currentTargetEl} />
</div>
</div>
);
}
export default App;

特別注意的是,這裡在 useEffect 裡, 當 targetElRef 拿到 Target 元件的 dom 節點的時候, 更新 currentTargetEl 的值, 以便讓 Parent 元件能夠拿到 Target 元件的節點參照。 (由於 ref 的改變不會觸發 re-render,因此必須用 state 儲存起來)

讓我們來看一下結果:

從 developer tool 可以看出,Child 元件包覆在 Target 元件裡面,而不是如 JSX 的結構包覆在 Parent 元件裡。

利用 React Portal,Child 元件成功地跨越世界線逃脫 Parent 元件的掌控啦~

就跟奇異博士一樣,從砂輪機的火花圈中走出來

dr.strange

Portal 與事件冒泡(Event Bubbling)

Section titled “Portal 與事件冒泡(Event Bubbling)”

React Portal 傳送門,將 element 傳送到我們想要的地方。 結果就是,dom 結構(DOM tree)改變了,但 JSX 的結構(React tree)卻不變。

也因此,Parent 元件這個 scope 裡面的所有東西(例如:props、function⋯⋯ 等),Child 元件仍然可以拿到。

所以「事件冒泡」當然也包含在內,我們在 Parent 元件裡,新增一個函式:handleClick

src/components/Parent.jsx

// ...
const handleClick = () => {
console.log("parent click!!");
};
//...

並在根節點,當 onClick 事件觸發的時候呼叫它:

// ...
return (
<div ref={forwardRef} style={STYLE} onClick={handleClick}>
Parent element
{targetEl && ReactDOM.createPortal(<Child />, targetEl)}
</div>
);

然後我們會發現,無論是滑鼠點擊 Parent 元件,還是 Child 元件,都會印出 parent click!!! 然而點擊 Target 元件則毫無反應。

因此我們可以證明,Child 元件跑去寄宿在 Target 元件底下, 但同時又享有 Parent 元件資產的使用權(functionprops 等)

這不就是拿美國護照、用台灣健保的概念嗎??(喂

以上就是 React Portal 的簡單介紹~

上面的 demo code,請點選這個連結

我們下次見囉~ 👋

React-Beautiful-DnD 簡單示範

React-Beautiful-DnD是一個很有人氣的 DnD 套件,除了基本的 Drag & Drop 功能之外,還有優雅的拖曳動畫、鍵盤拖曳支援等,使用起來也很容易入手。

React-Beautiful-DnD由三種要素組成,分別為:<DragDropContext /><Draggable /><Droppable />

下圖為官方的示意圖:

Structure

開發 React 的人對於<DragDropContext />應該不難理解, 看到context這個關鍵字就能領略箇中含義, <DragDropContext />所定義的是要實作拖拉功能的範圍

定義拖曳元件,將所要拖曳的元件包在<Draggable />裡面,結構如下:

import { Draggable } from "react-beautiful-dnd";
<Draggable draggableId={taskId} index={taskIndex}>
{(provided, snapshot) => {
const { draggableProps, dragHandleProps, innerRef } = provided;
const { isDragging } = snapshot;
return (
<div ref={innerRef} {...draggableProps} {...dragHandleProps} data-is-dragging={isDragging}>
<h4>I can DRAG! ✋</h4>
</div>
);
}}
</Draggable>;

要拖曳的元件不能直接作為children直接包在<Draggable />中間, 必須是一個函式(官方的教學影片說裡面是放一個renderProps),並在return的時候返回我們要拖拉的元件, 而函式的參數中,有<Draggable />的兩個物件:providedsnapshot

provided物件中包含draggablePropsdragHandlePropsinnerRef

  • innerRef:用來綁定拖曳的 DOM 元素
  • draggableProps:提供拖曳元件的props
  • dragHandleProps:則可另外定義拖拉的範圍,像是綁定一個 Icon 做這個拖曳元件的拖拉

snapshot物件中,我目前使用到的只有isDragging,在拖曳的狀態下,可以用來做樣式上的變換

定義放置的元件,如同<Draggable />一樣的結構,只是是加上放置的屬性,結構如下:

import { Droppable } from "react-beautiful-dnd";
<Droppable droppableId={columnId} type="task">
{(provided, snapshot) => {
const { droppableProps, innerRef, placeholder } = provided;
const { isDraggingOver } = snapshot;
return (
<div ref={innerRef} {...droppableProps} data-is-over={isDraggingOver}>
<h2>Drop on ME!! 🙌</h2>
{placeholder}
</div>
);
}}
</Droppable>;

provided物件中包含droppablePropsplaceholderinnerRef

  • innerRef:用來綁定可放置的 DOM 元素
  • droppableProps提供放置元件的props
  • placeholder就如其名,是一個佔位元素,在拖曳(但尚未 drop)的時候,提供一個放置的空間

snapshot物件中,我目前使用到的只有isDraggingOver,在被拖曳元件 hover 的狀態下,用來做樣式上的變換

最後附上範例連結

React-DnD 簡單示範

basic concept

Image Reference

HTML5 提供原生的HTML5 Drag & Drop API(以下簡稱 HTML5 DnD)實現 DOM 元素拖曳的功能。但是由於在 React 的生態底下,並不適合操作實體 DOM,因此 React-DnD 居於中間角色,作為雙方溝通的橋樑。對於 DOM 方監控 HTML5 DnD,對 React 方做 component 及 state 的處理。


監控 HTML5 DnD 事件,其中包含三個要素,Backends、拖曳項目(Item)、監視器(Monitors)

  • HTML5: 支援 HTML5 DnD 事件
  • Touch: 支援行動裝置的觸控螢幕拖曳
  • Test: 支援 DnD 的互動測試
  • 拖曳元件的身分識別
  • 定義拖曳元素可以放置在哪裡
  • 其中攜帶拖曳元件的相關資訊,提供給放置元素進行放置動作的資料操作
  • 提供 React 元件(component)來自於(實體)DOM 端的 DnD 事件
  • 監視器會將收集到來自於 DOM 的事件注入至context

Collectors 收集 React 所需要資訊,包括收集函式(Collector Functions)、拖曳元件(Drag Sources)、放置(目標)元件(Drop Target)

  • 收集函式將監視器(monitor)得到的資訊注入 React 元件的props當中
  • 綁定可拖曳的元件(component)
  • 攜帶拖曳元件的相關資訊,提供給放置元件
  • 定義可接受放置的拖曳元件

定義拖曳(來源)元件(Drag Sources)及放置(目標)元件(Drop Target)的範圍

import Backend from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
export default class YourApp {
render() {
return <DndProvider backend={Backend}>/* Your Drag-and-Drop Application */</DndProvider>;
}
}

拖曳(來源)元件(Drag Sources)

Section titled “拖曳(來源)元件(Drag Sources)”

綁定拖曳的元件、定義拖曳的行為

import { useDrag } from "react-dnd";
function DraggableComponent(props) {
const [collectedDragProps, drag, preview] = useDrag({
// 必填欄位
item: { type: "task", id: 1 },
// 選填欄位
begin: (monitor) => {}, // 開始拖曳的時候要做什麼事
end: (item, monitor) => {},
isDragging: (monitor) => {}, // return isDragging 的條件
canDrag: (monitor) => {}, // return canDrag 的條件
collect: (monitor) => ({
canDrag: Boolean(monitor.canDrag()), // 將上述的屬性值收集起來
isDragging: Boolean(monitor.isDragging()),
didDrop: Boolean(monitor.didDrop()),
}),
});
const { canDrag, isDragging, didDrop } = collectedDragProps; // 所有收集起來的值會 collectedDragProps 裡面
return <div ref={drag}>...</div>;
}

綁定放置的元件、定義放置的行為

import { useDrop } from "react-dnd";
function myDropTarget(props) {
const [collectedDropProps, drop] = useDrop({
// 必填欄位
accept: "task", // 接受元件的類別,可以是一個陣列,放置多個接受的類別, 例如:['task', 'story']
// 選填欄位
drop: (item, monitor) => {}, // 拖曳元件放開的時候要做的事
hover: (item, monitor) => {}, // 拖曳元件hover的時候要做的事
canDrop: (monitor) => {}, // 定義是否可以拖曳的條件,return一個布林值
collect: (monitor) => ({
isOver: Boolean(monitor.isOver()), // 將上述的屬性值收集起來
canDrop: Boolean(monitor.canDrop()),
}),
});
const { isOver, canDrop } = collectedDropProps; // 所有收集起來的值會 collectedDropProps 裡面
return <div ref={drop}>Drop Target</div>;
}

最後附上範例連結

[筆記] 阿德勒勇氣整理術:丸山的居家打掃排程

當阿德勒心理學與整理術碰在一起,會是什麼樣子呢?

這是丸山郁美所寫的一本書,書名為:「阿德勒勇氣整理術」。

偶然在家裡附近的早餐店發現了這本書。

因為對整理術有一點涉略,

再加上前陣子才看完闡述阿德勒心理學的人氣書:「被討厭的勇氣」,

所以引起了我的興趣。

丸山會如何將心理學用在整理術上呢?

因為在吃早餐的當下實在不夠時間,

所以跟老闆借回家。

老闆人真好!

實踐斷捨離後,心理狀態的改變

Section titled “實踐斷捨離後,心理狀態的改變”

其實這本書我很快就看完了。

為什麼呢?

因為拜讀過前陣子很紅的極簡作家佐佐木典士的著作「我決定簡單的生活」,

對於佐佐木的減物法則很有共鳴,也實踐了將近 6 至 7 成。

因此早已克服了大部分捨不得放手的心理狀態。

所以丸山利用阿德勒心理學去克服心理恐懼這方面我大概不需要再多做著墨 😂

謎:你不是來寫這本書的心得文嗎?重點卻帶過是不是想要偷懶啊?

丸山將阿德勒心理學融入整個整理的過程。

雖然整理這件事,看起來像是 100% 物質面的事情,

但在進行的過程中,心理狀態卻是千頭萬緒:

物品放置位置的抉擇、丟棄物品的猶豫再三、花費大量時間整理卻沒有成效、整理乾淨卻又在一夕之間恢復原狀的挫折感 ⋯⋯ 等等

因此克服心理層面是非常重要的一環。

相對於佐佐木這樣偏向「苦行僧」類型的極簡主義者,

丸山強調的是「打造居家舒適的空間」

不強迫丟東西,而是思考生活中

哪些是必要的、哪些是想要的

哪些東西讓我們覺得舒服

這個概念跟紅到歐美的整理大師「近藤麻理惠」所提倡的「怦然心動」(Spark joy)心境很類似 1

就我自己的分類而言,她們算是「中庸」的極簡主義者(或稱為溫和派、鴿派 😆)

在本書中讓我感到興趣的,是丸山自製的「年度打掃行事曆」。 這個表格如下圖:

table

她將家裡所有需要定期打掃的項目全部列出來(最左邊一欄),

右邊分為 1 月至 12 月

白色格子是當月要去要打掃的項目;

灰色的部分是不用清掃也無所謂的地方。

每個月檢視一次這張表,並在白色格子裡面紀錄打掃這個項目的日期。

這張表還有一個小細節,

丸山提到:

我每年只會在油污容易清除的八月進行一次抽油煙機大掃除,其他月份就做一些更換濾網之類簡單到掃而已。

清理冰箱方面,我每個月也只會清潔製冰盒的部分。至於把所有食品取出,清洗層架及抽屜的作業,只有在房間的室溫降到和冰箱相去不遠的二月才會進行。

因為冰箱與室內溫度不大,這麼一來,既不會影響食品的新鮮度,也能節省地電費。

很多人都會在年底進行大掃除的時候擦窗戶,但我擦窗戶的的月份是五月及十一月。

因為即使年底擦乾淨了,還是很快就會被花粉弄髒。所以我把擦窗戶的時間,訂在花粉季結束,以及十一月颱風季與秋天的綿綿陰雨結束,窗戶被風雨徹底弄髒之後。

她在某些清掃項目的時間決定上特別思考過,

而不是依照一般既定印象的大掃除習慣。

這跟我將每月維修項目時間錯開是類似的概念,

甚至連季節、天氣因素都考慮進去了,

真的很細心。

不過這份表格也不是一個標準,使用者可以隨著自己的喜好調整。

例如台灣沒有花粉季、颱風季也跟日本不太一樣,

隨著不同的地區做適當地修改,所以擦窗戶的時間可以更自由一點。

(我自己是每個月擦一次窗戶啦 😏)

有興趣的同學

可以參考我寫的關於維修日的文章

至於丸山的阿德勒整理術的精髓

有興趣的朋友可以閱讀這本書喔(喂

今天的分享就到這裡

我們下次見 👋

  1. 如果對「近藤麻理惠」有興趣的話,可以參考她的著作:「怦然心動的人生整理魔法

交錯的維修日:減輕日常維護工作的心理負擔

日常的維護工作既費時又繁瑣,但是不做又不行,

這是每個人一定會遇到的問題,也會造成心理上的負擔。

最有生產力的一年」這本書的作者:克里斯・貝利在書中提到,

他不想花時間在這些麻煩事上,因為這些事情雖然低回報,卻不得不做,

這些事很容易引發拖延症(Procrastination)。

克里斯提出了稱為「維修日」的方法,也就是將這些維護工作擱置,

並在某一天一次處理完。他在平日將所有低回報維護任務都先記錄下來(記得不要去做),

然後在週日一次處理。

當然了,克里斯將維修日訂在週日,

並不代表一定是週日(也不一定是每週一次),可以是任何我們自己選擇的一天。

至於維護工作是指哪些?克里斯在書中列出了他完整的維修日待辦事項清單:

  • 採買雜貨
  • 打掃房子和辦公室
  • 規劃飲食和健身計畫
  • 修剪鬢毛、刮鬍子
  • 洗衣服
  • 準備一整個星期的午餐,分裝至微波保鮮盒裡
  • 澆花
  • 閱讀我一整個星期收藏的文章
  • 審視我的各項計畫,並確定接下來的步驟
  • 查看我的「等待清單」
  • 決定未來一週想要達成的三項目標
  • 清楚我的全部的收件匣
  • 檢視我的「熱點」
  • 檢視我的「成就清單」

這份待辦事項的內容因人而異,沒有蓄鬍的人大概無法一週刮鬍子吧 😂

雖然在讀克里斯的這本書之前,我就一直有在用手機「提醒事項」APP 提醒自己做一些事,

但在看過他的維修日的概念之後,也因此做了些調整。

我沒有將所有的事集中在一週的某一天來做,

而是依照不同的事項所需要的頻率去設定提醒事項。

我的維修待辦事項(這裡不稱為維修,畢竟我沒有集中成一天)分為三大類,分別是:

  • 每日提醒(DailyLoop)
  • 週循環維護(WeeklyLoop)
  • 月循環維護(MonthlyLoop)

順帶一提,命名方式隨個人喜好,我也很滿意這個名稱。😎

list

首先是每日提醒

daily

也許你會問,剛才不是說維修日要集中執行嗎?怎麼還有每日提醒?

每日提醒主要是培養「習慣力」,將我們想要培養的好習慣列出來,

並且確實的按照規定的時間去執行,這裡要注意的是對自己誠實,沒有做就不要打勾。

每個項目的提醒時間底下,還可以列出需要花多少時間做這個項目(自己預估),

如此在有空閒時,不管是多短暫,例如等公車的 5 分鐘,

只要打開提醒事項 APP,就可以一目了然當下適合做什麼樣的項目。(當然僅適合手機上能夠完成的事項)

第二類是週循環的維護項目

weekly

  • 更換睡衣 - 每週
  • 擦家中地板 - 每週
  • 剪指甲 - 每 2 週

第三類,月循環項目

monthly

  • 擦窗戶 - 每月第 1 週週日
  • 皮件保養 - 每月第 2 週週日
  • 清洗鞋墊 - 每月第 3 週週日
  • 百葉窗簾清潔 - 每月第 4 週週日
  • 更換床單、枕套 - 每月
  • 剪髮 - 每 2 個月
  • 整理發票(兌獎)- 每 2 個月
  • 預約牙醫洗牙 - 半年
  • 蝦皮賣場價格調整 - 每月

前面四項都是想要每個月保養一次的項目,

但如果像是克里斯那樣,將所有項目塞在一整天裡面,實在是太累了,

而且我不想花太多週末寶貴的時間。

因此我將這四個每月循環的項目錯開,「第一週擦窗戶」、「第二週做皮件保養」、⋯⋯,以此類推。

如此一來,月循環保養的負擔將會減輕許多。其他的項目,

因為因為不需要花太多時間(不超過 10 分鐘),

像是一些衛生方面考量要替換的東西,我就沒有特別錯開時間了。

至於每半年一次的洗牙,是健保的規定。

以上是關於日常維護項目的一些分享,我們下次再見。👋

給物品找新主人:舊物的處理管道

自從我在露天拍賣賣出第一個自己的東西之後,

就踏上了「自家經營二手商店」的不歸路(誤)

從小學開始,就養成了整理自己東西的習慣

間隔時間印象中是一年一次,然後都是在時間最多的暑假

每一次的大整理都是除了回顧自己擁有哪些東西之外,

「丟棄」這個動作更是心理大考驗

自從我讀了日本極簡大師佐佐木典士的著作「我決定簡單的生活」之後,

就踏上了處理雜物的旅程

處理雜物最快也最簡單的方式,就是直接丟掉(或回收)

但我想一般人不會這麼做的

除非你是揮霍無度、或經濟狀況很好的人

把可以用的東西直接扔掉,不但浪費地球資源、也跟你的良心過不去

所以送人及出售轉讓就變成了「沒有罪惡感」的方式了

佐佐木在書中提到了幾個他處理雜物的方式:

第一是透過拍賣網站 mercari(メルカリ),它是日本新興(相較於樂天 Rakuten、日本 Yahoo 拍賣)的拍賣平台,在美國也有上市,台灣沒有

第二是拍賣代售平台「QuickDo」,這個我覺得很不錯的地方在於代售的功能

意思就是你不用自己經營拍賣,他們會派人到府把你要上架拍賣的東西收走

然後你就可以 ⋯⋯

什麼事都不用做,等著東西被買走後,錢進帳

上架過程、跟買家溝通、包裝出貨,

麻煩的手續完全還有他們包辦

多麼地愜意啊啊啊!!

很可惜就我所知,台灣沒有類似這樣的服務

好吧,那只有自己來了

也就是說,佐佐木的那些方法只能當作參考

我們必須尋找自己當地才有的平台

誠如本文一開始提到,我第一個經營的個人賣場是在露天上面

後來發現越來越多人使用蝦皮拍賣這個平台

再加上蝦皮推出了便利超商店到店寄送免運費一系列的活動

開啟了「免運紀元」,其他家拍賣平台也趕緊跟進加入「免運大作戰」

那是大約 2016 年的事(有錯請指正)

而我也在那時毅然決定從經營大約半年多的露天拍賣跳槽至蝦皮拍賣

好了 這個前情提要有點冗長了

進入正題吧

以我目前使用兩種平台來處理二手物品:

  • 第一就是前面提過的蝦皮
  • 第二就是 PTT

我在蝦皮至今為止也經營了約兩年的時間

蝦皮只是我目前個人的喜好

之後跳槽別家也說不定

台灣目前的拍賣平台有很多選擇:

PS. 可能還有些我不知道的請各位補充 🙏

可以依據個人喜好及習慣選擇拍賣平台

拍賣平台的優勢就是什麼都有、什麼都可賣、什麼都不奇怪(當然不要賣違法的東西就是)

但是也因為東西種類繁雜,如果是冷門的東西曝光率就會被稀釋

假設我們想要賣動漫周邊商品、模型

在買動漫上架,能見度或許會高出許多(不負責任分析(喂

針對某項類別的物品

可以尋找有分類的平台

這也就是我用 PTT 的原因

有分類的平台,就能提升曝光率

像是我若要賣日文書(或小說),就會在 NihonBook 版發文

要出售模型,可以去 model 版、或是 PVC-GK 版

賣輕小說,就在 LightNovel 版發文

曾經也買過一把吉他,在 guitar 發文

Facebook 也有類似這樣的社團 像是「二手 網球拍」交流這個社團

我們可以在這裡賣很少在用的球拍

而在這個社團裡的人,都是有可能會購買球拍的人(相較於其他平台)

利用拍賣平台將舊物脫手,是一個很好的管道

它有很多優點,同樣也有缺點:

  • 脫手舊物的同時,還可以拿回一點折舊金
  • 利用拍賣上架的過程(包括拍照、寫文案等),檢視自己擁有的物品,並做購物心理的分析(不明白我在說什麼?就是檢討自己為什麼亂買東西啦~)
  • 拍照上架、寫文案
  • 商品的管理
  • 商品要擺放在哪裡
  • 商品包裝、自己去出貨
  • 遇到奇怪的買家要如何應對
  • 買家要看更清楚的照片(例如:不同角度) ⋯⋯

應該還有很多

看到這裡或許會有人開始疑惑,這麼多缺點還介紹什麼拍賣

現在明白了,不浪費地球資源的代價有多大了

這樣往後就不要看到便宜或是喜歡就急著掏腰包

PS. 而我到現在,還在為自己當初亂買的東西消業障(賣東西 T_T

我相信會點進來看這篇的人

多少都覺得自己身邊的雜物有點多

想要整理卻有跨不出第一步

或許你看到這麼多的缺點都卻步了

覺得好麻煩

相信我,我也這麼覺得

雖然這三年多不斷地在賣東西、少買東西

但是這種中間不斷地在內心拉扯

你可能問,在拉扯什麼?

嫌麻煩不想做啦~

懶得拍照、懶得訂價格、懶得寫文案

然後就是各種拖延症爆發

好,你不要這麼麻煩

用送人的好了

自己很少用的都給你直接送人也無所謂

如果你的心理層面進入了這個層次

那就用贈送的吧!

如果你捨不得送人

我想分享幾句佐佐木書中的幾句話:

不要一直想著購買時的價格

「當初買多少錢,所以我要賣多少才不會虧太多」

我承認我也會有這種想法

誰不想要虧損少一點

但是舊物就是會貶值的啊

丟掉「回本」的念頭:承認虧損,即早放手

除非你要賣出的東西是會升值

而且比當時原價還高的那種

例如未拆封的第一代 iPhone 之類的

但我想這類東西你也不會想要脫手吧

說到回本,除非你當初買來就是看中它會漲價

拿著東西不是拿來用的,而是拿來投資的

承認失敗,當做花錢買經驗

這就是之前提過的,承認自己當時失心瘋

一時衝動買了卻很少拿來用的東西

告訴自己:就算虧損,也要得到經驗

下次買東西的時候三思,問自己是否真的需要,還是只是想要?

以上是心理層面的準備

我平常使用贈送的方式

是在 PTT 的 Give 版(贈送版)發文

這個版的發文頻率非常高

每天都有在送東西

你可以在上面發文,貼個圖片連結、物品說明

然後等有人來跟你聯絡取物

就這樣!

發文頻率之高,瞬間東西被掃光的情況很常見

贈物比起在拍賣上架、等人下標,速度快好幾倍

但就是免費送人啦~

而且版友大部分都是愛物惜物的人

相信你的舊物也不會直接進垃圾桶,可以安心啦~

另外,在 Facebook 也有類似的贈物社團、換物的社團

我曾經加入過這個社團:不要再買了!免費的幻物與幻務

裡面是用物品交換的方式

這樣也可以減輕一點放不下虧損的心理

又可以換到想要的東西

不對,是需要的東西

今天的分享就到這裡

我們下次見 👋

低成本的紙本數位化

紙本書數位化(圈內人稱為「自炊」)一定會用到的工具,那就是掃瞄器了

其中又有所謂的掃描神器之稱的「富士通 ScanSnap iX500」掃瞄器

我閱讀的下列這兩本書中,作者都有提到這台掃描器

拆掉一本書:把房間、書櫃、辦公桌清空,神奇的自炊紙整理術

我決定簡單的生活:從斷捨離到極簡主義,丟東西後改變我的 12 件事!

但是專業的掃描器價格並不便宜,至少也要 1 萬元台幣以上

此外如果沒有搭配專業的裁紙機的話(又是另一筆花費)

掃瞄器的高速掃描功能仍沒有辦法完全發揮

對於資料數量不多的人來說

實在不是一個輕易入門的選擇

對我而言也是如此

這次主要數位化的資料大宗並不是市面上販售的書籍

而是像是上課筆記、講義、日記等

因此在拆解方面難度就簡單許多

廢話不多說

這就開始介紹吧!

tools

  • 美工刀
  • 切割墊
  • 剪刀
  • 懶人夾
  • 智慧型手機
  • 掃描 APP

本次示範使用的 APP 是 Evernote 出品的「Scannable」,目前只有 iOS 的版本:

Evernote Scannable

App Store 上面也有很多的掃描 APP,Play 商店也是,像是微軟所推出的 Office Lens 就在兩大 APP 商店都有:

Microsoft Office Lens - Google Play 應用程式

Office Lens on the App Store - iTunes - Apple

在此就不多做介紹了

為了使數位化作業進行順暢

首先要先把書給解體

拆成一頁一頁之後在掃描

平整的紙才有最好的掃描品質

今天要數位化的是這本書

sample book

拆書方式是來自網路上部落格的文章

如下圖所示,這本是很常見的膠裝固頁方式

image 7

用手就能夠輕易地將封面及內頁分離,需要破壞書的心裡準備 😆

image 8

這本書的書背沒有線裝,因此直接一頁撕下來即可

當然也可以用前述部落格文章的方式

使用美工刀割下來

image 9

分解完成

分解完成之後,就開始掃描吧!

手機配件店很常見的「懶人夾」,這時候就派上用場了 😂

image 1

懶人夾角度的話,盡量是與桌面平行

距離的話,當然是越近越清楚

調整距離到紙佔了掃描畫面的 7~8 成左右

如下圖所示:

image 2

此外,掃面的時候建議文件底下的背景顏色盡量是反差色

這樣 Scannable 比較容易偵測文件的邊界

讓掃描作業更加順利

所以我在下面墊了綠色的切割墊

全部掃描完畢之後,可以選擇透過電子郵件寄送或是「共用」

image 3

  1. 可以修改檔案名稱
  2. 可以選擇存成圖片檔或是 PDF 檔
  3. 可以直接透過郵件傳送數位化的文件(但是要注意文件附件容量的限制)
  4. 共用內容

image 4

由於我是使用 MAC 電腦,所以可以直接用 Airdrop 的方式直接將檔案傳送到筆電裡

其他還有傳送到 LINE、MESSENGER 等方式

就看個人的習慣和選擇了

這樣就完成啦!💪

數位化後的文件,既不佔體積也沒有重量

既能保存文件又能夠隨時分享給別人

分享容易或許會擔心有檔案不想給某些人看到

這時候用 PDF 加密就行了,安全性也不輸紙本呢

若放在雲端硬碟上,就可以跨裝置存取

真的很方便啊!

低成本數位化教學簡易教學就到這裡

BYE BYE👋

零錢的斷捨離

原本隨身的物品有這些:

  • 左邊的手機(iPhone SE)
  • 零錢包
  • Bellroy 鑰匙包
  • Bellroy 皮夾

my wallet

PS. 依順時針順序

但是最近又想要再做更進一步地斷捨離

這次的目標是:零錢

雖然我的零錢包也不大,也不會時常塞滿零錢

但是如果可以再減少一樣隨身物的話那是再好不過了

回想起前幾天友人 E 跟我說,他身上的零錢都會在經過捷運站的時候,將所有身上的零錢儲值到一卡通裡

這似乎是個不錯的方法

因為友人 E 手上的皮夾是 Bellroy Micro Sleeve

所以零錢大概只能塞口袋

我手上這款則有一個小內袋專門設計用來放零錢的

於是就決定將零錢包的零錢放入這個內袋裡

這個內袋先前一直沒在使用的原因

是因為才放沒幾個銅板進去就覺得皮夾變厚重,而且也不好拿取

所以沒有太大的使用意願

但是這次卻正好能夠利用這個缺點來實踐零錢斷捨離去計劃

正因為只要放點零錢就會覺得很累贅(因為我都將皮夾放在口袋

所以就可以隨時提醒自己將這些零錢換成電子錢包(儲值)

嘿!這方法真不錯!

我連用手機設定提醒事項這個步驟都省了!

究竟成效如何?

就有待時間的考驗了

感謝收看 🙏

備註:

  1. Bellroy Micro Sleeve 的介紹
  2. 我的皮夾是這款 Bellroy Note Sleeve 的舊款,網站上只剩新款了,但是設計的差別並不大,紙鈔的後面都有一個小內袋
  3. 鑰匙包 Bellroy Key Cover,我很喜歡這個品牌的設計語言
  4. 手機殻是 Apple 官方的皮套(馬鞍棕色)
  5. 零錢包是我在日本一家販賣電車無人認領失物的小店買的,根據上面寫的是加拿大鹿皮的材質,¥300 購入

日本 BOSE 耳機 海外維修攻略

今天想要來分享一下 Bose 耳機的海外保固的維修流程

bose qc20i

  • 耳機型號:Bose QuietComfort-20i
  • 購買日期:2017/01/03
  • 購買地點:日本>名古屋>松坂屋百貨>BOSE 專櫃
  • 故障狀況:右耳無聲音
  • 故障日期:2017/03/31

1 月初才買的降噪耳機

3/31 週五下班時搭捷運的時候都很正常

下車後收進口袋,再拿出來聽的時候就覺得右耳發現沒有聲音了

以為是聽錯了,換了幾首歌,才意識到右耳真的沒聲音 ⋯⋯

耳機線喬了一下,發現是線控端下方、耳機線連接鋰電池的位置

在某個角度下,右耳就又有聲音了

很明顯地是這個地方接觸不良

於是我心中算了一下

用這副耳機也才三個月的時間而已!

心裡想:「當初就抱持著買國外水貨,運氣差壞掉無法保固就當作是教訓的心理準備,卻萬萬沒有想到日本國內保固一年,就在第三個月的時候壞掉了!」

頓時感到心情很糟 ⋯⋯

拿去光華三創百貨的 BOSE 專櫃詢問

想說:「就算在台灣沒有保固,付錢修理總行吧?」

結果得到的店員的回應是:「這款產品(QC-20i)比較特殊,客人如果在保固內有非人為送壞的話我們是直接換一個新的耳機給客人,沒有在維修的。」

⋯⋯⋯⋯

什麼?!意思不就是這耳機跟 APPLE PENCIL 一樣嗎?!

這運氣也太差了吧(;´Д`A

這麼多產品,就正好這款不能維修

我差點就衝動地掏出我的魔法卡信用卡再買一副 ⋯⋯

搭車通勤時降噪耳機是我的好夥伴啊(>﹏<)

陪同的友人 E 就說:「那問問看有沒有專門修耳機的店家,反正水貨也等同於過保固。」

網路上查了一下,發現一家叫做「東京快遞」的耳機店有在維修耳機

於是就打電話過去問

跟店員說明了一下耳機故障的狀況

店員說:「您壞掉的部分就您的敘述是在線材連接鋰電池、也就是線材尾端的話我們可能就無法維修,但是若是壞在線材中央的話就可以維修。這款耳機如果是海外買的話,您可能要聯絡看看當時購買的店家如何送修。」

我:「⋯⋯⋯⋯」

我要怎麼聯絡日本百貨公司的專櫃啊?!打國際電話嗎?

不要阻止我!我要刷卡!!(怒)

還好,友人 E 提出了一個可行的方案,魔法卡因此沒有派上用場(喂

我們查了一下 BOSE 的送修方法

有人在網誌分享了他們的送修流程:

愛用の BOSE イヤホンが壊れたので格安で新品交換してもらった手順 - 大学生論

PS. 是日本人寫的,所以是日文

於是我就照著他的方法去做

進入 BOSE 的官網,在以下連接填寫聯絡資料及耳機故障狀況:お問い合わせフォーム

包括:標題、產品序號、E-Mail、描述耳機狀況、日本住址、姓名、聯絡電話等

耳機狀況我輸入的內容如下:

今年の 1 月に買った Bose QC 20i は急に片耳(右)が聞こえなくなりました。

商品の箱、説明書やアフターサービス申込シールなどを持っていますが、レシートがなんとなく見つけられませんでした。

修理の手続きについて教えていただけませんか?よろしくお願いします。

中文翻譯大致如下:

今年 1 月買的 Bose QC 20i 突然右耳沒有聲音了。

商品的盒子、說明書及售後服務服務貼紙都還在,

但是發票不知為什麼不見了。

請問我要怎麼送修呢?謝謝。

基本上盒裝、保固貼紙之類的當然留著

我依據上面日本人分享的維修經驗,就說「發票弄丟了」,雖然事實上不是這樣

因為這耳機是拜託同事 R 買的,發票他早就丟了 ⋯

至於日本的住址,我是填寫友人 E 的「TENSO 轉送地址」

關於這部分之後再做解釋

過了一天,BOSE 維修部門來信了 內容如下:

XXX 様日ごろ、弊社製品をご愛用いただき誠にありがとうございます。

この度はご不便をおかけいたしております。

誠におそれ入りますが、お問い合わせいただきました内容から、 弊社サービスセンターにて製品を拝見させていただきたく存じます。

お手数をおかけし申し訳ございませんが、修理ご依頼品につきましては、

レシートや納品書などのご購入証明書

(ご購入店、ご購入年月日、ご購入製品名がわかる書類)をそえて、弊社サービスセンターまでご発送いただきますようお願いいたします。

(お手元にない場合はアフターサポートシールを添付の上お送りください)

保証期間内の製品につきましては、弊社にて現品を拝見後、無償対応の可否について判定させていただいております。

おそれ入りますが、あらかじめご了承いただきますようお願いいたします。

<<修理ご依頼品送付先>>

〒 206-0035  東京都多摩市唐木田 1-53-9

唐木田センタービルボーズ サービスセンター TEL:0570-080-021

配送伝票の備考欄に、「受付番号*****M」をお書き添えいただき、下記必要事項をご記入いただいたメモ紙を同梱のうえ、

ご発送くださいますようお願い申し上げます。

【メモ紙に記入いただく必要事項】

・お名前 ・電話番号 ・返却先住所

・症状ご不明な点がございましたら、お問い合わせください。

————————————————————————

BOSE カスタマーサービス

TEL:0570-080-021(平日 9:30 ~ 17:00  土・日・祝日を除く)

————————————————————————

※ メールでのお問い合わせに関しましては、

ご返答までお時間をいただく場合がございます。

中文翻譯:

XXX 您好

非常感謝您使用敝社的產品。

本次造成您的不便還請您多多包涵。

關於您所提出的狀況,首先要讓敝社的售後服務中心看過您的耳機。

關於送修,麻煩您將欲送修的產品,附上發票或收據等購買證明書(記載著購買店家、購買日期、購買產品型號等資訊的書面資料),寄送至弊社售後服務中心。

(如果手邊沒有相關證明的話,請附上保固貼紙)

繁瑣的送修程序,若造成不便,敬請見諒。

關於保固期間內的產品,讓弊社判斷是否為免費之維修服務,在此先告知您。

<<維修產品的送修地址>>

〒 206-0035 東京都多摩市唐木天 1-53-9 唐木田中心大樓 BOSE 售後服務中心

電話:0570-080-021

請在寄貨單的備註欄裡註明「維修單號*****M」,附上一張 Memo 紙夾帶在包裹內,並記載以下事項:

名字

電話號碼

送返地址

故障點

若有疑問,請與我們聯絡。

————————————————————————

BOSE 售後服務中心

電話:0570-080-021(平日:9:30 ~ 17:00 週末、國定假日除外)

————————————————————————

於是,我就依照信件所寫的將耳機、保固貼紙及 Memo 紙準備好

等去日本旅遊的時候帶過去寄送

送返地址則是填寫友人 E 的 TENSO 轉送地址

TENSO 是日本一家專門做日本海外轉送的公司

旅遊期間於日本郵局寄送

包裹重量 69.5 克

郵寄費用 140 日圓

05/13:收到 TENSO 轉送公司的集貨通知

Section titled “05/13:收到 TENSO 轉送公司的集貨通知”

等了將近兩個禮拜,以為沒有下文了

結果收到了 TENSO 收到 BOSE 售後服務中心的包裹

TENSO 正好在做 EMS 的促銷,只要 1750 日圓,配送日是兩天

當然毫不猶豫選了 EMS

因為我已經等不及拿到手啦!

配送日是兩天,應該今天要收到啊

怎麼還沒有消息哩?

約莫下午兩點,友人 E 說我的耳機已經送到他家了!

(因為 TENSO 是他的帳號,所以會寄到他家的地址)

下班馬上衝去他家拿!

開箱囉~

open box

看那如此精美的 EMS 郵包啊~

open box

維修金額:免費!

receipt

終於收到了,好感動 QQ

話說裡面又附上一組 S 號及 L 號的耳塞

這樣我就有兩組了 ⋯⋯

我好興奮啊~我好興奮啊~

維修流程到這裡告一段落

過程非常地耗時間

海外若要送修日本買的 BOSE 耳機

在此整理出必要的條件:

  • 懂得日文(說不定用英文寫信給 BOSE 公司也行得通)
  • 要有人去日本一趟(不然就是得寄海外包裹)
  • 擁有 TENSO 帳號,作為送返的地址(不確定 BOSE 公司是否接受海外住址,畢竟日本 BOSE 並沒有國際保固)
  • 要有耐心(等待的時間頗漫長的)

我也因此學到了教訓

買水貨所必須承擔的風險 ⋯⋯

還有小心使用這副新耳機

給遇到同樣問題的各位做參考

還沒有購入 BOSE 耳機的朋友

我的建議是:購買台灣公司貨

或是美國 BOSE 產品

因為美國的 BOSE 似乎有全球保固(有錯請指正)

保固無價

感謝各位的收看