【小(xiǎo)編推薦】團隊協作(zuò)工(gōng)具Workti •le技(jì)術(shù)架構揭秘

2015-07-02   |&✘‌nbsp;  發布者:梁國 ‍↓(guó)芳   | &nb≤&©sp; 查看(kàn):3320次

IT新聞
 本文(wén)節選自(zì)程序員(yuán)電(diàn)子(zǐ)® 刊7A封面專題“中國(guó)SaaS生(sh∏≥♠ēng)态‘元素周期表&rsλ☆>quo;”,Worktile作(zuò>¶π)為(wèi)一(yī)款傑出的(de)團隊協作(zuò)工(✔™gōng)具,自(zì)上(shàng)線兩年(nián)多(duō)以來(♦δlái),以良好(hǎo)的(de)用(yòng)戶體(tǐ)驗和(hé)穩定₩&δ♣的(de)服務,獲得(de)了(le)用(yòng)戶的(de)認可(k♣'→ě)和(hé)喜愛(ài),本文(wén$δ ∞)來(lái)自(zì)Worktile創始人(rén)兼CTO李會βα§(huì)軍關于Worktile整個(gè)☆₹≠↕技(jì)術(shù)架構的(de)揭秘分(fēn)享。

以下(xià)為(wèi)全文(wén):

Worktile自(zì)上(shàng)線兩≠↕年(nián)多(duō)以來(lái),以良好(hǎo)的(d∞<βσe)用(yòng)戶體(tǐ)驗和(hé)€$₽§穩定的(de)服務,獲得(de)了(le)用(yòng)戶的(de)認可(kě≠↑)和(hé)喜愛(ài)。截止筆(bǐ)者寫這(zhè)篇文(↕​wén)章(zhāng)的(de)時(sh↕±'₹í)候,已經有(yǒu)超過10萬家(jiā)團隊在使用(÷φyòng)Worktile。作(zuò)為(wèi)團隊協作(zuò)工(≥§↕gōng)具,從(cóng)技(jì)術≥<↔↑(shù)上(shàng)分(fēn)析σ∞λ₩首先要(yào)解決如(rú)下(xià)幾個‍∞(gè)問(wèn)題:

 

 

那(nà)麽Worktile是(shì)如(r∏γ€ú)何做(zuò)到(dào)這(zhè)幾點的(de)?今£∏→δ天筆(bǐ)者在這(zhè)篇文(wén)章(↔¥zhāng)裡(lǐ)一(yī)一(yī)♦ ≥↓為(wèi)大(dà)家(jiā)揭秘。

SPA設計(jì)

先來(lái)說(shuō)說(shuō)Worktile×÷α中SPA(單頁應用(yòng)程序)設計(jì),作(zuò)為(wèi)團β×隊協作(zuò)工(gōng)具,需要(yào₽↕)盡可(kě)能(néng)減少(shǎo)用(ε>σβyòng)戶在不(bù)同頁面之間(jiān)的(de)跳(tiào)轉,π$所以從(cóng)一(yī)開(kāi)始我們就(jiù)決定Worktil δφe必須是(shì)單頁應用(yòng)程序"★ ✘,當時(shí)面臨的(de)選擇有(yǒu)很("₹↓↓hěn)多(duō),首先我們考慮使用(yòng)大(dγ®à)名鼎鼎的(de)Backbone.js,但(dàn)是(s₩β™±hì)很(hěn)快(kuài)又(yòu)抛棄了(le)∑×∞↔,因為(wèi)在實際使用(yòng)中B™≠•ackbone.js太複雜(zá),另一(yī)方面∏©•開(kāi)發效率太低(dī),最終我們選擇了♦★β↔(le)Google出品的(de)Angula÷™rJs,下(xià)面這(zhè)幅圖是(€₽shì)AngularJS的(de)結構圖:

選擇它主要(yào)基于以下(xià)幾↔₹ ¥點考慮:

1. 自(zì)動化(huà)雙向數(shù)據綁定★≤​功能(néng),這(zhè)一(yī)點在W©₩¶orktile中非常重要(yào),如(δ←γrú)任務的(de)狀态變化(huà)都(dōuσ↕≈♥)要(yào)實時(shí)變更到(dào)其他(tā)φ α成員(yuán),如(rú)果具有(yǒu)自(zì)動化(huà↑♣)雙向數(shù)據綁定功能(néng),隻需φ₩×₩要(yào)綁定到(dào)UI的(de)數(shù)據源發生(shēng)變₩♦化(huà),UI會(huì)自(zì)動發生(shēng)改β★ 變,不(bù)需要(yào)工(gōng)程師(shī)再通(tōng)過代 ‌≠碼去(qù)修改UI元素的(de)改變,如(rú)下(xià)面這(zhè)段≥φπ代碼:

 

<div class="e∞≈ntry-task-main" 
   ×♣®  ng-class="{1:'task-compl§€ eted-style'}[task.completed]&q↕★←uot;>
    <a class="e↑‍"×ntry-task-check" 
     id=&quo&↓t;task_check_{{ task.tid }}←≥"
       wt-click=&quo✘Ω≈​t;js_complete_task($event, entry, task♠©)">
        <i '♥ng-class="{0: 'icon™φ♠-check-empty', 1: 'icon‍±-check-sign'}[task.c>≠‍ompleted]"></i>δ∞;
    </a>
    <a class=☆‌←"entry-task-title" hr↑↕ef="javascript:;"&≥✘δ₹gt;{{task.name}}</a>
</div&∞≤≥"gt;

 

2. 語義化(huà)标簽,AngularJΩ£×S在設計(jì)之初信奉的(de)理(lǐ)念就(jiù)是(shì)φ→€:當編寫UI的(de)同時(shí)又(yòu)需要(yào)編寫業(y÷>♠è)務邏輯時(shí),聲明(míng)式的(de)代碼遠(yuǎn)比↑‌≤命令式代碼要(yào)好(hǎo),命令式的(de)代碼更适合寫業(yè✘§γ)務邏輯,AngularJS在設計(jì)上(shàng)就(ji₹✘λù)通(tōng)過語義化(huà)的(de)标£$ε簽把對(duì)DOM元素的(de)操作(zuò)和(hé)邏輯代碼分( δΩ>fēn)離(lí),如(rú)我們需要(yào)展現(xiàn ")一(yī)個(gè)任務列表,隻需要(yào)σ♣α♣下(xià)面這(zhè)段代碼即可(kě):

 

<div ng-repeat="task in tasks↑←β">
     <wt-task vie•​♠φw-type="item" §‍; show-project="f¥✘alse" 
            cla £↕ss="slide-triggeδε₽r"
              ¥γ≠πhide_action="true<∏"
              ng-click="loε↑$©cator.openTask(task.pid, task.tid)ε∞">
     </wt-task>
&→♦$lt;/div>

 

3. 模塊化(huà)設計(jì),Angular↑'<₩JS堪稱模塊化(huà)設計(jì)方面的(de)典範,通(tōng)過模♦δ←α塊化(huà)設計(jì)我們可(kě)以非常好(h♠'ǎo)的(de)實現(xiàn)Worktile的(de)工(gōngδα∏")程化(huà),在Worktile中涉及的(de)元素非常多≥♦ (duō),如(rú)有(yǒu)項目、任務、日(rì)程、文 ¥±(wén)件(jiàn)、話(huà)題、Ω‌₹文(wén)檔等等,而這(zhè)每一(yī)個(gè)元素‌☆✘$都(dōu)可(kě)以設計(jì)為(wèi)一(‌±yī)個(gè)模塊,如(rú)下(xià)所示:

 

(function () {
    'use stric'∑ααt';
    angular.module(​≥'wtApp', [
        ​¶'wt.project.ctrl',
        'wt.tea¶↑πm.ctrl',
        'wt.tas↕ ♣k.ctrl',
        'wt.event.↕γ​ ctrl',
        'wt.postπβ'.ctrl',
        'wt.file.cΩ₹trl',
        'wt.page.ctrl',
    ♥™♥    'wt.mail.ctrl'
    ]);
}());

 

4. 引入依賴注入,依賴注入是(shì)面向對(duì)象中比較≥☆♣↔成熟的(de)設計(jì)模式之一(yī),為(wèi)了(le σ)解決面向對(duì)象中依賴問(wèn)題,得(de)到(dà™★o)了(le)廣泛的(de)應用(yòn"&$g),AngularJS中大(dà)膽使用(yòng)了(∞©₽le)依賴注入,極大(dà)的(de)減少(​♥shǎo)了(le)各個(gè)模塊之間(jiān)的(de)依∑ >$賴問(wèn)題:

 

taskListCtrl.$inject = [‌∑♠'$scope', '$stateParams', 
      δ¶       '$rootScope', '$popbox', 
φ¶☆            '$location×ε©↑', '$timeout', 
     ↑₽↓≤       'bus', 'globalDataConte£'xt', 
            'locator'];

 

結合以上(shàng)特點,我們最終決定βα了(le)前端框架使用(yòng)AngularJS來(lái)實現(α"φxiàn),從(cóng)Worktile上(shàng)線兩年(∑π§≠nián)多(duō)的(de)表現(xiàn)來(lái)看(kàn∏∏≥),我們的(de)選擇無疑是(shì)正确'∞的(de)。當然AngularJS也(yě)有(yǒu)一(yī)‌§些(xiē)缺點,在實際使用(yòng)中還(hái)是(shì)要(yào₹'☆↕)根據具體(tǐ)的(de)産品類型來(lái)選擇使用(yòng), ₩☆☆另外(wài)AngularJS 2.0也π÷(yě)已經初見(jiàn)端倪,和(hé)AngularJS 1.0有←≈(yǒu)很(hěn)大(dà)的(deπλ≈‍)不(bù)同,感興趣的(de)同學可(kě)以先去(qù)嘗鮮一( •>←yī)下(xià)。

服務設計(jì)

我們再來(lái)看(kàn)看(kàn)W​★↑®orktile的(de)後台服務設計(jì),Wor∞"↔ktile的(de)整體(tǐ)服務架構設計(jì)如(rú)下(<≤→xià)圖所示:

其中前端部分(fēn)在上(shàng)面的(de)SPA一(λ®¶yī)節中我們已經說(shuō)過了(le),∏↑♣下(xià)面一(yī)一(yī)分(fē>¥$↕n)析下(xià)其他(tā)的(de)服務:

1.  API服務,包括Web API、Mobile A ★±≈PI、Open API,這(zhè)些(xiē γ™)都(dōu)運行(xíng)于NodeJS之上(shàng),選用(yα ∏₽òng)NodeJS的(de)原因主要(yào)是(shì××)它的(de)異步事(shì)件(jiàn)驅動,對(duì)于高(gāo)并↓$&發的(de)支持比較好(hǎo),另外(wài★≤)一(yī)個(gè)原因是(shì)使用(yòng)簡單,對(d♣☆®uì)于前後端可(kě)以使用(yòng)同一(yī)門(méΩ§>n)語言去(qù)開(kāi)發。

2.  緩存和(hé)隊列服務,Worktil×≠γπe中的(de)緩存和(hé)隊列服務都(dōu)是®→✘​(shì)基于Redis來(lái)實現(xià♣‌n),Redis是(shì)一(yī)款非常優秀的(de)開±€✔(kāi)源緩存服務,并且可(kě)以選擇基于內(nèi)存>ε✔★還(hái)是(shì)進行(xíng)數(shù)據持久化(•∑€huà),它提供的(de)pub/subλ> •模型對(duì)于Worktile來(lái)說(shuō)非✘₩常重要(yào),對(duì)于一(yī)些(xiē)實時(shí)性要π'γΩ(yào)求不(bù)高(gāo)的(de)處✔​理(lǐ),我們都(dōu)是(shì)在Redis中pub一(yīδ™ ₩)條消息,告知(zhī)其他(tā)服務有(yǒu)數(shù)據發★↔ ♠生(shēng)了(le)變化(huà) ₩‌,那(nà)些(xiē)服務在接收到(dào)Redis中的(de)消息後,£&根據消息的(de)類型決定應該如(rú)何做(z≠γσuò)出處理(lǐ)。

3.  數(shù)據庫服務,Worktile>±産品本身(shēn)的(de)特點決定了(le)它是(‌→shì)一(yī)個(gè)對(duì)實時(shí↓Ω)性和(hé)性能(néng)的(de)要(yào)求,遠(yuǎn)超β‌ 過對(duì)事(shì)務性要(yào)求的Ω​φ€(de)産品,所以在選擇數(shù)據庫時(shí),我們選用•≈(yòng)了(le)MongoDB數(shù)據​​₩庫,性能(néng)高(gāo),集群方便,數(shù&β)據以BSON結構存儲,和(hé)Node.js天生(shēng)完美(m✔↑ěi)結合。

4.  文(wén)件(jiàn)預覽服務,使用(yòn€↑"‍g)Worktile的(de)同學肯定知(zhī)道(dào)在Workti♠ le中所有(yǒu)的(de)文(wén)件(jià♣♥Ωσn)都(dōu)可(kě)以做(zuò)到(dào)無需下 π(xià)載到(dào)本地(dì),而>π直接在線查看(kàn),這(zhè)一(yī)切都↑×♦(dōu)是(shì)預覽服務的(de)功勞,因為(wè‌ ™$i)文(wén)件(jiàn)類型的(de)各種各樣,在實現(xiàn)文☆•(wén)件(jiàn)預覽時(shí)也(yě)要(yào)根據文(wé♦≤"n)件(jiàn)的(de)類型做(zuò)出不(bù)同的(de)處理(lǐ₩ ¶),針對(duì)txt、pdf、代碼片段等文(wén)本型的(de)文(w"↔φén)件(jiàn),我們隻需要(yào)讀(dú)取文(wén)件★≤₹∑(jiàn)中的(de)內(nèi)容,然後再前端用(yòng)‌≈ 相(xiàng)應的(de)視(shì)φ↓♥圖展現(xiàn)出來(lái)即可(kě),相(xiàng)對(duì&‌ )比較簡單。但(dàn)是(shì)對(♦Ωγduì)于Office類型的(de)文(w‌©"én)件(jiàn),如(rú)ppt、doc、xls等文(wén)件(ji₩₽∞àn),就(jiù)不(bù)能(néng)這(zhè)麽簡單的(de)處理(∞←​lǐ),我們希望文(wén)件(jiàn)在&σ♠★Worktile中查看(kàn)的(de)效果和(hé)用(y ©πòng)戶在本地(dì)使用(yòng)→↔™Word、Excel、PowerPoint查看(kàn)的(de)效果差不(↓≠•bù)多(duō),經過我們的(de)調研,最±‍終選用(yòng)了(le)微(wēi)軟官方提供的(±®de)Office Web App服務。

消息推送

消息推送服務是(shì)Worktile>©¥最核心的(de)服務之一(yī),前面提到(dào)過作(zuò★σ)為(wèi)一(yī)款團隊協作(zuò)工(gōng)具®&​✘,要(yào)能(néng)夠實現(xiàn)非δ₽'φ常好(hǎo)的(de)實時(shí)性,任何數(shù)據的(de)變化'♠(huà)都(dōu)需要(yào)及時(shí)變更到(∞∞↔dào)團隊所有(yǒu)成員(yuán)當 ελ前所在的(de)視(shì)圖,如(rú)下(xià)面這(zh≥$≈è)幅圖,是(shì)一(yī)個(gè)典型的(de)任務看(kànσ♣​')闆,團隊所有(yǒu)成員(yuán)可(k₩§ě)能(néng)同時(shí)在操作(zuò)當前項目中↕✘≠™的(de)任務,每個(gè)操作(zuò)引起看(¶Ω"→kàn)闆的(de)變化(huà)都(dōu)會(hu₩≈ì)實時(shí)更新,不(bù)需要(yào)用(yòng)戶做(zuò÷​)任何刷新操作(zuò):

為(wèi)了(le)達到(dào)這(zhè)☆$©種效果,需要(yào)在Web客戶端和(hσΩé)服務器(qì)之間(jiān)維持一(yī)個(§₽ gè)長(cháng)連接,當有(yǒu)任何改變發生($‍↔¥shēng)時(shí),給客戶端發送不(bù)同的(÷±§de)消息,告知(zhī)客戶端哪些(xiē)數(sh∞δ ù)據發生(shēng)了(le)變化(huà),如(rú)下(xià)面是(§   shì)我們為(wèi)任務定義的(de)消息中的(de)其中幾個(g₹>™è):

實現(xiàn)實時(shí)消息推送,有(yǒu)↔÷♣以下(xià)幾種方式可(kě)供選擇:

 

on_task_trash            : "on♣π★_task_trash",
on_task_co♥≈¶βmplete         : "on_ta"↔¥sk_complete",
on_task_move •♥            : "on_tγ✘→ ask_move",
on_task☆δ_update           : "on_task↕←♦_update",
on_task_comment ☆ ↕¥         : "on_task_₽↔comment",
on_task_badges_f'↑ ile      : "on_ε♦δtask_badges_file",
on_task_un"•↕$archived       : "on_task_unarcγ÷hived",
on_task_badg&'"£es_check     : "on_task_badges≈↑♠α_check"

 

1.  短(duǎn)輪詢,頁面端通(tōng)過js定時§±γδ(shí)異步刷新,這(zhè)種方式優點在于實↕φ 現(xiàn)簡單,但(dàn)實時(shí)效果較差。

2.  長(cháng)輪詢。頁面端通(tōng)過js∞£≥ 異步請(qǐng)求服務端,服務端在接收到(dào)請(qǐ£✘≈&ng)求後,如(rú)果該次請(qǐng)求沒有(yǒu)數( ↔​¥shù)據,則挂起這(zhè)次請(qǐng)求,直到(dào)有(yǒ≤σu)數(shù)據到(dào)達或時(shí)•₹€♥間(jiān)片(服務端設定)到(dào),則返>•Ω回本次請(qǐng)求,客戶端接著(zhe)下(x& §ià)一(yī)次請(qǐng)求,這(zh•£✔®è)種方式對(duì)于服務的(de)要(yào✔✘)求較高(gāo),尤其在并發量很(hěn)大(dà)的(d₩¥>e)情況下(xià),對(duì)服務端的(®∞de)壓力很(hěn)大(dà)。

3.  Websocket✔∑。浏覽器(qì)通(tōng)過websocket協議(yì)連接服φ€§‌務端,實現(xiàn)了(le)浏覽器(qì)和(hé)×™服務器(qì)端的(de)全雙工(gōng)通(★§tōng)信。需要(yào)服務端和(hé)浏覽器(qì)都(dōu)支持wδ>ebsocket協議(yì)。

在Worktile一(yī)開(kāi)始我們選用(yò↔ αng)了(le)Socket.IO作(zuò&$÷↑)為(wèi)消息服務,但(dàn)是(shì)随著(zhe)訪問(wèn)量Ω→↓¶的(de)增大(dà),需要(yào)做(zuò)♦∑≠​集群化(huà)的(de)時(shí)候感覺©♣到(dào)力不(bù)從(cóng)心,尤其對¶≠(duì)于Socket.IO狀态數(shù)據的(de)存儲,由于并沒↓×有(yǒu)官方的(de)解決方案,當時(÷§shí)我們采用(yòng)了(le)一(yī‌®ε)個(gè)第三方的(de)開(kāi)源項目,使用(yòng)Redi‌ s來(lái)存儲,引起了(le)一(yī)些(xλπ✘iē)性能(néng)上(shàng)的(♠ ∞♥de)問(wèn)題,在後來(lái)重構時(shí)選用(yò≤✘ng)了(le)基于Erlang語言的(deππ$₽)開(kāi)源XMPP服務ejabberd作(zuò)為(↕™↕wèi)我們的(de)消息服務。

ejabberd是(shì)xmpp協議(yì)的≈‌®(de)一(yī)種實現(xiàn), xmpp廣泛應用(yòng)于即☆→₹↔時(shí)通(tōng)信領域。Xmpp協議(yì)的(de)實現(xiàα≈n)有(yǒu)很(hěn)多(duō)種,比如(rú)java的(de)≈≥<openfire,但(dàn)相(xiàng)較其他(tā)實現(xiàn♣λ&),ejabberd的(de)并發性能(néng)無疑πε✘©使最優秀的(de)。Xmpp協議(yì)的(de)前身(shēn)≠↔是(shì)jabber協議(yì),早期的(de)jabbe​✔π₽r協議(yì)主要(yào)包括在線狀态(presence)、ε‌好(hǎo)友(yǒu)花(huā)名冊(roster)、IQ(Info/←★↑™Query)幾個(gè)部分(fēn)。現(x↓↕✔iàn)在jabber已經成為(wèi)rfc的(de)‍γσ♠官方标準,如(rú)rfc2799, rfc‍&β→4622, rfc6121,以及xmpp• →γ的(de)擴展協議(yì)(xep)。Worktile就(jiù™ )是(shì)基于XEP-0124、XEP-0206定義的↔₹(de)BOSH擴展協議(yì)。

由于自(zì)身(shēn)業(yè)務的(★"∞λde)需要(yào),我們對(duì)ejabberd的(de)用(y♦♥òng)戶認證和(hé)好(hǎo)友(yǒ÷×≥≠u)列表模塊的(de)源碼進行(xíng)修改,通'∑(tōng)過redis保存用(yòng)戶的(de)在線狀态,而不(bù)是★∑✔(shì)mnesia和(hé)mysqγ  l。另外(wài)好(hǎo)友(yǒu)這(±"zhè)塊我們是(shì)從(cóng)₹♠•σ已有(yǒu)的(de)數(shù)據庫中(mo• ≈δngodb)中獲取Worktile中項目或團←β ♦隊的(de)成員(yuán)。Web端通(tōng)過stro☆↕Ω€phe.js來(lái)連接(http-bin₽"♠d),strophe.js可(kě)以以長(cháng)輪詢和(hé)"∞₽±websocket兩種方式來(lái)連接,由于ejabbe¥♥rd還(hái)沒有(yǒu)好(hǎo)的(de ​↓)websocket的(de)實現(xià☆≥n),就(jiù)采用(yòng)了(le)<¶≈BOSH的(de)方式模拟長(cháng)連接。整個(gè)系統$™的(de)結構如(rú)下(xià):

後記

關于Worktile整個(gè)的(de)技(jì)術(shù)架•Ω₩構就(jiù)揭秘到(dào)這(zhè)裡(lǐ),通 β₽(tōng)過上(shàng)面的(de)φ&€介紹,相(xiàng)信大(dà)家(jiā)也(yě)能±  '(néng)看(kàn)到(dào)Worktile本身(sh☆±ε↓ēn)就(jiù)是(shì)典型的(de)MEAN(Mong‍™₽"oDB、Express、AngularJS、NodeJS)架構,φ¶外(wài)加上(shàng)ejabberd作(z ↑★ uò)為(wèi)實時(shí)消息推送服務★•,建議(yì)大(dà)家(jiā)在自(z§₽ì)己的(de)産品中,根據産品自(zì)身(shēn)的(de)特點和(λ↓'∞hé)團隊的(de)技(jì)術(shù)背景,來↑ε✘(lái)選擇具體(tǐ)使用(yòng)哪種技(‌↑≥jì)術(shù)。