3月 12, 2024 CSS
在網站上載入圖片是需要消耗時間的,因為圖片的傳遞和文字比起來實在要大太多。因為在載入網站時常常會看到一個個的空窗,然後就突然填充了圖片。如果想要令到整件事件可以順暢一點,我們就需要用到 Placeholder 和 Shimmer 技巧了。

### 什麼是 Image placeholder

Image placeholder 是指為圖片預留空的一個填充物,一般在整網站時為了排版的可預測性 (predictability),我們都會加入 `width` / `height` / `min-widht` / `min-height` / `max-width` / `max-height` 設定圖片的大細,確保在載入後都在可控制的範圍來,使排版不會出現不能預期的效果。

這個時候我們可以用一個 `div` 來作為圖片在載入時的填補。可以看看下面代碼 : 

```css
/* 設定為 relative 及背景色彩 */
div.image {
    position: relative;
    background-color: #DDDDDD;
}
```

```html
<!-- 建立一個 div 作為 placeholder 並設定 width / height -->
<div class='image' style='width: 300px; height: 100px;'></div>
```

這樣就可以得一個灰色的 DIV 作為圖片載入的 placeholder 了。

### 設定 Shimmer 效果

Shimmer 就是在灰色的 div 上加入白色的閃光,令使用者知道這是一個正在載入中的提示。

我們可以通過使用 CSS 的 animation 去達成。

```css
/* 圖片的 placeholder */
div.image {
    position: relative;
	background: linear-gradient(-45deg, #eee 40%, #fafafa 50%, #eee 60%);
	background-size: 300%;
	background-position-x: 100%;
	animation: shimmer 1s infinite linear;
}

/* 圖片的內容 */
div.image .image-src {
    position: absolute;
	inset: 0px;
	display: block;
}

/* 動畫 */
@keyframes shimmer {
	to {
		background-position-x: 0%
	}
}
```

然後就可以在 div 但放入你想要載入的東西了。

```html
<!-- 建立一個 div 作為 placeholder 並設定 width / height -->
<div class='image' style='width: 300px; height: 100px;'>
  
  	<!-- 加入圖片的 element -->
  	<div class='image-src' style='background: url(https://source.unsplash.com/1024x600) center center / cover no-repeat;'></div>
</div>
```
3月 11, 2024 CSS
今日講下點樣可以用 CSS 來選取你想要的 DOM 元件。

### TLDR

直接去下面兩個網站睇用法

:first-child 用法 : https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child

:last-child 用法 : https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child

### CSS Selector

透過 CSS 的特定語法來選取 DOM 元件,名稱叫 Selector。

Selector 是一串文字用來指定某一個 / 一些 DOM 物件,例如我們有以下一個 html :

```html
<div>
	<table>
		<tbody>
			<tr>
				<td>
					Hello world A
				</td>
				<td>
					Hello world B
				</td>
			</tr>
			<tr>
				<td>
					Hello world C
				</td>
				<td>
					Hello world D
				</td>
			</tr>
			<tr>
				<td>
					Hello world F
				</td>
				<td>
					Hello world G
				</td>
			</tr>
		</tbody>
	</table>
</div>
```

如果你想要取出 Hello world A 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:first-child td:first-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

如果你想要取出 Hello world F 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:last-child td:first-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

如果你想要取出 Hello world G 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:last-child td:last-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

很方便的呢 !
3月 10, 2024 Javascript
IIFE 的全寫是 Immediately Invoked Function Expression,這是一個當你建立後就會立即執行的 function。通常都會是一次情而且是 anonymous 的。

### 例子

IIFE 會是下面這樣 :

```js
(() => console.log(‘Hello world’))();
```

這段 code 執行時,會立即畫出 Hello world 到你的 console 上。

### 使用原因

使用 IIFE 的原因是為了保護變數的可用性。IIFE 中定義的變數不能從外部存取。這是編寫方法可以保護程式碼並減少出錯的方法。

### 配入 function 的等性使用

另外你可能需要留位一個變數旳值,用來供日後存取用 (即類似於 factory pattern),可以看看以下例子 : 

```js
// 進行 10 
for( let i=0; i<10; ++i ) {
	
	// 使用 setTimeout 來推遲執行
	setTimeout(() =>{

		// 記錄到 console
		console.log(i);
	}, 1000);
}
```

以上的代碼你可能會想看到 : 0,1,2,3,4,5,6,7,8,9

但是實際上你應該會看到是 : 9,9,9,9,9,9,9,9,9,9

這是因為經過 setTimeout 的 delay 後,`i` 的值已經由 0 數去到 9 了,所以當第一個 `setTimeout` 進行 `console.log(i)` 是,只會提取到 `i` 是 9 的結果。

`setTimeout` 只是一個例子,同樣的情況也會發生到 event handler 上,例如 : 

```js
// 取出所有 .button 的 div
const elements = document.querySelectorAll('div.button');

// 每一個 element 也做一次
for( let i=0; i<elements.length; ++i ) {
	
	// 取出 element
	const element = elements[i];
	
	// 加入 ‵click` 事件
	element.addEventListener('click', function(evt) {
			
		// 記錄到 console
		console.log(i);
	})
}
```

你按下任可的 `div.button` element 時,都只會在 console 出現 9。

#### 解決方法

你可以透過使用 IIFE 和變數傳入 function 的特性來避免這個事情。

```js
// 取出所有 .button 的 div
const elements = document.querySelectorAll('div.button');

// 每一個 element 也做一次
for( let i=0; i<elements.length; ++i ) {
	
	// 取出 element
	const element = elements[i];
	
	// IIFE
	(i2 => {

		// 加入 ‵click` 事件
		element.addEventListener('click', function(evt) {

			// 記錄到 console
			console.log(i2);
		})
	})(i);
}
```

通過把變數 `i` 傳入到 function 內,因為數值及文字是 [pass by value](https://www.google.com/search?q=js+pass+by+value) 的關係,所以在 function 內收到的變數 `i2` 是一個值,而不是 `i` 的參照 (reference),所以在之後應用 `i2` 時也只會得到當時 `i2` 值的結果。
3月 08, 2024 Javascript
在 Web Development 上我們常常會儲存小量資料在 Client Side 上,有可能是使用者的使用習慣,或者對上一次動作遇原資料等等。

雖然 Browser 自帶的 `localstorage` API 已經好方便使用,因為再提升可用性,現在就介紹一個 library 用來管理 `localstorage` 的。

### store2

Github : https://github.com/nbubna/store

佢提供左簡單的方法來存取 `localstorage`,可以另你更加專心你 Business logic 的開法。

#### 簡單應用方法

我們可以通過 `store()` 來進行簡單的基本的應用

```js
// 設定 key value
store(key, data);

// 使用 key 讀取 value
store(key);

// 運用 transaction 來寫入 value 到 key
store(key, fn[, alt]);

// 一次過寫入多個 key value
store({key: data, key2: data2});

// 讀所 store 內的所有 key value
store();

// 一個一個咁讀出 key 及 value, 可以通過 `return false` 來停止
store((key, data) => { });

// 清除所有的資料
store(false);
```

#### 進階應用

如果要進行更多的功能,可以通過以下方式來達成。

```js
// 相等於 store(key, data)
store.set(key, data[, overwrite]);

// 相等於 store({key: data, key2: data})
store.setAll(data[, overwrite]);

// 相等於 store(key)
store.get(key[, alt]);

// 相等於 store()
store.getAll([fillObj]);

// 相等於 store(key, fn[, alt])
store.transact(key, fn[, alt]);

// 相等於 store(false)
store.clear();

// 檢查 store 內有沒有提升的 key 值
store.has(key);

// 清除 key 的值及回傳原有資料,如果沒有資料則回傳 alt
store.remove(key[, alt]);

// 相等於 store(fn)
store.each(fn[, fill]);

// 加入資料到 key
store.add(key, data[, replacer]);

// 回傳所有的 keys
store.keys([fillList]);

// 取得 key 的 lenght
store.size();

// 清除所有的 key
store.clearAll();
```
3月 07, 2024 Javascript
在取得 Dom 時我們常常會用到 `document.querySelector()` 用來取得向下的子元件。有時候我們也會可能需要向上取得母系元件,這時候我們就需要用到 `closest()` 了。

### 用法

在使用 `closest()` 時我們需要由一個 Dom 出發,例如有以下這個 html :

```html

<div id='div1'>

	<div id='div2'>

		<div id='div3'></div>

		<div id='div4'></div>		
	</div>

	<div id='div5'></div>
</div>
```

我們先用 `querySelector()` 取得 `#div3` 這個 Element 先 :

```js
const div3 = document.querySelector('#div3');
```

如果需要由 `#div3` 出發,使用 `closest()` 取得 `#div1` 的話,可以這樣 : 

```js
const div3 = document.querySelector('#div3');

const div1 = div3.closest('#div1');
```

那麼問大家一個問題,如果使用以下的語法會取得那一個 `div` 呢?

```js
// 問題 1
div3.closest('div');

// 問題 2
div3.closest('div div');
```

答案時間,問題 1 的答案是 `#div3` 自已本身,而問題 2 的答案也是 `#div3` 自已本身 !

因為 `closest()` 在搜索時也會包含自己在內,這點一定要注意。

### 使用 `closest()` 但是排除自己

如果我們只想要向上取得 Element 就先需要排除自身 Element 在外,可以通過以下方法達成 : 

```js
// 取得 div3 
const div3 = document.querySelector('#div3');

// 通過使用 parentNode 來向上取得 parent,再在 parent 上使用 closest() 就可以了
const div2 = div3.parentNode.closest('div');
```
以下要講下魔改 BlogArchive Widget,今次應該真係魔改,暫時在網站上找不到任何相關的文章和 API 講有關事情。

話說係 BlogArchive Widget 入面如果用 `HIERARCHY` 模式的話,可以得到一個 `data` 參數來畫出你需要的畫面。

因為是 `HIERARCHY` 的關係,所以 `data` 變數也是樹狀型態的。

依照[官方 Documentation](https://support.google.com/blogger/answer/47270) 所講,你可以得到以下的資料:

- title: The title of the widget.
- style: One of 'MENU', 'FLAT', or 'HIERARCHY'.
- data: A list of each archive unit, each of which contains:
    - name: The name of this archive interval, e.g. "August 2006."
    - url: The link to the page containing posts from this interval.
	- post-count: How many posts there are in this interval.

### 限制

但是如果你玩過呢個 Widget 之後,發覺佢只會提供現時所在位置 Post 的相關月份內容,而不是提全部的內容給你去畫出 UI。

例如你現在是正在看 2023 年 6 月的文章,Widget 只會給你以下資料:

- [2023 年](https://example.blogspot.com/2023/)
	- [7 月](https://example.blogspot.com/2023/07/)
	- [6 月](https://example.blogspot.com/2023/06/)
		- [你的文章 1](https://example.blogspot.com/2023/06/example-post1.html)
		- [你的文章 2](https://example.blogspot.com/2023/06/example-post2.html)
	- [4 月](https://example.blogspot.com/2023/04/)
	- [1 月](https://example.blogspot.com/2023/01/)
- [2022 年](https://example.blogspot.com/2022/)
	- [5 月](https://example.blogspot.com/2022/05/)
	- [4 月](https://example.blogspot.com/2022/04/)

你只能夠按下 2023 年 7 月的連結,是不能夠取得 2023 年 7 月的文章列表。

### 開始 Reverse Engineering

如果大家有玩過 Blogger 預設的 Theme,有可能會發現有一些好舊版的 Theme (千禧年主題)入面個 Blog Archive 是可以做到 Ajax 載入不同月份的文章列表。

呢個時間你可以打開 Chrome development tool 來看看到底叫了一個什麼的 Ajax 出去。你應該會看到類似下以的 Url:

```js
https://example.blogspot.com/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN
```

然後你可以試下去不同的頁面去開一開個 Blog Archive 上面個箭咀去觸發個 Ajax 載入:

```js
https://example.blogspot.com/2023/02/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN
```

會發現到無論你在任何的 URL 上 Request 去 server 都好,只要你 Url 上 Query 上以下的參數就可以 Call `text/javascript` 的 Response 回來。

- action
	暫時只發現 'getTitles' 這個值能用
- widgetId
	大約是填返你 BlogArchive 個 id,試過填第二啲野會出 HTTP 400 Error
- widgetType
	暫時只發現 'BlogArchive' 這個值能用
- responseType
	暫時只發現 'js' 這個值能用
- path
	這個必需要填對才能通過,如果你要取得 2023 年 6 月的文章列表,就必需要填入 'https://example.blogspot.com/2023/06/' 這個值,填錯出會 Error
- xssi_token
	可以用 JS 在 runtime 時找到 xssi token

### 如何找到 XSSI Token

你可以在你的 Blogger 網站上用 Browser 按 `檢視源始碼` 來看看由 Blogger Server 最終 Generate 出來的 HTML 到底是怎樣。

我們先 Scroll 到最底,會發現到類似的 JS Code :

```js
window['__wavt'] = 'ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD';
```

就是這個 `window['__wavt']` 變數裝住的,就是你需要的 XSSI Token。你可以在 Event `DOMContentLoaded` 後任何時間取出來使用。

```js
// DOMContentLoaded 事件處理
document.addEventListener('DOMContentLoaded', function() {
	
	// 儲起 XSSI TOKEN 備用
	const XSSI_TOKEN = window['__wavt'];
});
```

### 開始 Request

有齊上面資料後,就可以用 `axios` 出個 GET Request 比伺服器。

```js
// prepare url
const url = 'https://example.blogspot.com/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN';

// axios get request and extract 'data'
const { data } = axios.get(url);
```

如果成功沒有出 Error 的話,大約會得到以下的 Response :

```js
try {
	_WidgetManager._HandleControllerResult('BlogArchive1', 'getTitles',{'path': 'https://example.blogspot.com/2023/06/', 'posts': [{'title': 'Your post title', 'url': 'https://example.blogspot.com/2023/06/your-post.html'}]});
} catch (e) {
  if (typeof log != 'undefined') {
    log('HandleControllerResult failed: ' + e);
  }
}
```

但是問題來了,因為取得的不是 Json 格式,不能夠直接使用,必需要使用神器 `eval()` 來加工一下。

### JsonP

還記得先前有文章講解過 JsonP 嗎? 如果未睇過可以去睇下。

我們先整個 random function attach 去 window object 度先 :

```js
// 整條 Random String 作為 Function 名
const fncName = 'fnc_' + Math.random().toString(16).slice(2, 12);

// 用上面個 Random String 作為 window 的 Property, 指向一個 Function
window[fncName] = (widgetId, action, data) => {
	
	// log 去 console 度
	console.log(data);
	
	// 用完要 delete 返 (一次性 function)
	delete window[fncName];
};
```

然後然要搞搞個 Response 令到佢 `eval()` 時改為執後你個自訂 function。

```js
// 將 '_WidgetManager._HandleControllerResult' 改為你自訂的 function 名稱, 即係上面個 Random function 名
const jsCode = data.replace(/_WidgetManager\._HandleControllerResult/g, fncName);

// 執行 eval
eval(jsCode);
```

如果成功的話,就可以在 development console 上看到 print 左條 json 出來。

```js
{'path': 'https://example.blogspot.com/2023/06/', 'posts': [{'title': 'Your post title', 'url': 'https://example.blogspot.com/2023/06/your-post.html'}
```

有左呢條 json 的資料,就可以用佢來更新返相應的 UI 位置。

搞掂 ! 食碗麵 !

### 總結

呢個係魔改,不知道某一日會唔會突然用唔到,不過機會都好細,因為已經有成千上萬個 Blog 都用緊,所以不會輕易就會出改動。

同埋可能有更多更多的魔改有待發掘 !! 等大家繼續去發現 !!


3月 07, 2024 Google
到現在先發現原來 Google 係出過呢一種公開的 REST API 公開比人使用過。

### GData API Directory

Documentation : https://developers.google.com/gdata/docs/directory

之前幾個講 Blogger Live Search 的文章其實就是在用 GData API 去完成,所有的 URL Params 都有詳細的說明。

不過現在已經 Fadeout 左一大部份了,因為公開的 API 好難管理,現在大多都搬到 Cloud Platform 最少也要一條 API Key 才能使用。

這樣也比較安全,管理起來也比較容易。只要把 API Key revoke 就不用再回傳 API 了,能有部減少資源運用或者誤殺 IP 的情況。

### Blogger API v3

這個大約是十多年前的產物,是次搬 Blog 主要也是靠它來完成。

Documentation : https://developers.google.com/blogger/docs/3.0/getting_started

當中有大量 Endpoint 可以使用,如果只在 Client Side 使用者只要到 [Cloud Platform](https://cloud.google.com/) 開個 Project 申請一條 API Key 就可以使用了。

如果要是 `Create`, `Delete`, `Put`, `Patch` 操作就需要 OAuth 取得 Token 後,使用 Bearer token 方法來 Call API 才能做用。

好像是要實作 Search 功能的話,可以使用以下呢個 API 就行了:

```js
GET https://www.googleapis.com/blogger/v3/blogs/YOUR-BLOG-ID/posts/search?q=KEYWORD&key=YOUR-API-KEY
```

如果成功的話,就會收到 HTTP 200 及以下的 Response body:

```json
{
  "kind": "blogger#postList",
  "nextPageToken": "CgkIChiAj86CpB8QzJTEAQ",
  "items": [
  {
    "kind": "blogger#post",
    "id": "1387873546480002228",
    "blog": {
      "id": "3213900"
    },
    "published": "2012-03-23T01:58:00-07:00",
    "updated": "2012-03-23T01:58:12-07:00",
    "url": "http://code.blogger.com/2012/03/blogger-documentation-has-moved-to.html",
    "selfLink": "https://www.googleapis.com/blogger/v3/blogs/3213900/posts/1387873546480002228",
    "title": "Blogger Documentation has moved to developers.google.com",
    "content": "content elided for readability",
    "author": {
      "id": "16258312240222542576",
      "displayName": "Brett Morgan",
      "url": "http://www.blogger.com/profile/16258312240222542576",
      "image": {
        "url": "https://resources.blogblog.com/img/b16-rounded.gif"
      }
    },
    "replies": {
      "totalItems": "0",
      "selfLink": "https://www.googleapis.com/blogger/v3/blogs/3213900/posts/1387873546480002228/comments"
    }
  },
  ...
  ]
}
```

### 點樣 Call?

用 Axios 啦!

Git hub : https://github.com/axios/axios

有需要的話再寫其他文章講解。