Lab Gradient

Gradient

一早起來就看到這篇文章視覺上的完美漸層 Chromatic,介紹了一個 Sketch plugin 可以用不同色彩系統的漸層來產生更好的視覺效果,之前其實也有注意到這個問題好幾次,就是覺得 CSS gradient 的效果不好,也有注意到一些其他的漸層方法,不過一直沒記錄下來,所以趁這時候把一些資源記錄一下,該篇文章的作者 Samuel 是推薦 Lab 色彩系統的漸層。

目前因為 CSS 就是只有 RGB 漸層,所以要用其他系統的漸層就只能用模擬的,SASS 的話有chromatic-sass,PostCSS 則是postcss-easing-gradients,這套背後用的則是首篇文章也有介紹的chroma.js來轉換的,不過它其實主要是在做 easing gradient 的,然後還有一些線上的模擬工具可以讓人直接看看效果,第一個其實是 easing gradient 的工具,其實就是在 easing gradient 標準提案時有人做來讓人體驗的,另一個介面比較不 fancy,但是我覺得比較實用的Lch and Lab colour and gradient picker


使用 VSCode 讀程式碼

VSCode peek window

偶爾看到些有趣的 library 會對它的原理和實做方式有興趣,就會花時間看看他們的程式碼,像是曾經介紹過的immer,或是最近還在看的lit-html,然後最近發現VSCode已經有把閱讀程式碼需要的功能都做好,而且因為原生對 JavaScript 支援很好,看 JavaScript 專案的時候很方便,不用特別安裝或設定什麼直接就可以開始。

閱讀程式碼其實也沒需要什麼特殊功能,就是看到不知道是什麼的東西(constant, variable, function, class ...)時,能不能快速移動到定義的地方,看完後還可以回到原處這類的 reference 和導覽的功能,在 VSCode 當中,這兩個功能就是F12移動到定義處和Ctrl+-移動回到上個位置,簡單一點的使用只要記得這兩個快速鍵就可以了,然後其實還有幾個進階的功能:

  1. Opt+F12Peek Definition,用 peek window 預覽定義,peek window 就是像上圖那樣一個浮在現在視窗上面的子視窗,通常右邊都會列出一些項目讓你可以挑選(peek),這個指令似乎也可以列出多個定義的位置,不過我還不確定是怎樣的情形會一個變數有多個地方定義它就是。
  2. Cmd+KF12Open Definition to the Side,這是兩段式的指令,先按Cmd+K然後再按F12,就會垂直分割出一個新視窗,然後新開的視窗就是所尋找的定義的程式碼。
  3. Shift+F12Peek References,這也是開 peek window,不過不一樣的點是它是列出專案內其他有用到這個東西的地方,有時候看定義不太理解可以直接看看怎麼使用,也還蠻有幫助的。

大概就這樣,Vim 的話其實也有內建的導覽功能,不過要產生對應的 reference 資訊還要些工具幫助,等研究研究後再來介紹嘍。


CSS 屬性排序

CSS Box

上週在弄Stylelint的設定,然後理所當然的處理到屬性排序的問題,以前我是用 CSScomb 的zen這組設定, 這組排序規則是從Zen Coding那邊來的,它的規則其實應該就是Concentric CSS的規則,concentric 是同心的的意思,同心圓的那個同心,這組規則的基本原則就是從外到內,從外部的定位、排版方式、到邊距、外框、內距、寬高等 box model 的屬性,然後才到內文的屬性,這組也是我第一次知道的有系統的 CSS 屬性排序規則,而且我覺得這個規則很直覺,而且也是很有邏輯的設計,所以就一直用到現在了。

最近這次要弄 Stylelint 設定時順便又研究了一下現在主流的排序規則,發現現在 lint tool 如果有內建排序規則的話,一定會有的其實是照字母順序排序,我自己是覺得在 CSS 領域照字母排序根本是 anti-pattern 啦,不過一直都有聽說有些大型企業的專案會用這種排序規則。事實上在 CSS-Tricks 的Poll Results: How do you order your CSS properties?的這篇文章,裡面就有 14% 的人是使用字母排序,當年還是 2012,2017 年 SitePoint 的調查結果其實沒有很大改變,變成 13%,所以雖然我覺得 anti-pattern 但其實還是有一定比例的人真的這樣用,其實要說的話也還是有好處啦,因為其它的屬性排序規則都是有個基本原則,實際上要仔細排列所有屬性的時候都還是有可能會有些地方有灰色地帶。

最高比例的排序方法則是照屬性類型分類,照屬性分類其實算是比較籠統的說法,Concentric CSS 的排序規則也算是照屬性分類的,同樣符合這樣條件的另外還有 Nicolas 的Idomatic CSS和 Jonathan Snook 的SMACSS的排序規則,其實這兩種方法的大方向也和 Concentric 的很接近,雖然是分成幾個大類別,但是大類別的排序基本上一樣是從外到內的方向,然後根據 npm 的安裝數字,目前使用度最高的應該是 SMACSS 的排序建議了吧,細部的完整排序可以在 Stylelint plugin package 的repo那邊看到。而除了 Stylelint 之外,PostCSS 也有 plugin 叫css-declaration-sorter來幫你排,也內建了Concentric CSSSMACSS,而且該 plugin 還號稱:

  • Up-to-date CSS properties fetched from theMDN Web Platform.
  • Thought-out sorting orders out of the box, approved by their authors.

感覺很不錯的樣子,它們的完整排序清單也可以在 repo 內找到:SMACSSConcentric CSS,不過排序這東西應該還是要在 lint 的時候做啊,PostCSS 的產出物通常是 production 環境用的 code 了,只是 stylelint 那邊用的排序清單和這邊的又不同,不過其實StylelintVSCode都可以拿 PostCSS 的來用的樣子,還沒測試過就是~

最後提兩個 Concentric CSS 灰色地帶的問題:

  1. Grid、 column、 flex、 float 這幾個屬性你會怎麼排呢~?
  2. 如果box-sizing: border-box;設下去後,width/height 要放在 padding 前面還後面呢?

async, await and try catch

New Zeland

這篇想說一下asyncawait語法的一些小細節,首先從async來說吧,一般來說,async function 是在內部有需要用await等 Promise 結果的時候才使用,也由於這個特性,async function 的回傳值都會是個 Promise,意思就是你回傳非 Promise 的值,會自動被包成 Promise,所以像下面的程式:

async function wow () {
    return Promise.resolve(100);
}

wow().then(v => { console.log(v); });

就等同於:

async function wow () {
    return 100;
}

wow().then(v => { console.log(v); });

和直接回 Promise value 比起來,效能上不會有什麼顯著差異,從建議的實做方法來看就是多一個判斷。再來看看await吧,首先一樣,await一般是用來接 Promise 的,不過其實也是可以接非 Promise value 的

async function wow () {
  var r = await 1;
  console.log(1);
}

wow();
console.log(2);

所以這樣的程式碼也可以正確執行,不過 await 那邊的執行方式還是會維持非同步的(實際上應該是後面的東西都會用 Promise 包起來一次),所以這段程式碼的輸出會是先輸出2再輸出1

再來這點可能比較多人知道,就是連續的多個await不會讓這些非同步操作同時開始:

async function wow () {
  const a = await fetch('/a');
  const b = await fetch('/b');
  const c = await fetch('/c');

  return [a, b, c];
}

這樣其實三個請求會照順序執行,a有結果了才去要bb有結果了才去要c,而不是同時處理,如果要同時發出請求則還是需要用Promise.all,然後不用async了:

function wow () {
  return Promise.all([
    fetch('/a'),
    fetch('/b'),
    fetch('/c')
  ]);
}

不要await的話,也是可以先 assign 給變數的:

function wow () {
  const a = fetch('/a');
  const b = fetch('/b');
  const c = fetch('/c');

  return Promise.all([a, b, c]);
}

然後其實Promise.all是要所有的 Promise 都 fulfilled 時才會 resolve,另外一個角度來看,就是其中只要一個 rejected 的話,就不會 resolve,實際上使用起來變化有點少,而且要做忽略錯誤的fetch也有點麻煩,所以現在 TC39 還有個新的草案叫Promise.allSettled,不管是 resolve 還是 reject,只要所有參數內的 Promise 都結束了,allSettled就會 resolve,目前這草案還在 stage 1,過幾天的會議有望升到 stage 2,不過這是題外話。

最後一個想說的就是await處理 rejected Promise 的問題,如果是從 jQuery 時期就開始寫 Deferred/Promise 的人,可能會很習慣的把 Promise 的兩種狀態拿來當成值的一部份,事實上這也是jQuery.ajax的設計,如果用這種想法來寫await接值的時候,就會覺得很難處理rejected的狀態,因為要用try...catch

async function wow () {
  try {
    const a = await fetch('/a');     
  } catch (error) {
    // deal with non-ok fetch
  }
}

要這樣寫還不如用舊的.then來接看起來還漂亮一點。不過實際上,這是錯誤的理解 Promise,Promise 不是用來取得兩種狀態用的,而是用來非同步取得單一個數值用的機制,而所謂rejected的狀態,其實就是發生非預期狀況(unexpected exception)的情形,這也就是為什麼 ECMAScript 版的 Promise 是用throw Error的方式來 reject Promise。

我一直覺得用 HTTP 請求來比較這兩種設計蠻好理解的,使用 jQuery 的ajax,server 端回非 200 的 status 的話,就會被當成是錯誤,然後回傳的 Promise 就會被 reject,但是在使用 ECMAScript Promise 的fetch中,不管 server 端回應的 status code,fetch 都會 resolve,而會 reject 的情形,就只有網路有問題的時候,像是網路斷線、存取被拒絕(CORS)等完全碰不到遠端主機的情形,也就是對於一個 HTTP 請求來說,真正的非預期狀況,所以如果你有兩種狀況要處理,那應該是回傳值的一部份,後面再用if...else來做分支。

回來看await的使用,究竟應該什麼時候來用try...catch呢,我自己有一個很簡單的初步判斷條件,就是這個取值的程式碼,如果不是非同步,沒有使用await的話,你會不會用try...catch包起來,不會的話,那改成非同步操作的程式碼應該也不用try...catch。不過現實世界當然還是比較難一點,非同步的取值風險和狀況還是比較多的,例如fetch遇到網路問題會 reject,但是還是需要處理這種狀況,不用try...catch的話,怎樣寫比較好呢?我的想法是,用.then/catch處理好需要處理的情形,然後把結果包起來傳回去,所以要處理fetch的非預期狀況的話,就可以改成:

async function wow () {
  const a = await fetch('/a').catch(error => {
    return {
      ok: false,
      status: -1,
      error: error,  
    };
  });
  
  if (a.status === -1) {
    // exception error handling
  }
}

這邊我設計成有非預期狀況時,status code 為-1,並且把 error 資訊也傳回去,然後後面就可以直接拿來判斷是不是非預期狀況,當然也可以把這個處理包成一個自己的myFetch

const myFetch = (url, options) =>
  fetch(url, options)
    .catch(error => {
      ok: false,
      status: -1,
      error: error,  
    });

然後原來的程式就可以直接拿myFetch取代fetch了。

如果要通用一點的,其實有一個叫await-to-js的套件我覺得蠻不錯的,直接拿官方的範例看吧:

import to from 'await-to-js';

async function asyncTaskWithCb(cb) {
  let [err, user] = await to(UserModel.findById(1));
  if (!user) return cb('No user found');
}

它可以包裝 Promise 物件,然後不管那個 Promise 成功還是失敗,它自己都會 resolve,resolve 的值就是[error, value]這樣形式的陣列,一來符合 node 的error-first callbacks,再來就是配合 destructuring assignment 其實程式碼是蠻漂亮的。


ES Module for NPM Package

Queenstown

For English reader:https://github.com/othree/til/blob/master/js/esm-package.md

這個問題我卡蠻久了,最近才解決加上找好一些資訊的來源,目標就是要讓一個 NPM package 同時提供 CommonJS module 和 ES module 的版本,現在很多地方可以用 ES module 了,像是 Node.js 自己有經有在測試用mjs副檔名,webpack 和 rollup 也都支援 ES module 的 bundle,而且要tree shaking的功能也需要使用 ES module,用以前的 CommonJS 是不支援的,不多廢話,直接看怎樣做吧:

{
  "name": "smartypants",
  "version": "0.1.1",
  "main": "smartypants",
  "module": "smartypants.es6.js",
  "jsnext:main": "smartypants.es6.js",
  ...
}

package.json 這樣寫,然後需要提供以下三個檔案:

-rw-r--r--  1 othree  staff  21874 Jul 14 10:38 smartypants.es6.js
-rw-r--r--  1 othree  staff  24885 Jan  9 17:12 smartypants.js
-rw-r--r--  1 othree  staff  21874 Jul 14 10:38 smartypants.mjs

這段是我從smartypants.js那邊拿來的,重點在:

  1. main裡面的檔名不寫副檔名,該檔名要同時提供jsmjs兩種
  2. 多加上module這筆設定

說明一下,Node.js 現在判斷是哪種模組格式的方式是看副檔名,所以一定要mjs的檔案才會當成 ES module,然後剛好解析main檔案時的副檔名會自動補,所以就乾脆拿掉,同時提供smartypants.jssmartypants.mjs兩個檔案,其實都是main用的;再來是module這個設定和 Node.js 以及 NPM 無關,其實是rollup提出來的pkg.module,rollup 如果在解析模組實有看到這個設定,就可以把這個檔案拿來用,當時設計是這個設定 ES module,以前的 main 則是 CommonJS module,雖然是 rollup 提出的,不過 webpack 現在也支援了,範例中還有一筆jsnext:main則是比較早期用的 key。

再更進階一點,還有目標對象的問題,就是產出是瀏覽器用的還是 server 端用的,以前這問題不太常見,不過隨著 server side rendering 越來越普及,這問題就開始比較多人關注了,webpack 就有支援 bundle 的目標對象,也有支援pkg.browser設定,webpack 的issue #5673有不少討論,有興趣的可以參考看看,不過要注意的是browser似乎是第一順位,設定的時候要小心點。


➡ 看看其它文章