螞蟻金服網紅技術團隊分享,用 JavaScript 全棧打造商業級應用

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

你們好,我是螞蟻金服語雀產品技術負責人 不四(死馬),想跟你們分享也許是西湖區最複雜的 Node.js 應用的相關實踐。git

寫在前面程序員

紙上得來終覺淺,絕知此事要躬行。web

文章中所涉及的PPT 已經上傳到:個人語雀專欄,歡迎下載,同時語雀也參與了Gitee 七週年記念日,爲你們提供了一份小小的福利,戳這裏能夠領取 3 個月的語雀 VIP 會員,但願你們能夠經過體驗這個產品,瞭解咱們的一些技術實現。數據庫

 

語雀是什麼?編程

語雀是一個專業的雲端知識庫,面向我的和團隊,提供不同凡響的知識管理,打造輕鬆流暢的工做協同,它提供各類格式的在線文檔(富文本、表格、設計稿等)編輯能力,支持實時在線多人協同編輯,數據雲端保存不丟失。而語雀與其餘文檔工具最大的不一樣是,它經過知識庫來對文檔進行組織,讓知識創做者更好的管理知識。canvas

語雀技術架構演進瀏覽器

原型階段緩存

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

底層服務徹底基於體驗技術部內部提供的 BaaS 服務和容器託管平臺:

  • Object 服務:一個類 MongoDB 的數據存儲服務;
  • File 服務:阿里雲 OSS 的基礎上封裝的一個文件存儲服務;
  • DockerLab:一個容器託管平臺;

這些服務和平臺都是基於 Node.js 實現,專門給內部創新型應用使用,也正是因爲有這些下降創新成本的內部服務,纔給工程師們提供了更好的創新環境。

應用層服務端天然而然的選用了體驗技術部開源的 Node.js Web 框架 Egg(螞蟻內部的封裝 Chair),經過一個單體Web 應用實現服務端。應用層客戶端也選用了 React 技術棧,結合內部的 antd,並採用 CodeMirror 實現了一個功能強大、體驗優雅的 markdown 在線編輯器。

 

 

這時能夠算做語雀的「原型階段」,它僅僅是一個工程師的業餘項目,採用內部專爲創新應用提供的 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 進行深刻開發,同時也自定義了一個獨立的內容存儲格式,以提供更高效的數據處理和更好的兼容性。

在內部服務階段,語雀已經成爲了一個正式的產品,和螞蟻的其餘項目沒有什麼區別了,經過在阿里內部的磨鍊,語雀的產品形態基本定型。

商業化階段

隨着語雀的內部影響力愈來愈大,一些離職出去創業的阿里校友們開始找到玉伯:「語雀挺好用的,有沒有考慮商業化以後讓外面的公司也可以用起來?」 通過小半年的醞釀和重構,18 年初,語雀開始正式對外提供服務,進行商業化。

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

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

咱們將底層的依賴徹底上雲,所有遷移到了阿里雲上,阿里雲不只僅提供了基礎的存儲、計算能力,同時也提供了更豐富的高級服務,同時在穩定性上也有保障。

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

而在應用層,語雀的服務端依然仍是以一個基於 Egg 框架的大型的 Node.js web 應用爲主。可是隨着功能愈來愈多,也開始將一些相對比較獨立的服務從主服務中拆出去,能夠把這些服務分紅幾類:

  • 微服務類:例如多人實時協同服務,因爲它相對獨立,且長鏈接服務不適合頻繁發佈,因此咱們將其拆成了一個獨立的微服務,保持其穩定性;
  • 任務服務類:像語雀提供的大量本地文件預覽服務,會產生一些任務比較消耗資源、依賴複雜。咱們將其從主服務中剝離,能夠避免不可控的依賴和資源消耗對主服務形成影響;
  • 函數計算類:相似 plantuml 預覽、mermaid 預覽等任務,對響應時間的敏感度不高,且依賴能夠打包到阿里雲函數計算中,咱們會將其放到函數計算中運行,既省錢又安全;

隨着編輯器愈來愈複雜,在 slate 的基礎上進行開發遇到的問題愈來愈多。最終語雀仍是走上了自研編輯器的道路,基於瀏覽器的 contenteditable 實現了富文本編輯器,經過 canvas 實現了表格編輯器,經過 SVG 實現了思惟導圖編輯器。

語雀富文本編輯器相關的介紹,能夠看看 Lake Editor 之父隆昊的分享:富文本編輯器的技術演進

語雀的這個階段(也是如今所處的階段)是商業化階段,可是咱們仍然保持了一個很小的團隊,經過 JavaScript 全棧進行研發。底層的服務全面上雲,借力雲服務打造語雀的特點功能。同時爲企業級用戶和我的知識工做者者提供知識創做和管理工具。

JavaScript 全棧

 

在社交網絡上,你們好像對 JavaScript 全棧的見解都比較負面,「樣樣通,樣樣鬆」多是你們聽到全棧工程師這個名詞後的第一印象。那爲何語雀選擇了 JavaScript 全棧的方向呢?

JavaScript 全棧與產品工程師

在語雀,咱們並不將用 JavaScript 全棧進行開發的工程師定義爲全棧工程師,而是「一專多能」型的產品工程師

  • 他們是產品的「技術合夥人」,他們對產品有 owner 感,和產品經理一塊兒參與產品討論設計,從技術的角度上對產品設計方案提出建議,獨立的完成產品功能的全棧研發,並跟蹤發佈後的產品結果。
  • 同時他們也是某一個技術領域的領域專家,例若有人多是服務端領域的專家、測試領域的專家、前端構建領域的專家、CSS 領域的專家。他們能夠用本身的專業領域知識來優化團隊研發工具鏈,提高產品研發效率。

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

  • 在產品設計階段,產品工程師就會參與進去進行討論,最終會產出一份 final design 的產品設計稿。因爲前期產品工程師參與充分討論,通常此處定下的產品設計稿到後期的研發過程當中不會遇到技術上的問題;
  • 隨後會在語雀上進行文檔化的系統分析設計。會在語雀上發起異步的評審。一些大的技術方案會有其餘的領域專家加入進來一塊兒進行評審,確保將全部的技術難點都梳理清楚;
  • 系統設計清晰後,進入研發階段;
  • 對全部的代碼,都須要有自動化測試覆蓋。對全部新增代碼和修改的業務邏輯都須要有徹底覆蓋的單元測試,對關鍵鏈路的功能同時也要提供端到端測試。編寫完自動化測試是進入代碼評審前的必備流程。
  • 階段性的功能研發完成、測試編寫完善後會發起異步的代碼評審。會邀請相關業務的負責人和對應的一些領域專家來進行代碼評審。從業務邏輯的正確性,安全性,可維護性等多個角度來進行代碼評審。
  • 最終在發佈上線時,必須遵循三板斧原則:可灰度、可應急、可監控。避免功能變動可能帶來的 bug 影響到大量用戶。

 

語雀是如何進行全棧 JavaScript 測試的呢?感興趣的同窗能夠看看語雀團隊大前端自動化測試大牛達峯老師的分享:大前端測試的思考和在語雀的實踐

經過 JavaScript 全棧,語雀團隊能夠更高效、高質量的的完成產品研發:

  • 從代碼層面上來講,有大量的代碼能夠複用,以編輯器舉例,它不只僅能夠在 Web 端使用,也能夠在桌面端使用。同時許多數據處理的能力還能夠在服務端使用。
  • 從產品研發效率上來講,全棧研發減小了大量溝通成本,在語雀當前的階段是很是高效的。而 JavaScript 全棧避免了開發者在不一樣的語言中進行切換,不用考慮前端使用的 lodash / moment 等工具類在其餘語言中應該用什麼,大大提高全棧的研發效率。
  • 最後從工程師角度來看,全棧研發讓工程師有機會深度參與到產品研發的整個流程中,你們會自發的去思考產品有什麼優化點,從技術上能幫助產品作什麼。例如語雀最近新上的 OCR 搜圖功能,就是語雀的全棧工程師自發從技術預研到產品落地完成整個產品優化的。

JavaScript 全棧與 Node.js

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

其實隨着 JS 語言的發展,許多問題已經獲得瞭解決,例如 Async Function 的出現,可讓開發者以同步的方式編寫異步代碼,理解起來更簡單,異常處理也變簡單了。同時隨着社區的進一步完善,大量高質量的工具模塊、框架涌現出來。語雀的服務端部分基於 Egg 框架,已經集成了大量 Web 開發須要的模塊和服務,同時基於 Async Function 編程模型也更加簡單。TypeScript 的出現也打消了許多人對 JavaScript 進行大型項目開發的疑慮。除此以外,語雀還有一些其餘的方式來保障代碼質量和可維護性(語雀甚至是一個純 JavaScript 項目,沒有一行 TypeScript 代碼)。

語雀作的第一件事情就是肯定核心系統和外部系統的邊界。經過六邊形架構(也叫作端口適配器架構),咱們把語雀核心系統和外界系統和用戶之間的交互固定下來。經過「端口」的形式,來肯定輸入和輸出。外部系統經過「適配器」來將系統對接到語雀暴露的端口之上,只須要按照「端口」定義來實現,外部系統能夠自由替換。

在這個模型下,Controller 就是語雀暴露給用戶接口的 HTTP 適配器。在 Controller 中,咱們對用戶請求參數進行格式校驗和轉換,檢查用戶權限,並格式化輸出。

咱們定義好語雀與第三方平臺和服務之間的交互方式(通常是一系列方法),經過適配器,將不一樣環境的不一樣服務封裝成統一的方法,並在調用時記錄好調用日誌。

數據模型層便是數據層的 Model,以 Doc 模型舉例,它的 meta 信息數據被存儲在了 MySQL 中,而文檔正文數據被加密後存儲在 OSS 中。對於語雀核心的業務邏輯來講,徹底不感知底層的存儲在哪裏。更進一步來講,只要語雀是使用SQL 和數據庫進行交互,底層數據能夠無縫遷移到 OceanBase 等其餘支持完整 SQL 語法的數據庫中,即便有少許修改也能夠在 Model 層封裝掉。

最終以一次文檔發佈舉例,用戶經過調用 HTTP 接口與語雀進行交互,數據會經過 Model 層寫入到存儲中,包括MySQL 和 OSS,更新文檔緩存。同時出發異步消息給其餘系統,觸發釘釘的 WebHook,並將數據同步到搜索引擎中。這些和外界系統的交互經過適配器封裝以後各司其職,參數轉換、權限校驗、日誌記錄,不只確保核心邏輯的精簡,也讓系統調用鏈路跟蹤更加簡單。

混合應用架構

當系統發展到必定程度後,究竟是應該繼續在大單體應用上加功能,仍是拆分紅微服務呢?這兩種架構既然存在,確定有各自的優劣,具體選擇那種架構形式,應該是與當前的業務規模和團隊分佈決定的。因此語雀的技術架構隨着語雀的業務形態也變成了一個混合式的技術架構。

語雀的主服務是一個大型的 Node.js 服務,集中了全部的應用業務邏輯。而在主服務以外,還有一些不一樣形態的其餘服務。

  • 微服務:一些獨立而穩定的功能模塊,或者有額外部署架構需求的服務,會經過微服務的形式獨立部署,系統間暫時經過 HTTP 接口進行交互。例如實時協同服務,因爲其自身比較獨立穩定,並且是長鏈接服務,不能頻繁發佈重啓,因此將其部署成了一個獨立的微服務。
  • 任務集羣:一些 CPU 密集型的任務,或者依賴了一些複雜的第三方依賴的服務,會放到一個獨立的任務集羣中。例如各類文件預覽服務,可能依賴到了其餘服務,且須要消耗大量計算成本,放到任務集羣經過隊列消除併發後最爲合適。
  • 函數計算:一些對響應時間比較高且能夠函數化的服務,咱們會盡可能遷移到阿里雲的函數計算,例如plantuml、mermaid 等文本繪圖服務。

以 mermaid 的渲染舉例。用戶輸入一段 mermaid 代碼調用語雀,語雀調用一個部署在阿里雲函數計算的函數,在函數中運行 puppeteer 渲染成 svg 返回。

爲何要特別把 Serverless 單獨拿出來講呢?還記得以前說 Node.js 是單線程,不適合 CPU 密集型任務麼?因爲Serverless 的出現,咱們能夠將這些存在安全風險的,消耗大量 CPU 計算的任務都遷移到函數計算上。它運行在沙箱環境中,不用擔憂用戶的惡意代碼形成安全風險,同時將這些 CPU 密集型的任務從主服務中剝離,避免出現併發時阻塞主服務。按需付費的方式也能夠大大節約成本,不須要爲低頻功能場景部署一個常駐服務。因此咱們會盡可能的把這類服務都遷移到 Serverless 上(如阿里雲函數計算)。

語言以外的通用領域

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

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

  • 前端安全風險:XSS、跳轉釣魚、跨站請求等
  • 服務端安全風險:水平權限問題、未受權訪問、敏感信息泄露、SSRF、SQL 注入等
  • 雲服務的安全風險:短信/郵件轟炸、數據泄露風險、內容安全等

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

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

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

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

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

什麼叫作避免引入沒必要要的強依賴呢?以語雀的場景舉例,MySQL 就是一個沒法去除的強依賴,而緩存不該該是一個強依賴,可是最先語雀的 session 是存儲在緩存(Redis)中的,一旦 Redis 集羣出問題,用戶資料沒法獲取就致使用戶沒法登陸。這就把緩存變成了一個強依賴。因此咱們將 session 存儲放到了 MySQL 中,Redis 就變成了一個弱依賴,它掛了系統還能正常運行。另外一個例子,語雀前段時間上線了多人實時協同編輯的功能,而在這個功能上線以前,是經過文檔加鎖的方式避免多我的同時編輯同一篇文檔的。然而多人實時協同引入了另外一個服務,一旦實時協同服務掛了,用戶就沒法編輯文檔了,它又變成了語雀系統的一個強依賴,爲了解決他,咱們在用戶鏈接協同服務失敗的時候,自動切換到老的鎖模式。這樣協同服務也變成了語雀的一個弱依賴。

語雀如何選擇技術棧

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

  1. 技術棧選型要匹配產品發展階段。產品在不一樣的階段對技術提出的要求是不同的,越前期,對迭代效率的要求越高,商業化規模化以後,對穩定性、性能的要求就會變高。不須要一上來就用最早進的技術方案,而是須要和產品階段一塊兒考慮和權衡。
  2. 技術棧選型要結合團隊成員的技術背景。語雀選擇 JavaScript 全棧的緣由是孵化語雀的團隊,大部分都是 JavaScript 背景的程序員,同時 Node.js 在螞蟻也算是一等公民,配套的設施相對完善。
  3. 最重要的一點是,不論選擇什麼技術棧,安全、穩定、可維護(擴展)都是要考慮清楚的。用什麼語言、用什麼服務會變化,可是這些基礎的安全意識、穩定性意識,如何編寫可維護的代碼,都是決定項目可否長期發展下去的重要因素。
相關文章
相關標籤/搜索