glob

最近在搞 jsctags-oasis 這個專案,因此認真的研究了一下 glob,glob 這東西其實有在使用 CLI 的話,一定是使用過的,例如:

ls *.js

後面的*.js就是 glob,應該可以稱為一種表達式吧,沒有正規表示式(Regular Expression)強大,是專用於匹配檔案的,現在也已經是內建於 Linux shell 內的功能了,所以其實只要man glob.7或是man 7 glob就可以找到官方文件了(不過 macOS 上沒有),然後 glob 和正規表示式相比,有個很關鍵的差異就是 glob 是有判斷路徑階層的,也就是其實?*雖然是任意字元,但是/不屬於任意字元,/又被稱為 path separator,如果要找不同層子目錄的檔案,就要把路徑寫好,不然比對時不會如願找到想要的目標,而這個差異其實也說明了為什麼ls subfolder/*只會印出該層子目錄下的檔案,而不是把第二第三層子目錄下的東西也都印出來,雖然有**這個寫法,不過我是在 nodejs 開始蓬勃發展之後才在 node-glob 文件上看到的。

其實我第一次看到 glob 這個單字也是node-glob,不過當時以為 node-glob 和命令列的那套不相容,只是借用名字而已,因為那個**/*.js的語法我以前沒看過,一直以為是 node-glob 自己做的,直到這次研究才發現其實**bash提供的擴充語法,bash 的 extglob 提供了一些更接近正規表示式的語法:

?(pattern-list)
       Matches zero or one occurrence of the given patterns
*(pattern-list)
       Matches zero or more occurrences of the given patterns
+(pattern-list)
       Matches one or more occurrences of the given patterns
@(pattern-list)
       Matches one of the given patterns
!(pattern-list)
       Matches anything except one of the given patterns

另外還有很多設定可以調整 glob 的行為,其中一樣叫做globstar的,就是讓**可以 recursive 的 match 子目錄的檔案,這個功能是在bash 4.0 alpha版的時候新增的,到今天其實也已經超過十年了。

至於為什麼會研究起 glob 呢?是因為我在做 jsctags-oasis 時,要盡量的支援Exuberant Ctags支援的參數,其中做到exclude的時候,一開始偷懶用了 node-glob 的 ignore,但是實際上要拿vim-gutentags來用時卻行為不如預期,為了能正確支援就研究起這實際上怎麼串起來的,首先是 vim-gutentags 會拿 Vim 那邊的wildignore送給 ctags,wildignore 使用的表達式是 Vim 自己的filepattern,和 glob 有點接近,像是*都是正規表示式的.*,還有?都是正規表示式的.,不過*有特別說到:

Unusual: includes path separators

這行為就和 glob 不一樣了,所以假設 ctags 的exclude也是用 glob 表示式,那是不是表示 vim-gutentags 這邊實做有不正確呢?結果我發現 Exuberant Ctags 的文件是這樣說的:

each pattern specified using this option will be compared against both the complete path (e.g. some/path/base.ext) and the base name (e.g. base.ext) of the file, thus allowing patterns which match a given file name irrespective of its path, or match only a specific path. If appropriate support is available from the runtime library of your C compiler, then pattern may contain the usual shell wildcards (not regular expressions) common on Unix (be sure to quote the option parameter to protect the wildcards from being expanded by the shell before being passed to ctags; also be aware that wildcards can match the slash character, '/').

這時候就要感謝那時期的文件都有寫得很詳細,不用花時間去看程式碼,這邊的說明就是說會比對 basename (檔名加附檔名)和完整的 pathname,另外對於 wildcard 的支援則是看系統,是用shell wildcards,其實就是 glob 表達式,不過照這樣說,應該就和 Vim filepattern 不一樣了,研究許久才注意到關鍵的地方就在上面那段文件的最後一句,提到 wildcards 也會 match 到/字元,也就是最前面提到的 path separator,結果就是, Vim filepattern 和 Exuberant Ctags 的exclude用的表示式基本上是相容的,但是也因為特性就無法用 node-glob 的ignore來支援了。所以我就照著說明自己實做了比對的部分,然後有用到一個叫globrex的 npm package,這個是tiny-glob底層用的工具,算是個偷吃步,不管 path separator 直接把 glob 轉成正規表示式的作法,根據原始碼,它會直接把*轉換成.*,這樣就會 match 到/字元了,本來是偷吃步的作法,卻意外的剛好合用,理論上這樣就可以正確的支援 ctags 的exclude才是吧。


addEventListener 的第三個參數

addEventListener

2007 年我寫過一篇一樣標題的addEventListener 的第三個參數,介紹了事件發生時, DOM Node 的 capture 和 bubbling,事隔十多年,前陣子定睛一看,發現 DOM spec 有變,第三個參數除了可以收 boolean 型別的 useCapture 之外,還可以收options物件,又稱為EventListenerOptions,而這個 options 物件現在支援三個屬性,分別是:

  • capture-就是以前的第三個參數 useCapture,Boolean 型別。
  • once-新的選項,也是 Boolean 型別,用途就像是 jQuery 的one一樣,想不到現在也直接在 DOM 層原生支援了
  • passive-也是新選項,一樣是 Boolean 型別,用途是告訴瀏覽器,這個事件 handler function 會不會呼叫event.preventDefault來停止瀏覽器的原生行為,我最初其實是在 Google 的關於scroll performance 的文件看到的,就是如果你是 scroll event,以前會因為瀏覽器要判斷會不會被preventDefault,所以讓 scroll 的效能變差,加上這個選項可以直接告訴瀏覽器說沒有要 preventDefault 後,原生的事件行為就可以不管 event handler 直接處理了,如果裡面硬是執行event.preventDefault的話,那就會被忽略掉,然後根據使用的瀏覽器的話,有的會有警告訊息出現在 console。

Passive Event 的效果也有人做了影片可以看(來源):

EventListenerOptions 這個東西大概是在 2015 開始討論的,然後2016進到 WICG 討論,瀏覽器開始實做,一開始就是只有passivecaptureonce則是後來才加上的,所以可以看到 MDN 的瀏覽器支援度表格,once還要比較新一點的瀏覽器才支援,像是 Chrome 51 就支援passive,然後要到 55 才支援once,如果再仔細看,會發現最後一列是touchstarttouchmove事件如果是在 document 層的話,預設改為 passive 事件,這是 2017 年 Chrome 主導修改的行為,Firefox 也有跟進,主要就是希望能讓這些事件處理預設效能好一點,這部分的行為修改其實到現在都還沒標準化,目前還是在 WICG 那邊有個open issue,除了 touch 事件外,其實連 document 層的 wheel 事件也在Chrome 73,也就是現在的穩定版本也預設改為 passive 事件了,然後也是有 WICG 的open issue,MDN 的表則是還沒有。

EventListenerOptions 也是有 polyfill 和工具 package 的,首先來說一下 polyfill 吧,我知道的有dom4,其實搜尋一下還蠻容易找到其它的,不過差異沒很大,feature detection 的方式幾乎都是用 Object 的 getter 來看送進去的 options 物件的passive屬性有沒有被讀取過,有的話就表示瀏覽器有支援,然候 polyfill 其實也只有行為上的補完,不會真的讓效能提升,不過 polyfill 在現在的支援度下來看也是不太需要了。Package 的話有個default-passive-events會幫忙把 scroll、wheel、mouse、touch 等等有需要的事件都改為預設passive: true,如果是新專案先加一下似乎不錯,不過感覺上 Google 是很想都改掉的樣子,現在其實也只差 mouse 事件而已,應該是還怕影響太大不敢下手吧。


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似乎是第一順位,設定的時候要小心點。


ECMAScript 2015 新功能間的關係

ECMAScript 2015,

這篇想說的是 2015 年的那個 ECMAScript 6(後面簡稱 ES6),也就是之前 ECMAScript Harmony 計畫的主要成果,那版 ES6 其實是這幾年來改動最多的一版,新增了很多的新功能和語法,而這一堆新功能很多是環環相扣的,我以前曾經在 Facebook 上提過,不過那邊的東西容易就消失在網路上,所以還是另開一篇文章來記錄,剛好也可以做為下一篇文章的參考資料。

首先要從Map/Set這兩個新的資料型態說起,ECMAScript 一直以來都只有少少的資料型態,直到 ES6 才加了些新的,其中比較容易注意到的就是 Map 和 Set 了,其實這兩種資料型態以前就是直接用 object 來 array 來做,兩邊蠻接近的,最主要的差異則是 Map 的 key 可以是任意型態, 而以前 object 的 key 只能是字串,Set 是 unique 的,array 則否,另外就是在適合的情境下,現在 Map/Set 的效能不一定會比較差。

Map、Set 其實都算是 collection 的資料,所以會需要有個方法可以遍歷所有元素,像是 array 的forEach或是 for...in 語法,不過 for...in 先拿到的東西是 key,還需要拿 key 去取元素實際的值:

for (let k in arr) {
  let v = arr[k];
}

一直以來,其實開發者社群都希望有個語法能直接取 collection 內的元素,所以像是 CoffeeScript 就是把for...in換成直接拿到 value,於是 ES6 就有了個for...of語法,可以遍歷 collection 類型的資料並直接取得值:

for (let v of arr) {
    
}

ES6 的這個語法,其實底層是透過iteration protocols這些內部協定來運作的,包括了 iterable protocol 和 iterator protocol,for...of其實就是透過 iterable protocol 去拿物件的 iterator,利用 iterator 來遍歷元素,所以自己寫的物件也可以實做 iterable protocol,然後就可以讓該物件支援for...of語法了;除此之外,iterator 是不能重複使用的,所以其實每次for..of,都是拿一個該物件的新的 iterator,而為了可以簡單產生這個 iterator,又有了generator function,generator function 每次執行都會回傳一個新的 iterator(精確一點說是 generator object,同時是 iterator 也是 iterable),正好適合這個情境。

Iterable protocol 的定義其實很簡單,就是定義怎樣把 generator function 放在物件裡的方式,實際上是利用 ES6 另一個新的資料型態:Symbol來達成的,為什麼不直接定個屬性名稱給它呢?最主要就是不要讓這些內部 protocol 的東西在for...in操作的時候被遍歷到,所以定義了新的 Symbol 型別,利用它的特性來把內部 protocol 做了一定程度的保護,iterable protocol 就是把 generator function 用一個預先定義好的 Symbol 來儲存,這個 Symbol 又稱為Symbol.iterator,這種預先定義好的 Symbol 則統稱為 Well-Known Symbols,ES6 其實定義了好幾個,不是只有 iterable 用的到,透過定義這些 Well-Known Symobls,可以介入改變一些 JavaScript 比較基礎的運作。

Well-Known Symobls 可以做到的事情,其實有點像是改變程式語言的運作,而這種類型的機制又稱為meta programming,除了 Well-Known Symobls 之外,ES6 其實還提供了ProxyReflect,這兩個東西應該比較多人知道 Proxy 是幹嘛的,對 Reflect 比較陌生,其實 Reflect 有點像是為了 JavaScript 一些設計不好的地方,想了解詳細一點推薦可以看主要的 polyfill [harmony-reflect][] 的 Wiki 頁:Why should I use this library?

最後總結一下,這串從 Map/Set 開始,接著連到for...of語法、Iterator、Generator、Symbol、Well-Known Symobls 最後到 meta programming 的 Proxy 和 Reflect,其實也差不多佔了 1/4 的 ES6 新功能。


eslint-plugin-pep8-blank-lines

eslint-plugin-pep8-blank-lines,

我的第二個 ESLint plugin 終於進 beta 了,這是我自己期望很久的檢查規範,上一次介紹 ESLint plugin 的時候就有說到接下來想處理空行,其實 ESLint 內建的 rule 已經有蠻多是用來檢查空行的了,不過沒有一個能符合我想要的規範,我想要的規範其實很簡單,就是希望能在大一點的物件中間能多一點空行,比較有段落的感覺,這樣閱讀起來感覺也比較好(如上圖),剛好我這兩年寫了一點 Python,有用 Flake8 做語法檢查,其中的PEP8 coding style 中關於空行的規範,就符合我想要的樣子,而且很簡單,這個規範是在大部分地方都允許最多一行空行,但是最上層(top level)的 function, class 前後要兩行空行。

於是這個 ESLint plugin 的主要目標,就是把 PEP8 這部分的規範搬過來,一開始想的實做方式有兩個,其一是參考padding-line-between-statements的作法,比較兩個相鄰 token/node 間的 line number,另一個則是用sourceCode來一行一行看,不過同時也要知道該行的 context 是什麼才能判斷,所以也是跑不掉要進去看 AST,加上我想要玩玩看 JavaScript 的 AST,所以最終我是選擇第一種作法,不過不是用 ESLint 內建的 walker,而是在Program:exit的時候才用自己寫的 walker 進去看 AST;而經過一輪重構後,現在的架構其實是靈活度很高的,我實際上做出了一個比 padding-line-between-statements 還要更多功能的規範定義格式,然後根據這個格式寫出我想要的空行規範,只是目前還沒開介面出來給使用者輸入自訂的空行規範就是了;其實我自己覺得這個 plugin 實做的理想型式應該還是要用實做方案二,並搭配使用 ESLint 的 AST walker,實際上 padding-line-between-statements 也就是這樣做,會這樣想最主要的原因是現在的實做只看 AST,但是 AST 其實不能 100% 表達原來的程式碼,這也是這次開發經驗中我最大的體悟,所以其實一些奇怪地方的空行就會很難抓到,例如await 1這兩個 token 中間如果有空行就會跳過,不過會在這種地方放空行的情形應該都是蠻少見的,所以目前也沒打算繼續改下去,短期內都會以處理 bug 為主,過陣子應該會試著加上 fix 的功能,總之歡迎測試並回報問題,雖然有寫測試,不過還沒什麼實際跑在真實的程式碼上,目前唯一的就是它自己的 code base 本身是有用吧,另外就是使用時如果是搭配其它 style 可能會需要把其它 style 的空行規則關掉,例如搭配standardjs時的.eslintrc範例:

{
  "extends": "standard",
 
  "plugins": [
    "pep8-blank-lines",
    "no-parameter-e"
  ],
 
  "rules": {
    "semi": [2, "always"],
    "no-extra-semi": 2,
    "comma-dangle": ["error", "always-multiline"],
    "no-multiple-empty-lines": 0,
    "pep8-blank-lines/pep8-blank-lines": 2,
    "no-parameter-e/no-parameter-e": 2
  }
}

這組其實也是我目前在用的設定啦~


Tern 0.22 released

大約七月初的時候,我開始接手幫忙維護Tern,Tern 是一個獨立的 JavaScript inference engine,用於協助撰寫 JavaScript 程式碼,就和之前介紹過的 Microsoft 的 LSP 後面的 Language Server 一樣,都是獨立於編輯器/IDE之外,不過 TernJS 是 2013 年就有開始發展的,所以是走自己的溝通介面;其實我幾年前也有幫忙貢獻過 TernJS,以前弄過我還有印象的有 Promise 支援、fetch 的定義、CoffeeScript plugin。

後來作者Marijn暫停維護 Tern 跑去弄其他東西像是AcornCodeMirror還有 ProseMirror 等(這位很厲害,改天再來介紹),並公開找人接手,在一些文字內有找到他的說法是說現在這個架構有些問題處理不了,很難再發展下去了,總之所以就停了一年多沒更新了,我也是斷斷續續注意到這個狀況,不過在研究 LSP 的時候發現其實還蠻多東西是依賴 TernJS 的,讓他這樣荒廢下去好像有點可惜,認真考慮了一兩週後決定接手維護工作,考慮的點主要在於不知道能不能順利接手處理問題,因為 TernJS 的 code base 實在不容易理解,尤其是我沒有相關的 compiler、工具的訓練和開發經驗,以前那些貢獻其實都是花很大心力下去才弄出來的,幾乎是處於那種「程式碼會動了,但是我不知道為什麼」的狀態,不過這兩年相關的知識補了不少,還玩了好一陣子的 JavaScript AST,有覺得比較看的懂 TernJS 的程式碼了,就心一橫報名說要幫忙維護了,Marijn 看到我過去有發過一些 PR 後,很迅速的就開協作者權限給我了。

正式開始接手後,我就開始把要做的事情整理出來,我的目標是在保持現有架構之下,盡可能的繼續支援新語法,直到真的這個架構撐不住為止,所以一開始就是把一些落後的語法支援和定義補上,這次發佈的0.22 版就是包括 0.21 之後的一些小 bugfix,還有我加入之後開始弄的 async/await、async iteration(包括for await of) 以及**支援,下一版我會開始一些內部的修改、還有看看 bug,不過 Emacs 相關的我現在是真的無法處理。

最後一段來說說目前感想吧,Tern 真是我目前為止看過最難理解的 code 了,不知道是不是會寫 compiler 的人腦袋都會轉換到常人無法理解的形狀,我目前為止看的第二辛苦的 code base 是 Kibana 的,不過 Kibana 單純只是東西很多,找入口找很久,Tern 難的點在於它用了很多 side effect 來做事,而且 code 內沒什麼文件說明,所以像下面這行我就花了很多時間才看懂實際上做什麼事:

infer(node.right, scope, new HasMethodCall(":Symbol.iterator", [], null,
                                           new HasMethodCall("next", [], null,
                                                             new GetProp("value", target))))

這行程式碼是先拿node.right:Symbol.iteratormethod 的執行結果,再看它的nextmethod 的執行結果,然後取最後這個結果的valueproperty 的資訊(可能的 type 之類的)塞給 target 物件,然後這行下面你又看不到 target 做何用,因為 target 物件是在上面已經有和其它會回傳的物件有建立關聯的;除此之外,這裡有個new GetProp,其它地方還有個AVal.getProp又是不同功能,一開始看的真的是黑人問號...


命名記錄 1

Карта Одинокой Горы

記錄一下這兩年自己覺得還不錯的命名,本來是想累積更多再貼的。

Herculus

海克力士,用在測試服務名稱上,是希臘神話中的大力士,曾經完成十二偉業,這兩年還有部電影,取名源由其實是 Fate/stay night 中,他做為 Berserk 的 Servant 所持有的寶具,稱為 十二の試練,試練就聯想到 test ~

Erebor

用在資料庫相關的服務,哈比人歷險記的孤山的精靈語,因為孤山裡面有很多矮人的寶物,資料庫內的資料某種層面來說也算是個寶物。

Olympus

應該不用介紹來源了,用在 client 端自己寫的 API helper,取這個名字的原因是覺的客戶端跟 server 端要資料就如同在跟眾神請求一樣。


SmooshGate

之前應該沒在這邊提過這件事,總之就是之前 TC39 有個 flatten 的 proposal,搞一搞後,發現這東西實做啟用後 MooTools 會爛掉,詳細的原因 Google 那邊有一篇文章解釋,標題就是 SmooshGate,Smoosh 這個單字其實有點少見,我大概查一下,雖然有說是 flatten、squash 的意思,不過目前覺得應該是作畫時把顏料抹平那種動作感覺最對吧,總之,當初為了這個問題第一個提案的改名就是smoosh,不過這個單字整體感覺和 flatten 差距實在太大,所以出現一堆聲音,有的是建議加上"use es2019"的 statement 來開啟flatten,像是 stirct mode 一樣,有的建議其他名字,當然也有些人是覺得管 MooTools 去死的,不過因為 Don't Break the Web 的大原則所以還是要處理這個問題。

其實我覺得當初提案 smoosh 的人搞不好是故意挑這個字的,藉此增加話題性引發討論和建議,效果其實很好,在 Twitter 上還出現了#SmooshGate這樣的 hashtag,不過,總之在最近一次五月的 TC39 會議,確定 proposal 改新的名稱:flat,雖然詞性不同,不過大家都還蠻可以接受,或許也可能是相較於 smoosh 來說很可以接受吧。


ESLint Plugin 入門

ESLint

最近寫了個簡單的 ESLint plugin,來記錄一下一些基礎知識,我做的 plugin 很簡單,叫做eslint-plugin-no-parameter-e,這個 ESLint plugins 做的事情只是檢查所有 function 的參數,然後如果有任何一個參數名是e的話就警報,這條 rule 其實是為了避免把errorevent簡寫成e,會容易混淆。

接下來進入正題,ESLint 基本上就是透過ESPree這個 parser 先把程式碼轉為ESTree相容的 AST,EStree 是個 de facto standard,是從 Mozilla Spider Monkey 用的 AST 演化而來,現在幾乎做 JavaScript 工具,會需要轉 AST 的話都會用這個格式;有了 AST 後,才來分析 AST 做檢查,然後現在有工具叫AST Explorer,非常方便,可以線上直接修改 code sample 看 AST 變化,可以用它來看你想要處理的 code 的 AST 結構,至於怎麼寫 rule 就看個人了,基本上就是監聽要注意的 node,然後檢查 AST 結構,有問題就呼叫 report 這樣。

第二點,npm module 的名稱要用eslint-plugin-開頭,官方說的規則,應該不遵守還是可以抓的到,不過就還是遵守一下免的有意外。

第三點,測試其實 ESLint 有 RuleTester 可以拿來寫測試用:

const rule = require('../rule.js')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester();

ruleTester.run('no-parameter-e', rule, {
  valid: [
    'function a (event) {}',
  ],
  invalid: [
    {
      code: 'function e (e) {}',
      errors: [{ message }],
    }
  ],
});

很方便,都不用 test framework 了,並且有特別要求 valid 和 invalid 都要有 test case,不然測試就會失敗。

然後測試的時候是每個 rule 獨立跑,每個 plugin 可以有多個 rule,很多 plugin 是把不同 rule 都獨立一個檔案,每個 rule 可以丟的東西除了檢查外還有不少,像是說明文件、自動修復的動作等,詳見官方文件,我一開始是參考eslint-plugin-import的,不過現在初心者應該也可以先看我的eslint-plugin-no-parameter-e,東西更少一些。

下一個想來挑戰處理空行,看了一下感覺是比較困難啊~


此類別所有文章