CoffeeScript的全局變量污染與Node.js的模塊加載

最近發現Continuation.js的一個Bug:命令行使用-c開啓緩存模式的時候,有時候更新了代碼緩存不會更新,這個Bug時隱時現,難以捕捉。今天發現這個Bug升級了,不僅僅是在緩存模式的時候會有問題,即時沒有開啓-c一樣會發生這個問題。再到後來發現這個Bug只在CoffeeScript代碼中出現,於是就鎖定了目標開始調試。

Continuation.js和CoffeeScript一樣支持動態加載編譯,就是可以在Node.js中使用require的方法加載原始代碼,運行時編譯。這樣的好處不言而喻,給用戶提供了一致而透明的接口,無需事先編譯好再加載。具體的實現方法是,修改require.extensions下面的回調函數,把加載定向到自定義的函數來處理,最後再調用require.main.compile運行編譯後的代碼。

requiremodule是Node.js運行時的兩個重要變量,所有模塊的運行其實都是在一個這樣的函數中的:

function(module, require) {
  // Your code
}

所以requiremodule是模塊內的全局變量。require.extensions是一個對象,用於根據擴展名註冊require的回調函數,默認情況下,require.extensions是這樣的(Node.js 0.10.12):

{
  '.js': function (module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
    module._compile(stripBOM(content), filename);
  },
  '.json': function (module, filename) {
    var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
    try {
      module.exports = JSON.parse(stripBOM(content));
    } catch (err) {
      err.message = filename + ': ' + err.message;
      throw err;
    }
  },
  '.node': function () { [native code] }
}

在Node.js代碼中使用require(filename)時,實際上會根據filename的後綴擴展名依次來選擇加載的回調函數,例如我想增加一種自定義的自動加載類型.byv,只需設置require.extensions['.byv']即可。同理,也可以修改已有的後綴的加載函數,Continuation.js就是這麼做的(修改了.js文件的加載函數)。

爲了透明支持CoffeeScript,Continuation.js修改了.coffee的加載函數,在自定義的回調函數中調用CoffeeScript模塊,調用CoffeeScript編譯,然後再使用Continuation.js編譯。問題就在這裏,是加載CoffeeScript的時候require.extensions['.coffee']被修改了。閱讀CoffeeScript的代碼(版本1.6.3),發現在'coffee-script'模塊中,有這麼幾行代碼:

if require.extensions
  for ext in ['.coffee', '.litcoffee', '.coffee.md']
    require.extensions[ext] = loadFile

這段代碼不應該在加載CoffeeScript模塊中運行,而應該在通過命令行運行coffee命令的時候運行,可惜CoffeeScript沒有注意到這一點,應該算是一個Bug吧。給CoffeeScript提交了一個推送請求https://github.com/jashkenas/coffee-script/pull/3054 ,等待審覈中。

更新:這個issue被標註爲重複,已經在 https://github.com/jashkenas/coffee-script/issues/2323 合併了,CoffeeScript 2.0.0(未發佈)以後默認已經取消這個特性了。

寫書小記

我的書《Node.js開發指南》將要在7月10日左右出版上市,回想過去一年花費的心血,不禁感慨良多。藉此良機,我記錄了一下過去一年的些許經歷,向大家大致介紹一下這本書是如何誕生的。

我已經忘了是從什麼時候開始接觸到Node.js的了,大概是2009年末或2010年初吧,那時Node.js還不像現在這麼火,祗是剛剛誕生的衆多前途未卜的新技術框架之一。一言以蔽之,Node.js就是一個讓JavaScript運行在瀏覽器之外的東西。說起JavaScript,市場上JavaScript的書幾年前就已經氾濫了,許多書的一個共同特徵就是介紹以瀏覽器爲基礎的JavaScript,大多都是花很少篇幅介紹JavaScript語言本身,花大量篇幅放在DOM、BOM的分析或jQuery這樣的框架上。想想看這也是理所應當的,如果在當時不介紹瀏覽器中的開發,學習JavaScript有什麼用呢?

說起Node.js,它本質上並不是什麼新東西,服務端的JavaScript十幾年前就有了,異步程序設計也不是什麼新思路,祗是把它們結合起來放在服務器端,多少看來像一個異端。我呢,雖然對前端開發有所涉獵,但並不工於此道,偏偏我又對JavaScript這個語言有所偏好,Node.js簡直就是爲我量身定做的了。不知爲何,Node.js這個「異端」就這麼火起來了。2010年是Node.js飛速發展的一年,它從一開始的默默無聞突然間變得大紅大紫,這是我接觸之初都始料未及的。2011年它的發展更是迅猛,衆多巨頭都加入了支持,甚至連開源宿敵微軟都伸出了橄欖枝,這讓Node.js實現了真正的完全跨平臺。

任何一個新技術在誕生之初遇到的最大的障礙就是文檔的匱乏,這一點祗有喜歡嘗試的人纔有所體會。早期的Node.js文檔非常糟糕,沒有任何教程,也沒有成熟的「解決方案」供參考。遇到問題以後上Google搜索一般都是沒有答案的,能依靠的祗有去郵件列表詢問。詢問還不一定有結果,所以很多時候要閱讀源代碼。

2011年7月,我剛剛放暑假,想想閒來無事,不如寫一個Node.js的入門教程。想到便做,於是我打開當當網,在上面搜索了關鍵詞Node.js,期望能有什麼已經出版了的書以供參考,但沒有任何結果,影印版也沒有,這並不令我意外。我又上Amazon.com搜索,令我始料未及的是竟然纔祗有一個搜索結果,那便是當時還未上市的《Node Web Development》。在我的印象中,凡是在國內買不到的書在Amazon上肯定有英文版,但這次竟然Amazon也沒有!看來這個技術實在是太新了,導致市場上還沒什麼書。當下我的心中便萌生了一個大膽想法,我何不寫第一本書呢?

這個想法簡直太猖狂了!看看很多書的作者都是大學教授,或者產業界的領軍人物,有無數的經驗。而我呢,纔不過一個大二的學生,寫的書會有人看嗎?甚至連能不能出版都是個問題。此外我除了有一個維護了五年的博客以外,沒有任何寫作經驗,要寫一本書談何容易?也算是初生之犢不畏虎,我當時就決定大膽去嘗試了,反正大不了就放到網上大家看唄。儘管完全不知從何下手,但想起某些國外的書上作者寫着自己是用LaTeX寫成的,所以就從學LaTeX下手吧。於是我花了一個星期看各種LaTeX的入門資料,學會了編輯一些簡單的文檔,看了許多高人留下的宏包和模板,但LaTeX的繁複還是遠超我的預期,想排版出一本書真是太難了。挫敗感第一次擊敗了我,五分鐘熱度過了以後,我對寫書也提不起興趣了。後來我纔知道作者其實不用太關心排版的,出版社有專業的編輯,而且他們都用Word……不過我找到了更好的工具Sphinx,此後的整本書都是用它寫成的。

一放就是一個多月,當我再次提起興趣的時候,是我開始加入Accounts9的開發。之前雖然也用Node.js寫過不少好玩的東西,但都是自己折騰,這是第一次用Node.js開發真正要應用起來的東西。實際項目開發的過程中讓我學到了許多以前沒有注意到的細節,我對Node.js的興趣重新被點燃,於是我準備好好研究一下Node.js。我從Amazon花了幾十美元買了全世界惟一的一本關於Node.js的書《Node-Web-Development》,到貨以後便如飢似渴地讀完了所有的篇章。令我有些失望的是,這本書寫得並不怎麼樣,而且版本太老,Node.js在誕生之初的變化是極快的,許多API如過眼煙雲,換一個版本就改頭換面了。這畢竟也難以苛責作者,總要有人做先驅嘛。好在Node.js版本在0.6以後API已經趨於穩定,不會再「翻天覆地」了。靜言思之,我發現面對的是一片未經開墾的處女地,因爲祗要是第一本書,無論好壞總會有人買單。

冷靜下來,我決定先擬定一個提綱,然後就開始動筆。我花了一個月寫了兩章,分別是「Node.js簡介」和「JavaScript高級特性」,前者是一個對Node.js的綜述性介紹,而後者則是針對服務器端開發的需求講解JavaScript,與大多數圍繞瀏覽器介紹JavaScript的書有很大不同。寫了兩章以後,熱度再次減退了,因爲暑假已經結束,各種課業的壓力紛至沓來。儘管寫成一本書很有成就感,但遠水不解近渴,於是我又被擊敗了,寫書之事又擱置了下來。

有道是「無巧不成書」,世事偏偏就是那麼巧。有一次我和一個學姐(徐可可)一起吃飯,聊到她的家教工作,她說道她的一個學生家長是某個什麼出版社的。我一聽精神爲之一振,便向她介紹了我已經擱置了的寫書計劃。她聽完以後也挺感興趣的,答應幫我詢問具體出版事宜。後來得知,這位在出版社工作的學生家長竟然就是圖靈出版公司的總編謝工。圖靈出版公司啊,我仰慕已久,不知道讀過他們的多少好書(《C++ Primer》、《JavaScript高級程序設計》、《黑客與畫家》……)。通過她我與圖靈的另一位總編楊海玲取得了聯繫,並且在CNode社區的北京交流會上見面認識了。楊海玲也是IT出版界赫赫有名的人物,她在來到圖靈公司之前,她曾經是機械工業出版社下華章公司那一套黑皮的「計算機科學叢書」的出版負責人,像《算法導論》什麼的當年都是她翻譯引進的。我斗膽把寫好的兩章稿子和提綱交給了她,忐忑不安地期待評價。幾天以後,我得到了不錯的反饋,簡直像是給我注入了一針強心劑,我又有動力繼續創作了。

轉眼到了寒假,我開始把全部精力投入寫書當中,回到學校時我的書也完成了接近一半。但隨着寫作的進度深入,終於遇到了瓶頸——有些章節我實在不知道該怎麼寫。再加上又一學期開始了,我寫作的進程再次漸漸陷入了停滯。這次我清醒地認識到必須在找點激勵了,於是我找到圖靈,簽訂了出版協議。現在,有合同約定我要在4月1日之前交稿,我便不得不克服困難了。事實再次驗證,往往是期限即將到來的那幾天工作是最有動力的,我最終還是按照合同約定交稿了。寫作到後期時,我找了不少人幫助審稿,在審稿的過程中我發現原本自己認爲寫得不錯的地方還有很大改進的餘地。我比較嚮往的出版方式是持久地修訂,而不是一次性交稿,但限於目前的現實條件,這還做不到。

並不是說交稿以後就沒事了,我還要和編輯共同審教、排版,後期的排版過程中我又發現了一些錯誤。所以說,一個出版公司的好壞很大程度上看編輯後期是否認真,圖靈在這方面可以說是佼佼者。再到後來圖靈還幫我設計了幾個封面,最終我選中了下圖這個方案:

感謝對這本書提出寶貴意見的我的朋友們,他們是牟瞳、李垚周越鍾音蕭騏、楊旭東、孫嘉龍范澤一、宋文杰、續本達田勁鋒孟亞蘭和李宇亮。他們爲本書的結構、內容、語言表述等方面給出了許多有建設性的建議。感謝CNodeJS社區的賈超和田永強,微軟亞洲研究院的楊懋,以及VMware公司的柴可夫。他們不僅幫助審閱了本書,還解決了許多技術問題,給這本書提出了許多改進方案。還要感謝我的朋友徐可可、圖靈教育出版公司的楊海玲、謝工、王軍花以及其他各位編輯,他們給我提供了許多幫助和鼓舞,沒有他們的激勵,我很難頂着巨大的學業壓力堅持寫完這本書。

特別要感謝的是弓辰開發的Rime輸入法,我依靠它完成了本書的創作。本書寫作的時候是準確的傳統漢字,出版時按照版署要求轉換爲了簡化字(通過OpenCC)。

最後引書中的一段文字: 在過去JavaScript一直不被人重視,很大程度上是因爲它太低效了——不僅速度慢,還佔用大量内存。但如今Javascript的效率卻令人驚訝。歷史總是如此相似,正如沒有Shockley發明晶體管就沒有電子科技革命一樣,如果沒有2008年以來的JavaScript引擎革命,Node.js就不會這麼快地誕生。

2008年Mozilla Firefox的一次改動,使Firefox 3.0的JavaScript性能大幅提升,從而引發了JavaScript引擎之間的效率競賽。緊接着WebKit開發團隊宣告了Safari 4新的JavaScript引擎SquirrelFish(後來改名 Nitro)可以大幅度提升腳本執行速度。Google Chrome剛剛誕生就因它的JavaScript性能而備受稱讚,但隨着WebKit的Squirrelfish Extreme和Mozilla的TraceMonkey技術的出現,Chrome的JavaScript引擎速度被超越了,於是Chrome 2發佈時使用了更快速的V8引擎。V8一出場就以其一騎絕塵般的速度打敗了所有對手,一度成爲JavaScript引擎之王者。於是其他瀏覽器開發者的重新開始奮力追趕,與以往不同的是,Internet Explorer也加入了這次競賽,並取得了不俗的成績。

時至今日,各個JavaScript引擎的效率已經不相上下,通過不同引擎根據不同測試基準測得的結果各有千秋。更有趣的是,在不知不覺中JavaScript的效率已經超越了其他所有傳統的腳本語言,並帶動了解釋器的革新運動。JavaScript已經成爲了當今速度最快的腳本語言之一,昔日「醜小鴨」終於成了如今驚艷絕俗的白天鵝。

這是單獨介紹本書的頁面:Node.js開發指南

OpenCC網頁版發佈

經過一段時間的開發,OpenCC(開放中文轉換)網頁版( http://www.byvoid.com/application/opencc/ )終於問世了。這次發佈是真正意義上的OpenCC網頁版,成爲了一個Web 2.0應用,不再是以前一個簡陋的頁面。新版本的變化除了在於優美簡潔的頁面外,更提供了新的轉換選項,支持「簡體」、「繁體」、「簡繁混雜」,還支持異體字轉換,如「爲」「為」、「裏」「裡」等字,以兼容不同偏好的使用者。除此之外,OpenCC網頁版還集成了「地區用詞轉換」,這個本不屬於簡繁轉換,但與簡繁轉換相關性很强的功能。

不過OpenCC網頁版最大的兩點是提供了交互式校對的「精細轉換」功能:

使用「精細轉換」模式時,OpenCC將會把所有可能轉換不正確的部分標註,使用者通過點擊可以更換不同的轉換,以提高轉換的正確率。

OpenCC是開源軟件,以Apache Software License 2.0授權發佈,請大家支持。

OpenCC支持地區用詞轉換了

OpenCC剛剛發佈了0.3.0版本,這次發佈最大的變化是終於支持地區習慣用詞和異體字轉換了,這是之前一直被大家呼籲的功能。上個版本0.2.0發佈已經是一年前的事情了,這一年來花在OpenCC上面的時間屈指可數。其實我在半年前就發佈了一個用QT寫的OpenCC圖形界面,只是做得水平很一般,這次也修改了不少。

爲了展示地區轉換的效果,我隨便造了幾個句子: 原文:

鼠标里面的二极管坏了,导致光标分辨率降低。
我们在老挝服务器硬盘需要使用互联网算法软件解决异步的问题。
什么你在面睡
轉換到臺灣標準:
滑鼠裡面的二極體壞了,導致游標解析度降低。
我們在寮國伺服器硬碟需要使用網際網路演算法軟體解決非同步的問題。
什麼你在面睡

以上例子前兩個句子展示了地區用詞轉換,第三個句子是臺灣標準的異體字,如“為、床、裡、著”,OpenCC默認轉換的結果是“什麼你在面睡?”。

此次發佈新增了Mac版,和Windows版一樣都包含圖形界面:

至於libopencc和命令行,還是說一下怎麼使用吧,這次一下增加了10個默認配置文件,如下:

  • zhs2zhtw_p.ini
  • zhs2zhtw_v.ini
  • zhs2zhtw_vp.ini
  • zht2zhtw_p.ini
  • zht2zhtw_v.ini
  • zht2zhtw_vp.ini
  • zhtw2zhs.ini
  • zhtw2zht.ini
  • zhtw2zhcn_s.ini
  • zhtw2zhcn_t.ini

看起來貌似很嚇人,到底用哪個呢?我來分别解釋一下,首先,爲了保證轉換的準確性和異體字的統一,OpenCC引入了一種“能分則不和”的繁體作爲中間表示,任何轉換都要經過這個中間表示,而這個中間表示同時也是我所推崇的用字標準。同時區域轉換又分爲用字轉換和用詞轉換,即異體字(裡、裏)和地區習慣詞彙(服務器、伺服器)。這10個配置文件分别是不同的源頭和目標:

  • zhs2zhtw_p.ini 簡體到臺灣標準(只轉換詞彙)
  • zhs2zhtw_v.ini 簡體到臺灣標準(只轉換異體字)
  • zhs2zhtw_vp.ini 簡體到臺灣標準(轉換異體字和詞彙)
  • zht2zhtw_p.ini 繁體到臺灣標準(只轉換詞彙)
  • zht2zhtw_v.ini 繁體到臺灣標準(只轉換異體字)
  • zht2zhtw_vp.ini 繁體到臺灣標準(轉換異體字和詞彙)
  • zhtw2zhs.ini 臺灣標準到簡體(不轉換詞彙)
  • zhtw2zht.ini 臺灣標準到繁體(不轉換詞彙)
  • zhtw2zhcn_s.ini 臺灣標準到中國大陸標準(轉換詞彙,並轉換爲簡體)
  • zhtw2zhcn_t.ini 臺灣標準到中國大陸標準(轉換詞彙,保持繁體)

其中後綴p表示phrases,v表示variants,s表示simplified,t表示traditional。我們到底應該用哪個呢?多數情况下簡繁轉換只需用zhs2zht.ini即可,如果需要轉換臺灣常用詞彙,一般用zhs2zhtw_vp.ini;一般從臺灣繁體到簡體用zhtw2zhcn_s.ini,如果不需要轉換詞彙,用zht2zhs.ini。其他的配置依據需求使用,更複雜的情况,我們還可以自定義配置文件,參見說明。當然,如果你僅僅使用OpenCC的圖形版,完全沒有必要理會這些細節。

爲什麼要設計得看起來這麼複雜呢?目的是爲了適應各種不同的需求,例如很多時候轉換一片文章需要將其中的地區習慣用詞連帶轉換了,但在輸入法中就完全不能,否則就會干擾使用者的原意。更複雜的情形還有異體字的選擇等等。OpenCC創立之初的宗旨就是建造一個最好的簡繁轉換工具,而不是僅僅能用而已。

編碼自動識別工具 uchardet

最近在給OpenCC做圖形界面,遇到一個問題:OpenCC默認只能轉換utf-8文本,其他編碼像GB18030,BIG5只能轉換成utf-8以後,纔能用OpenCC轉換。這個問題說大不大,說小也不小。我完全可以增加一個選項,在打開的時候讓用戶選擇文本編碼,然後再轉換就行了,但這卻給用戶非常糟糕的體驗,因為很多非專業用戶根本不知道什麼是文本編碼,更別說辨別了。GB18030/BIG5硬要用utf-8打開的話,肯定會遇到亂碼。由於Windows默認是GB18030/BIG5編碼,一般情況下文本會被保存成默認編碼,這樣更大大增加了用戶遇到亂碼的概率。爲了提高體驗,我計劃實現文本編碼的自動檢測。

最早接觸到編碼是從做網站開始的,記得如果忘了在head中顯式地向瀏覽器指定編碼,就經常會出現亂碼,但亂碼也並不總是出現,這是怎麼回事呢?瀏覽器還是有自動識別的能力的。發現Firefox瀏覽器中有一個編碼選項,裏面有「自動檢測」,使用它絕大多數時候都能正確識別。

事實上純文本的編碼檢測是一個非常複雜的問題,甚至理論上根本不可能實現。確切地說,「檢測」應該叫「探測」或者「推測」纔更恰當。自動編碼探測的實現原理主要是統計學的方法,每個編碼會有一定的特徵,首先檢測特徵是否符合,再使用常用的匹配,類似於蒙特卡羅法。具體方法可以參考Mozilla

mozilla在很多年前就做了一個非常優秀的編碼檢測工具,叫chardet,後來有發佈了算法更加優秀的universalchardet,用於Firefox的自動編碼識別。我想,這麼出名的一個工具,應該肯定已經有不少人在用了。有意思的是,我在網上找到了chardet和universalchardet的各種移植:

惟一沒有的,竟然是C/C++的接口封裝。debian更是收錄了python-chardet和ruby-rchardet,卻沒有libchardet或者libuniversalchardet。莫非沒有C/C++的應用在使用chardet嗎?用強大的Google代碼搜索,發現的確有,但幾乎都是把chardet的代碼內嵌到了項目中,耦合十分緊密。更有直接調用python-chardet的,實現不夠純淨。

總覺不該是這樣,但經過反復確認,真的沒有一個獨立的universalchardet的C函數庫封裝。還是自己動手好了,我從mozilla上面取下來了代碼,做了一點點補丁,寫了一個接口和命令行界面,取名uchardet,大功告成。測試了一些GB18030和UTF8的文本,感覺準確率非常高,而且速度很快。但是當我試圖識別幾個字節的短文本的時候,卻出現了識別錯誤,開始以為是我的錯,後來發現我用Firefox直接打開,也是無法識別的,而且錯誤識別的編碼一樣。看來是上游的問題,應該是算法本身的缺陷吧。想想看,畢竟文本越短歧義的可能性越強。不過既然能達到和Firefox同樣的水平,一般應用也就够了。

項目主頁在Google code上:

http://code.google.com/p/uchardet/

代碼在github上:

https://github.com/BYVoid/uchardet

我爲什麽用universalchardet?其實編碼自動識別的解決方案不止一個,有icu提供的解決方案,IE也有API,還有已經在很多Linux發行版中的enca。我之所以用universalchardet,是因為它是最合適的。IE的API不能跨平臺,icu實現太龐大,enca是GPL(注意不是LGPL),使用它意味著我也要讓我的所有源碼使用GPL,而不是更加開放的Apache。universalchardet是MPL的,和LGPL差不多寬鬆,使用它是沒有問題的。我非常不喜歡以GPL發佈的函數庫,這給開發者的限制太大了。

簡易國際音標輸入工具

這是一個簡易的輸入國際音標的小工具,製作這個工具的初衷是爲了方便偶爾要輸入國際音標,又不想花功夫學習複雜的錄入方法的使用者。這個工具像屏幕鍵盤一樣,點一下就可以了,非常方便輕巧。如果需要大量錄入國際音標,推薦使用SIL提供的輸入法,或者潘悟雲先生的雲龍國際音標輸入法。

使用說明

使用前,請確保您的電腦中至少有 Doulos SIL, Charis SIL, IPAPANNEW字體之一,否則某些國際音標符號可能將無法正確顯示。

將ScreenIPA.exe和HookEx.dll放在同一目錄下,運行ScreenIPA.exe。將輸入焦點選取在文本編輯器中,點擊主界面中的按鈕即可輸入。如果無法運行,請確保安裝了.Net Framework,並將壓縮包中的文件放在同一目錄下面。本工具暫不支持Windows 64位系統。

2011年3月 製作

下載地址 ScreenIPA_1.0.0.2

我之所以做這個東西,是因為上學期選了一門音韻學課,課上看見老師用著一個笨拙的輸入工具,效率極其低下。我問老師爲什麼不學習一下國際音標輸入法呢?這樣就可以又快又準地錄入了。但老師卻告訴我說太麻煩了,不想學,而且安裝輸入法太麻煩,換一臺電腦就要重裝,還需要管理員授權。我纔意識到不少人都有這樣的想法,於是便心血來潮,寫了一個小小的輸入工具。由於自認為寫得不怎麼樣,就沒有想著發佈出去,過了幾個月竟然忘了。正好今天有人找我要這個工具,說非常好用,我纔想起來。既然有人欣賞,就無論如何發佈出去吧。恰巧當時正在對.Net感興趣,就用C#寫了這個東西,可能有些老的沒有安裝.Net Framework的Windows XP系統無法使用,在此非常抱歉。