[譯] CSS 載入的未來

這週閱讀到這篇有意思的文章,於是便動手
寫下間單的翻譯,如果有理解錯誤的地方歡迎指教。

Chrome 正在試圖改變當 <link rel="stylesheet"> 寫在 <body> 的行為,從 blink-dev 的文章並不能很清楚的知道其優點。
所以這篇文章想要深入的介紹這點。

blink 是 Chrome 和 Opera 渲染引擎,而 blink-dev 是其開發社群

目前的 CSS 載入機制

1
2
3
4
5
6
<head>
<link rel="stylesheet" href="/all-of-my-styles.css">
</head>
<body>
…content…
</body>

當 CSS 段落在渲染時,會讓使用者瞪著白白的頁面直到 all-of-my-styles.css 完全下載完畢。

通常我們會把站內所有的 CSS 封裝成較少,可能只有一兩個資源檔,但這同時也意味著使用者需要下載大量
的樣式設定(CSS Rules)卻沒有在該頁面使用。因為一個網站包含著各種不同的頁面與元件,而這些東西需要
套用不同的樣式規則,如果因此分拆成許多檔案而產生大量的請求 request 這在 HTTP/1 是非常耗效能的。

不過在 SPDYHTTP/2 卻不是這樣,傳輸許多分散的小資源只會增加一點點的開銷,並且這些東西
可以個別暫存(cached)。

1
2
3
4
5
6
7
8
9
10
<head>
<link rel="stylesheet" href="/site-header.css">
<link rel="stylesheet" href="/article.css">
<link rel="stylesheet" href="/comment.css">
<link rel="stylesheet" href="/about-me.css">
<link rel="stylesheet" href="/site-footer.css">
</head>
<body>
content
</body>

這樣一來就解決了許多問題,我們可以拆成很多小檔案個別載入,不過也意味著當我們在 <head><link> 時,
得要知道這些頁面各自需要哪些資源。
另外,瀏覽器在開始輸出之前,仍然得下載所有的 CSS。如果出現一個下載比較慢的 CSS 例如 /site-footer.css
將會造成渲染的東西延遲。請觀察範例

現階段載入 CSS 較先進的機制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<head>
<script>
// https://github.com/filamentgroup/loadCSS
!function(e){"use strict"
var n=function(n,t,o){function i(e){return f.body?e():void setTimeout(function(){i(e)})}var d,r,a,l,f=e.document,s=f.createElement("link"),u=o||"all"
return t?d=t:(r=(f.body||f.getElementsByTagName("head")[0]).childNodes,d=r[r.length-1]),a=f.styleSheets,s.rel="stylesheet",s.href=n,s.media="only x",i(function(){d.parentNode.insertBefore(s,t?d:d.nextSibling)}),l=function(e){for(var n=s.href,t=a.length;t--;)if(a[t].href===n)return e()
setTimeout(function(){l(e)})},s.addEventListener&&s.addEventListener("load",function(){this.media=u}),s.onloadcssdefined=l,l(function(){s.media!==u&&(s.media=u)}),s}
"undefined"!=typeof exports?exports.loadCSS=n:e.loadCSS=n}("undefined"!=typeof global?global:this)
</script>
<style>
/* The styles for the site header, plus: */
.main-article,
.comments,
.about-me,
footer {
display: none;
}
</style>
<script>
loadCSS("/the-rest-of-the-styles.css");
</script>
</head>
<body>
</body>

上面程式碼,我們有一些內嵌的樣式(inline style)來讓我們可以快速的渲染初始化的樣式,接著把那些
還沒取得樣式的部分隱藏起來,然後開始透過 Javascript 非同步下載剩餘的樣式。這些剩餘的 CSS
會覆寫掉在 .main-article 和其他選擇器內的 display: none

這種第一次先快速的初始化渲染,然後持續匯入的方法是許多效能專家所推薦的。

看看範例

原作者實作了wiki-offline並將狀況紀錄如下圖

上面這張圖片是在 3G 的環境下測試。

不過這樣的方法還是有些不足的地方:

需要一個輕量的 Javascript 函式庫

不幸的,因為 WebKit 的實作。當 <link rel="stylesheet"> 一被加到頁面時,WebKit 會阻塞
渲染(render),直到樣式都被載入,即使這個樣式是透過 Javascript 加入的。

在 Firefox 和 IE/Edge,透過 JS 加入的樣式完全是非同步載入。Chrome 當前穩定版本然仍是遵循
WebKit 的行為,不過在 Canary 已經跟 Firefox/Edge 一樣了。

載入流程被限制在兩個階段(Inline css and A css file)

根據上面的模式,內嵌 CSS(inline CSS) 透過 display: none 隱藏尚未套用樣式的內容,然後非同步
得載入 CSS 之後呈現內容。如果您需要增加多個 CSS 檔案那麼結果可能就是內容不按順序出現

檢視範例

如果內容還在異動的過程結果周圍的廣告就先出現這通常會讓使用者覺得不開心就關閉你的網站了。

因此被限制在只有兩個載入階段,你必須要決定哪些是第一次渲染時就要出現,哪些是比較晚的。
當然,你希望上方的區塊越快顯示越好,不過所謂”上方的區塊”取決於 viewport 可視區域的大小。
最後你可能決定定義一個尺寸範圍套用在所有人身上。

如果你想讓事情更加複雜,當然你可以選擇客製 CSS 相依屬性來建立 CSS 之間渲染的相依性

更簡單,更好的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<head>
</head>
<body>
<!-- HTTP/2 push this resource, or inline it, whichever's faster -->
<link rel="stylesheet" href="/site-header.css">
<header></header>

<link rel="stylesheet" href="/article.css">
<main></main>

<link rel="stylesheet" href="/comment.css">
<section class="comments"></section>

<link rel="stylesheet" href="/about-me.css">
<section class="about-me"></section>

<link rel="stylesheet" href="/site-footer.css">
<footer></footer>
</body>

這個概念是透過每一個 <link rel="stylesheet"> 在其下載樣式時去阻塞跟在後面的內容,但允許
前面的內容先開始渲染。
樣式表(CSS)本身的載入機制是平行的,但是套用樣式卻是要照順序的。這讓 <link rel="stylesheet"> 的行為類似為 <script>

假設說 site-header, article, footer 的樣式已經載完了,但剩下的還沒,其行為如下:

  • Header: 已輸出
  • Article: 已輸出
  • Comments: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css)
  • About me: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css)
  • Footer: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css),即使自己的 CSS 已經載完了

這讓我們可以照順序輸出頁面。您甚至不需要決定哪些是”上面的區塊”,只要在元件之前匯入元件需要的 CSS 即可。

不過你還是需要注意當使用內容決定佈局(layout system),例如 table, flexbox 時,在載入期間
應避免內容異動。
這不是現在才產生的問題了,只是在逐步顯示這種機制之下更常遇到。如果你看不懂這段在描述什麼看一下下面的影片就知道了。

意思是說雖然 flexbox 已經很不錯了,但 Grid 還是更推薦的 Layout system。

Chrome 的改變

HTML規範並不涵蓋網頁渲染時是否應該或該如何被 CSS 阻塞,
並且也不鼓勵把 <link rel="stylesheet"> 寫在 body 中,不過所有瀏覽器都允許這麼做。

當然他們也都各自使用了自己的方式處理在 body 中的 <link>

  • Chrome & Safari: 一旦發現 <link rel="stylesheet"> 就停止渲染(render),直到發現的樣式被載入完畢。往往導致在 <link> 上面還未渲染的內容被卡住。
  • Firefox: <link rel="stylesheet"><head> 中會阻塞渲染直到所有發現的樣式都被載入完畢。在 body 中的話不會阻塞渲染,除非 head 裡已經有樣式阻塞住
    這可能會導致 FOUC (Flash of unstyled content) ,就是畫面先以預設的樣子呈現後閃一下才套上樣式。
  • IE/Edge: 中斷分析器直到樣式載完,不過允許 <link> 上面的內容渲染(render)。

我們偏好像 IE/Edge 的行為,所以 Chrome 將會跟隨這樣的機制。目前 Chrome/Safari 的行為就只是
會卡比較久的時間,Firefox 的行為相對較複雜一些,不過有一些小技巧。

Firefox 處理方式

因為 Firefox 不會因為 body 中的 <link> 阻塞渲染。我們需要一點小技巧來避免 FOUC。幸虧有個非常簡單的方式就是透過 <script> 中斷解析器
讓他等一等待處理狀態的樣式

1
2
<link rel="stylesheet" href="/article.css"><script> </script>
<main></main>

這個標籤不能完全沒有內容,所以我們需要留一個”空白”。

實際執行的結果

查閱範例

Firefox 和 Edge/IE 將會循序的載入輸出,而 Chrome 和 Safari 則是先看到空白的頁面一陣子直到
CSS 全部載完。目前 Chrome/Safari 的行為比起把樣式連結放在 <head> 也沒有差到哪去,所以
現在就可以開始使用這個方式,很快的 Chrome 將會往 Edge 的機制修正,這麼一來渲染會更加迅速。

以上就是一個簡單的技巧協助我們加快速度並逐步載入 CSS

Javascript Array 筆記

方法範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// push
var arr = [1, 2, 3];
var result = arr.push(4); // result: 4, arr: [1, 2, 3, 4]

// pop
var arr = [1, 2, 3];
var result = arr.pop(); // result: 3, arr: [1, 2]

// length
var arr = [1, 2];
arr.length; // 2

// unshift
var arr = [1, 2, 3];
var result = arr.unshift(0); // result: 4, arr: [0, 1, 2, 3]

// shift
var arr = [1, 2, 3];
var result = arr.shift(); // result: 1, arr: [2, 3]

// splice 拼接
// Array.splice(start_index, remove_num, element01, element02...)
var arr = [1, 2, 3, 4, 5];
var result01 = arr.splice(2, 1, 6); // result01: [3], arr: [1, 2, 6, 4, 5]
var arr = [1, 2, 3, 4, 5];
var result02 = arr.splice(2, 0, 6); // result02: [], arr: [1, 2, 6, 3, 5, 6]
// 沒移除元素會放在該 index 元素會被往後推
var arr = [1, 2, 3, 4, 5];
var result03 = arr.splice(2, 0, 6, 7, 8) // result03: [], arr: [1, 2, 6, 7, 8, 3, 4, 5]

// slice 切片
var arr = [1, 2, 3, 4, 5];
var result01 = arr.slice(1, 3); // result01: [2, 3], arr: [1, 2, 3, 4, 5]
var arr = [1, 2, 3, 4, 5];
var result02 = arr.slice(-2); // result02: [4, 5], arr: [1, 2, 3, 4, 5]
var arr = [1, 2, 3, 4, 5];
var result03 = arr.slice(-2, 0); // result03: [], arr: [1, 2, 3, 4, 5]
var arr = [1, 2, 3, 4, 5];
var result04 = arr.slice(0); // result04: [1, 2, 3, 4, 5], arr: [1, 2, 3, 4, 5]

// indexOf
var arr = [1, 2, 3, 4, 5];
var result01 = arr.indexOf(3); // result01: 2
var arr = ['a', 'ab', 'abc', 'abcd'];
var result02 = arr.indexOf('a'); // result02: 0
var result03 = arr.indexOf('b'); // result03: -1
var result04 = arr.indexOf('ab'); // result04: 1

// lastIndexOf
var arr = [1, 2, 3, 2, 1];
var result01 = arr.lastIndexOf(1); // result01: 4
var arr = ['a', 'ab', 'abc', 'ab', 'a'];
var result02 = arr.lastIndexOf('a'); // result02: 4
var result03 = arr.lastIndexOf('b'); // result03: -1
var result04 = arr.lastIndexOf('ab'); // result04: 3

// some 陣列是否有元素符合測試
var arr = [1, 2, 3, 4, 5];
var result01 = arr.some(function (element, index, array) {
return element > 3
}); // result01: true
var result02 = arr.some(element => element > 5); // result02: false

// every 陣列中是否所有元素都符合測試
var arr = [1, 2, 3, 4, 5];
var result01 = arr.every(function (element, index, array) {
return element > 3
}); // result01: false
var result02 = arr.every(element => element < 6); // result02: true

// join 串聯成字串
var arr = [1, 2, 3, 4, 5];
var result = arr.join(','); // result: "1,2,3,4,5"

// sort 由小排到大
var arr = [5, 4, 3, 2, 1];
var result = arr.sort(); // result: [1, 2, 3, 4, 5], arr: [1, 2, 3, 4, 5]
var arr = ['c', 'b', 'a', 0];
var result = arr.sort(); // result: [0, 'a', 'b', 'c'], arr: [0, 'a', 'b', 'c']

// reverse 由大排到小
var arr = [1, 2, 3, 4, 5];
var result = arr.reverse(); // result: [5, 4, 3, 2, 1], arr: [5, 4, 3, 2, 1]
var arr = [0, 'a', 'b', 'c'];
var result = arr.reverse(); // result: ['c', 'b', 'a', 0], arr: ['c', 'b', 'a', 0]

// concat 組合並回傳一個新的陣列(flat element)
var a = [1, 2];
var b = [3, 4];
var c = a.concat(b); // a: [1, 2], b: [3, 4], c: [1, 2, 3, 4]
var d = [...a, ...b]; // a: [1, 2], b: [3, 4], d: [1, 2, 3, 4]
var e = a.concat(0, [3, 4]); // e: [1, 2, 0, 3, 4]

// forEach
var arr = [1, 2, 3, 4, 5];
arr.forEach(function (element, index, array) {
console.log(element, index, array);
});
// 1, 0, [1, 2, 3, 4, 5]
// 2, 1, [1, 2, 3, 4, 5]
// 3, 2, [1, 2, 3, 4, 5]
// 4, 3, [1, 2, 3, 4, 5]
// 5, 4, [1, 2, 3, 4, 5]
// 注意: 例外無法阻止 forEach 停止

// map 遍歷所有元素至參數 callback 然後回傳一個新的陣列
var arr = [1, 2, 3, 4, 5];
var result01 = arr.map(function (element) {
return element + 10;
}); // arr: [1, 2, 3, 4, 5], result01: [ 11, 12, 13, 14, 15 ]
var arr = [1, 4, 9];
var result02 = arr.map(Math.sqrt); // arr: [1, 4, 6], result02: [1, 2 ,3]

// filter 過濾元素,產生新陣列
var arr = [1, 2, 3];
var result = arr.filter(function (element) {
return element > 2;
}); // arr: [1, 2, 3], result: [3]

// reduce 從左至右當作累加器把陣列變成單值
var arr = [1, 2, 3, 4, 5];
var result = arr.reduce(function (previous, current, currentIndex, array) {
return previous + current;
}, 100); // result: 115

// reduceRight
var arr = ['ab', 'bc', 'de'];
var result = arr.reduceRight(function (pre, cur, index, arr) {
return pre + cur;
}, 'I am initial:'); // result: "I am initial:debcab"

// 其他補充
var a; // a: undefined
var b = a += 2; // a: NaN, b: NaN => undefined += 2 => NaN

var a = null;
var b = a += 2; // a:2, b: 2

Array.forEach 額外補充

根據 MDN 的定義 arr.forEach(callback[, thisArg]) 當我們要在 forEach 的匿名函數中使用 this 我們可以透過 thisArg 來設定。
如果不設定(undefined, null)或者不用 var self = this; 的方式保留 context,那麼預設會是指向 global

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var sum = 100;
function Counter () {
this.sum = 0;
}

Counter.prototype.add = function (arr) {
arr.forEach(function (el, i, arr) {
this.sum += el;
})
};

var o = new Counter();
o.add([1, 2, 3]);
console.log(o.sum); // 0
console.log(sum); // 106

在 Chrome 或者 Firefox console 底下執行的確會執向 global,但是在 nodejs v5.6.0 底下結果卻是

1
2
console.log(o.sum); // 0
console.log(sum); // 100

要注意。

正確的範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter () {
this.sum = 0;
}

Counter.prototype.add = function (arr) {
arr.forEach(function (el, i, arr) {
this.sum += el;
}, this); // <-- here this "thisArg"
}

var o = new Counter();
o.add([1, 2, 3]);
console.log(o.sum); // 6
console.log(sum); // undefined

callback methods

  • filter (has thisArg)
  • some (has thisArg)
  • every (has thisArg)
  • forEach (has thisArg)
  • map (has thisArg)
  • reduce
  • reduceRight

從<琅琊榜>學 Redux

前言

這篇文章希望從另外一個角度來看關於 Redux 的機制與運用,在網路上各式的教學文章中值得先推薦的自然是官方教學
如果英文不是很好的可以參考繁體中文版。這是小弟認為要學習 Redux 必讀的文件。另外還有A cartoon intro to redux也是值得一讀的好文可以加深記憶。

接著就讓我們開始吧,當然最後如果有任何錯誤,也歡迎您的指教。

繼續閱讀

NodeJS path

範例筆記

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

/**
* 詳細教學
* http://www.tutorialspoint.com/nodejs/nodejs_path_module.htm
*/

// 正常化
path.normalize('/1/../2'); // => /2

// 把所有路徑整合在一起並且正常化
path.join('a', 'b'); // =>'a/b'
path.join('a', './b'); // => a/b'
path.join('a', '/b'); // =>'a/b'
path.join('/1', '/2/3', '../4'); // => /1/2/4


// 從第一個路徑,照著後面切換最後回傳絕對路徑
path.resolve('/from', '/to/path1', '/to/path2'); // => /to/path2
path.isAbsolute('/'); // => true

// 從 from 到 to 的相對路徑
path.relative('..', './A/B'); // => [project]/A/B

// 回傳該檔案所在的目錄
path.dirname('node_modules/bin/webpack'); // => node_modules/bin
path.dirname('./node_modules/bin/webpack'); // => ./node_modules/bin

// 回傳路徑最後的部分,包含副檔名
path.basename('node_modules/bin/webpack'); // => webpack
path.basename('/1/2/3.js'); // => 3.js

// 只取副檔名 + .
path.extname('/1/2/3.js'); // => .js

// 將路徑解析成物件
path.parse('/1/2/3.js'); // =>

// 從物件轉成字串
path.format({ root: '/', dir: '/1/2', base: '3.js', ext: '.js', name: '3' }); // => /1/2/3.js

認識 ES6 Decorator

在這篇文章中我們將要探討如何使用 ES7 的新功能.

ES6 新增了一個簡單更具可讀性的語法讓我們可以建立類別(class). 搭配 ES6 匯入匯出模組的語法讓我們的程式更加清楚易懂.

而 Decorators 讓我們可以在設計時期透過註記的方式修改類別與屬性.
在 ES5 物件實字(Object Literal)支援可以使用任意的表達式(Expression)
而 ES6 類別單純只支援使用函式表達式或稱作函式常量(Function Literal)
現在 Decorator 讓 JS 具備了可維護性與可讀性的宣告式語法

繼續閱讀

參透 OOCSS

OOCSS 的兩個核心觀念

  • 分離結構(html tag 結構)與樣式(ui 的樣式) Separate structure and skin
  • 分離容器(layout 佈局)與內容(直接包 content 的 tag) Separate container and content
    最後達到重複使用樣式的最高原則
繼續閱讀

再讀一遍 BEM

前言

BEM 不是什麼新東西了,會有這一篇純粹是因為之前都只是依樣畫葫蘆的去使用 BEM 看了幾篇 slider 就上沒有認真理解過。當然寫起來就滿頭包。
這一篇花了一點時間歸納總結這個看似簡單卻實用的 css 組織的方法

繼續閱讀