2016

北京天安門

又到了一年一度的照片回顧了,一樣也是一個月基本上挑一張。首先是一月的北京天安門,當時是去參加公司年會,這張照片拍攝的位置是無法步行走到的,我是搭巴士經過的時候拍的,那天的空氣也還不錯,隔天晚上空氣品質整個就爆炸了。

二月則是我第一次參加的 SITCON,也是記錄組,其實我覺得 SITCON 這幾年的活動完成度都蠻高的啊,我想一部分原因也是因為學生比較有時間能投入吧,2016 的 SITCON 我印象最深刻的是整套的過場畫面,基本上觀眾是看不到切換畫面的,一切都有做到無縫轉換。

SITCON 2016

三月是剛購入的 Ricoh Theta 拍的,本來也想要寫篇文章介紹一下,不過目前難產中,之後看看有沒有時間吧。

101

四月的照片是五月舉辦的 g0v summit 在四月辦的工人同樂會,其實五月 g0v 也有一張很喜歡不過後來還是選了另外一張。

g0v summit 2016 工人同樂會

五月的照片則是挑了 PF24 的綾波零的 cosplay,今年拍的 cosplayer 是比較沒有印象深刻的,最近也越來越少去了,去得太頻繁的話其實也會看到很多重複出場的玩家,感覺有點浪費時間XD。

PF24 Cosplay

六月因為 HKOSC 的關係去了一趟香港。

香港 2016,

七月是 HITCON CMT 的工人大會,實際上活動是八月辦的,今年我只有協助 CMT 的部分,年底的 Pacific 就沒進去幫忙了,主要是現在工作比較忙碌,而且常常去幫幾個整天後就會生病,只好減少場次。

HITCON 2016 CMT 工人大會

八月是 iTHome 辦的 Modern Web 研討會會場,會場在松山菸廠那邊的台北文創大樓,平常不太有機會可以進去,會議廳那邊有大片落地窗可以看到停工的大巨蛋,主辦單位則是很善用這片玻璃窗,讓他變成留言板,可以用白板筆留言

Modern Web

九月整個月沒有拍照,所以改拿六月去的瑞士,特別挑的照片是歐洲最高峰少女峰,不過一共有三張,第一張是少女峰車站的近照,最左邊的山峰就是少女峰了,右下小點就是觀光客了。

Jungfraujoch

第二張則是回程在山腳下拍的少女峰車站,可以下載原始檔案找找看車站在哪,然後第三張是在差不多地方用望遠鏡頭拍的,可以比較一下大小差異。

Jungfraujoch

Jungfraujoch

十月是在往 MOPCON 的火車上,突然發現到,台鐵車上跑馬燈的點陣字形還蠻不錯的,字體蠻好看之外,中英文混排也沒也有不協調的問題(不過照片上沒有),有人說可能是以前倚天系統的點陣字集。

台鐵

十一月是 Firefox 慶生會,今年是第十二年了,地點是摩茲工寮。

Happy Birthday Firefox

十二月是月底去日本跨年的照片,這是第一次在日本跨年,也第一次逛了一下日本的神社祭典攤位,雖然時間比較早,和新年參拜的人潮錯開,不過也因此可以歸在 2016 年十二月~

伏見稻荷大社


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這個專案。


Vim Filename Complete

Vim Filename Complete,

Vim 有一個內建的自動補完功能是針對檔案名稱的,使用的方法是<C-X><C-F>,我目前在維護的autocomplpop也有支援這種補完模式,只要輸入./後就會自動幫忙觸發,不過我比較有機會觸發到是在使用 ECMAScript 6 的 import 和 CSS 的 import 時,不過常常就是發現他查看的路徑不太對,不是拿目前編輯檔案的位置做為起點的,研究過後發現是因為 Vim 找檔案的起點是看他的工作目錄($PWD),加上我會使用ctrlp這種工具,所以實際上在編輯的檔案通常是不在工作目錄下,對於這個問題,其實我覺得最理想的解決方式是 Vim 應該要提供兩種模式來決定要從那邊開始找,不過目前似乎沒這個計畫,唯一在文件是有提到的是未來可能會支援path的設定,理論上,如果有支援的話,應該就可以解決問題了,因為預設的path值包括了.,不過目前還沒有相關時程,就只能自救了。

最簡單的方法,其實就是開啟autochdir,這個選項打開後就會自動在切換 window 時也更改工作目錄,不過這個選項是為了相容早期系統才提供的,文件也有說可能會和部分 Vim Script 不相容,實際上我也有找到一些不相容的 Vim Script,所以想避免,就搜尋了一下其它可能的解決方法,在 StackOverflow 上有看到一篇,裡面有兩個人提供了解法,第一個是用autocmd,然後在進入 insert mode (在這時候才有機會用到檔名補完的功能)時自動開啟autochdir,離開時自動關閉autochdir,不過這樣的方式(感覺上)還是不太安全,因為還是用到autochdir,所以下面有另外一個方法改用lcd,作法是改成修改 Key Mapping 的方式,改的 mapping 是./<C-X><C-F>,不過這樣對我來說又不合用,因為我用 autocomplpop 的話,不會真的打<C-X><C-F>,所以基本上觸發不到這事件,所以我就決定把這兩種解法合併起來,改成用autocmd加上lcd

:autocmd InsertEnter * let save_cwd = getcwd() | execute 'lcd %:p:h'
:autocmd InsertLeave * execute 'lcd' fnameescape(save_cwd)

進入 insert mode 時改變該 window 的工作目錄,離開 insert mode 時把工作目錄還原。這是我目前認為影響最小的調整方式,不過其實可能執行一次lcd換工作目錄就夠了,沒深入研究 autochdir 所產生的問題,不過我推測是影響到 Vim Script 建立的 window 的工作目錄,像是 NERD Tree 之類的側邊欄那種,總之目前這樣運作還算正常,接下來就是等 Vim 加上path的支援吧(或是有人送 patch)。


➡ 看看其它文章