天天看點

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

作者|  不四(死馬)螞蟻金服 語雀産品技術負責人

語雀是什麼?

語雀是一個專業的雲端知識庫,面向個人和團隊,提供與衆不同的知識管理,打造輕松流暢的工作協同,它提供各種格式的線上文檔(富文本、表格、設計稿等)編輯能力,支援實時線上多人協同編輯,資料雲端儲存不丢失。而語雀與其他文檔工具最大的不同是,它通過知識庫來對文檔進行組織,讓知識創作者更好的管理知識。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

語雀技術架構演進

原型階段

語雀誕生于 2016 年,當時螞蟻金融雲需要一個工具來承載它的文檔。當時負責的技術同學利用業餘時間,開始搭建這個文檔工具。項目的初期,沒有任何人員和資源支援,同時也為了快速驗證原型,技術選型上選擇了最低成本的方案。

底層服務完全基于體驗技術部内部提供的 BaaS 服務和容器托管平台:

  • Object 服務:一個類 MongoDB 的資料存儲服務;
  • File 服務:阿裡雲 OSS 的基礎上封裝的一個檔案存儲服務;
  • DockerLab:一個容器托管平台;

這些服務和平台都是基于 Node.js 實作,專門給内部創新型應用使用,也正是由于有這些降低創新成本的内部服務,才給工程師們提供了更好的創新環境。

應用層服務端自然而然的選用了體驗技術部開源的 Node.js Web 架構 Egg(螞蟻内部的封裝 Chair),通過一個單體 Web 應用實作服務端。應用層用戶端也選用了 React 技術棧,結合内部的 antd,并采用 CodeMirror 實作了一個功能強大、體驗優雅的 markdown 線上編輯器。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

這時可以算作語雀的“原型階段”,它僅僅是一個工程師的業餘項目,采用内部專為創新應用提供的 BaaS 服務和一系列的開源技術解決方案,驗證了線上文檔工具這個産品原型。

PS:當時我還不在語雀團隊,但是巧的是我卻在給語雀提供 Object、File 等 BaaS 服務和 Egg.js Web 架構的支援。

内部服務階段

随着線上文檔工具得到了團隊内部的認可,語雀的目标已經不僅僅是金融雲的文檔工具,而是志在替代 confluence 等競品,成為阿裡内部十萬員工的知識管理平台。語雀要面向知識創作者,隻提供 Markdown 編輯器肯定無法讓非技術人員更高效的使用語雀。盡管有不少真愛粉因為語雀開始學習甚至愛上了 Markdown,但是我們仍然義無反顧的踏入了富文本編輯器領域的深坑。同時和 Word 等富文本編輯器不同,我們選擇了更“Web”的路線,在富文本編輯器中加入了公式、文本繪圖、思維導圖等特色功能。而随着語雀在知識管理領域的不斷探索,知識管理的三層結構(團隊、知識庫、文檔)開始成型。在此之上的協作、分享、搜尋與消息動态等功能越來越複雜單純的依靠 BaaS 服務已經無法滿足語雀的業務需求了。

為了應對業務發展帶來的挑戰,我們主要從下面幾個點進行改造:

  • BaaS 服務雖然使用簡單成本低,但是它們提供的功能不足以滿足語雀業務的發展,同時穩定性上也有不足。是以我們将底層服務由 BaaS 替換成了内部的 IaaS 服務(MySQL、OSS、緩存、搜尋等服務)。
  • Web 層仍然采用了 Node.js 與 Egg 架構,但是業務層借鑒 rails 社群的實踐開始變成了一個大型單體應用,通過引入 ORM 建構資料模型層,讓代碼的分層更清晰;
  • 前端編輯器從 codeMirror 遷移到 Slate。為了更好的實作語雀編輯器的功能,我們内部 fork 了 Slate 進行深入開發,同時也自定義了一個獨立的内容存儲格式,以提供更高效的資料處理和更好的相容性。
“雲”端的語雀:用 JavaScript 全棧打造商業級應用

在内部服務階段,語雀已經成為了一個正式的産品,和螞蟻的其他項目沒有什麼差別了,通過在阿裡内部的磨煉,語雀的産品形态基本定型。

商業化階段

随着語雀的内部影響力越來越大,一些離職出去創業的阿裡校友們開始找到玉伯:“語雀挺好用的,有沒有考慮商業化之後讓外面的公司也能夠用起來?” 經過小半年的醞釀和重構,18 年初,語雀開始正式對外提供服務,進行商業化。

當一個應用走出公司内到商業化環境中,面臨的技術挑戰一下子就變大了。最核心的知識創作管理部分的功能越來越複雜,表格、思維導圖等新格式的加入,多人實時協同的需求對編輯器技術提出了更高的挑戰。而為了更好的服務企業使用者與個人使用者, 語雀在企業服務、會員服務等方面也投入了很大精力。在業務快速發展的同時,服務商業化對品質、安全和穩定性也提出了更高的要求。

為了應對業務發展,語雀的架構也随之發生了演進:

我們将底層的依賴完全上雲,全部遷移到了阿裡雲上,阿裡雲不僅僅提供了基礎的存儲、計算能力,同時也提供了更豐富的進階服務,同時在穩定性上也有保障。

  • 豐富的雲計算基礎服務,保障語雀的服務端可以選用最适合語雀業務的的存儲、隊列、搜尋引擎等基礎服務;
  • 更多人工智能服務給語雀的産品帶來了更多的可能性,包括 OCR 識圖、智能翻譯等服務,最終都直接轉化成為了語雀的特色服務;

而在應用層,語雀的服務端依然還是以一個基于 Egg 架構的大型的 Node.js web 應用為主。但是随着功能越來越多,也開始将一些相對比較獨立的服務從主服務中拆出去,可以把這些服務分成幾類:

  • 微服務類:例如多人實時協同服務,由于它相對獨立,且長連接配接服務不适合頻繁釋出,是以我們将其拆成了一個獨立的微服務,保持其穩定性;
  • 任務服務類:像語雀提供的大量本地檔案預覽服務,會産生一些任務比較消耗資源、依賴複雜。我們将其從主服務中剝離,可以避免不可控的依賴和資源消耗對主服務造成影響;
  • 函數計算類:類似 plantuml 預覽、mermaid 預覽等任務,對響應時間的敏感度不高,且依賴可以打包到阿裡雲函數計算中,我們會将其放到函數計算中運作,既省錢又安全;

随着編輯器越來越複雜,在 slate 的基礎上進行開發遇到的問題越來越多。最終語雀還是走上了自研編輯器的道路,基于浏覽器的 contenteditable 實作了富文本編輯器,通過 canvas 實作了表格編輯器,通過 SVG 實作了思維導圖編輯器。

語雀富文本編輯器相關的介紹,可以看看 Lake Editor 之父隆昊的分享: 富文本編輯器的技術演進
“雲”端的語雀:用 JavaScript 全棧打造商業級應用

語雀的這個階段(也是現在所處的階段)是商業化階段,但是我們仍然保持了一個很小的團隊,通過 JavaScript 全棧進行研發。底層的服務全面上雲,借力雲服務打造語雀的特色功能。同時為企業級使用者和個人知識工作者者提供知識創作和管理工具。

JavaScript 全棧

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

在社交網絡上,大家好像對 JavaScript 全棧的看法都比較負面,“樣樣通,樣樣松”可能是大家聽到全棧工程師這個名詞後的第一印象。那為什麼語雀選擇了 JavaScript 全棧的方向呢?

JavaScript 全棧與産品工程師

在語雀,我們并不将用 JavaScript 全棧進行開發的工程師定義為全棧工程師,而是“一專多能”型的産品工程師:

  • 他們是産品的“技術合夥人”,他們對産品有 owner 感,和産品經理一起參與産品讨論設計,從技術的角度上對産品設計方案提出建議,獨立的完成産品功能的全棧研發,并跟蹤釋出後的産品結果。
  • 同時他們也是某一個技術領域的領域專家,例如有人可能是服務端領域的專家、測試領域的專家、前端建構領域的專家、CSS 領域的專家。他們可以用自己的專業領域知識來優化團隊研發工具鍊,提升産品研發效率。
“雲”端的語雀:用 JavaScript 全棧打造商業級應用

在語雀,産品工程師們的産品研發流程是這樣的:

  • 在産品設計階段,産品工程師就會參與進去進行讨論,最終會産出一份 final design 的産品設計稿。由于前期産品工程師參與充分讨論,一般此處定下的産品設計稿到後期的研發過程中不會遇到技術上的問題;
  • 随後會在語雀上進行文檔化的系統分析設計。會在語雀上發起異步的評審。一些大的技術方案會有其他的領域專家加入進來一起進行評審,確定将所有的技術難點都梳理清楚;
  • 系統設計清晰後,進入研發階段;
  • 對所有的代碼,都需要有自動化測試覆寫。對所有新增代碼和修改的業務邏輯都需要有完全覆寫的單元測試,對關鍵鍊路的功能同時也要提供端到端測試。編寫完自動化測試是進入代碼評審前的必備流程。
  • 階段性的功能研發完成、測試編寫完善後會發起異步的代碼評審。會邀請相關業務的負責人和對應的一些領域專家來進行代碼評審。從業務邏輯的正确性,安全性,可維護性等多個角度來進行代碼評審。
  • 最終在釋出上線時,必須遵循三闆斧原則:可灰階、可應急、可監控。避免功能變更可能帶來的 bug 影響到大量使用者。
“雲”端的語雀:用 JavaScript 全棧打造商業級應用
語雀是如何進行全棧 JavaScript 測試的呢?感興趣的同學可以看看語雀團隊大前端自動化測試大牛達峰老師的分享: 大前端測試的思考和在語雀的實踐

通過 JavaScript 全棧,語雀團隊可以更高效、高品質的的完成産品研發:

  • 從代碼層面上來說,有大量的代碼可以複用,以編輯器舉例,它不僅僅可以在 Web 端使用,也可以在桌面端使用。同時許多資料處理的能力還可以在服務端使用。
  • 從産品研發效率上來說,全棧研發減少了大量溝通成本,在語雀目前的階段是非常高效的。而 JavaScript 全棧避免了開發者在不同的語言中進行切換,不用考慮前端使用的 lodash / moment 等工具類在其他語言中應該用什麼,大大提升全棧的研發效率。
  • 最後從工程師角度來看,全棧研發讓工程師有機會深度參與到産品研發的整個流程中,大家會自發的去思考産品有什麼優化點,從技術上能幫助産品做什麼。例如語雀最近新上的 OCR 搜圖功能,就是語雀的全棧工程師自發從技術預研到産品落地完成整個産品優化的。
“雲”端的語雀:用 JavaScript 全棧打造商業級應用

JavaScript 全棧與 Node.js

說到 JavaScript 全棧,有一個繞不過去的技術就是 Node.js。作為一個與前端結合緊密的服務端運作時,基本上就成為了全棧的代言人。那 Node.js 是不是真的是一個适合大型商業化項目的語言呢?大家對它都有頗多質疑:

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

其實随着 JS 語言的發展,許多問題已經得到了解決,例如 Async Function 的出現,可以讓開發者以同步的方式編寫異步代碼,了解起來更簡單,異常處理也變簡單了。同時随着社群的進一步完善,大量高品質的工具子產品、架構湧現出來。語雀的服務端部分基于 Egg 架構,已經內建了大量 Web 開發需要的子產品和服務,同時基于 Async Function 程式設計模型也更加簡單。TypeScript 的出現也打消了許多人對 JavaScript 進行大型項目開發的疑慮。除此之外,語雀還有一些其他的方式來保障代碼品質和可維護性(語雀甚至是一個純 JavaScript 項目,沒有一行 TypeScript 代碼)。

語雀做的第一件事情就是确定核心系統和外部系統的邊界。通過六邊形架構(也叫做端口擴充卡架構),我們把語雀核心系統和外界系統和使用者之間的互動固定下來。通過“端口”的形式,來确定輸入和輸出。外部系統通過“擴充卡”來将系統對接到語雀暴露的端口之上,隻需要按照“端口”定義來實作,外部系統可以自由替換。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

在這個模型下,Controller 就是語雀暴露給使用者接口的 HTTP 擴充卡。在 Controller 中,我們對使用者請求參數進行格式校驗和轉換,檢查使用者權限,并格式化輸出。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

我們定義好語雀與第三方平台和服務之間的互動方式(一般是一系列方法),通過擴充卡,将不同環境的不同服務封裝成統一的方法,并在調用時記錄好調用日志。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

資料模型層即是資料層的 Model,以 Doc 模型舉例,它的 meta 資訊資料被存儲在了 MySQL 中,而文檔正文資料被加密後存儲在 OSS 中。對于語雀核心的業務邏輯來說,完全不感覺底層的存儲在哪裡。更進一步來說,隻要語雀是使用 SQL 和資料庫進行互動,底層資料可以無縫遷移到 OceanBase 等其他支援完整 SQL 文法的資料庫中,即使有少量修改也可以在 Model 層封裝掉。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

最終以一次文檔釋出舉例,使用者通過調用 HTTP 接口與語雀進行互動,資料會通過 Model 層寫入到存儲中,包括 MySQL 和 OSS,更新文檔緩存。同時出發異步消息給其他系統,觸發釘釘的 WebHook,并将資料同步到搜尋引擎中。這些和外界系統的互動通過擴充卡封裝之後各司其職,參數轉換、權限校驗、日志記錄,不僅確定核心邏輯的精簡,也讓系統調用鍊路跟蹤更加簡單。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

混合應用架構

當系統發展到一定程度後,到底是應該繼續在大單體應用上加功能,還是拆分成微服務呢?這兩種架構既然存在,肯定有各自的優劣,具體選擇那種架構形式,應該是與目前的業務規模和團隊分布決定的。是以語雀的技術架構随着語雀的業務形态也變成了一個混合式的技術架構。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

語雀的主服務是一個大型的 Node.js 服務,集中了所有的應用業務邏輯。而在主服務之外,還有一些不同形态的其他服務。

  • 微服務:一些獨立而穩定的功能子產品,或者有額外部署架構需求的服務,會通過微服務的形式獨立部署,系統間暫時通過 HTTP 接口進行互動。例如實時協同服務,由于其自身比較獨立穩定,而且是長連接配接服務,不能頻繁釋出重新開機,是以将其部署成了一個獨立的微服務。
  • 任務叢集:一些 CPU 密集型的任務,或者依賴了一些複雜的第三方依賴的服務,會放到一個獨立的任務叢集中。例如各種檔案預覽服務,可能依賴到了其他服務,且需要消耗大量計算成本,放到任務叢集通過隊列消除并發後最為合适。
  • 函數計算:一些對響應時間比較高且可以函數化的服務,我們會盡量遷移到阿裡雲的函數計算,例如plantuml、mermaid 等文本繪圖服務。
“雲”端的語雀:用 JavaScript 全棧打造商業級應用

以 mermaid 的渲染舉例。使用者輸入一段 mermaid 代碼調用語雀,語雀調用一個部署在阿裡雲函數計算的函數,在函數中運作 puppeteer 渲染成 svg 傳回。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

為什麼要特别把 Serverless 單獨拿出來說呢?還記得之前說 Node.js 是單線程,不适合 CPU 密集型任務麼?由于 Serverless 的出現,我們可以将這些存在安全風險的,消耗大量 CPU 計算的任務都遷移到函數計算上。它運作在沙箱環境中,不用擔心使用者的惡意代碼造成安全風險,同時将這些 CPU 密集型的任務從主服務中剝離,避免出現并發時阻塞主服務。按需付費的方式也可以大大節約成本,不需要為低頻功能場景部署一個常駐服務。是以我們會盡量的把這類服務都遷移到 Serverless 上(如阿裡雲函數計算)。

語言之外的通用領域

除了語言之外,任何的商業化系統還有更多需要考慮的方面,其中最重要的兩點可能就是安全性和穩定性了。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

一個系統從前端、服務端到底層的依賴都存在着各種各樣的安全風險:

  • 前端安全風險:XSS、跳轉釣魚、跨站請求等
  • 服務端安全風險:水準權限問題、未授權通路、敏感資訊洩露、SSRF、SQL 注入等
  • 雲服務的安全風險:短信/郵件轟炸、資料洩露風險、内容安全等

這些安全問題想要解決基本都沒有銀彈,隻能一個個單獨處理,但是有一些基本的原則:

  • 不要信任使用者的任何輸入
    • 任何渲染富文本的地方都需要防範 XSS,内容也可能并不是通過 IDE 輸入的;
    • 要在服務端執行使用者的代碼一定要放在沙箱中;
    • 要從服務端請求使用者傳遞的資源,一定要經過 SSRF 過濾;
  • 沉澱标準的編碼範式來處理安全風險,且需要在 Code Review 中重點關注
    • 所有接口都必須有權限校驗;
    • 響應序列化方法過濾敏感資訊;
    • 不允許拼接 SQL;

語雀從商業化一開始就和安全團隊通力協作,從内部的安全意識教育訓練、内部安全團隊測試,到内部的紅藍攻防、外部的白帽子滲透測試,安全是一場持久戰。

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

為了保障語雀的穩定性,我們從前端到服務端和雲服務上都做了許多工作,和安全一樣,穩定性也是一個從前到後的長期工程。語雀的穩定性保障主要在兩個次元:

  • 保障服務可用性:從架構設計上要杜絕單點,底層的資料都需要進行容災和備份,服務需要多單元、可用區部署。同時避免引入不必要的強依賴;
  • 異常可監控和追溯:從前端的業務埋點日志、異常日志監控,到服務端的全鍊路日志跟蹤和采集,系統性能監控和分析。最終我們可以達到異常可及時感覺和追溯,性能問題可以定位分析;

什麼叫做避免引入不必要的強依賴呢?以語雀的場景舉例,MySQL 就是一個無法去除的強依賴,而緩存不應該是一個強依賴,但是最早語雀的 session 是存儲在緩存(Redis)中的,一旦 Redis 叢集出問題,使用者資料無法擷取就導緻使用者無法登入。這就把緩存變成了一個強依賴。是以我們将 session 存儲放到了 MySQL 中,Redis 就變成了一個弱依賴,它挂了系統還能正常運作。另一個例子,語雀前段時間上線了多人實時協同編輯的功能,而在這個功能上線之前,是通過文檔加鎖的方式避免多個人同時編輯同一篇文檔的。然而多人實時協同引入了另一個服務,一旦實時協同服務挂了,使用者就無法編輯文檔了,它又變成了語雀系統的一個強依賴,為了解決他,我們在使用者連接配接協同服務失敗的時候,自動切換到老的鎖模式。這樣協同服務也變成了語雀的一個弱依賴。

語雀如何選擇技術棧

“雲”端的語雀:用 JavaScript 全棧打造商業級應用

語雀這幾年一步步發展過來,背後的技術一直在演進,但是始終遵循了幾條原則:

  1. 技術棧選型要比對産品發展階段。産品在不同的階段對技術提出的要求是不一樣的,越前期,對疊代效率的要求越高,商業化規模化之後,對穩定性、性能的要求就會變高。不需要一上來就用最先進的技術方案,而是需要和産品階段一起考慮和權衡。
  2. 技術棧選型要結合團隊成員的技術背景。語雀選擇 JavaScript 全棧的原因是孵化語雀的團隊,大部分都是 JavaScript 背景的程式員,同時 Node.js 在螞蟻也算是一等公民,配套的設施相對完善。
  3. 最重要的一點是,不論選擇什麼技術棧,安全、穩定、可維護(擴充)都是要考慮清楚的。用什麼語言、用什麼服務會變化,但是這些基礎的安全意識、穩定性意識,如何編寫可維護的代碼,都是決定項目能否長期發展下去的重要因素。
阿裡巴巴雲原生 關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術圈。”

繼續閱讀