Javascript 的前后端統(tǒng)一是個"笑話"嗎?

最近發(fā)現(xiàn)知乎上有些人批評 Node.js策精,說 Javascript 的前后端統(tǒng)一是一個笑話舰始。

“呵呵”。

所謂的統(tǒng)一當(dāng)然是不可能的咽袜,前端自身都統(tǒng)一不了丸卷,何況前后端。不過询刹,相當(dāng)程度的重用是完全可行的谜嫉。在這里我用一個實際的項目來說明,"i瑞士"凹联。

“i瑞士”的主頁

該網(wǎng)站由瑞士國家旅游局立項沐兰、開發(fā)和維護,從新浪微博上不同的賬號抓取和瑞士有關(guān)的內(nèi)容蔽挠,進行分詞識別住闯,打上不同的標(biāo)簽供用戶分類瀏覽。這個產(chǎn)品的目的是澳淑,讓關(guān)心瑞士資訊的用戶可以有一個無干擾的比原、免廣告、純凈的資訊獲取環(huán)境(既有自動分類過濾偶惠,也有編輯人工審核)春寿。

我是實現(xiàn)該網(wǎng)站的程序員,這是我做的第二個和前端有關(guān)的項目忽孽,第一個是 NextDay 的應(yīng)用介紹網(wǎng)站 ?http://www.gotonextday.com绑改。

這是一個人的項目,前后端一起開發(fā)兄一,歷時4個半月左右(最后上線光等備案和各種審核就花了小1個月)厘线。

系統(tǒng)架構(gòu)

在介紹前后端如何重用之前,首先需要了解一下系統(tǒng)架構(gòu):

“i瑞士”架構(gòu)簡圖

從左到右來看:

Crawler

Crawler 要做這幾件事情:

1. 從新浪微博抓取瑞士有關(guān)的微博信息出革。

2. 對這些信息進行分析和處理造壮,包括:中文分詞,微博標(biāo)簽獲取骂束,“i瑞士”的標(biāo)簽歸納耳璧,對于圖片長寬的預(yù)取(瀏覽器布局用)展箱,對于優(yōu)酷視頻要獲取元信息旨枯,短鏈接事先轉(zhuǎn)換成長鏈接等,總之就是為后續(xù)程序干好各種臟活累活混驰。

3. 根據(jù)不同的微博賬號的來源和具體內(nèi)容進行內(nèi)容發(fā)布攀隔,有些內(nèi)容可以直接發(fā)布皂贩;有些則需要編輯人工審核;有些則延時發(fā)布昆汹,給編輯一個處理緩沖等等明刷。

chinese-seg?是我為這個項目寫的分詞框架,有興趣的同學(xué)可以自己閱讀 CoffeeScript?源碼满粗。本文中提到的我開源出來的幾個 github repos 都沒有時間寫詳細(xì)的說明文檔辈末,但是如果懂 CoffeeScript 的話不難讀懂(不建議你看編譯出來的 JS 代碼,那是優(yōu)化給機器執(zhí)行的败潦,不是給人看的)本冲。

總之 Crawler 就是源源不斷地將新浪微博的內(nèi)容預(yù)處理之后送入不同的發(fā)布隊列中(或者直接發(fā)布)。

DB

這里的 DB 不是指 MySQL劫扒、MongoDB 或者 Redis 這樣現(xiàn)有的數(shù)據(jù)庫管理系統(tǒng)檬洞,而是我自己寫的數(shù)據(jù)存儲服務(wù),最最底層是用的?LevelDB沟饥。

之所以不用現(xiàn)成數(shù)據(jù)庫管理系統(tǒng)添怔,有以下原因:

這個項目的服務(wù)器都是托管在阿里云上的,而這種云OS的磁盤IO都比較慢贤旷,不適合直接安裝既有的數(shù)據(jù)庫服務(wù)(除了 Redis)广料。如果要購買阿里云的 RDS 專業(yè)的數(shù)據(jù)庫服務(wù),則有兩個問題幼驶,第一艾杏,目前只有關(guān)系數(shù)據(jù)庫的選擇,而我要保存的數(shù)據(jù)用 ER 關(guān)系來表達并不太適用盅藻;第二购桑,就是這些關(guān)系數(shù)據(jù)庫沒有 4G 以上內(nèi)存都不太帶得動,而者這造成價格呈指數(shù)翻上去氏淑。這種年年要交費的東西勃蜘,省點就都是自己的。

其實如果所有內(nèi)容在內(nèi)存中都放得下假残,用 Redis 是很好的選擇缭贡。NextDay?的后臺服務(wù)就把用戶的禮物數(shù)據(jù)都保存在 Redis 中,經(jīng)過壓縮和精簡處理辉懒,1G 內(nèi)存保存 5 年的用戶數(shù)據(jù)都沒問題(別拿來記 log 就好)阳惹。

至于阿里云的開放結(jié)構(gòu)化數(shù)據(jù)服務(wù)(OTS)這種私有服務(wù)還真不敢現(xiàn)在就用。

至于為什么用 LevelDB 或者如何用眶俩,那就需要開一個專題來討論了穆端,有興趣的同學(xué)可以從下面的視頻入手,或者從 LevelUp repo?開始仿便。

https://www.youtube.com/watch?v=C-SbXvXi7Og体啰。

API Server

API Server 為瀏覽器提供 Websocket 的調(diào)用服務(wù),也幫助實現(xiàn)新浪微博的 OAuth 認(rèn)證嗽仪,保存用戶收藏以及后臺轉(zhuǎn)發(fā)微博等荒勇。

API Server 以 Client 的身份通過 TCP 連接 DB,以 Server 身份供瀏覽器通過 Websocket 調(diào)用闻坚。作為 Server沽翔,API Server 使用 connect?來完成基本的 HTTP 路由。由于 API Server 實現(xiàn)的 WEB 相關(guān)的功能非常少窿凤,因此沒有勞動?express?的大駕仅偎。

Server Proto

既然都是 Server,那么 Crawler, DB 和 API Server 它們都共享一個公共的 Server框架雳殊,稱為?server-proto橘沥。這是為 “i瑞士” 項目做的一個開源項目,同樣是用 CoffeeScript 寫的夯秃,缺少文檔說明(對不起大家:( )座咆。

server-proto 將 Server 常用的功能抽象出來,例如仓洼,configuration (配置信息獲取)介陶,一個任務(wù)調(diào)度系統(tǒng)(基于 node-resque),redis 訪問色建,通過 REPL 在運行時訪問內(nèi)部狀態(tài)哺呜,supportData 用來實現(xiàn)自定義配置文件的獲取與刷新,actions 用來載入自定義rpc方法實現(xiàn)箕戳,以及 stats(performance counter)某残,streams實現(xiàn)自定義的 NodeJS 的 stream 插件等等。

和其他 Server Framework 不同漂羊,server-proto 沒有包含任何通信協(xié)議相關(guān)的部分驾锰,其原因是我后面要講的重點(天空飄來五個字,那都不是事(兒))走越。

由于缺少用法的說明和實例(例子都在 Crawler, DB, API Server 這些閉源項目中)椭豫,所以目前不適合其他人閱讀和使用,希望最終有機會做出一個完整的可被大家重用的 repo旨指。

另外赏酥,我一直在想是用 Promise 還是 Generator + Promise 重寫這個框架,但是也要看后面項目機緣了谆构。

WEB CDN

用戶看到的所有網(wǎng)頁內(nèi)容相關(guān)的 HTML裸扶、JS、CSS搬素,IMAGE和SVG呵晨,都被部署到了七牛的CDN服務(wù)上魏保。用七牛的原因很簡單,它是我找到的唯一提供 Free Plan 的比較靠譜的服務(wù)商摸屠。所以谓罗,這個項目沒有真正的 WEB Server。以上資產(chǎn)都是從開發(fā)機上季二,通過 Grunt 構(gòu)建出不同的版本檩咱,然后直接部署到 Testing、Staging 或者 Production 環(huán)境中胯舷。對用戶來說刻蚯,也可以從根上就享受到 CDN 的速度,對我來說桑嘶,則又省了一臺云服務(wù)器:)炊汹。

瀏覽器代碼的基礎(chǔ)框架有兩個,一個是 AngularJS不翩,還有就是 NodeJS 兵扬。無論是 AngularJS 的框架本身,還是 NodeJS 系統(tǒng)的 Core Modules口蝠,本項目用到的 NodeJS User Land 的 Modules (NPM Modules)器钟,或者專為本項目寫的代碼,最終都通過 node-browserify?打包成一個 js 文件(modules 之間就是以 NodeJS 的 require 方式引用)妙蔗,minification 之后大約 439K傲霸,gzip 之后 138K。

在前端代碼中集成 Node.JS眉反,帶來的最大好處就是前后端通訊模式的統(tǒng)一昙啄。

通訊模式

在“i瑞士”中,無論是兩個后臺 Server 之間的通信(API Server <-> DB寸五,或者 Crawler <-> DB)梳凛,還是 Browser 和 API Server,其通訊模式主要有兩種:

RPC 和?States Synchronization(狀態(tài)同步)梳杏。

RPC 模式就是 request/reponse 方式韧拒,Client 發(fā)起請求,然后等待 Server 的回應(yīng)十性,這是大家都很熟悉的方式叛溢。不過有一點,之前 Server 和 Server 之間要走一種協(xié)議劲适,而瀏覽器到 Server 之前則只能走另外一種協(xié)議(例如:WebSocket楷掉,或者 Comet, faye...)。

States Synchronization(狀態(tài)同步)是指霞势,當(dāng)某一臺服務(wù)器上的狀態(tài)變化了烹植,將自動同步到其他服務(wù)器斑鸦,無需手工發(fā)起 RPC 請求。

Scuttlebutt-狀態(tài)同步協(xié)議

在“i瑞士”中草雕,兩種方式都被大量使用鄙才。例如:用戶進行“收藏”是一個典型的 RPC 調(diào)用,從瀏覽器到 API Server 到 DB促绵。而天氣信息則是狀態(tài)同步的一個使用場景。

1. Crawler 從某天氣服務(wù)商獲取瑞士各大城市當(dāng)前和未來的天氣嘴纺,隨后通過 RPC 調(diào)用保存到DB 中败晴。DB 是咱自己寫的,因此會自動更新服務(wù)器上的保存 Weather 對象栽渴。

2. 其他 Server尖坤,例如: API Server 從一啟動設(shè)置好將自己的 Weather 對象和 DB 的 Weather 進行同步。

3. 而每個瀏覽器訪問 API Server 時闲擦,當(dāng) Websocket 連接建立后慢味,也會將自己的 Weather 對象與 API Server 的 Weather 對象設(shè)定為同步。

如下圖:

Weather Sync. Model

從安全角度考慮墅冷,DB -> API Server -> Browsers 之間的 Stream (是指 NodeJS Stream)都是只讀的纯路,也就是不允許 Browsers 反過來通過變更 Weather 對象來引起整個網(wǎng)絡(luò)的 Weather 對象變化。

同步算法采用的是 Scuttlebuttdominictarr?撰寫)寞忿,其基本原理是通過不同的 Peer 之間利用 Vector Clock 算法發(fā)現(xiàn)較新的狀態(tài)驰唬,從而將這些較新的狀態(tài)同步到自身,再擴散到其他將自己當(dāng)做 Reader 的 Peers 上腔彰。

當(dāng)時為了學(xué)習(xí)理解 Scuttlebutt 的原理和代碼叫编,我 Fork 了原始代碼,寫了一篇文檔作說明霹抛,同時在原來的代碼上加了很多注釋搓逾。

Scuttlebutt 是基礎(chǔ)同步算法,在其之上可以衍生出不同的數(shù)據(jù)結(jié)構(gòu)的同步(編寫 Scuttlebutt 的特定子類)杯拐,例如霞篡,同步單層對象,多層對象藕施,Global Counter寇损,甚至包括協(xié)同編輯中的文檔連續(xù)同步等等。當(dāng)然裳食,其同步的基準(zhǔn)是時間矛市,前提是各個 Peers 都擁有一致的時間(如果不僅僅是只讀的)。有些場景不能保證時間的一致性诲祸,例如瀏覽器浊吏,那么先實現(xiàn)一個簡單的時間同步算法作為前提而昨。

實現(xiàn)?Scuttlebutt 并不簡單。如果在沒有 NodeJS 和 node-browserify 的世界中找田,我們只能用不同的語言歌憨,在不同的平臺下都實現(xiàn)一遍。而現(xiàn)在墩衙,起碼在瀏覽器前端和 NodeJS 的后端間實現(xiàn)狀態(tài)同步都擁有完全相同的代碼务嫡。

dnode - 一個 RPC 的 JS 實現(xiàn)

那么如何在瀏覽器和 Server之間,以及 Server 與 Server 之間采用相同的 RPC Codebase 呢? 這就要感謝同樣是 node-browserify 的作者?substack?的 dnode?了漆改。

dnode 實現(xiàn)了一種自由風(fēng)格的 RPC 模式心铃,無論是 Client 還是 Server 都可以自用聲明自己所支持的方法原型,連接后相互交換(如果不需要 Server 調(diào)用 Client 的方法挫剑,那么僅僅需要 Server 告訴 Client 自己的方法原型即可)去扣。這種方法原型的交換在 RPC 的概念中相當(dāng)于互換 IDL,只不過不是事先綁定樊破,而是動態(tài)交換的愉棱。

dnode 概念簡單算墨,易于使用沐旨,老少咸宜。但是最關(guān)鍵的谜喊,也是和?Scuttlebutt 一樣的地方就是惫恼,通信的 peer 之間只要有 NodeJS stream 的管道即可档押,而不是綁定到某一種具體的網(wǎng)絡(luò)協(xié)議上(如 TCP 或者 Websocket)。那么換句話說祈纯,只要我們讓 TCP 或者 Websocket 支持 NodeJS 的 stream令宿,即可自由地使用 stream 上的各種算法實現(xiàn)了。幸運的是腕窥,這些幾乎都已經(jīng)存在了粒没。

Stream 和 網(wǎng)絡(luò)協(xié)議

首先 NodeJS 的 Core Modules 中的 TCP 已經(jīng)是 stream 的實現(xiàn),所以 Server to Server 之間已經(jīng)無需自己做了簇爆。而瀏覽器到 Server 之間癞松,目前常用通信 Modules 有socket.ioSockJS, ws, engine.io?等等入蛆。他們都有 stream 接口的對應(yīng)實現(xiàn):socket.io-stream,?shoes, websocket-stream,?engine.io-stream响蓉。我選用的是 websocket-stream,因為它不像其他框架哨毁,都實現(xiàn)了瀏覽器不支持 Websocket 的 fallback枫甲。這一點我不需要,因為 IE 10 之前我都不支持(其實連 IE10我都不想支持啊:( )。

因此想幻,無論是瀏覽器還是 Server粱栖,都擁有了相同的 RPC 框架和同步框架,于是就只剩下了最后一個問題脏毯,連接復(fù)用闹究。

連接復(fù)用

每個瀏覽器到 Server 的 Websocket 連接越少越好。如果僅僅是一般的基于 stream 的管道食店,一個管道就會消耗一個 Websocket 連接渣淤。那么 dnode,weather 同步就要消耗兩個連接吉嫩,而我要同步的東西可不僅僅是 weather砂代。因此,在一個既有的 stream 上如何同時承載多個的其他 streams率挣,則是要解決的新問題。

dominictarr?的?mux-demux?就是來解決這個問題的露戒。我也搞了個 Fork椒功,漢化了其 readme。另外智什,這里有一個例子动漾,演示了如何在一個 Websocket stream 上完成 dnode RPC 調(diào)用 和 scuttlebutt 同步。

總結(jié)

上面提到的還只是最主要的重用部分荠锭。其實還有很多小地方也都復(fù)用了代碼和算法旱眯,例如:網(wǎng)絡(luò)連接的自動重連算法?reconnect-core?以及其 websocket-stream 的具體實現(xiàn)?reconnect-ws?(這是我少有的直接用 JS 寫的:) )。

讀到這里大家可能也和我一樣能體會到证九,如果沒有 NodeJS 和 node-browserify删豺,這個項目不可能由一個人在這么短的時間內(nèi)完成的項目。如果前后臺都由一個人來寫愧怜,采用完全不同的技術(shù)平臺呀页,在同一時間段內(nèi)是很割裂的事,就算能做拥坛,其項目復(fù)雜度也只能大大降低蓬蝶。

用好 NodeJS,深入理解和使用 Stream 是必須的猜惋。NodeJS 當(dāng)年引入 Stream丸氛,就是看到管道操作在 Unix 上的巨大成功。這一層標(biāo)準(zhǔn)的抽象著摔,雖然并不完美缓窜,卻讓不同的開發(fā)者不約而同地構(gòu)造出高度可復(fù)用的代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雹洗,隨后出現(xiàn)的幾起案子香罐,更是在濱河造成了極大的恐慌,老刑警劉巖时肿,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庇茫,死亡現(xiàn)場離奇詭異,居然都是意外死亡螃成,警方通過查閱死者的電腦和手機旦签,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寸宏,“玉大人宁炫,你說我怎么就攤上這事〉” “怎么了羔巢?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長罩阵。 經(jīng)常有香客問我竿秆,道長,這世上最難降的妖魔是什么稿壁? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任幽钢,我火速辦了婚禮,結(jié)果婚禮上傅是,老公的妹妹穿的比我還像新娘匪燕。我一直安慰自己,他們只是感情好喧笔,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布帽驯。 她就那樣靜靜地躺著,像睡著了一般书闸。 火紅的嫁衣襯著肌膚如雪界拦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天梗劫,我揣著相機與錄音享甸,去河邊找鬼。 笑死梳侨,一個胖子當(dāng)著我的面吹牛蛉威,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播走哺,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蚯嫌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起择示,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤束凑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栅盲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汪诉,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年谈秫,在試婚紗的時候發(fā)現(xiàn)自己被綠了扒寄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡拟烫,死狀恐怖该编,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情硕淑,我是刑警寧澤课竣,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站置媳,受9級特大地震影響稠氮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜半开,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赃份。 院中可真熱鬧寂拆,春花似錦、人聲如沸抓韩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谒拴。三九已至尝江,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英上,已是汗流浹背炭序。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苍日,地道東北人惭聂。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像相恃,于是被迫代替她去往敵國和親辜纲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容