在前端開發中,CORS 幾乎是很常遇到的問題。
尤其是開發環境使用 Vite 時,通常會直接設定:
target : ' https://api.example.com '
正式環境則可能透過 Nginx:
這時候原本的 CORS 問題往往就消失了。
因此很容易產生一個疑問:
如果 Vite Proxy 或 Nginx Reverse Proxy 就能「避開」CORS,那 CORS 到底是在防什麼?
更進一步,如果攻擊者自己的網站也架一個 Proxy,是否也能繞過 CORS?
要回答這些問題,必須先把幾個常被混在一起的概念拆開:
Same-Origin Policy
CORS
Cookie
SameSite
CSRF
Origin / Referer
Authentication / Authorization
Reverse Proxy
1. CORS 不是用來阻止 API 被呼叫#
這是理解 CORS 最重要的一點。
假設:
雖然都是 example.com,但兩者 Origin 不同:
因此瀏覽器會套用 Same-Origin Policy。
前端:
fetch ( ' https://api.example.com/users ' )
瀏覽器會關心:
https://app.example.com 的 JavaScript 是否被允許讀取 api.example.com 的 response?
Server 可以回:
Access-Control-Allow-Origin : https://app.example.com
告訴 Browser:
我允許這個 Origin 的 JavaScript 讀我的 response。
這就是 CORS。
2. CORS 防的是「讀取 Response」,不是「發送 Request」#
這是最容易產生誤解的地方。
很多人會把 CORS 理解成:
其實並不完全是這樣。
更接近:
Request 可能已經送出去了。
但如果 bank.com 沒有允許:
Access-Control-Allow-Origin : https://evil.com
那麼 evil.com 的 JavaScript 就不能讀取 response。
所以可以簡化成:
控制「Cross-Origin JavaScript 能不能讀 Response」
而不是:
3. 那為什麼 Vite Proxy 可以解決 CORS?#
例如原本:
Browser 會遇到 CORS。
但是 Vite Proxy 可以改成:
http://localhost:5173/api
前端實際呼叫:
Browser 看到的是:
這是 Same-Origin。
所以 Browser 根本不需要啟動 CORS 機制。
真正跨 Origin 的部分:
是 Server-to-Server request。
瀏覽器不會對這個 request 套用 Browser CORS restriction。
4. Nginx Reverse Proxy 也是同樣的概念#
正式環境可能是:
Nginx:
proxy_pass https://api.example.com/ ;
於是:
│ https://app.example.com/api/users
Browser 只知道:
https://app.example.com/api/users
所以它是 Same-Origin。
Nginx 再替 Browser 去呼叫真正的 API。
因此這不是「繞過 CORS」,而是:
把 Cross-Origin boundary 從 Browser 移到了 Server-to-Server 的網路層。
這時候就會發現 CORS 並沒有因此失去意義。
假設:
自己架了一個 Proxy:
│ https://evil.com/api/bank
Browser 實際請求的是:
https://evil.com/api/bank
而不是:
因此 Browser 不會因為這個 request 自動把 bank.com 的 Cookie 帶給 evil.com。
例如:
Proxy 自己去呼叫 bank.com 時,也不會神奇地擁有使用者 Browser 裡的:
除非使用者自己把 credentials 提供給 evil.com。
所以 Proxy 並沒有讓攻擊者取得使用者在 bank.com 的登入身份。
6. Same-Origin Policy 才是更底層的概念#
CORS 可以看成是建立在 Browser Same-Origin Policy 上的一套授權機制。
Same-Origin Policy 的核心概念是:
不同 Origin 的 JavaScript 原則上不能任意讀取彼此的資料。
但現代 Web 應用程式需要大量跨 Origin 的功能。
例如:
因此 CORS 讓 Server 可以明確表示:
Access-Control-Allow-Origin : https://frontend.com
意思是:
我允許這個 Origin 的 Browser JavaScript 讀取我的 response。
所以可以把關係理解成:
CORS 最終成為 Web 平台標準,目前相關行為主要由 WHATWG Fetch Standard 定義。
這就進入另一個問題。
假設 evil.com:
action = " https://bank.com/transfer "
< input type = " hidden " name = " amount " value = " 100000 " >
JavaScript:
document . forms [ 0 ] . submit ()
Browser 可以真的送:
這不是 CORS 的主要防護範圍。
因為 CORS 的核心問題是:
JavaScript 能不能讀 Cross-Origin response?
而 HTML <form> 本來就被設計成可以送出 request。
因此:
不能單獨解決:
Cross-Site Request Forgery
也就是 CSRF。
8. CSRF 是什麼?#
假設使用者已經登入:
Browser 裡有:
此時使用者瀏覽:
evil.com 嘗試:
POST https://bank.com/transfer
如果 Browser 在這個 request 中帶上 bank.com 的 Session Cookie:
bank.com 可能會以為:
這是已經登入的使用者發出的 request。
這就是 CSRF 的核心問題。
9. SameSite Cookie#
現代 Browser 有一個非常重要的防護:
三個 attribute 分別解決不同問題。
例如:
防止:
而:
防止:
直接取得 Session Cookie。
而:
則限制 Cross-Site request 帶 Cookie 的情況。
10. SameSite=Lax 就足夠了嗎?#
不能一概而論。
在設計良好的網站中:
已經可以阻擋大量典型 CSRF。
但是 SameSite 通常不應該被視為所有 CSRF 防護的唯一一層。
例如:
API 錯誤使用 GET 修改資料
SameSite=None
舊環境 / 特殊 Client
同一個 registrable domain 下的不受信任 subdomain
某些 application-level CSRF 問題
因此常見的安全設計仍然會使用:
形成 Defense in Depth。
11. CSRF Token#
CSRF Token 的概念非常簡單:
Server 給自己的頁面一個只有自己網站可以取得的隨機 token。
例如:
前端:
Server:
而 evil.com 可以嘗試:
但是因為 Same-Origin Policy:
它無法正常讀取 bank.com 頁面裡的 CSRF Token。
所以它只能:
Server:
這就是 CSRF Token 的價值。
12. CSRF Token 不是 Bearer Token#
這兩個很容易混在一起。
例如:
Authorization : Bearer eyJ...
通常是:
Authentication / Authorization credential
而:
是:
CSRF protection token
它們用途完全不同。
13. Authorization: Bearer 是標準嗎?#
是。
Authorization 是 HTTP 標準的 request header。
而:
Authorization : Bearer <token>
其中 Bearer authentication scheme 是 OAuth 2.0 Bearer Token Usage(RFC 6750)定義的。
因此:
但:
並不是同樣意義上的統一標準 authentication scheme。
它比較像是 Web framework / application 常見的 convention。
你甚至可以自己定義:
或:
只要 Client 和 Server 約定一致即可。
14. Origin Validation#
除了 CSRF Token,Server 還可以檢查:
例如正常請求:
Server:
if ( origin !== ' https://bank.com ' ) {
而 evil.com 發起:
Browser 會產生:
Server 發現:
因此拒絕。
15. 為什麼攻擊者不能直接偽造 Origin?#
這裡要區分:
curl / Postman / 自己寫的 Server
和:
後者受到 Browser security model 限制。
例如 evil.com:
fetch ( ' https://bank.com/api ' )
不能隨意告訴 Browser:
因此 Server 可以利用 Browser 自己產生的 Origin header 作為 CSRF 防護訊號。
當然,如果攻擊者直接控制自己的 HTTP client:
他可以偽造任何 header。
但這已經不是典型 CSRF 的攻擊模型了。
16. Referer Validation#
另一個可以檢查的是:
例如:
Referer : https://bank.com/account
它比 Origin 包含更多資訊。
Origin:
Referer:
因此 Server 可以檢查:
如果不存在,再視情況使用:
但 Referer 可能受到:
Referrer-Policy
Browser privacy settings
navigation type
等因素影響,因此 Origin 通常是更直接的檢查對象。
17. CSP 又是在做什麼?#
CSP 很容易被誤認為是 CSRF 防護。
例如:
connect-src 'self' https://api.bank.com;
CSP 主要控制的是:
「我的網頁允許載入、執行、連線到哪些資源?」
例如:
└── Font / Media 等資源可以去哪裡?
它主要是另一個安全邊界,尤其常用來降低 XSS 和資源注入造成的風險。
所以:
三者不要混在一起。
18. 這些機制到底各自防什麼?#
可以用這張表快速理解:
19. 一張圖理解整個關係#
Same-Origin Policy Cookie
SameSite CSRF Token Origin
20. 最後用三句話記住#
如果只想記住最重要的概念,可以記這三句:
CORS#
「另一個 Origin 的 JavaScript 能不能讀我的 Response?」
CSRF#
「另一個網站能不能利用使用者目前的登入身份,讓我的 Server 執行敏感操作?」
Authentication / Authorization#
「你是誰?你有沒有權限做這件事?」
因此,CORS 並沒有被 Vite Proxy 或 Nginx 「破解」。
Proxy 做的事情其實是:
把跨 Origin 的部分移出了 Browser。
而真正完整的 Web Security 通常不是依賴單一機制,而是:
Request ─────────┼─ SameSite Cookie
├─ Origin / Referer validation
└─ Server-side validation
每一層處理的是不同問題。
也因此,「CORS 能不能被 Proxy 解決?」和「API 是否安全?」其實是兩個不同層次的問題。
人工智慧的出現,讓許多人開始重新思考自己的價值。
一位工程師花費十年累積的技藝,可能在幾秒鐘內被一個大型語言模型複製;一個曾經需要反覆推敲數日的問題,如今只需要幾次對話便能得到足夠好的答案。
於是,人開始感到失落。
但真正失落的,或許從來不是程式碼。
而是那個曾經透過技藝確認自己的自己。
我們以為自己害怕的是 AI。
其實,我們害怕的是一件更古老的事情——當原本相信的價值開始崩解,我們該依靠什麼活下去?
尼采說,當舊價值死亡,人必須成為創造價值的人。
這句話聽起來充滿力量,也因此成為許多人面對 AI 時代的鼓勵:既然寫程式不再是唯一的價值,那就重新定義工程師;既然工具愈來愈強,那就追求判斷力、品味、創造力與願景。
然而,當我們繼續追問時,一個新的疑惑又浮現。
如果價值本來就是自己創造的,那麼,我又如何知道自己不是只是急著找一個新的信仰,好填補內心的不安?
也許昨天相信的是「技術」。
今天相信的是「創造」。
明天相信的是「AI 協作」。
這些價值彼此不同,但它們是否只是同一種渴望的不同形式?
渴望有一個理由,證明自己值得存在。
如果是這樣,那麼所謂的創造價值,是否只是換了一座新的神壇?
海德格或許會提醒我們,真正的危險不是找錯了價值,而是我們始終急著找到一個價值。
現代人的思考,早已深受技術影響。
我們習慣把所有問題都當作需要解決的問題,把所有未知都當作等待填補的空白,把所有人生都當作一項等待完成的專案。
甚至連「人生的意義」都變成一個待辦事項。
我們焦急地問:
「我的使命是什麼?」
「我的價值是什麼?」
「AI 時代我應該成為什麼樣的人?」
但或許,這份焦急本身,就是技術思維留下的痕跡。
我們並不是在尋找意義。
而是在尋找一個可以停止不安的答案。
然而,有些問題並不存在立即的答案。
有些問題,本身就是生命的一部分。
海德格因此提出一種近乎沉默的態度。
他認為,人不必急著征服世界,也不必急著替世界命名。
有時候,更重要的是學會停留。
停留在不知道之中。
停留在還沒有答案之中。
讓那些真正重要的問題,有足夠長的時間改變我們,而不是急著被我們回答。
卡繆則把這份沉默推向另一個方向。
他說,人最大的處境,不是世界沒有意義。
而是人渴望意義,而世界保持沉默。
這就是荒謬。
我們總希望努力能帶來回報,善良能得到理解,創造能留下痕跡。
可是世界從未承諾這些事情。
AI 的出現,只是讓這份沉默變得更加明顯。
它讓人不得不承認:
也許努力不一定保證價值。
也許專業不一定保證不可取代。
也許創造本身,也不一定保證永恆。
如果如此,人還能依靠什麼?
卡繆沒有給出答案。
他只是說,人真正的自由,不是在找到意義之後才開始,而是在沒有保證的情況下,依然選擇生活。
薛西弗斯明知道石頭終究會滾落。
但他依然彎下身,再一次推動巨石。
不是因為他相信這次一定成功。
而是因為,那是他面對世界的方式。
於是,我開始懷疑,我們一直問錯了問題。
我們總是在問:
「我要如何確保自己沒有走錯?」
但這個問題背後,其實藏著另一個更深的假設。
我們相信,一定存在一條永遠正確的人生道路。
只要找到它,就能證明自己沒有白活。
可是,如果人生從來不是一張等待破解的地圖,而更像一片沒有盡頭的海洋呢?
航海的人,從來無法在啟航之前證明自己的方向一定正確。
他只能依靠星光、風向、經驗與一次又一次的修正。
真正讓他抵達彼岸的,不是一次找到正確答案。
而是始終保有修正自己的能力。
也許,人生也是如此。
真正使我們不至於盲目的,不是找到一個絕對正確的價值。
而是始終保持一種誠實。
誠實地承認自己的有限。
誠實地承認自己的恐懼。
誠實地承認今天相信的事情,明天仍然可能改變。
這樣的誠實,不會讓人變得軟弱。
相反地,它讓人不再需要用任何絕對的答案保護自己。
於是,創造價值,不再是為了逃離虛無。
而是因為,人願意在虛無之中,仍然與世界建立關係。
不是因為這條路一定正確。
也不是因為終點一定存在。
而是因為,每一次真誠的思考、每一次願意修正自己的勇氣、每一次仍然選擇創造的行動,都在回答同一個問題。
不是「人生有沒有意義」。
而是:
當世界沒有保證任何意義時,我是否仍願意,以清醒而誠實的方式活著?
也許,真正的不盲目,從來不是因為找到了永恆的答案。
而是在每一次懷疑之後,我們依然保留提問的能力;在每一次失去之後,我們依然保留選擇的自由;在每一次價值崩解之後,我們依然不急著尋找新的神,而願意安靜地與未知同行。
因為,人之所以自由,不是因為終於抵達真理。
而是因為,即使真理始終沉默,我們仍然願意一步一步,走向它。
我很喜歡這個脈絡,因為它形成了一條很完整的思想路徑:
AI 打碎了「技藝即價值」 → 尼采問「如何重新創造價值」 → 海德格問「為何急著創造價值」 → 卡繆問「如果沒有價值,你是否仍願意活著?」
有時候,人會以為「客觀」是一種可以抵達的地方。
就像站在山腳仰望山頂,我們直覺地相信,只要走得夠遠、看得夠清楚,終究能抵達那個不受任何偏見影響的視角——一個純粹、透明、無關於「我」的世界。
但事情或許不是這樣開始的。
當我們說「世界是什麼樣子」時,其實已經站在某種觀看之中。時間、空間、因果,這些我們以為理所當然的東西,並不是世界自帶的標籤,而更像是我們觀看世界時所戴上的結構。於是,「客觀」不再是外在的實體,而是一種在所有理性主體之間反覆成立的秩序——一種每個人都能使用、都能重複的觀看方式。
於是,我們開始彼此對齊。
你看見蘋果落下,我也看見蘋果落下;
你測量時間的流逝,我也在相似的節奏中確認它。
在這些重疊的經驗之中,一種穩定逐漸浮現。我們稱之為「客觀」。
但這種穩定,並不是從主觀中消失而來,
反而是從無數主觀的重合之中浮現。
它不是被發現的,而是被對齊的。
不是單一視角的真理,而是多重視角之間的交集。
然而,就在我們逐漸相信這種穩定的同時,也開始忽略了另一件事:
這些被稱為「客觀」的東西,從來都不是在真空中形成的。語言、文化、教育、權力——這些無形的結構,悄悄地塑造了我們如何觀看、如何同意、如何認定某些事物「理所當然」。於是,客觀不再只是共識,也成為一種被維持的秩序。
這讓人產生動搖:
如果客觀既無法脫離主觀,又可能被結構影響,那我們還有必要追求它嗎?
也許問題的方向需要被調整。
我們之所以追求客觀,並不是因為它能帶我們抵達一個終極的真理,而是因為,在沒有它的情況下,我們將失去彼此對話的可能。當每個人都只停留在自己的感受與理解中,世界會變成無數孤立的島嶼;而「客觀」,即使只是暫時的、近似的,仍然像橋一樣,讓這些島嶼之間出現連結。
它不是終點,而是一種協調。
不是答案,而是一種讓問題得以被共同理解的方式。
或許可以這樣說——
我們從未真正離開主觀,但也從未停止試圖靠近彼此。
而在這段靠近的過程中,
那個被稱為「客觀」的東西,便短暫地出現了。
Deno 環境使用ES modules
VSCode plugin#
cmd#
deno run main.ts a b -c --quiet
console . log ( Deno . args ) // [ "a", "b", "-c", "--quiet" ]
automatically be restarted / formatted / tested / bundled.
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://esm.run/dayjs';
exmaple#
Managing dependencies#
管理所有載入的套件, Deno沒有像NPM package管理方式, Deno 的解決方式是新增一個檔案(deps.ts)並將所使用到的依賴統一匯入匯出。也可以分成開發依賴、生產依賴
import dayjs from ' https://esm.run/dayjs ' ;
Fetch data#
<p>:段落
<h1> - <h6>:標題,有六個層次
<b>:粗體文字
<strong>:強調文字(通常以粗體形式顯示)
<i>:斜體文字
<em>:強調文字(通常以斜體形式顯示)
<span>:用於將文本的一部分標記,以應用樣式或其他操作
<a>:超連結
<abbr>:縮寫
<cite>:引用標題
<code>:代碼
<small>:較小的文字
<sub>:下標文字
<sup>:上標文字
<del>:刪除的文字
<ins>:插入的文字
<mark>:標記的文字
<q>:短引用
<blockquote>:長引用
<pre>:預格式化文字
<br>:換行
<hr>:水平線
<time>:時間
<dfn>:定義名詞
<kbd>:鍵盤輸入
<var>:變數
<samp>:樣本輸出
<address>:地址
<bdo>:文字方向
<ruby>:注音文字
<wbr>:可打斷的文字
<figcaption>:為 <figure> 元素提供標題。
layout#
<header>:定義網頁或區段的標頭。
<nav>:定義導航連結的容器。
<main>:定義網頁的主要內容。
<section>:定義文檔中的區段,通常包含一個標題。
<article>:定義獨立的內容,如一篇文章或一個獨立的區塊。
<aside>:定義與主內容無關的內容,例如側邊欄。
<footer>:定義網頁或區段的頁腳。
將dist 資料夾內的東西推至 指定github 分支
Gitter是GitHub儲存庫的開發人員和使用者的即時通訊聊天室系統。
Release Note#
Vue#
Release Note 統一在 CHANGELOG.md 顯示
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# " 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 ' )
顯示各項目的編譯速度
const SpeedMeasurePlugin = require ( ' speed-measure-webpack-plugin ' )
const smp = new SpeedMeasurePlugin ({
disable : ! process . env . MEASURE // MEASURE=true npm run build
outputFormat : ' humanVerbose ' ,
filePath : ` ./buildInfo. ${ system ?. length === 1 ? system [ 0 ] : ' index ' } .json `
configureWebpack : smp . wrap ({
透過 worker 建構子便可以產生 worker 物件,並且執行 JavaScript 檔案。在 worker 中的 JavaScript 運行在不同於 window 的執行緒環境,所以在 worker 中存取全域物件應該要透過 self ,如果透過 window 會導致錯誤發生。
Dedicated workers#
// 產生 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 )
onmessage = function ( e ) {
var workerResult = ' Result: ' + e . data [ 0 ] + e . data [ 1 ] ;
postMessage ( workerResult ) ;
補充
Note: 在主執行緒中存取 onmessage 與 postMessage 需要主動掛在 worker 物件上,在 worker 執行緒則不用,這是因為 worker 執行緒的全域物件便是 worker 物件。
結束 worker#
請注意不論 worker 正在執行的運算完成與否,一但呼叫後 worker 便會立刻被終止。
而在 worker 執行緒裡,worker 可以呼叫自己的 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 ) ;
myWorker . port . postMessage ([ 8 , 8 ]) ;
onconnect = function ( e ) {
port . onmessage = function ( e ) {
var workerResult = ' Result: ' + ( e . data [ 0 ] * e . data [ 1 ]) ;
port . postMessage ( workerResult ) ;
port . start () ; // 註冊 onmessage 事件的同時也自動建立連線,所以說不需要呼叫start() 了
other worker#
mounted : el => el . style . background = ' blue '
< div v-background > 55688 </ div >
目前網頁開啟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
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 的文字內容可編輯
目前網頁開啟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 下堆疊這些效果。
吸積盤:以 Keplerian 螺旋回收粒子
重力透鏡:螢幕空間後製,僅在陰影邊緣扭曲
光子環:釘在陰影半徑上的細環,交給 bloom 發光
我們如何用空間隱喻 理解時間?一篇關於感知與敘事的隨筆。
時間是一條線,還是一片可以探索的空間?當我們把回憶排成星圖,過去就不再是背後,而是四面八方。
說明 partial hydration 的取捨,以及何時該把一塊 UI 變成 island。
預設送出零 JavaScript,只有真正需要互動的區塊才 hydrate——這就是 island 架構讓頁面又快又互動的關鍵。
webpack dev server 自定義訊息顯示
this . info = options . info || {}
compiler . hooks . done . tap ( ' Show something ' , () => console . log ( this . info ))
module . exports = TestPlugin
const TestPlugin = require ( ' ./TestPlugin ' )
npm run server後就會顯示info 所帶入的訊息
IntersectionObserver 讓你非同步地監聽某個元素是否進入畫面(或某個容器)的可視範圍 ,不必自己在 scroll 事件裡反覆計算位置,效能更好。常見用途:
圖片延遲載入(lazy-loading)
無限捲動(infinite scrolling)
元素進場才觸發動畫,或計算它露出畫面的比例
基本用法#
建立 observer 時傳入兩個參數:一個 callback,以及一組 options。
// 作為「可視範圍」基準的容器;null(預設)代表整個瀏覽器視窗
root : document . querySelector ( " #scrollArea " ) ,
// 對 root 範圍做內縮/外擴,語法同 CSS margin
// 露出多少比例才觸發 callback,可傳單一數字或陣列
// 0.0(預設):只要露出 1px 就視為可見
// 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 瀏覽器上顯示地圖:
圖表與時間軸#
動畫與視覺特效#
< summary > Click here to see more from Earth 616 </ summary > < table >
從 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#
補充
When an unknown command is used, pnpm will search for a script with the given name
workspace (monorepo)#
workflow#
npm i pnpm
pnpm init
新增 pnpm-workspace.yaml 檔案. 並增加下方資訊
# all packages in subdirs of packages/ and components/
新增 packages 資料夾,
底下再新增各模組(e.g. pageages/ui, pageages/web),
並在模組底下執行pnpm init 初始化 package.json
package name分別為 @test/ui, @test/web
package type設定為 module
在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# 為什麼「極簡 」是一種持續的取捨,而不是一次性的減法。
簡單不是把東西拿掉到不能再拿,而是每個留下來的東西都有存在的理由。
* @typedef { Object } Toast
* @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.json 建立 packageManager
執行 corepack enable
pnpm run script
< script setup lang = " tsx " >
const Foo = (props: { msg : string } ) => {
return < div > { props . msg} </ div >
在 ES6 之前,物件(Object)的屬性順序是沒有保證的。ES6 之後則有了明確規則,走訪屬性時會依照以下順序:
整數鍵(integer keys) :由小到大升序排列。
一般字串鍵 :依照「加入物件的先後順序」排列。
Symbol 鍵 :同樣依照插入順序,排在最後。
這裡有個容易踩到的細節:只有「標準的非負整數字串」才算整數鍵。像 "001"、"002" 這種有前導零 的字串,並不是標準寫法,所以會被當成一般字串鍵處理;而 "100" 才是真正的整數鍵。
[ Symbol ( " first " )] : " first " ,
[ Symbol ( " second " )] : " second " ,
console . log ( Reflect . ownKeys ( obj )) ;
// ["100", "002", "c", "b", "001", Symbol(first), Symbol(second)]
對照上面的規則來看這個結果:
"100" 是整數鍵,排最前面。
"002"、"c"、"b"、"001" 都是字串鍵,依插入順序排列。
兩個 Symbol 依插入順序排在最後。
取得相同父元素下,前一個子元素節點(Node)
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
" test " : " echo \" Error: no test specified \" && exit 1 "
" @test/utils " : " workspace:^1.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 " , {
postRequest ( context : any , params : axiosParams ) {
url : ` ${ base }${ params . url } ` ,
Origin
- Host
Origin
- Host
Origin
- Host
Origin
- Host
vue cli 顯示自訂訊息
new FriendlyErrorsWebpackPlugin ({
compilationSuccessInfo : {
messages : system . map ( sysName => ` ${ sysName } : \t` + chalk . cyan ( ` http://localhost: ${ port } / ${ sysName } ` ))
取得OpenSSL
Git已內建OpenSSL,預設路徑為C:\Program Files\Git\usr\bin\openssl.exe
建立Private Key與Certificate
新增一個 server.conf 檔案
subjectAltName = @alt_names
開啟命令列並執行以下指令以建立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 () {
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 () {
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 () {
yield * generateCharSequence ( 48 , 57 ) ;
for ( const code of generateAPassword ()) {
console . log ( " password : " , password ) ; // "0123456789"
Intl.NumberFormat 是瀏覽器內建的數字格式化工具,不用自己寫正則就能加上千分位、貨幣符號等。輸出格式會依「語系(locale)」而不同。
const number = 123456.789 ;
// 不指定語系,使用執行環境的預設語系(例如 en-US)
console . log ( new Intl . NumberFormat () . format ( number )) ;
console . log ( new Intl . NumberFormat ( " en-IN " ) . format ( number )) ;
new Intl . NumberFormat ( " zh-TW " , { style : " currency " , currency : " TWD " }) . format (
沒有指定語系時,結果取決於執行環境(瀏覽器或 Node 的 ICU 設定),所以正式產品建議明確帶入語系,避免不同裝置顯示不一致。
語法
void function saySomething ( msg ) {
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 ...
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
9:00 - JST 1945 Sep 21 1:00
nvm-Window , 下載nvm-setup.exe
基本操作:
nvm install <version> - 安裝指定版本 node
nvm ls - 顯示 nvm 已安裝清單
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 >
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">
console . log ( Math . floor ( record . amount )) // amount 的型別會是 number
hidden 是一個布林屬性 :只要加在元素上,瀏覽器就不會顯示它,效果等同於 display: none——元素不佔版面、螢幕閱讀器也會略過。
< p hidden > 這段文字不會顯示在畫面上。 </ p >
title = " Strategic Homeland Intervention, Enforcement, and Logistics Division "
要再顯示出來,用 JavaScript 移除屬性即可:
element . removeAttribute ( " hidden " ) ;
注意:CSS 的 display 會蓋過 hidden。若元素被設定了 display: block,即使加上 hidden 也依然會顯示。
即使專案是純 JavaScript,也可以透過 JSDoc 的 @type 註解 讓 VS Code 推斷型別,得到自動補全與錯誤提示——不必改寫成 TypeScript。
以下用 Pinia store 為例,替 state 的每個欄位標註型別:
export const useTodos = defineStore ( " todos " , {
/** @type { { text: string, id: number, isFinished: boolean }[] } */
/** @type { 'all' | 'finished' | 'unfinished' } */
這樣一來,存取 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 ) {
' 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 )
解決 key 無法型別判斷的問題 參考
const keys1 = Object . keys ( user ) as Array <keyof typeof user >
const keys2 = Object . keys ( user )
console . log ( user [ key as keyof typeof user ])
取得viewport x,y 軸所在位置的dom
npm 7.以上的版本才能使用
function calculationBMI ( bmi
} else if ( bmi >= 18.5 && bmi < 24 ) {
} else if ( bmi >= 24 && bmi < 27 ) {
} else if ( bmi >= 27 && bmi < 30 ) {
} else if ( bmi >= 30 && bmi < 35 ) {
calculationBMI ( 20 ) ; // '正常'
改為 FP
const lessThan = x => bmi => bmi < x ;
const calculationBMI = BMI =>
. on ( lessThan ( 0 ) , () => ' 資料錯誤 ' )
. on ( lessThan ( 18.5 ) , () => ' 體重過輕 ' )
. on ( lessThan ( 24 ) , () => ' 正常 ' )
. on ( lessThan ( 27 ) , () => ' 過重 ' )
. on ( lessThan ( 30 ) , () => ' 輕度肥胖 ' )
. on ( lessThan ( 35 ) , () => ' 中度肥胖 ' )
calculationBMI ( 20 ) ; // '正常'
在HTML結構的文字 抓取html content
htmlString.replace(/<[^>]*>/g, '').replace(/ /g, ' ')
當專案越來越大、模組越來越多,「程式碼要放在幾個 Git 倉庫」就成了一個需要取捨的問題。這篇整理三種常見策略,以及對應的建置工具。
三種策略#
Monolith:一個專案、一個 Git 倉庫。
優點:實作簡單,一庫到底。
缺點:複雜專案的程式碼複用性低,不利於團隊協作。
Multi-repo:拆成多個模組,一個模組一個 Git 倉庫。
優點:模組劃分清晰,每個模組都是獨立的 repo,利於團隊協作。
缺點:程式碼管理難度增加。例如某個模組出現 bug,相關模組都要各自編譯、上線,還要手動控制版本,非常繁瑣;issue 也散在各個 repo,管理麻煩。
Mono-repo:拆成多個模組,但所有模組放在同一個 Git 倉庫。
優點:程式碼結構清晰、利於協作,同時集中在一個倉庫也降低了專案管理、程式碼管理與除錯的難度。
缺點:專案本身會變得龐大,模組一多也會遇到一些新問題:
幽靈依賴(phantom dependency) :APP1 依賴了 [email protected] ,APP2 也想用 [email protected] 。此時在 APP2 裡直接 import X from 'A' 竟然跑得起來,看似沒問題,但其實 APP2 的 dependencies 裡根本沒宣告 A——它是「借用」了 APP1 裝下來的依賴。
peerDependencies 衝突 :APP1 依賴 [email protected] 、APP2 依賴 [email protected] ,若兩個版本完全不相容,專案就會直接壞掉。
因此 Mono-repo 需要更好的建置工具來管理,例如 Lerna、Nx。
建立一個新的 workspace:
npx create-nx-workspace@latest
Nx 與 Lerna 的差異 :
Lerna 專注於串連同一專案中的多個 package,並管理 npm 發佈,大致就這些。主要處理的是 package(而非完整應用),常搭配 gulp 或 rollup 輸出。
Nx 更專注於管理多個 package 的整體開發流程,適合完整的應用,常搭配 webpack。
上手範例#
需要 npm 7.0 以上版本(要用到 npm workspaces)。
git clone https://github.com/lerna/getting-started-example.git
cd getting-started-example
其他大型工具#
同類型的還有各大公司自家的建置系統:Bazel(Google)、Buck(Facebook)、Pants(Twitter)。
參考資料:
Git refresh the list of remote branches
git remote update origin --prune