
起因
很早就接觸了 Markdown虹蓄,也用過幾款 Markdown 編輯器犀呼。由于我用的是 Linux,一直無法在 Linux 上找到一款美觀順手的編輯器薇组。Mac 上貌似有不少優(yōu)秀的編輯器圆凰,可一直無緣得見。
其實(shí)很早就有了自己實(shí)現(xiàn)一個 Markdown 編輯器的想法体箕,可一直覺得像編輯器這樣的東西做起來應(yīng)該不會太簡單,工作量應(yīng)該會非常大挑童。我也一直沒有弄明白這其中的原理是什么累铅,雖然網(wǎng)上有不少開源的 Markdown 編輯器,但在沒有說明的情況下閱讀別人的代碼是一件十分困難的事情站叼,所以也一直沒有去讀娃兽。
直到最近讀到了一片文章:Node Webkit (NW.js) tutorial: creating a Markdown editor。在這篇文章里作者簡述了一個極其簡單的 Markdown 編輯器的實(shí)現(xiàn)尽楔,作者用到的技術(shù)雖然我不太熟悉投储,不過原理我還是看懂了。就在這篇文章的基礎(chǔ)上阔馋,我開始實(shí)現(xiàn)自己的 Markdown 編輯器: Mango玛荞,已經(jīng)在 github 上開源。
我給自己的編輯器取名為 Mango ---- 一種水果的名字呕寝,logo 為藍(lán)底白字的一個 M (見上圖)勋眯,M 既代表 Markdown 也代表 Mango,字體是在 PhotoShop 里隨便選了一種看得過去的字體。logo 的設(shè)計(jì)模仿了另一個 Markdown 編輯器(Remarkable)的設(shè)計(jì)客蹋。有了 logo 之后就可以開始動工了塞蹭。
一開始我本來打算用 gtk+ 來寫,不過我對 C 語言的一些第三方庫了解得不多讶坯,不知道能否方便地實(shí)現(xiàn)我想要的功能番电,比如代碼高亮,LaTeX 支持辆琅,而 JavaScript 在這方面有非常成熟的庫漱办。而我又是一個對新技術(shù)非常感興趣的人,所以想嘗試一下用我沒有接觸過的一些技術(shù)來實(shí)現(xiàn)涎跨。于是選擇了跟上文作者相同的技術(shù):NW.js 來實(shí)現(xiàn)洼冻。
NW.js 又叫 node-webkit,把 Node.js 跟 Chromium 結(jié)合在了一起隅很,使得可以用 web 的技術(shù)來寫桌面 App撞牢,不僅可以使用 html、css叔营、js屋彪,還可以使用 Node 大量的第三方庫,而且輕松跨平臺绒尊,實(shí)在是一種相當(dāng)酷的技術(shù)畜挥,更多的介紹請參見項(xiàng)目主頁。不過我之前并沒有學(xué)過Node.js婴谱,我的前端技術(shù)(html蟹但、css、js)也只是屬于在 W3Schools 上速成的水平谭羔。所以在頭三天花了一些時間學(xué)習(xí) Node华糖,以及惡補(bǔ)了一些 JavaScript 的知識。
開始實(shí)現(xiàn)
說實(shí)話瘟裸,“會寫一個” 跟 “寫了一個” 的區(qū)別真的相當(dāng)大客叉,雖然原理都弄明白了,可真正做起來還是有相當(dāng)大的困難话告。這也是我寫這篇文章的原因兼搏,希望給后續(xù)想自己實(shí)現(xiàn)一個編輯器的人一些幫助。
其實(shí)我需要的功能不多沙郭,一個美觀的 UI佛呻,代碼高亮,LaTeX支持(我是數(shù)學(xué)系的病线,這個是必須的)件相,實(shí)時預(yù)覽和同步滾動再扭,以及方便的導(dǎo)入導(dǎo)出功能,尤其是在導(dǎo)出 HTML 和 PDF 后仍能保持美觀的 UI夜矗。在很多方面馬克飛象都做得很好泛范,而且功能比我要求的多,但卻無法讀寫本地文件紊撕,同步功能也不是免費(fèi)的罢荡。而NW.js 可以通過 Node 的模塊輕松實(shí)現(xiàn)讀寫文件的功能。
什么是 Markdown 呢对扶?Markdown只是一種標(biāo)記語言(Markup language)区赵,不過比HTML簡單直觀,非常適合寫作和記筆記浪南。瀏覽器并不能直接解析 Markdown笼才,而是所以我們首先需要通過Markdown解析器(parser)把 Markdown 的語法解析成 HTML 語法,再由瀏覽器的引擎渲染成我們所見的頁面络凿。原理就是這么簡單骡送。parser并不需要我們自己寫,已經(jīng)有很多 Markdown的實(shí)現(xiàn)了絮记,這里我選了Marked摔踱。所以我們只需要在左邊放一個 Editor,編輯 Markdown 源碼怨愤,然后實(shí)時把 Editor 里面的 Markdown 通過 Marked 轉(zhuǎn)換成 HTML 放在右邊的 Viewer 里就可以了派敷。要實(shí)現(xiàn)實(shí)時預(yù)覽,必須監(jiān)聽 Editor 里的變化撰洗,每次有所改變的時候篮愉,重新用 Marked 解析一次(放在reload()
函數(shù)里)。
同步滾動實(shí)現(xiàn)
同步滾動功能實(shí)際上非常簡單差导,只要監(jiān)聽 Editor 和 Viewer 的滾動事件潜支,每次一個滾動的時候改變另一個的滾動軸,使得它們的百分比一樣柿汛。就是下面的代碼(我也是 google 來的):
var $divs = $('textarea#editor, div#preview');
var sync = function(e){
var $other = $divs.not(this).off('scroll'), other = $other.get(0);
var percentage = this.scrollTop / (this.scrollHeight - this.offsetHeight);
other.scrollTop = percentage * (other.scrollHeight - other.offsetHeight);
setTimeout( function(){ $other.on('scroll', sync ); },200);
}
$divs.on('scroll', sync);
代碼高亮實(shí)現(xiàn)
代碼高亮我選擇了 highlight.js,只要把 highlight.js 的代碼嵌入 html,然后在每次更新頁面的時候埠对,重新初始化一下络断,就是在reload()
函數(shù)里嵌入如下兩行代碼:
hljs.initHighlighting.called = false;
hljs.initHighlighting();
LaTex支持
這個是最難實(shí)現(xiàn)的,也是我花時間最多的项玛。所以我會詳細(xì)講一講具體的做法貌笨。首先 MathJax 庫肯定是首選,渲染出來的數(shù)學(xué)公式非常漂亮,可以見下圖:

要想實(shí)現(xiàn)數(shù)學(xué)公式的實(shí)時渲染襟沮,就必須在reload()
函數(shù)里調(diào)用 MathJax 的Typeset
方法重新渲染一遍整個數(shù)學(xué)公式锥惋,而渲染需要有一定的時間昌腰,這就造成了在每次輸入的時候有數(shù)學(xué)公式的地方都會不斷的跳(不知如何形容,就是你首先會看到源碼膀跌,然后看到數(shù)學(xué)公式)遭商,這真的是一個非常影響用戶體驗(yàn)的問題。國內(nèi)一些在線編輯器做得非常好捅伤,沒有這個問題劫流,不過國外的 stackedit仍然有這個問題,只要輸入速度快一點(diǎn)丛忆,數(shù)學(xué)公式會不斷變大變小祠汇。
解決這個問題的一個方法是:首先把經(jīng)由 Marked 解析出來的 html 源碼放入一個 buffer 里,而這個 buffer 是不顯示的熄诡。然后由 MathJax 把 buffer 里的 html 中的數(shù)學(xué)公式排版成可見的格式可很,然后再把 buffer 里的 html 送到 Viewer 顯示出來,這樣 Viewer 得到的 html 就總是經(jīng)過 MathJax 排版過的凰浮。這里有一個問題我抠,就是Typeset
函數(shù)是異步的,我們必須要在Typeset
函數(shù)完成后导坟,再把 buffer 里的 html 送到 Viewer屿良,這里要借助一下 MathJax 提供的Queue
。部分代碼如下:
//reload函數(shù)部分片段
var resultDiv = global.$('.md_result');
var buffer = global.window.document.getElementById("buffer");
var textEditor = global.$('#editor');
var text = textEditor.val();
buffer.innerHTML = (marked(text));
MathJax.Hub.Queue(["Typeset",MathJax.Hub,buffer],
["preview",this]);
//preview函數(shù)里面實(shí)現(xiàn)了把buffer里的html送到Viewer:resultDiv.html(buffer.innerHTML);
看起來非常完美惫周,可我經(jīng)過測試之后發(fā)現(xiàn)問題任然存在尘惧。原因是因?yàn)槲覀儾粩嗑庉媽?dǎo)致reload
函數(shù)頻繁觸發(fā),可能第二個reload
函數(shù)運(yùn)行到buffer.innerHTML = (marked(text))
這一步的時候递递,前一個preview
函數(shù)剛好運(yùn)行resultDiv.html(buffer.innerHTML)
喷橙,而此時的buffer.innerHTML
是未經(jīng)Typeset
函數(shù)處理的 。所以我想了個加鎖(lock)的辦法登舞,就是在前一個preview
函數(shù)沒有運(yùn)行完的時候贰逾,后來的reload
函數(shù)不能運(yùn)行buffer.innerHTML = (marked(text))
這段代碼。代碼如下:
function reload(){
if (lock == false) {
buffer.innerHTML = (marked(text));
MathJax.Hub.Queue(["Typeset",MathJax.Hub,buffer],
["preview",this]);
}
}
function preview(){
if (lock == false){
lock = true;
resultDiv.html(buffer.innerHTML);
lock = false;
}
}
當(dāng)然加鎖之后實(shí)時更新可能會有一次延遲菠秒,不過這個問題不大疙剑。
這里還有一個問題,就是 LaTeX 的語法跟 Markdown 的語法有部分沖突践叠,主要是雙下劃線_..._
與\
言缤,LaTeX 里使用_
表示下標(biāo),當(dāng)有兩個下標(biāo)的時候禁灼,會先被 Marked 解析為斜體管挟,然后 LaTeX 就無法渲染了。\\
會被 Marked 轉(zhuǎn)義成\
弄捕,這樣 LaTeX 里就無法使用\\
了僻孝,必須使用\\\
导帝。要解決這個問題必須修改 parser,要不然就重新實(shí)現(xiàn) parser 使得 parser 不解析$$...$$
和$...$
中的內(nèi)容穿铆。這里參考了讓marked與MathJax和諧共存這篇文章的解決辦法您单,修改了 Marked 的部分源碼,不過就無法在 Mango 中使用_..._
來表示斜體了悴务,可以使用*...*
睹限。
導(dǎo)出功能實(shí)現(xiàn)
一個合格的 Markdown 必然要有導(dǎo)出 HTML 和 PDF 的功能。導(dǎo)出 HTML 的功能比較容易實(shí)現(xiàn)讯檐,因?yàn)檎麄€界面本身就是 HTML羡疗,只要把不該出現(xiàn)的東西(比如工具欄,編輯區(qū))在導(dǎo)出的時候隱藏掉就可以了别洪。而 PDF 的功能有些困難叨恨。這里我不得不吐槽一下 npm。npm 雖然非常好用挖垛,庫也非常龐大痒钝,隨手一搜發(fā)現(xiàn)很多庫都可以實(shí)現(xiàn)此功能,但是這些庫的質(zhì)量參差不齊痢毒,有些文檔都寫不清楚送矩,上手相當(dāng)有困難。我也是試了幾種不同的庫才終于找到一個有用的:phantom-html2pdf哪替。不過這個庫也好不到哪里去栋荸,文檔不太清楚,作者貌似也不太管事凭舶,別人在 github 上提了幾個 issue 都沒有得到回應(yīng)晌块。我也提了一個,是關(guān)于使用多個css
的問題帅霜,作者理都不理我匆背。。身冀。具體的實(shí)現(xiàn)請參見exportToHTML
和exportToPDF
這兩個函數(shù)钝尸,比較簡單,就不細(xì)說了搂根。
美觀的 UI
對于一個優(yōu)秀的軟件來說珍促,一個好的 UI 必然會為其增色不少。Markdown 解析器只是把 Markdown 轉(zhuǎn)為 HTML兄墅,而沒有規(guī)定格式,所以不同的編輯器轉(zhuǎn)化出來的格式并不是一樣的澳叉,簡書有簡書的 UI隙咸,Medium 有 Medium 的 UI沐悦,馬克飛象有馬克飛象的 UI。我個人非常喜歡馬克飛象和作業(yè)部落的字體顏色五督,所以在 Mango 中選了跟它們一樣的字體顏色藏否。我的css
水平真的非常差,不過幸好 bootstrap 提供了不錯的格式充包,再此基礎(chǔ)上修改一些就可以了副签。其中blockquote
的格式是 google 來的(在一個專門講 css 技巧的網(wǎng)站)。具體的css
代碼可以見preview.css
.為了在導(dǎo)出的時候仍然有美觀的 UI基矮,css
都是直接在 html 里面寫的淆储,并沒有外鏈。
結(jié)語
NW.js 的優(yōu)點(diǎn)和缺點(diǎn)
說實(shí)話 NW.js 非常好用家浇,及其方便容易就可以創(chuàng)建一個桌面App本砰,Node 大量的第三方包讓你幾乎可以找到任何你想要的功能,可是必須要在 NW.js 環(huán)境才能運(yùn)行钢悲,可是 NW 可執(zhí)行文件有70多MB5愣睢!莺琳!即使你的程序很小还棱,打包在一起也會十分龐大。如果你的程序也非常大惭等,那就更麻煩了珍手。比如在 Mango 中為了有 PDF 導(dǎo)出功能,需要phantomjs
咕缎,可這個包有30多MB珠十,這就使得程序非常大了。
另外凭豪,報錯信息太不詳細(xì)了焙蹭,經(jīng)常解決一個 bug 花很長時間,總是報一些百思不得其解的錯(不知道到這是 NW.js 的原因還是 JavaScript 的原因)嫂伞。
Mango 的未來
其實(shí) Mango 還很不完善孔厉,比如連查找替換的功能都沒有,也沒有其他編輯器的流程圖功能帖努。因?yàn)?Mango 的定位是用來記筆記和寫一些小文章(我想這也是所有 Markdown 編輯器的定位)撰豺,又不是寫代碼,所以我想查找替換的功能很少會用到拼余。而流程圖污桦,語法太繁瑣,違背了簡約的原則匙监,而且估計(jì)也很少會用凡橱,所以也沒有實(shí)現(xiàn)了小作。其實(shí)還是有一些功能我想做的,比如與一些云服務(wù)相結(jié)合稼钩,實(shí)時同步到云端(就像馬克飛象那樣顾稀,當(dāng)然也不一定跟印象筆記結(jié)合)。另一個是實(shí)現(xiàn)一些自定義的功能坝撑,比如自定義css
等静秆。如果 Mango 有用戶使用的話,我將繼續(xù)完善巡李。