James Universe

  • AI 價值革命2026.07.09 · 在沒有答案的世界裡,如何不盲目地活著
  • AI 主觀與客觀探討2026.07.08 · 客觀不是脫離主觀,而是主觀之間達成的穩定共識
  • other|Deno2025.10.28 · Deno 環境使用ES modules
  • HTML|HTMl elements2025.10.19 · 1. `<p>`:段落
  • Git Page2025.10.09 · 將dist 資料夾內的東西推至 指定github 分支
  • node|取得指令內的參數2025.09.27 · 取得指令內的參數 相關筆記與範例整理。
  • plugin|SpeedMeasurePlugin2025.08.13 · 顯示各項目的編譯速度
  • JS|web workers2025.07.05 · 透過 worker 建構子便可以產生 worker 物件,並且執行 JavaScript 檔案。在 worker 中的 JavaScript…
  • Vue SFC|在元件內寫一個字定義directive2025.06.15 · 在元件內寫一個字定義directive 相關筆記與範例整理。
  • iOS|Universal Link2025.05.03 · 點選連結直接開啟 APP、不經過瀏覽器,需設定 apple-app-site-association 檔案。
  • James Universe Blog2025.05 · 以宇宙為主題、星星即內容的個人部落格。
  • HTML attribute|contenteditable2025.03.10 · 可以使 DIV, P, UL 的文字內容可編輯
  • iOS|Deep Link2025.03.05 · iOS9 以前使用 URL Scheme 開啟 APP 的方式,未安裝時會出現轉跳錯誤訊息。
  • Rendering a Black Hole2025.03 · 用 Three.js 打造吸積盤、光子環與重力透鏡的即時渲染。
  • The Shape of Time2025.02 · 關於時間、記憶與敘事結構的一段思考。
  • Islands & Partial Hydration2025.01 · Astro 的 island 架構如何在預設零 JS 下做互動。
  • plugin|Writing a Plugin2024.12.04 · webpack dev server 自定義訊息顯示
  • JS|Intersection Observer API2024.12.01 · 用非同步的方式監聽元素是否進入畫面,常用於圖片延遲載入、無限捲動與進場動畫。
  • Shader Field Notes2024.12 · 累積的一些片段著色器技巧與筆記。
  • Library|前端好用工具收藏2024.11.25 · 收藏一些前端好用的函式庫,涵蓋地圖、圖表、時間軸、動畫與視覺特效。
  • HTML tag|details & summary2024.11.04 · details & summary 相關筆記與範例整理。
  • Type-Safe APIs2024.11 · 用型別把 API 契約鎖在編譯期。
  • PNPM2024.10.09 · performant npm
  • On Simplicity2024.10 · 簡單不是少,而是剛好。
  • vs code|JSDoc block tags2024.09.24 · JSDoc block tags 相關筆記與範例整理。
  • Lib|Protocol Buffers 的使用場景2024.09.07 · 若在純粹的 Web 應用下,大多數情況,我們不需要處理二進位資料,或是需要非常精準的資料格式,也不會進行單筆高流量的資料交換,所以使用 JS…
  • web|beforeunload2024.08.02 · 當window 準備釋放它的資源時,該事件被觸發。此時文件仍然可見,且事件仍可被取消的。
  • Lib|protobuf2024.06.11 · Protocol Buffers are a language-neutral, platform-neutral, extensible …
  • 語言代碼 (Language code)2024.06.09 · zh-TW
  • node|packageManager2024.06.09 · node.js v16.9.0 的實驗功能, 需搭配 Corepack 使用
  • Vue SFC|在SFC內寫一個元件2024.05.16 · 在SFC內寫一個元件 相關筆記與範例整理。
  • JS|Object 屬性的排序規則2024.05.10 · ES6 之後,物件屬性的走訪順序有明確規則:整數鍵由小到大、字串鍵依插入順序、Symbol 最後。
  • JS|previousSibling2024.03.29 · 取得相同父元素下,前一個子元素節點(Node)
  • node|shelljs2024.03.14 · 移除檔案
  • OAuth 2.0|Bearer Authentication2024.03.07 · RFC 6750
  • NPM|module mode2024.02.13 · package.json
  • SOLID五大原則|開閉原則 OCP (The Open/Closed Principle)2023.12.15 · 開閉原則OCP(開放/封閉原則)的描述是:
  • JS|insertBefore2023.11.25 · 同一個節點並不會同時出現在兩個地方。所以當節點已經有父節點,它會先被移除,然後被插入在新的位置上。
  • money format|regex2023.10.05 · regex 相關筆記與範例整理。
  • cookies|Third-party cookies2023.08.30 · https://www.chromium.org/updates/same-site
  • plugin|FriendlyErrorsWebpackPlugin2023.08.23 · vue cli 顯示自訂訊息
  • node|https2023.08.17 · 1. 取得OpenSSL
  • JS|Generators2023.05.14 · function* 定義的產生器函式可以暫停與恢復執行,透過 next() 一次取一個值。
  • money format|Intl.NumberFormat2023.05.03 · 用瀏覽器內建的 Intl.NumberFormat 依語系格式化數字,做出千分位、貨幣等呈現。
  • JS|void2023.04.26 · 語法
  • TypeScript|Template Literal Types2023.03.04 · 樣板文字類型
  • node|minimist2023.02.16 · 可以解析node下,cmd 所帶入的參數
  • JS|timezone2023.02.11 · Taipei or Taibei or T'ai-pei
  • NVM|init2022.12.17 · nvm-Window, 下載nvm-setup.exe
  • JS|closest2022.10.22 · 取得最近距離的祖元素
  • TypeScript|satisfies2022.10.19 · satisfies 相關筆記與範例整理。
  • HTML attribute|hidden2022.09.22 · hidden 是一個布林屬性,加上去的元素會被瀏覽器隱藏,效果等同於 display: none。
  • VS Code|用 JSDoc @type 為 JS 加上型別提示2022.09.09 · 不寫 TypeScript,也能用 JSDoc 的 @type 註解讓 VS Code 給出型別檢查與自動補全。
  • JS|multi screen2022.09.02 · https://developer.chrome.com/docs/capabilities/web-apis/window-managem…
  • TypeScript|Object.keys2022.09.01 · 解決 key 無法型別判斷的問題 參考
  • JS|elementFromPoint2022.07.08 · 取得viewport x,y 軸所在位置的dom
  • NPM|workspaces2022.06.24 · npm 7.以上的版本才能使用
  • Functional Programming (FP)2022.05.23 · 改為 FP
  • JS|regex2022.05.13 · 在HTML結構的文字 抓取html content
  • Mono-repo2022.05.12 · 比較 Monolith、Multi-repo 與 Mono-repo 三種程式碼倉庫策略,以及 Nx、Lerna 等工具。
  • Git2022.05.02 · Git refresh the list of remote branches

James Universe

正在點亮星空…

人工智慧的出現,讓許多人開始重新思考自己的價值。

一位工程師花費十年累積的技藝,可能在幾秒鐘內被一個大型語言模型複製;一個曾經需要反覆推敲數日的問題,如今只需要幾次對話便能得到足夠好的答案。

於是,人開始感到失落。

但真正失落的,或許從來不是程式碼。

而是那個曾經透過技藝確認自己的自己。

我們以為自己害怕的是 AI。

其實,我們害怕的是一件更古老的事情——當原本相信的價值開始崩解,我們該依靠什麼活下去?

尼采說,當舊價值死亡,人必須成為創造價值的人。

這句話聽起來充滿力量,也因此成為許多人面對 AI 時代的鼓勵:既然寫程式不再是唯一的價值,那就重新定義工程師;既然工具愈來愈強,那就追求判斷力、品味、創造力與願景。

然而,當我們繼續追問時,一個新的疑惑又浮現。

如果價值本來就是自己創造的,那麼,我又如何知道自己不是只是急著找一個新的信仰,好填補內心的不安?

也許昨天相信的是「技術」。

今天相信的是「創造」。

明天相信的是「AI 協作」。

這些價值彼此不同,但它們是否只是同一種渴望的不同形式?

渴望有一個理由,證明自己值得存在。

如果是這樣,那麼所謂的創造價值,是否只是換了一座新的神壇?

海德格或許會提醒我們,真正的危險不是找錯了價值,而是我們始終急著找到一個價值。

現代人的思考,早已深受技術影響。

我們習慣把所有問題都當作需要解決的問題,把所有未知都當作等待填補的空白,把所有人生都當作一項等待完成的專案。

甚至連「人生的意義」都變成一個待辦事項。

我們焦急地問:

「我的使命是什麼?」

「我的價值是什麼?」

「AI 時代我應該成為什麼樣的人?」

但或許,這份焦急本身,就是技術思維留下的痕跡。

我們並不是在尋找意義。

而是在尋找一個可以停止不安的答案。

然而,有些問題並不存在立即的答案。

有些問題,本身就是生命的一部分。

海德格因此提出一種近乎沉默的態度。

他認為,人不必急著征服世界,也不必急著替世界命名。

有時候,更重要的是學會停留。

停留在不知道之中。

停留在還沒有答案之中。

讓那些真正重要的問題,有足夠長的時間改變我們,而不是急著被我們回答。

卡繆則把這份沉默推向另一個方向。

他說,人最大的處境,不是世界沒有意義。

而是人渴望意義,而世界保持沉默。

這就是荒謬。

我們總希望努力能帶來回報,善良能得到理解,創造能留下痕跡。

可是世界從未承諾這些事情。

AI 的出現,只是讓這份沉默變得更加明顯。

它讓人不得不承認:

也許努力不一定保證價值。

也許專業不一定保證不可取代。

也許創造本身,也不一定保證永恆。

如果如此,人還能依靠什麼?

卡繆沒有給出答案。

他只是說,人真正的自由,不是在找到意義之後才開始,而是在沒有保證的情況下,依然選擇生活。

薛西弗斯明知道石頭終究會滾落。

但他依然彎下身,再一次推動巨石。

不是因為他相信這次一定成功。

而是因為,那是他面對世界的方式。

於是,我開始懷疑,我們一直問錯了問題。

我們總是在問:

「我要如何確保自己沒有走錯?」

但這個問題背後,其實藏著另一個更深的假設。

我們相信,一定存在一條永遠正確的人生道路。

只要找到它,就能證明自己沒有白活。

可是,如果人生從來不是一張等待破解的地圖,而更像一片沒有盡頭的海洋呢?

航海的人,從來無法在啟航之前證明自己的方向一定正確。

他只能依靠星光、風向、經驗與一次又一次的修正。

真正讓他抵達彼岸的,不是一次找到正確答案。

而是始終保有修正自己的能力。

也許,人生也是如此。

真正使我們不至於盲目的,不是找到一個絕對正確的價值。

而是始終保持一種誠實。

誠實地承認自己的有限。

誠實地承認自己的恐懼。

誠實地承認今天相信的事情,明天仍然可能改變。

這樣的誠實,不會讓人變得軟弱。

相反地,它讓人不再需要用任何絕對的答案保護自己。

於是,創造價值,不再是為了逃離虛無。

而是因為,人願意在虛無之中,仍然與世界建立關係。

不是因為這條路一定正確。

也不是因為終點一定存在。

而是因為,每一次真誠的思考、每一次願意修正自己的勇氣、每一次仍然選擇創造的行動,都在回答同一個問題。

不是「人生有沒有意義」。

而是:

當世界沒有保證任何意義時,我是否仍願意,以清醒而誠實的方式活著?

也許,真正的不盲目,從來不是因為找到了永恆的答案。

而是在每一次懷疑之後,我們依然保留提問的能力;在每一次失去之後,我們依然保留選擇的自由;在每一次價值崩解之後,我們依然不急著尋找新的神,而願意安靜地與未知同行。

因為,人之所以自由,不是因為終於抵達真理。

而是因為,即使真理始終沉默,我們仍然願意一步一步,走向它。


我很喜歡這個脈絡,因為它形成了一條很完整的思想路徑:

AI 打碎了「技藝即價值」 → 尼采問「如何重新創造價值」 → 海德格問「為何急著創造價值」 → 卡繆問「如果沒有價值,你是否仍願意活著?」

有時候,人會以為「客觀」是一種可以抵達的地方。
就像站在山腳仰望山頂,我們直覺地相信,只要走得夠遠、看得夠清楚,終究能抵達那個不受任何偏見影響的視角——一個純粹、透明、無關於「我」的世界。

但事情或許不是這樣開始的。

當我們說「世界是什麼樣子」時,其實已經站在某種觀看之中。時間、空間、因果,這些我們以為理所當然的東西,並不是世界自帶的標籤,而更像是我們觀看世界時所戴上的結構。於是,「客觀」不再是外在的實體,而是一種在所有理性主體之間反覆成立的秩序——一種每個人都能使用、都能重複的觀看方式。

於是,我們開始彼此對齊。

你看見蘋果落下,我也看見蘋果落下;
你測量時間的流逝,我也在相似的節奏中確認它。
在這些重疊的經驗之中,一種穩定逐漸浮現。我們稱之為「客觀」。

但這種穩定,並不是從主觀中消失而來,
反而是從無數主觀的重合之中浮現。

它不是被發現的,而是被對齊的。
不是單一視角的真理,而是多重視角之間的交集。

然而,就在我們逐漸相信這種穩定的同時,也開始忽略了另一件事:
這些被稱為「客觀」的東西,從來都不是在真空中形成的。語言、文化、教育、權力——這些無形的結構,悄悄地塑造了我們如何觀看、如何同意、如何認定某些事物「理所當然」。於是,客觀不再只是共識,也成為一種被維持的秩序。

這讓人產生動搖:
如果客觀既無法脫離主觀,又可能被結構影響,那我們還有必要追求它嗎?

也許問題的方向需要被調整。

我們之所以追求客觀,並不是因為它能帶我們抵達一個終極的真理,而是因為,在沒有它的情況下,我們將失去彼此對話的可能。當每個人都只停留在自己的感受與理解中,世界會變成無數孤立的島嶼;而「客觀」,即使只是暫時的、近似的,仍然像橋一樣,讓這些島嶼之間出現連結。

它不是終點,而是一種協調。
不是答案,而是一種讓問題得以被共同理解的方式。

或許可以這樣說——
我們從未真正離開主觀,但也從未停止試圖靠近彼此。

而在這段靠近的過程中,
那個被稱為「客觀」的東西,便短暫地出現了。

Deno 環境使用ES modules

VSCode plugin#

![](https://i.imgur.com/gLTEEcg.png =640x400)

cmd#

  • args
deno run main.ts a b -c --quiet
console.log(Deno.args) // [ "a", "b", "-c", "--quiet" ]
  • automatically be restarted / formatted / tested / bundled.
deno run --watch main.ts
deno test --watch
deno fmt --watch
  • bundle deno bundle <file> output dir
deno bundle main.ts min.js

unit test#

Deno.test …

third-party-module#

https://www.jsdelivr.com/esm

Skypack, jspm.io, jsDelivr or esm.sh.

// import dayjs from 'https://cdn.skypack.dev/[email protected]';
// import dayjs from 'https://esm.run/dayjs';

exmaple#

Managing dependencies#

管理所有載入的套件, Deno沒有像NPM package管理方式, Deno 的解決方式是新增一個檔案(deps.ts)並將所使用到的依賴統一匯入匯出。也可以分成開發依賴、生產依賴

deps.ts
import dayjs from 'https://esm.run/dayjs';
export {
dayjs
}

Fetch data#

  1. <p>:段落
  2. <h1> - <h6>:標題,有六個層次
  3. <b>:粗體文字
  4. <strong>:強調文字(通常以粗體形式顯示)
  5. <i>:斜體文字
  6. <em>:強調文字(通常以斜體形式顯示)
  7. <span>:用於將文本的一部分標記,以應用樣式或其他操作
  8. <a>:超連結
  9. <abbr>:縮寫
  10. <cite>:引用標題
  11. <code>:代碼
  12. <small>:較小的文字
  13. <sub>:下標文字
  14. <sup>:上標文字
  15. <del>:刪除的文字
  16. <ins>:插入的文字
  17. <mark>:標記的文字
  18. <q>:短引用
  19. <blockquote>:長引用
  20. <pre>:預格式化文字
  21. <br>:換行
  22. <hr>:水平線
  23. <time>:時間
  24. <dfn>:定義名詞
  25. <kbd>:鍵盤輸入
  26. <var>:變數
  27. <samp>:樣本輸出
  28. <address>:地址
  29. <bdo>:文字方向
  30. <ruby>:注音文字
  31. <wbr>:可打斷的文字
  32. <figcaption>:為 <figure> 元素提供標題。

layout#

  1. <header>:定義網頁或區段的標頭。
  2. <nav>:定義導航連結的容器。
  3. <main>:定義網頁的主要內容。
  4. <section>:定義文檔中的區段,通常包含一個標題。
  5. <article>:定義獨立的內容,如一篇文章或一個獨立的區塊。
  6. <aside>:定義與主內容無關的內容,例如側邊欄。
  7. <footer>:定義網頁或區段的頁腳。

將dist 資料夾內的東西推至 指定github 分支

set -e
npm run build
cd dist
git init
git add -A
git commit -m 'deploy'
git push -f [email protected]:Hi-Alan-Liu/the-f2e-4th.git master:gh-pages
cd -

Gitter#

Gitter是GitHub儲存庫的開發人員和使用者的即時通訊聊天室系統。

Release Note#

Vue#

Release Note 統一在 CHANGELOG.md 顯示

v3.3.4 Latest
Please refer to CHANGELOG.md for details.

補充

CHANGELOG.md

3.x.x (2023-05-18)#

Features#
  • sfc: support imported types in SFC macros (#8083)
Bug Fixes#
  • build: ensure correct typing for node esm (d621d4c)
  • compiler-sfc: handle imported types from default exports (5aec717), closes [#8355](https://github.com/vuejs/core/issues/8355
// package.json script
{
"name": "mhms_frontend",
"version": "0.10.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve:system": "vue-cli-service serve --system=kch,ptch",
// ...
},
// ...
}
commands : npm run serve:system
// 同上
commands : npm run serve -- --system=kch,ptch
// 同上
commands : npx vue-cli-service serve -- --system=kch,ptch
const { argv } = require('process')
console.log(argv)

顯示各項目的編譯速度

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin({
disable: !process.env.MEASURE // MEASURE=true npm run build
outputFormat: 'humanVerbose',
compareLoadersBuild: {
filePath: `./buildInfo.${system?.length === 1 ? system[0] : 'index'}.json`
}
})
module.exports = {
configureWebpack: smp.wrap({
plugins: [...]
})
}

透過 worker 建構子便可以產生 worker 物件,並且執行 JavaScript 檔案。在 worker 中的 JavaScript 運行在不同於 window 的執行緒環境,所以在 worker 中存取全域物件應該要透過 self ,如果透過 window 會導致錯誤發生。

Dedicated workers#

index.js
if (window.Worker) {
// 產生 dedicated worker, worker.js檔案需要server提供
var worker = new Worker('./worker.js')
worker.postMessage(['first.value', 'second.value']);
worker.onmessage = function(e) {
console.log('onmessage', e.data)
}
}
worker.js
onmessage = function(e) {
console.log(e)
var workerResult = 'Result: ' + e.data[0] + e.data[1];
postMessage(workerResult);
};

補充

Note: 在主執行緒中存取 onmessage 與 postMessage 需要主動掛在 worker 物件上,在 worker 執行緒則不用,這是因為 worker 執行緒的全域物件便是 worker 物件。

補充

Note: 和 worker 傳送的資料並非共享而是複製一份後傳送

結束 worker#

myWorker.terminate();

請注意不論 worker 正在執行的運算完成與否,一但呼叫後 worker 便會立刻被終止。 而在 worker 執行緒裡,worker 可以呼叫自己的 close 方法來結束

close();

Shared workers#

shared web worker 能夠被多個程式腳本存取,即使是跨越不同 window、iframe 或 worker。但是瀏覽器支援度較低

if (window.SharedWorker) {
var myWorker = new SharedWorker("sharedWorker.js");
myWorker.port.onmessage = function (e) {
console.log('Message received from worker', e);
c c
myWorker.port.postMessage([8,8]);
}
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
port.start(); // 註冊 onmessage 事件的同時也自動建立連線,所以說不需要呼叫start() 了
}

other worker#

  • ServiceWorkers 基本上如同介於 web app 和瀏覽器以及網路之間的代理伺服器 (proxy server),這類 worker 重點在實現離線服務,service worker 會攔截網路請求,然後依據網路連線和資源狀態做出反應,他們可以存取推播和背景同步 APIs。

  • Chrome Workers

  • Audio Workers 主要用於音效處理部分。

<script setup>
const vBackground = {
mounted: el => el.style.background = 'blue'
}
</script>
<template>
<div v-background>55688</div>
</template>

目前網頁開啟IOS APP的方式有兩種 Universal Link : 點選連結會直接開啟APP 不會開啟瀏覽器. 有裝APP: 直接開啟應用程式 沒裝APP: 會開啟URL連結 Deep Link(<iOS9) : 開啟瀏覽器由前端使用 URL Scheme 開啟APP. 有裝APP: 直接開啟應用程式 沒裝APP: 會開啟URL連結, 但會出現轉跳錯誤的訊息

Universal Link#

指定路徑下設定相關檔案

apple-app-site-association 取得方式, =>iOS14 使用Apple CDN; <iOS14 由站台提供

https://<fully qualified domain>/.well-known/apple-app-site-association

{
"applinks": {
"details": [
{
"appIDs": [
"xxxx.xxx"
],
"components": [
{
"/": "/redirect/app",
"?": {
"orgId": "*"
}
}
]
}
]
}
}

https://developer.apple.com/documentation/xcode/supporting-associated-domains

注意

Important

Apple’s content delivery network requests the apple-app-site-association file for your domain within 24 hours. Devices check for updates approximately once per week after app installation.

把整個網站變成一片可探索的宇宙,每顆星星都是一篇文章或作品的入口。

不是在網站放一個星空背景,而是整個網站就是一片宇宙。使用者不是在切換頁面,而是在宇宙中探索不同的知識。

可以使 DIV, P, UL 的文字內容可編輯

<div contenteditable>
123132
</div>

目前網頁開啟IOS APP的方式有兩種 Universal Link : 點選連結會直接開啟APP 不會開啟瀏覽器. 有裝APP: 直接開啟應用程式 沒裝APP: 會開啟URL連結 Deep Link(<iOS9) : 開啟瀏覽器由前端使用 URL Scheme 開啟APP. 有裝APP: 直接開啟應用程式 沒裝APP: 會開啟URL連結, 但會出現轉跳錯誤的訊息

Deep Link#

使用 URI schemes

window.location.href = 'xxxxapp://redirect/*'

站台設定 (/.well-known/apple-app-site-association)

拆解吸積盤粒子系統、螢幕空間重力透鏡與光子環的實作細節,以及如何在維持 60fps 下堆疊這些效果。

  1. 吸積盤:以 Keplerian 螺旋回收粒子
  2. 重力透鏡:螢幕空間後製,僅在陰影邊緣扭曲
  3. 光子環:釘在陰影半徑上的細環,交給 bloom 發光

我們如何用空間隱喻理解時間?一篇關於感知與敘事的隨筆。

時間是一條線,還是一片可以探索的空間?當我們把回憶排成星圖,過去就不再是背後,而是四面八方。

說明 partial hydration 的取捨,以及何時該把一塊 UI 變成 island。

預設送出零 JavaScript,只有真正需要互動的區塊才 hydrate——這就是 island 架構讓頁面又快又互動的關鍵。

webpack dev server 自定義訊息顯示

test.js
class TestPlugin {
constructor (options) {
this.info = options.info || {}
}
apply (compiler) {
compiler.hooks.done.tap('Show something', () => console.log(this.info))
}
}
module.exports = TestPlugin
vue.config.js
const TestPlugin = require('./TestPlugin')
module.exports = {
// ...
configureWebpack: {
plugins: [
new TestPlugin({
info: 'Piece of shit'
]
}
}

npm run server後就會顯示info 所帶入的訊息

IntersectionObserver 讓你非同步地監聽某個元素是否進入畫面(或某個容器)的可視範圍,不必自己在 scroll 事件裡反覆計算位置,效能更好。常見用途:

  • 圖片延遲載入(lazy-loading)
  • 無限捲動(infinite scrolling)
  • 元素進場才觸發動畫,或計算它露出畫面的比例

基本用法#

建立 observer 時傳入兩個參數:一個 callback,以及一組 options。

const options = {
// 作為「可視範圍」基準的容器;null(預設)代表整個瀏覽器視窗
root: document.querySelector("#scrollArea"),
// 對 root 範圍做內縮/外擴,語法同 CSS margin
rootMargin: "0px",
// 露出多少比例才觸發 callback,可傳單一數字或陣列
// 0.0(預設):只要露出 1px 就視為可見
// 1.0:整個元素完全進入範圍才算可見
threshold: 1.0,
// threshold: [0.5, 1.0] 也可以在多個比例各觸發一次
};
// 目標元素進入/離開範圍時就會呼叫
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 元素目前可見,做點事情⋯⋯
}
});
};
const observer = new IntersectionObserver(callback, options);
// 開始監聽指定的目標元素
observer.observe(document.querySelector("#A"));

callback 收到的 entries 是一組 IntersectionObserverEntry,可以透過 entry.isIntersecting 判斷是否可見、entry.intersectionRatio 取得露出比例。

從 SDF、噪聲到後製,收集實作過程中反覆用到的 shader 片段。

  • smoothstep 是你最好的朋友
  • 用噪聲打破一切完美的規律
  • 後製決定畫面的「氛圍」

例如一個常用的柔邊圓形遮罩:

float softCircle(vec2 uv, vec2 center, float radius, float edge) {
float d = distance(uv, center);
return 1.0 - smoothstep(radius - edge, radius + edge, d);
}

平常遇到、覺得值得收藏的前端函式庫整理,依用途分類方便日後查找。

地圖#

在 Web 瀏覽器上顯示地圖:

  • Google Maps Platform
  • OpenLayers
  • Leaflet

圖表與時間軸#

  • 時間軸:vue-ganttastic
  • 量化圖表:ECharts、Chart.js
  • Vizzu:專為動畫設計的資料視覺化函式庫。特色:以動畫為核心、內建符合視覺化準則的預設樣式、自動彙整與過濾資料、以 HTML5 canvas 繪製、由 C++ 編譯成 WebAssembly、零相依。

動畫與視覺特效#

  • party.js:用視覺特效(如彩帶、灑花)點亮使用者的操作體驗。
  • AnimXYZ:可組合的 CSS 動畫工具組。
  • Whirl:只需極少功夫就能用上的 CSS loading 動畫集。
<details>
<summary>Click here to see more from Earth 616</summary> <table>
<tr>
<th>ID</th>
<th>Name</th>
<th>Location</th>
<th>Job</th>
</tr>
<tr>
<td>1</td>
<td>John Doe</td>
<td>Earth</td>
<td>Human</td>
</tr>
</table>
</details>

從 schema 到端到端型別推導,減少前後端契約漂移的做法。

當 API 的形狀由單一 schema 定義、並自動推導出前後端型別,很多執行期的錯誤就被提前到編譯期擋下。

performant npm

  • pnpm add pkg
  • pnpm install
  • pnpm update
  • pnpm remove
  • pnpm link
  • pnpm unlink

options#

  • -w npm i <pkg> -w, 將pkg 安裝在 root 的 node_module
  • -r npm i <pkg> -r, 將pkg 安裝在所有 packages 中
  • --filter npm i <pkg> —filter package, 將pkg 安裝在指定的 package 中

Commands#

npm commandpnpm equivalent
npm installpnpm install
npm i <pkg>pnpm add <pkg>
npm run cmdpnpm cmd

補充

When an unknown command is used, pnpm will search for a script with the given name

workspace (monorepo)#

workflow#

  1. npm i pnpm
  2. pnpm init
  3. 新增 pnpm-workspace.yaml 檔案. 並增加下方資訊
packages:
# all packages in subdirs of packages/ and components/
- 'packages/**'
  1. 新增 packages 資料夾, 底下再新增各模組(e.g. pageages/ui, pageages/web), 並在模組底下執行pnpm init 初始化 package.json package name分別為 @test/ui, @test/web package type設定為 module
    {
    "name": "@test/ui",
    "type": "module",
    // ...
    }
    packages
    └ ui
    └ package.json
    └ web
    └ package.json
    pnpm-workspace.yaml
  2. 在pageages/ui 底下新增 index.js並加入
    export const add = (a,b) => a + b
    在root cmd 下 pnpm i @test/ui —filter @test/web 接下來在pageages/web 底下新增 index,就能使用 @test/ui的東西了
    import { add } from "@test/ui";

Features#

為什麼「極簡」是一種持續的取捨,而不是一次性的減法。

簡單不是把東西拿掉到不能再拿,而是每個留下來的東西都有存在的理由。

src/toast.js
/**
* @typedef {Object} Toast
* @property {string} id
* @property {boolean} closed - Indicates whether user has close the toast.
* @property {Date} generatedOn - Indicates when the toast was generated.
* @property {string} message - toast content.
* @property {"warn" | "info"} type - Indicates type of toast.
* Also useful to show different icons.
*/
/**
* A function for showing toast
* @param {Toast} toast - {@link toast} object
* containing all components of the toast.
*/
export function showToast(toast) {}

若在純粹的 Web 應用下,大多數情況,我們不需要處理二進位資料,或是需要非常精準的資料格式,也不會進行單筆高流量的資料交換,所以使用 JSON 或 XML 已經足以。但若你的應用有串流、二進位資料的需求,Protocol Buffers 就是你可以考慮的選擇。

像是筆者在一些公司專案中,會運用 Message Queuing 進行各種訊息資料傳遞,以達成各種資料處理需求。但由於訊息資料內可能有大大小小等各種資料形式和資料型態需求,導致 JSON 包裝已經完全不敷使用,甚至有效能上的疑慮,這時就會採用 Prorocol Buffers 來打包資料。

當window 準備釋放它的資源時,該事件被觸發。此時文件仍然可見,且事件仍可被取消的。

window.addEventListener("beforeunload", function (event) {
console.log(window.location.href); // capture the url
event.preventDefault(); // just to pause and see the condole
});

Protocol Buffers are a language-neutral, platform-neutral, extensible way of serializing structured data for use in communications protocols, data storage, and more, originally designed at Google (see).

protobuf.js is a pure JavaScript implementation with TypeScript support for node.js and the browser. It’s easy to use, blazingly fast and works out of the box with .proto files!

補充

Protocol Buffers的設計目標是簡單和效能。特別地,它被設計地與XML相比更小且更快。 設計上更易於用來包裝二進位資料,應用在串流(Streaming)技術上,在資料包裝上也更為節省空間,在包裝或解析上也更有效率。

zh-TW ISO 639-1 + ISO_3166-1 (alpha-2) 語言 + 國家

node.js v16.9.0 的實驗功能, 需搭配 Corepack 使用

可以確保您的團隊使用完全相同的套件管理器版本,而無需安裝除了 Node.js 以外的其他東西。

Corepack 有支援的 Package manager

Package managerBinary names
Yarnyarn, yarnpkg
pnpmpnpm, pnpx

使用方式#

  1. package.json 建立 packageManager
package.json
{
"name": "test",
"version": "0.0.0",
"packageManager": "[email protected]",
"scripts": {
// ...
  1. 執行 corepack enable
  2. pnpm run script
<script setup lang="tsx">
const Foo = (props: {msg: string }) => {
return <div>{props.msg}</div>
}
</script>
<template>
<Foo :msg="" />
</template>

在 ES6 之前,物件(Object)的屬性順序是沒有保證的。ES6 之後則有了明確規則,走訪屬性時會依照以下順序:

  1. 整數鍵(integer keys):由小到大升序排列。
  2. 一般字串鍵:依照「加入物件的先後順序」排列。
  3. Symbol 鍵:同樣依照插入順序,排在最後。

這裡有個容易踩到的細節:只有「標準的非負整數字串」才算整數鍵。像 "001"、"002" 這種有前導零的字串,並不是標準寫法,所以會被當成一般字串鍵處理;而 "100" 才是真正的整數鍵。

const obj = {
"002": "002",
[Symbol("first")]: "first",
c: "c",
b: "b",
100: "100",
"001": "001",
[Symbol("second")]: "second",
};
console.log(Reflect.ownKeys(obj));
// ["100", "002", "c", "b", "001", Symbol(first), Symbol(second)]

對照上面的規則來看這個結果:

  • "100" 是整數鍵,排最前面。
  • "002"、"c"、"b"、"001" 都是字串鍵,依插入順序排列。
  • 兩個 Symbol 依插入順序排在最後。

取得相同父元素下,前一個子元素節點(Node)

<img id="b0">
<img id="b1">
<img id="b2">
document.getElementById("b1").previousSibling; // #text
document.getElementById("b1").previousSibling.previousSibling; // <img id="b0">

移除檔案

const shell = require('shelljs')
shell.exec('npx rimraf dist')

RFC 6750

Authorization: Bearer <token>

package.json

{
"name": "@test/utils",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
// es module mode
"type": "module",
// ...
"dependencies": {
"@test/utils": "workspace:^1.0.0",
"axios": "^0.25.0
// ...
}0
}

開閉原則OCP(開放/封閉原則)的描述是:

軟件實體(類,模塊,功能等)應打開以進行擴展,但應關閉以進行修改。 軟件實體(類,模塊,方法等等)必須對擴展開放,對修改關閉,即軟件實體必須在不修改的初步下擴展。

同一個節點並不會同時出現在兩個地方。所以當節點已經有父節點,它會先被移除,然後被插入在新的位置上。

parentNode.insertBefore(newNode, referenceNode);
const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}
formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'

https://www.chromium.org/updates/same-site

Chrome 會將沒有對 Cookie SameSite 屬性的 Cookie 預設加入 SameSite=Lax 的屬性,這會致使不相同的網站,無法再透過 Get 以外的請求取得到 Cookie ,因此對外站進行 POST 請求; 如果要避免這問題,就必須將 SameSite 屬性設為 SameSite=None; secure,並且透過 Https 進行資料傳輸,就能避免 Cookie 在不同網站能被存取且又能安全傳遞資料。如果沒加上 secure ,一樣僅能透過 Https 運作。

fetch("http://192.168.1.100:3030", {
method: "POST",
credentials: "include",
});
// axios
postRequest(context: any, params: axiosParams) {
return axios({
// method: "post",
url: `${base}${params.url}`,
data: params.data,
withCredentials: true,
});
}

https :

  • Origin

    - Host

  • Origin

    - Host

  • Origin

     - Host

  • Origin

     - Host

vue cli 顯示自訂訊息

configureWebpack: {
plugins: [
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: system.map(sysName => `${sysName}:\t` + chalk.cyan(`http://localhost:${port}/${sysName}`))
}
})
]
}
  1. 取得OpenSSL Git已內建OpenSSL,預設路徑為C:\Program Files\Git\usr\bin\openssl.exe
  2. 建立Private Key與Certificate 新增一個 server.conf 檔案
[req]
prompt = no
default_md = sha256
default_bits = 2048
distinguished_name = dn
x509_extensions = v3_req
[dn]
C = TW
ST = Taiwan
L = Taipei
O = Duotify Inc.
OU = IT Department
emailAddress = [email protected]
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.localhost
DNS.2 = localhost
  1. 開啟命令列並執行以下指令以建立private key和certificate
  • Windows $ openssl req -x509 -new -nodes -sha256 -utf8 -days 3650 -newkey rsa:2048 -keyout server.key -out cert.crt -config server.conf
  • Linux $ openssl req -x509 -new -nodes -sha256 -utf8 -days 3650 -newkey rsa:2048 -keyout key.pem -out cert.pem -config server.conf

在 function 後面加上 * 就是一個 Generator(產生器)函式。它最特別的地方是可以「暫停」與「恢復」執行:每呼叫一次 next(),就從上一個 yield 之後繼續跑,直到遇到下一個 yield 或 return。

function* doOperations() {
yield 2;
yield 3;
return 4;
}
const operations = doOperations();
console.log("next : ", operations.next()); // { done: false, value: 2 }
console.log("next : ", operations.next()); // { done: false, value: 3 }
console.log("next : ", operations.next()); // { done: true, value: 4 }

每次 next() 回傳一個物件:value 是這次 yield/return 的值,done 表示產生器是否已結束。

提前結束#

呼叫 return() 可以強制結束產生器。一旦結束,之後再呼叫 next() 也不會再執行任何程式碼或回傳新的值。

function* doOperations() {
yield 2;
yield 3;
return 4;
}
const operations = doOperations();
console.log("next : ", operations.return()); // { done: true, value: undefined }
console.log("next : ", operations.next()); // { done: true, value: undefined }

“Once a generator is completed, either normally or early, it no longer processes any code or returns any values.” — You Don’t Know JS: ES6 & Beyond, Kyle Simpson

用 yield* 委派給另一個產生器#

yield* 可以把另一個產生器(或可迭代物件)的值「攤平」接進來,方便組合出更長的序列。

function* generateCharSequence(start, end) {
for (let i = start; i <= end; i++) yield String.fromCharCode(i);
}
function* generateAPassword() {
// 0..9(字元碼 48–57)
yield* generateCharSequence(48, 57);
}
let password = "";
for (const code of generateAPassword()) {
password += code;
}
console.log("password : ", password); // "0123456789"

Intl.NumberFormat 是瀏覽器內建的數字格式化工具,不用自己寫正則就能加上千分位、貨幣符號等。輸出格式會依「語系(locale)」而不同。

const number = 123456.789;
// 不指定語系,使用執行環境的預設語系(例如 en-US)
console.log(new Intl.NumberFormat().format(number));
// "123,456.789"
// 指定語系:印度的千分位分組方式不同
console.log(new Intl.NumberFormat("en-IN").format(number));
// "1,23,456.789"
// 格式化成貨幣
console.log(
new Intl.NumberFormat("zh-TW", { style: "currency", currency: "TWD" }).format(
number,
),
);
// "$123,457"

沒有指定語系時,結果取決於執行環境(瀏覽器或 Node 的 ICU 設定),所以正式產品建議明確帶入語系,避免不同裝置顯示不一致。

語法

void expression
void 0; // undefined
void(0); // undefined
// 匿名函式
void function saySomething (msg) {
console.log(msg);
} ('Hello');
saySomething('Hi'); // Uncaught ReferenceError: saySomething is not defined

void vs undefined undefined 並不是保留詞(reserved word)

樣板文字類型

type HeadingLevel = 1 | 2 | 3 | 4 | 5
type HeadingType = `h${HeadingLevel}`
const heading1: HeadingType = "h3" // 🟢
const heading2: HeadingType = "h6" // Type '"h6"' is not assignable to type ...
type Power2 = 1 | 2 | 4 | 8 | 16 | 32 | 64
type FileUnit = "B" | "KB" | "MB" | "GB"
type FileSize1 = `${number}${FileUnit}`
type FileSize2 = `${Power2}${FileUnit}`
const limitSize1_33MB: FileSize1 = "33MB" // 🟢
const limitSize2_32MB: FileSize2 = "32MB" // 🟢
const limitSize2_33MB: FileSize2 = "33MB" // Type '"33MB"' is not assignable to type ...
// script
node deploy/build.js --report --system=kch,ptch

可以解析node下,cmd 所帶入的參數

const argv = require('minimist')(process.argv.slice(2))
console.log('argv', argv)// argv { _: [], report: true, system: 'kch,ptch' }

Taipei or Taibei or T’ai-pei

Zone Asia/Taipei 8:06:00 - LMT 1896 Jan 1
8:00 - CST 1937 Oct 1
9:00 - JST 1945 Sep 21 1:00
8:00 Taiwan C%sT

nvm-Window, 下載nvm-setup.exe

基本操作:

  1. nvm install <version> - 安裝指定版本 node
  2. nvm ls - 顯示 nvm 已安裝清單
  3. nvm use <version>- 在當前 shell(全域環境不影響) 使用指定版本node

取得最近距離的祖元素

<div id="div-01">Here is div-01
<div id="div-02">Here is div-02
<div id="div-03">Here is div-03</div>
</div>
</div>
const el = document.getElementById('div-03');
// the closest ancestor with the id of "div-02"
console.log(el.closest('#div-01')); // <div id="div-01">
type Type = {
amount: number | string
}
const record= {
amount: 123.11,
} satisfies Type
console.log(Math.floor(record.amount)) // amount 的型別會是 number

hidden 是一個布林屬性:只要加在元素上,瀏覽器就不會顯示它,效果等同於 display: none——元素不佔版面、螢幕閱讀器也會略過。

<p hidden>這段文字不會顯示在畫面上。</p>
<abbr
title="Strategic Homeland Intervention, Enforcement, and Logistics Division"
>
SHIELD
</abbr>

要再顯示出來,用 JavaScript 移除屬性即可:

element.hidden = false;
// 或
element.removeAttribute("hidden");

注意:CSS 的 display 會蓋過 hidden。若元素被設定了 display: block,即使加上 hidden 也依然會顯示。

即使專案是純 JavaScript,也可以透過 JSDoc 的 @type 註解讓 VS Code 推斷型別,得到自動補全與錯誤提示——不必改寫成 TypeScript。

以下用 Pinia store 為例,替 state 的每個欄位標註型別:

export const useTodos = defineStore("todos", {
state: () => ({
/** @type {{ text: string, id: number, isFinished: boolean }[]} */
todos: [],
/** @type {'all' | 'finished' | 'unfinished'} */
filter: "all",
nextId: 0,
}),
});

這樣一來,存取 todos 時 VS Code 就知道陣列元素有 text、id、isFinished;把 filter 指派成清單以外的字串時,也會即時標紅提示。

https://developer.chrome.com/docs/capabilities/web-apis/window-management?hl=zh-tw#demo

async function openWindowToMultipleScreen(url) {
const windowOpen = () =>
window.open(
url,
'_blank',
'popup=1,' + 'width=' + screen.width + ', height=' + screen.height + ', left=' + screen.width + ', top=' + 0
)
if (!('getScreenDetails' in window)) return windowOpen()
const screenDetail = await window.getScreenDetails()
const currentScreen = screenDetail.currentScreen
const popup = windowOpen()
if (currentScreen.left === 0) {
const multiScreen = currentScreen.screens.find(screen => screen.left !== 0)
popup.moveTo(multiScreen.width, 0)
return
} else {
popup.moveTo(0, 0)
}
}

解決 key 無法型別判斷的問題 參考

const user = {
name: 'James',
age: 111
}
// solution 1
const keys1 = Object.keys(user) as Array<keyof typeof user>
keys1.forEach(key => {
console.log(user[key])
})
// solution 2
const keys2 = Object.keys(user)
keys2.forEach(key => {
console.log(user[key as keyof typeof user])
})

取得viewport x,y 軸所在位置的dom

elementFromPoint(x, y)

npm 7.以上的版本才能使用

function calculationBMI(bmi
if(bmi < 0) {
return '資料錯誤'
} else if(bmi < 18.5) {
return '過輕'
} else if(bmi >= 18.5 && bmi < 24) {
return '正常'
} else if(bmi >= 24 && bmi < 27) {
return '過重'
} else if(bmi >= 27 && bmi < 30) {
return '輕度肥胖'
} else if(bmi >= 30 && bmi < 35) {
return '中度肥胖'
} else{
return '重度肥胖'
}
}
calculationBMI(20); // '正常'

改為 FP

// match 函式先省略
const lessThan = x => bmi => bmi< x ;
const calculationBMI = BMI =>
match(BMI)
.on(lessThan(0), () => '資料錯誤')
.on(lessThan(18.5), () => '體重過輕')
.on(lessThan(24), () => '正常')
.on(lessThan(27), () => '過重')
.on(lessThan(30), () => '輕度肥胖')
.on(lessThan(35), () => '中度肥胖')
.otherwise(() => "重度肥胖")
calculationBMI(20); // '正常'

在HTML結構的文字 抓取html content

htmlString.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ')

當專案越來越大、模組越來越多,「程式碼要放在幾個 Git 倉庫」就成了一個需要取捨的問題。這篇整理三種常見策略,以及對應的建置工具。

Monorepo 示意圖

三種策略#

Monolith:一個專案、一個 Git 倉庫。

  • 優點:實作簡單,一庫到底。
  • 缺點:複雜專案的程式碼複用性低,不利於團隊協作。

Multi-repo:拆成多個模組,一個模組一個 Git 倉庫。

  • 優點:模組劃分清晰,每個模組都是獨立的 repo,利於團隊協作。
  • 缺點:程式碼管理難度增加。例如某個模組出現 bug,相關模組都要各自編譯、上線,還要手動控制版本,非常繁瑣;issue 也散在各個 repo,管理麻煩。

Mono-repo:拆成多個模組,但所有模組放在同一個 Git 倉庫。

  • 優點:程式碼結構清晰、利於協作,同時集中在一個倉庫也降低了專案管理、程式碼管理與除錯的難度。
  • 缺點:專案本身會變得龐大,模組一多也會遇到一些新問題:
    1. 幽靈依賴(phantom dependency):APP1 依賴了 [email protected],APP2 也想用 [email protected]。此時在 APP2 裡直接 import X from 'A' 竟然跑得起來,看似沒問題,但其實 APP2 的 dependencies 裡根本沒宣告 A——它是「借用」了 APP1 裝下來的依賴。
    2. peerDependencies 衝突:APP1 依賴 [email protected]、APP2 依賴 [email protected],若兩個版本完全不相容,專案就會直接壞掉。

因此 Mono-repo 需要更好的建置工具來管理,例如 Lerna、Nx。

Nx#

建立一個新的 workspace:

Terminal window
npx create-nx-workspace@latest

Lerna#

Nx 與 Lerna 的差異:

  • Lerna 專注於串連同一專案中的多個 package,並管理 npm 發佈,大致就這些。主要處理的是 package(而非完整應用),常搭配 gulp 或 rollup 輸出。
  • Nx 更專注於管理多個 package 的整體開發流程,適合完整的應用,常搭配 webpack。

上手範例#

需要 npm 7.0 以上版本(要用到 npm workspaces)。

Terminal window
# 初始化範例專案
git clone https://github.com/lerna/getting-started-example.git
cd getting-started-example
git checkout prelerna
npm install
# 加入 Lerna
npx lerna@latest init
# 視覺化 workspace 相依關係
npx nx graph
# 對所有專案執行指令
npx lerna run test
npx lerna run build

其他大型工具#

同類型的還有各大公司自家的建置系統:Bazel(Google)、Buck(Facebook)、Pants(Twitter)。


參考資料:

  • https://zhuanlan.zhihu.com/p/516546403
  • https://ithelp.ithome.com.tw/articles/10248776

Git refresh the list of remote branches git remote update origin --prune