CodeceptJS + puppeteer

看起來一切似乎都很美好,直到真的下去用。

這幾天就在這組合裡面打滾,昨天還花了幾乎半天在查一個問題,總之先條列一下目前覺得幾個重點:

  • CodeceptJS文件裡面有 code sample 用 generator function 的非同步取值,現在支援用 async await 了,不過 code sample 還沒改。
  • 每種 helper 可以用的 method 不完全相同,大部分一樣,不過也沒列出基本組合,所以好像也不是很好一組 test 測所有 helper。
  • Puppeteer helper 裡面其實有很多地方是直接跟 CDP(Chrome DevTools Protocol) 溝通的,這部分也可以印 debug log:env DEBUG="puppeteer:protocol" codeceptjs run --steps --verbose
  • 開 CDP 的 log 的話資訊量會超多,訊息內容還算好理解,細節網路上也有文件,左邊 sidebar 有很多不同領域的,上面的 DEBUG 參數也可以自己修改只顯示想要的,詳見puppeteer 文件
  • puppeteer 的page.goto有個選項是 waitUntil,預設是 load 事件,不過我發現這個事件有時候會觸發不到,雖然我看開發工具的 network 圖是有線出來,不過總之我後來會這樣的案例就先都改成networkidle2了。

然後昨天花很多時間查的問題已經上去發了issue,總之就是發點擊事件點連結後,要檢查新頁面的內容會出現錯誤:

Protocol error (Runtime.callFunctionOn): Cannot find context with specified id undefined

目前探究下來狀況應該是:puppeteer 的點擊回傳的 promise,在點擊完成就 resolve 了,這時候瀏覽器去開新網頁,才要開始發出請求,新的網頁還沒準備好,所以要做檢查的時候就會沒有 context。然後我有用 Nightwatch helper 測試過,是沒這問題的,總之就是個實做問題,puppeteer 目前這樣邏輯上也不算是錯誤的設計,不知道最後會怎麼修改,當然簡單一點就是 click 觸發 browser navigate 到別的網頁時就要等新網頁回來。目前的 work around 是自己多 wait 一下。

最後就是,我終於可以順暢的把 puppeteer 這個單字打出來了QQ


分號大戰 again

今天一早起來就看到有人說 TC39 要準備建議 JavaScript 程式碼應該要加分號:

然後下面就一大串了,本來想說standardjs要哭哭了,難道semistandard要扶正了嗎?不過我仔細端詳了一下,發現這個PR還是 open 狀態,而且 Brendan Eich 甚至表態反對:

原因之一是已經有很多 standardjs 的 code 其實運作的很好,不過另外一個原因我覺得更有力,就是 TC39 的文件,做為 spec 似乎不該提出建議,當然提出這個 PR 的 Daniel Ehrenberg 其實也不是單純因為支持加分號才提的,他其實是Class field declarations的主要貢獻者,這是什麼呢?就是:

class Counter extends HTMLElement {
  x = 0;

  clicked() {
    this.x++;
    window.requestAnimationFrame(this.render.bind(this));
  }
}

這種在 class method 外面定義 class 屬性和預設值的語法(另外還有 private property),而這種很像是 expression 的語句,一定要 semicolon,不然會有他稱為 AST hazard 的情形,也就是很難評斷開發者實際上意圖的情形發生,也就無法用 ASI 自動補分號,對此 Brendan Eich 有個建議是在 class field 裡面關掉 ASI 機制,也就是這些 property 定義一定要加分號做結。

目前看起來,Brendan Eich 提的那點,TC39 不該做語法的建議實在很強而有力,應該也反駁不了,所以結果應該就是沒有收 PR,或是改成更中性的文字,不是建議的文字,不過想來想去還是沒有比較適合的,畢竟在 spec 文件裡面還提建議、警告開發者用的語法就很怪。


Web F2E 看 Python Syntax

Bruce Eckel's keynote,

雖然主業是 Web Front End,不過其實要搞好 Front End,後端也不可不知,所以我工作內容其實也寫 Python 寫了不少,最近終於可以跟Flake8相安無事,所以想來記錄一些對我來說很有趣的 Python Syntax,不全是喜歡的就是了,以下內容以 2.7 為主。

內建支援 String Formatting

第一個我覺得很棒的是,Python 內建有String Formatting Operations可以用,超方便的,所以我只要寫:

'%d: The answer to the ultimate question of life, the universe and everything' % 42

就可以把 42 填進去字串裡面了,雖然我第一次看到放最後面還以為是什麼奇妙的註解符號;傳統的 formatting 用來做翻譯字串就會發現,如果有多個變數,它們的順序在不同的語言可能有不同,傳統的 formatting 只能處理固定順序,不適合這種情景,這時候還有新的format()可以用,幫變數命名好、然後丟參數進去就可以了,例如:

'{author} wrote {name}'.format(author='JRR', name='TLOR')

雖然 Python 的 string format 很好用,不過文件寫得太高深了,所以還有人做了pyformat.info這個站,收集了不少實用的範例幫助大家理解,而且仔細看過之後發現舊的格式也可以用 dict 格式丟命名變數進去。

Multiline String

多行字串也是我蠻喜歡的,像是要弄 template 的時候就很方便,JavaScript 一直到 ES6 的 tempalte string 才算是有內建,Python 就用三個引號框起來就可以了:

template_string = """<div>
                       Wow
                     </div>"""

不過很理所當然的,那些為了縮排所填入的空白,就都是真的字串內容,所以Wow前面就是有 23 個空白字元,如果字串在 class 或是 function 定義裡面,那空白就會更多,在一些使用情境下,空白數量是影響很大的,所以就會有到底該怎樣排的問題,StackOverflow 上就可以找到相關的問題,還好我處理 HTML template 的話,影響不大,當然結果會造成一些多餘的資料傳輸啦。

Keyword Arguments

很久以前我寫過一篇options object的文章,為的就是處理參數太多造成程式碼不好解讀的問題,沒想到 Python 可以在呼叫函數的時候,傳入參數的名稱,例如以下的函數:

def func(a, b, c, d):
    return a + b + c + d

呼叫的時候可以分別說明每個參數的 key 和 value 對應關係:

func(a=1, b=2, c=3, d=4)

而且也可以混用:

func(1, 2, c=3, d=4)

覺得這語法真是領先超多,當然 ECMAScript 現在可以用 destructing assignment 的語法做到類似效果,不過我覺得還是有些差距。

而針對 Keyword Argument 其實還有特殊的 syntax 是**kwargs,其實我一開始是先看到這個語法的,想說 Python 怎麼有個很像 C++ 指標的東西,看了許久,某天終於會意到 kw 是 keyword 的意思,然後才終於理解是怎麼回事,後來查資料才知道還有*args,現在的 ECMAScript 的話可以用...spread operator 做到。

Circular Dependency

恩,可以做到循環相依,第一次看到真是覺得不可思議,不過後來慢慢瞭解限制,大概也知道怎麼實際上是如何跑的了。

Ternary Operator

三元運算,Python 的語法真的是比較特別一點,其它語言比較常看到的是用?,不過 Python 是用後置的if else

reality = True if isReal else False

其實我還蠻喜歡後置的if語句,第一次看到這種寫法是在 CoffeeScript,我很常用在一些參數特殊狀況的處理,一樣 CoffeeScript,不用後置if的時候:

filename = file.name

if file.hash
  filename = filename + '-' + file.hash
  
if file.ext
  filename = filename + '.' + file.ext

用了後置if的話可以寫成:

filename = file.name

filename = filename + '-' + file.hash if file.hash

filename = filename + '.' + file.ext if file.ext

看起來整齊許多,視覺上(?)少了一層縮排,不過 Python 的三元運算,和 CoffeeScript 的後置if語法是不一樣的東西,雖然可以用來做類似的事情,但是因為他是三元運算,所以一定要提供else區段:

filename = filename + '.' + file.ext if file.ext else filename

就比較不喜歡這樣就是了。

Tuple

Python 的 List 資料型態可以比做 JS 的 Array、Dict 可以比做 Object,兩種資料型態分別是使用中括號和大括號,不過在 Python 語言裡,還有一種用小括號的 Tuple 資料型態。

Tuple 資料型態似乎還蠻少見的,我第一次聽到這個名詞的時候是在學校學資料庫系統的時候,一筆資料稱為一個 Tuple,不知道為什麼印象很深,然後第一次看到使用 tuple 的程式碼自然是不太理解,不過還算直觀看的懂,後來不知道為什麼查到這種語法其實是一種資料型態叫 Tuple 的,意義上和資料庫系統的 Tuple 感覺還蠻像的,理解這是個資料型態之後用起來覺得順手很多,而且 Python 還蠻自由,很多地方和 List 都可以用一樣的操作,像是in運算,或是作為 function 的多個回傳值(多回傳值的函數也蠻方便的)。

in 運算

上面提到的in運算,用來判斷一個 List 或 Tuple 是否包含特定元素:

if target.stat in ('ACTIVE', 'PREMIUM')
    ok()

對於常在古早 JS 開發的人,真的是超羨慕的,可能有人說可以用indexOf做,雖然 JS String 的 indexOf 很早就有了,但是 Array 的 indexOf 卻是到 ES5.1 才正式進標準,IE9 之前的都不支援,所以要用他來判斷一個元素是否在一個陣列內,首先要確定你不支援 IE8 之前的瀏覽器,不過就算支援,其實程式碼也沒in運算來的漂亮,後來 ES2015 有個比較好一點的Array.includes可以用就是了。

Dict

Dict 可以比做 JS 的 Object 比較好理解,對於這個我不能適應的有兩個地方,一是 Dict 不是 class,所以不能直接用.取屬性,一定要用[]或是內建的get(),再來就是用[]取屬性的時候,一定要 key 存在,用到不存在的 key 就會噴錯誤,如果一定要這樣操作就要改用get(),get 還有一個特點是可以給 default 值,如果是複雜的結構,想要一口氣很深入就可以寫成:

data.get('attr1', {}).get('attr2', {}).get('attr3', None)

實在是有點難看,CoffeeScript 是有 Existential Operator 可以做這種多階層的取值:

data.attr1?.attr2?.attr3?

在 TC39 的草案也有類似的Optional Chainging,這兩樣都是上一篇文章有提到的東西。

Unix Timestamp

內建的 datetime 似乎沒有支援直接輸出 Unix Timestamp,是說目前有需要都用Pendulum,還蠻好用的,API 介面也蠻直接,也有完整的時區、Period、Duration 等觀念。

PEP8, Flake8

文章一開始提到的 Flake8 把好幾個 code checker 包進去,包括了官方的PEP8、PyFlakes、pycodestyle 等,我用 Vim 的Syntastic都有支援,只要有安裝就會偵測到執行檔,然後就可以用來檢查了,一開始裝起來的時候就和第一次用 JSLint 一樣傷感情,不過兩個月過後到是還蠻適應的,其中比較和以往習慣不一樣的就是 function 參數的值,不論是定義時的 default value 還是呼叫時的 keyword argument,=的左右兩邊都是不加空白的,例如:

def hello(name='John'):
  return 'Hello ' + name

hello(name='Hancock')

另外就是特殊情況需要循環相依,或是 import 但是不使用時,會需要關閉一些檢查,可以在該行末端加上註解關閉特定項目:

import pages  # noqa: F401

錯誤的編號可以參考 Flake8 的文件

Python Enhancement Proposals (PEPs)

之前在研究 Joda Time 的時候,發現 Java 有個JSR(Java Specification Requests),在找 Python 的 coding style 的時候則是發現了PEPs(Python Enhancement Proposals),不過 ECMAScript 目前是沒有類似的、完整的收集並編目各個 Proposal 文件的地方,甚至連語言本身的官網都沒有啊...XDrz


問號出頭天

Mario

剛剛掃了一下 TC39 新的草案,發現和?相關的還不少,稍微來介紹一下這幾個很初期的草案吧:首先第一個是已經見過一陣子的 Optional Chaining,第一次看到這種語法是CoffeeScript,在 CoffeeScript 現在是叫 Existential Operator,不知道是不是以前就這個名字,對付多階層的物件特別好用:

let zip = lottery.drawWinner?.()?.address?.zipcode

可以像這樣用,中間任何一層回傳 falsy value 就會直接把值給 zip 變數,而不會繼續往下找,不會造成 Script 執行錯誤,不過目前看起來對於 function 的處理比 CoffeeScript 麻煩一點,要寫成?.(),而不是?(),其實我覺得也比較醜一些。

第二個是Nullish Coalescing,這是正港的用來設定變數 default 值用的,以前通常的作法是用||

function (option = {}) {
  let quick = option.quick || true;
}

在上面的範例中,quick預期是 boolean 值,可能是trueflase,預期的預設值是true,不過這樣寫其實,如果傳false進來會誤判,結果會變成用預設值的true,新的 Nullish Coalescing 就是要來解決這個問題,把||換成??

function (option = {}) {
  let quick = option.quick ?? true;
}

這樣傳false時就不會被當成沒傳值,只有nullundefined才會用預設值,其實判斷的方式和第一個 Optional Chaining 一樣。

第三個是Partial Application,其實就是字面上的意思,不過是從 syntax 上就支援 partial 執行 function:

function add(x, y) { return x + y; }

const addOne = add(1, ?); // apply from the left
addOne(2); // 3

我覺得用?這招還蠻漂亮的,雖然我還不太有機會用到 就是。


Temporal - 下個世代的 Date

這篇文章寫到快寫完的時候,決定到Modern Web 2017分享,所以就比較晚發佈 ,其實 Modern Web 演講內容比較多,文末有放相關參考資料。

JSConf EU 2017 前陣子放出演講影片,蠻多場次都不錯,這篇要主要是從其中的一場演講而來,演講是「 The Past, Present, and Future of JavaScript Date and Time APIs」,講者是 Matt Johnson,Moment.js的作者,下面是這場演講的影片:

長度不長,推薦可以看一下,主要是在談 JavaScript 的 Datetime,提出這老東西的問題,我覺得可以稱為 WAT JavaScript 的 Datetime 篇,像是 0 起始的月份、不支援 Time Zone、難以運算、是 mutable 物件等等,接著介紹了目前檯面上比較多人用的幾個 library 和他們的特色,都是品質不錯的 library,有需求的可以從中選用,包括了:

最後則是提到他們目前在進行的,改進 JavaScript Datetime 的計畫,也就是新的 ECMAScript Datetime 的 proposal,叫temporal,除了 Matt Johnson 之外,還有一位 Microsoft 的 Maggie Pint 也是目前草案的主力推手,他的 blog 上就有兩篇相關的,裡面有列出目前Date的主要問題:

  1. 不支援 timezone,只有 UTC 和 local
  2. Parser (轉譯日期字串轉成日期物件)的行為不可靠且難以使用
  3. Date object 是 mutable 物件
  4. 日光節約時間的行為無法預期
  5. 日期計算 API 很難用
  6. 不支援Gregorian以外的日曆(例如農曆)

事實上,目前的 Date 物件,當初 Brendan Eich 因為時間緊迫,所以 Datetime 的 API 是直接參考 Java 的,當時是 1995,參考的應該是 Beta 版 Java 的java.util.Date,後來 1996 年 1 月 Java 1.0 發佈,但是到了 1997 年 2 月的 Java 1.1 發佈時,java.util.Date大部分的設計都被捨棄了,然後 1997 年 6 月,ECMAScript 標準 1.0 發佈,結果這個在 Java 語言只活了 1 年多的設計,就活在 JavaScript 世界活了 20 年之久,相信有用過的人都能多少都知道使用起來的痛苦。

不過要改善 JavaScript 從來就不是一件容易的事情,最大的困難點就是你不能隨便改動任何已有的東西,像是已經存在 20 年的Date,即使它設計不好,隨便改動都可能造成大量的網站壞掉,不像是 Java 1.0 升級到 Java 1.1 那樣,各自用各自的,在改善 JavaScript 時基本上就是要當成有人從不升級,不能有 broken change,最簡單的方法就是增加新的東西,而不要去修改舊的,這也是目前 temporal 的方向(其實 ES 5.1 後,舊有的東西該修的東西大概都修完,之後就是一直加新的而已),在 draft 文件已經有一點基礎和預期的 code sample 了:

var ldt = new CivilDateTime(2017, 12, 31, 23, 59);

var addHours = new CivilDateTime(2017, 12, 31, 23, 00)
    .add(2, 'hours');

var zdt = ldt.withZone('America/New_York');

可以看到有方便的加減時間的 API、immutable 特性、還有時區支援等等。事實上這份草案還非常初步而已,還缺非常多細節,預期會有的物件目前已經有八種了,不過這八種物件的 API 也都還沒定義完,不過也正因為如此,想參與的人反而這時候比較有機會提供想法,推薦有興趣的人可以關注關注,給點意見。

最後列一些參考文件:



NodeJS and ES Module

香港 2016

今天看了 TC39 一月會議的 Agenda 後才注意到,nodejs 用的CommonJS ModuleECMAScript Module(ES Module) 在特定情況下會有混淆的情形發生,所謂的特定情形就是沒有import/require也沒export/exports的模組,例如寫東西在 root 物件上,只產生 side effect 的模組:

(function (root) {

  root.lib = {};

}(this));

像這樣的檔案,Parser 就無法判斷他是 CommonJS Module 還是 ES Module,這樣會產生什麼問題呢,其實 ES Module 有一些特色,例如它必須要使用 strict mode 來解析並執行,而光是這個差異,就會讓相同的程式碼有不一樣的執行結果了,而需要同時支援 CommonJS Module 和 ES Module 的主要是 NodeJS 環境,當然它目前還沒有兩種都支援,但是勢必需要支援 ES Module 的,所以 NodeJS 需要能夠百分之百正確的判斷每個 JavaScript 程式碼是屬於 CommonJS Module 還是 ES Module,這在目前是辦不到的,也因為這個問題所以 NodeJS 雖然已經支援大部分的 ES2015 的新功能,但卻遲遲還無法支援 ES Module,相關的討論至少也半年有了,當時還提出了新的副檔名.mjs這種解法,多一種副檔名聽起來有點不可思議,也引此還有個 Twitter帳號專門在關注相關情報的,不過目前最新的解決方法,則是 ES Spec修改Module 的 Grammer 來解決這個問題,修改的方式是就是以後 ES Module 一定要至少有一個import或是exportstatement,如果是上面那種沒有需要 import 也沒有 export 的模組,那就要加上export {},變成:

(function (root) {

  root.lib = {};

}(this));

export {};

語意上剛好等於沒有匯出任何東西,所以不會和現在的 ES2015 版的 Module 有衝突,這份提案已經是 accept 狀態了,所以沒意外應該今年的 ES2017 就會包含進去了,當然這會影響到以前寫出這種 ES Module 的程式碼,不過目前也還沒有那個環境有直接使用 ES Module 的能力,都還是先過 bundler 轉成現在環境可以使用的形式,Web 的<script type="module">也才正要有瀏覽器支援,所以這個時間點做出這個修改影響還算是很小,之後大概就是有記得應該就沒問題了,我自己是比較期待 nodejs 能快點原生支援 ES Module 啦。


smartypants.js

大阪新年

SmartyPants這個東西也是很久了,和 Markdown 差不多時期,都是 John Gruber 幫當時的網路文字出版軟體(ex: MovableType)所做的,而 SmartyPants 是用來處理一些標點符號的,其中,最容易被人注意到的就是引號「"」的轉換了。

雖然鍵盤上的引號按鍵只有一個,但是傳統的文書寫作上,引號是有分左邊(開始)的「“」和右邊(結束)的「”」,只不過早期為了減少鍵盤按鍵數,還有字元編碼上的限制,所以合併成為只有一個,不過隨著計算機的發展,可以使用的字元編碼資料量增加後,就還是有定義了開始和結束的兩種引號,並且有單引號和雙引號兩種:

  • 左單引號 ‘
  • 右單引號 ’
  • 左雙引號 “
  • 右雙引號 ”

理想上,寫作文章時也應該正確的使用這些引號,不過其實因為輸入上比較麻煩,一般人打字也不會特別注意,所以常常被忽略,比較常見的是軟體本身在使用者輸入文字時自動做轉換,例如 Apple 的 Pages、Keynote、微軟的 Skype 等等,講到 Keynote 自動轉換引號這點就要另外岔題一下,就是偶爾都會看到頭影片裡面的程式碼,其中的引號也被轉換過,其實就是因為貼上 code 到 Keynote 的時候被轉換了,如果作者沒有注意到的話就直接釋出,然後讀者 copy 程式碼出來試試看時,就會編譯失敗而無法執行。回到標點符號上,這類標點符號其實不止有引號,SmartyPants 可以處理的還包括:

  • --轉成 en-dash –
  • ---轉成 em-dash —
  • ...轉成 ellipsis …

而且它對於引號的轉換還算是聰明,會判斷是不是真的用來包起文字的,還支援用 backtick 來模擬的雙引號,看起來像:

``quoted string''

另為也會針對年代的特殊寫法做處理,例如'80s會轉成 ’80s,並且會避開 HTML 標籤的部分,不會把 HTML 標籤裡面的屬性值的引號也做轉換,像是<html lang="en">這種。前陣子因為工作上要處理翻譯字串,想要順便好好的處理這些符號,所以就想到了這個工具,因為我一般寫小 script 做事情是用 JavaScript 然後用 node 來執行,所以要用 SmartyPants 就沒辦法那麼直接了,一開始先找 JavaScript 的 solution,找到一個簡單的,只用幾條 regular epxression 的版本,其實運作的也還不錯,但是無法避開不處理 HTML 標籤,所以產生出來的結果也不能用,接著改成用 STDIO 丟給 John Gruber 的 Perl 版,結果這效率實在太差,所以又繼續研究一番,發現還有一套叫typogr的文字處理工具有實做,不過不想要太多功能,所以最後我決定自己把 Perl 版 SmartyPants 移植成 NPM module,也因此有了smartypants.js

這次開發我選擇的語言是 TypeScript,原因可以參考我上一篇文章20k-for-of,然後用 Makefile 加上一些指令轉成類似jQuery UMD形式的 JavaScript 檔案發佈到 NPM 上,目前的 smartypants.js 是完完全全把 Perl 版的邏輯翻過來,不過只有實作轉換編碼的部分加上 CLI 的介面,另外 Perl 版的只能轉換成 HTML entity 的格式,不過現在 UTF-8 已經算是很廣泛應用的文字編碼了,所以我還加上了一個轉成 UTF-8 字元的版本,用 UTF-8 編碼的雙引號其實很不錯,在 JSON 或是 csv 裡面也不用 escape,肉眼看起來也美觀許多。

實做過程比較緊張的地方大概就是要把 Perl 的 regular expression 轉成 JavaScript 的版本了,一度看到沒看過的用法都覺得會不會沒辦法用 JavaScript 做起來,還好沒用到很神奇的語法。另外它處理 HTML 語法的部分,其實是先過一個簡單的 lexer 轉成 token list,裡面兩種 token 分別就是一般文字和 HTML 標籤兩種,接著只針對一般文字 token 來做標點符號轉換,不過其實這個 tokenize 的部分有 bug,例如:

<span title=">">HAHA</span>

就會被分解成如下的 token:

  • <span title=">
  • ">HAHA
  • </span>

而且這種方法就也無法處理到一些文字內容的 HTML 屬性,例如 title 和 alt 等,所以接下來還可以做的改進,一個就是改進這部分的 lexer,然後也要對這些屬性值轉換標點符號,大概需要建立一個白名單,另外還有一個想加強的地方就是補上 test case 了,目前完全沒有相關的 test case 可以用,所以其實也沒很正式的驗證過,不過總之目前是都還運作正常,如果有相似需求的人可以參考一下。


20K for...of

for...of是 ECMAScript 2016 的新語法,有了他之後,要用迴圈跑過陣列不用像以前一樣先用for...in或是用傳統的取長度,然後i++的方法:

var arr = [1, 2, 3];
var i, v, len;

for (i in arr) {
    v = arr[i];
    console.log(v);
}

for (i = 0, len = arr.length; i < len; i++) {
    v = arr[i];
    console.log(v);
}

現在只要用簡單的for...of就可以了:

var arr = [1, 2, 3];

for (let v of arr) {
    console.log(v);
}

不過目前還是需要考慮只有 ECMAScript 5 的環境,例如 IE11,所以一般都還是會用像是Babel之類的 transpiler 來把 ES2015 的 syntax 轉成 ES5 的 code,結果轉出來如下:

"use strict";

var arr = [1, 2, 3];

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
    for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
        var v = _step.value;

        console.log(v);
    }
} catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
} finally {
    try {
        if (!_iteratorNormalCompletion && _iterator.return) {
            _iterator.return();
        }
    } finally {
        if (_didIteratorError) {
            throw _iteratorError;
        }
    }
}

結果其實有點意外,一個簡單的for...of竟然變的這麼長,事實上是因為for...of其實沒想像中簡單,因為它可以用的地方其實不只是陣列,而是iterable 物件,不過為了要完整的支援for...of,就變成需要有 iterator, generator, symbol 等等的支援,當然上面的程式碼不能在 ES5 環境下執行,而 Babel 依靠的是babel-polyfill,裡面其實就是core-jsregenerator,不過這一整包,其實有點龐大,要 228KB,即使最小化之後也還要 95KB,所以,就想著是不是能夠只捆包進需要的部分就好了,研究過後,發現有 Babel plugin 叫做transform-runtime,套用上去後:

import _getIterator from "babel-runtime/core-js/get-iterator";
var arr = [1, 2, 3];

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
  for (var _iterator = _getIterator(arr), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
    var v = _step.value;

    console.log(v);
  }
} catch (err) {
  _didIteratorError = true;
  _iteratorError = err;
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError;
    }
  }
}

可以看到原來用Symbol取 iterator 的地方變成用_getIterator了,而且還有一行:

import _getIterator from "babel-runtime/core-js/get-iterator";

如果要真的把這部分也打包進來,則需要讓 bundler 處理,我個人是偏好rollup,搭配以下兩個 plugin:

然後用以下的設定:

babel({
  exclude: 'node_modules/**',
  plugins: ['transform-runtime'],
  presets: ['es2015-loose-rollup'],
  runtimeHelpers: true
}),
nodeResolve({ jsnext: true }),
commonjs({
  include: 'node_modules/**'
})

結果,就可以得到夢寐以求的 20KB 的程式碼了,當然 20KB 的部分不是預期的啦,相較於一開始的程式碼只有 72Bytes,為了一個for...of變成 20KB 好像有點本末倒置,畢竟我只有要在 Array 上用,難道不能只是簡單的轉成for...in型式嗎。

事實上是有辦法的,第一個就是改寫TypeScript,TypeScript 對於for...of只有兩種處理方法,而且結果都不會如此膨脹,第一種就是變成for...in,第二種則是不變動,保留for...of的語法,後者是在 target 設定成 ES6 的時候使用的,官網也有相關的說明

第二種方法則是用Bublé取代 Babel 做為 transpiler,Bublé 是 rollup 的作者Rich Harris的另外一個作品,我個人是蠻喜歡他的哲學的,Bublé 的哲學則是對於 code 做簡單、直接明瞭的轉換,所以for...of就只會轉成for...in的型式,不過也因此無法支援 iterable 物件,所以預設是不開啟支援的,歸類在dangerious transofrm 之下,另外 Bublé 也還不支援 Async/Await,因為要做出支援 ES3/5 的同樣效果的 code 會增加太多的複雜度,不符合他的哲學理念,所以目前還沒有計畫支援,這點倒是 TypeScript 支援比較完整,目前的2.1 RC已經支援把 Async/Await 轉成 ES3/5 的版本了。

最後結論,基本上就是個取捨,Babel、TypeScript、Bublé 各自有它們的優缺點,所以只能看情況選擇了,如果要 Map/Set 也要在這些物件上用for...of語法然後也要 Async/Await,那就只能用 Babel 加上 babel-polyfill;如果可以不要 Map, Set 或是可以接受不在這些物件上使用for...of語法(還可以用 forEach),那可以選擇 TypeScript,然後加上 Map/Set 的 polyfill,如果不用 Async/Await,也不用 Map/Set 的話,可以考慮用個 Bublé 看看。不過如果完全不需要考慮 ES3/5 的環境的話(Edge, Firefox, Chrome 都已經對 ES2015 支援很完整了),好像問題突然就小很多了XD,最後附上這篇文章提到的各種作法產生的檔案參考,目前都放在 github 上的20k-for-of這個專案。


Input Event

Playtime Credit Card,

今天做了一個特殊的 input 欄位,其實目標只是做成類似像輸入信用卡號那樣,輸入1234完,準備要輸入5的時候,會在4後面補上一個-,變成1234-5,不過我預期做的完美一點,所以考慮了很多狀況,例如:

  • 複製貼上沒有-的資料後會自動格式化
  • 已經輸入一部分資料後,游標移到前面插入資料也會正確格式化
  • 直接用DELBackspace來刪除資料,要讓使用者感覺不到-
  • 先選取一些字元然後用DELBackspace甚至是剪下來刪除資料後會重新格式化
  • 以上幾種操作都不會讓游標亂跳

簡單看過目前一些信用卡相關的 library,在卡號輸入的部分是沒有全部達到的,要達成這些目標,幾乎是等於每個使用者的操作都要攔截下來,然後要抓到當欄位內的值,會用到的事件包括了 keyup、keydown、paste 和 input,等,其中本來我對於一般使用者敲打鍵盤輸入的事件是用 keyup,keyup 事件後會判斷游標位置和輸入的內容,如果需要的話就加上-,然後調整游標位置,通常是 +1,弄好後測試一陣,發現如果按鍵輸入很快的話,游標位置會亂掉,應該要 +1 的卻錯過了,深入除錯一陣子之後發現,keyup 事件其實和欄位內的 value 變更是非同步的,所以不能確保 keyup 事件拿到的欄位值是正確的,能確保欄位值正確的,其實是input 事件,不過 input 事件沒有 keyCode,所以只能自己判斷輸入了什麼,另外刪除內容時也不會觸發 input,還好DELBackspace是用 keydown 事件來處理,兩邊剛好錯開了。

雖然 input 事件似乎很好用,不過其實它在早期的時候支援度是不太好的,算是比較新的事件,有類似狀況的還有一個是 change 事件,我的印象中是某些瀏覽器的行為會不太正確,所以其實我一直都還不太使用,至於 input 事件,我則是需要在不支援的瀏覽器中 fallback 到 keyup 事件,所以就會需要偵測,找了一下在 Modernizr 有支援,仔細看一下內容其實可以發現不是很好偵測,然後我也不是很喜歡 Modernizr 的介面,所以目前用的是在 ModernizrIssue 210裡面 AndyE 提供的版本,稍微精簡一些:

var inputSupport = "oninput" in document.body || checkEvent(document.body);
/*
   The following function tests an element for oninput support in Firefox.  Many thanks to
        http://blog.danielfriesen.name/2010/02/16/html5-browser-maze-oninput-support/
*/
function checkEvent(el) {
    // First check, for if Firefox fixes its issue with el.oninput = function
    el.setAttribute("oninput", "return");
    if (typeof el.oninput == "function")
        return true;

    // Second check, because Firefox doesn't map oninput attribute to oninput property
    try {
        var e  = document.createEvent("KeyboardEvent"),
            ok = false,
            tester = function(e) {
                ok = true;
                e.preventDefault();
                e.stopPropagation();
            }
        e.initKeyEvent("keypress", true, true, window, false, false, false, false, 0, "e".charCodeAt(0));
        document.body.appendChild(el);
        el.addEventListener("input", tester, false);
        el.focus();
        el.dispatchEvent(e);
        el.removeEventListener("input", tester, false);
        document.body.removeChild(el);
        return ok;
    } catch(e) {}
}

測試困難的主因是 Firefox 4 有 bug,所以需要真的建立一個 input 元件,然後用完整模擬 input 事件。然後雖然這個版本的比較精簡好懂,不過之後還是會因為 license 的關係改用 Modernizr 的版本吧。至於我做的 input field 呢,現在當然還是公司資產,大概要等我有空在假日重寫一個 Credit Card 的版本才會放出來吧。


此類別所有文章