掌握了開源框架還不夠,你更需要掌握原始碼
摘要:本篇文章將以解決 Element Plus 問題的經歷開始,循序漸進討論開源專案或開源框架的問題,進一步討論駕馭開源專案原始碼的方法和技巧,分享自己閱讀、理解和更改原始碼的思路。
本文分享自華為雲社群《優秀開源框架就一定靠譜麼?五招助你駕馭原始碼》,原文作者:Marvin Zhang 。
前言
The most incomprehensible thing about the world is that it is comprehensible.
世界上最不可理解的地方就是它竟然是可以理解的。-- 阿爾伯特·愛因斯坦
開源(Open-Source)造就瞭如今繁榮活躍的軟體行業。開源讓全世界的開發者都能夠協力編寫出優秀的工具類專案,也就是所謂的 “輪子”,在造福大大小小的公司個人的同時,也可以展現創作者或貢獻者的技術實力。如今很多開發者都在大量使用開源專案作為自己專案的第三方庫或依賴,更快更高效的完成開發任務。
筆者也不例外。我最近在用 Vue 3 重構 Crawlab 前端的時候,用到了 Element 團隊開發的升級版的 ElementUI,也就是 Vue 3 重構的新 UI 框架 Element Plus。Element 團隊在 Element Plus 中將該專案用 Vue 3 完全重構,全面擁抱了TypeScript;而且相比於之前的 Vue 2 版本豐富了部分元件;而整體風格和使用方式跟之前的版本一致;一些 API 在使用上還變得更精簡了。因此,筆者在重構 Crawlab 前端初期過程中沒有遇到太大的障礙,再加上之前的編寫經驗,開發過程中顯得駕輕就熟。然而,好景不長,隨著專案的不斷開發,筆者遭遇到一些技術上的困難。更準確的說,在實現一些複雜功能時遇到了來自於 Element Plus 框架本身的限制。雖然最終想方設法將問題解決了,但是我也深刻體會到了硬啃(Hacking)開源專案原始碼的困難。因此,也希望藉此機會將自己駕馭開原始碼的經驗分享給讀者。
本篇文章將以解決 Element Plus 問題的經歷開始,循序漸進討論開源專案或開源框架的問題,進一步討論駕馭開源專案原始碼的方法和技巧,分享自己閱讀、理解和更改原始碼的思路。本篇文章主要是方法論的討論,不涉及太多技術細節,任何專業背景的讀者都可閱讀。
硬啃 Element Plus
框架簡介
如果你是使用過 Vue 的前端工程師,你肯定聽說過 ElementUI。這是一個 UI 框架,也就是說它是幫助你構建 Web 專案的工具類框架,其中包含很多常用的元件(Component)、佈局(Layout)以及主題(Theme)等。早期的 ElementUI 是用 Vue 2 寫的,是 Vue 中最受歡迎的 UI 框架,在 Github 上有 49k 標星,是第二名 iView(24k)的兩倍多。隨著 Vue 作者尤雨溪釋出 Vue 3 版本,宣告全面擁抱 TS 之後,原 Element 團隊在 Vue 3 的基礎上開發了新版本 Element Plus,也就是這個故事的主角。如果對 Vue 3 甚至 Vue 不瞭解,可以閱讀本部落格之前的技術文章《TS 加持的 Vue 3,如何幫你輕鬆構建企業級前端應用》。
初試牛刀
Element Plus 的使用方法跟之前的 Vue 2 版本一樣簡單易用,文件風格也跟老版本一致,非常全面。按照官方文件鼓勵的,筆者也嘗試用元件化的方式來優化之前的 Crawlab 老元件,例如表格(Table)。除了將列表(Column)、資料(Data)、分頁(Pagination)等進行簡單的封裝以外,筆者還希望加入一些新的實用功能,例如自定義表格的篩選(Filter)和排序(Sort),以及表格能夠允許使用者自定義(Customize)要展示的列和調整列的順序,等等。雖然 Element Plus 框架有排序和篩選功能,但筆者個人覺得太基礎了,通常使用者需要既簡單又好用的 UI 元件。
下圖是篩選表格的前端截圖,比較類似 Excel 的展示和操作方式。
實現這個功能的問題不是非常大,按照官方的文件都能解決。其中主要利用了列元件中的 el-table-column 的 header 插槽(Slot),在其中加入彈出框(Popover)。很好,So far so good!接下來就是加入列的自定義功能了,看上去很快就要大功告成了嘛。
問題初顯
要實現列的自定義,最完美的實現方法就是讓使用者點選操作按鈕,彈出對話方塊(Dialog),裡面可以選擇要展現的列,用拖拽的方式將其排序,然後點選確認。然後我在 Element Plus 官網上快速找到了一個新元件,穿梭框(Transfer),效果如下圖。理論上,只要我在對話方塊中引用該彈出框來控制展現列的陣列就可以了,例如將要展示的列放到 “列表2” 中。鏘鏘!目前看起來似乎一切非常順利。
不過,在仔細閱讀文件之後我發現該元件並不支援拖拽,因此暫時無法實現列排序的功能。雖然有點沮喪,但這個影響不大,我們主要想實現的是列選擇功能。排序雖然也重要,但暫時先擱置一下,先實現元件選擇再說!
然而,現實總是出乎意料。筆者在進一步試驗中發現,這個元件似乎存在一個重大 Bug:當穿梭框中全選候選元素時,竟然無法選擇或取消選擇了(向左移動或向右移動)!趕緊仔細除錯查詢原因,反覆確認是不是我自己寫的程式碼有問題。然後發現是 Vue 在更新元件時在 runtime-core.esm-builder.js 中的 patchBlockChildren 方法下出現了 oldChildren 為 null 的情況。進一步閱讀 Element Plus 中穿梭框元件的相關原始碼之後,懷疑是 transfer-panel.vue 這個子元件出了問題,它更新渲染的時候 el-checkbox-group 不存在,因此導致更新時出錯。隨後我在 Element Plus 的 Github 倉庫中提交了 Bug Issue。不過維護者並不認為我反映的問題是 Bug,因為在 CodePen 上似乎沒有問題。但這個 Bug 確實在我的專案中客觀存在,可以在 crawlab-frontend 的 歷史提交 中復現。
再遇困境
無奈之下,自行琢磨半天無果之後,果斷決定自己造輪子。很快從頭寫了一個類似穿梭框的元件出來,元件效果如下圖。新寫的元件在外觀上與之前的非常類似,不過自由度很高,因此也順便支援了拖拽功能。看上去問題似乎快要解決了。
不過,當我滿懷期望準備測試拖拽排序功能時,意想不到的事情發生了:列順序資料改變之後,表格列竟然沒有任何改變!改變列元件 el-table-column 的順序對介面展示的竟然沒有作用!而在官方文件的定義中,el-table-column 的順序決定著列的實際展示順序。經過重複多次試驗之後,筆者只好承認原先的實現方式有問題。簡單的改變模版中的列元件順序並不會影響介面上的展示。
筆者當時想到一個 “聰(yu)明(chun)” 的辦法。我試圖用強制重新整理表格元件的方式讓其資料重新渲染,達到改變列順序的目的。但經過測試,發現這樣的暴力方式存在很大的效能問題,每次點選 “Apply” 之後會卡頓近一秒。
於是,筆者又陷入了絕望的境地。
抽絲剝繭
在進退兩難的情況下,筆者停下來仔細思考。是不是我對 Element Plus 的框架還不夠了解?表格元件本身是如何實現的?它有什麼限制和缺點?這些問題都推動我進一步來拆解 Element Plus 框架本身,也就是去閱讀它的原始碼,理解元件本身的程式碼邏輯和工作原理。於是,筆者克隆了 Element Plus 程式碼倉庫 到本地。
非常幸運,Element Plus 專案的程式碼質量相當高,程式碼組織結構和命名方式都非常清晰。雖然註釋相對來說偏少,但它清晰的邏輯結構和良好的命名規範讓可讀性變得很強。在驚歎於大廠工程師職業素養的同時,我也快速定位到了表格元件的原始碼位置 packages/table。整個 Element Plus 專案是用 MonoRepo 的方式管理的,簡單來說是一個 Git 倉庫裡有很多個 NPM 專案。管理工具使用了 Lerna。下面是 el-table
元件 NPM 專案的程式碼組織結構。
. ├── __tests__ │ └── table.spec.ts ├── index.ts ├── package.json └── src ├── config.ts ├── filter-panel.vue ├── h-helper.ts ├── layout-observer.ts ├── store │ ├── current.ts │ ├── expand.ts │ ├── helper.ts │ ├── index.ts │ ├── tree.ts │ └── watcher.ts ├── table │ ├── style-helper.ts │ └── utils-helper.ts ├── table-body │ ├── events-helper.ts │ ├── index.ts │ ├── render-helper.ts │ ├── styles-helper.ts │ └── table-body.d.ts ├── table-column │ ├── index.ts │ ├── render-helper.ts │ └── watcher-helper.ts ├── table-footer │ ├── index.ts │ ├── mapState-helper.ts │ └── style-helper.ts ├── table-header │ ├── event-helper.ts │ ├── index.ts │ ├── style.helper.ts │ ├── table-header.d.ts │ └── utils-helper.ts ├── table-layout.ts ├── table.type.ts ├── table.vue ├── tableColumn.ts └── util.ts
從這個結構可以看到,整個 el-table 表格元件由幾個子元件組成,例如 table-body、table-column、table-footer 等。而整個專案只有 table.vue 這個一個元件。根據 Vue 開發的經驗,筆者很快意識到這就是整個元件的入口。咱們先去瞅瞅看吧!
整個 table.vue 檔案有469 行程式碼,算是比較大的檔案,限於篇幅原因就不在這裡詳細解釋了。其中,最重要的發現是一個叫 store 的變數。仔細研究後發現這是由 Vuex 建立的狀態管理器。好傢伙!原來表格元件是用 Vuex 來管理資料的啊。這下清楚了,只要能搞定 Vuex 的部分,剩下的問題應該就可以迎刃而解了。
話不多說,盤它!
問題解決
我開始不斷嘗試查詢跟列相關的程式碼,開始全域性搜尋 “columns” 等類似的關鍵字。在發揮了名偵探柯南的洞察能力之後,我逐漸注意到 src/store/index.ts 這個檔案中的 useStore 方法,這就是整個問題的關鍵!好了,整個問題原因找到了,關鍵在於store.states._columns 這個內部狀態變數,它是渲染列資料的關鍵。但它只有初始化或新增刪除列的時候才會被賦值,調整 el-table-column 的順序根本不會改變這個變數!
找到原因,解決辦法就簡單了。筆者添加了 setColumns 這個 Mutation 方法,以更方便的設定列陣列。具體實現過程限於篇幅原因就不詳述了。感興趣的朋友可以看 Crawlab Frontend 的 原始碼。
下面是完整的表格自定義列的效果圖。大功告成!
開源框架的利與弊
世界上沒有完美的東西,優秀開源框架也不例外。而這一次硬啃(Hacking)原始碼的經歷讓筆者深刻體會到這個道理。筆者認為非常有必要討論關於使用和開發開源框架或開源專案的相關問題。首先我們來看看它的優勢。
優勢
很多知名優秀的開源專案,例如 Nginx 和 Redis,都成為了軟體開發的核心技術。那麼,我們為什麼要使用優秀的開源框架?筆者認為開源框架主要有以下幾個優勢:
- 免費。誰能拒絕不要錢的東西呢,況且很多免費的開源框架已經足夠優秀了;
- 透明。開源框架的所有原始碼都是公開的,任何人都可以看到;
- 可更改。大部分開源專案都是自由度很高的 MIT 或 BSD 開源版權,可以按需定製開發;
- 可協作。Github 是最大的開源專案平臺,全球的開發者都可以參與迭代開源專案;
- 影響力。優秀的開源專案可以提升作者或貢獻者在行業內的知名度和影響力。
劣勢
雖然開源專案有不少突出優勢,但在使用開源專案的時候卻經常會遇到各種各樣的問題。筆者認為使用開源框架存在一定的風險。筆者認為開源框架主要有以下幾個劣勢:
- 安全隱患。雖然很多優秀開源專案都由企業或資深專家開發維護,但由於不完全是自己使用,導致貢獻者容易對安全性造成疏忽,知名開源專案爆出安全漏洞的例子多不勝數,例如 OpenSSL Heartbleed、Fastjson 遠端程式碼漏洞、Antd 聖誕彩蛋等;
- 良莠不齊。開源專案開發者、貢獻者和維護者可以是任何人,他們各自的經歷和專業背景不同,所以必然導致程式碼或開源專案的質量存在一定的差異;雖然程式碼規範(Coding Standard)可以規避一些問題,但優秀的專案畢竟是少數,看看託管了幾百萬專案的 NPM 或 Maven 公共倉庫吧;
- 學習成本。筆者承認有一部分優秀開源框架有很成熟和完善的文件體系,但大部分還是缺乏有效的文件教程支援;即使有了詳盡的文件,開發者要閱讀學習也會投入很多時間成本;而大部分付費產品則包含專業技術支援,可以有效幫助開發者節省時間;
- 持續性問題。優秀程式設計師可以開發非常高質量的開源專案,但由於開源專案本身並不帶來現金收益,因此很多作者不願意長期投入在開源專案上面,導致優秀開源專案得不到持續的維護和迭代。
- 未知風險。再優秀的框架都會存在風險,由於開源框架初期並沒有經歷太多的實際業務測試,很多問題無法得到及時修復,因此在使用開源框架的過程中或多或少都會遇到一些出乎意料的問題,解決它們會花大量時間,甚至有些問題還無法解決。
問題覆盤
就筆者前面提到的解決開源框架問題的經歷來說,未知風險和學習成本尤為突出。筆者在採用 Element Plus 的穿梭框(Transfer)元件時,想當然的認為它可以有效滿足我的需求。然而,沒想到的是它既不能支援我計劃的列排序需求,又存在重大 Bug 導致無法使用。這些問題都讓我放棄使用該元件,從而尋求重新造輪子的高成本解決方案。
在之後硬啃 Element Plus 表格元件的過程中,我沒有放棄使用該元件,而是通過閱讀該元件的原始碼,根據邏輯和經驗判斷出了核心程式碼的位置以及其可能產生問題的原因,並針對性的做了調整,最終順利的解決了問題。這次閱讀原始碼而進行調整的決定,給我節省了大量的時間和精力,因為要重複造表格元件這樣的複雜輪子要做大量的工作,這是非常不划算的。
上圖是官方文件其中一個元件的截圖。Element Plus 的文件其實已經非常完善了,關於元件的使用方法、例子、API 等都寫得很詳細,每當開發時遇到不確定的直接去官方文件檢視即可。不過,ElementUI 最早是誕生於餓了麼,因此其中的元件主要是為了支援餓了麼的外賣業務,因此它有一定的侷限性。作為 ElementUI 的 升級版,Element Plus 雖然是同一個團隊的產品,但它的功能以及 API 相比於老版本並沒有太大的變化。作為對比,誕生於螞蟻金服的 UI 框架 Ant Design 相對來說就要靈活自由很多,因為它支援的業務線很廣,而阿里推行的中臺系統也要求 UI 框架需要設計得很通用。因此,Element Plus 的侷限性很可能來自於它的開發團隊背景。Crawlab 前端框架選擇 Element Plus 主要是因為老版本是 ElementUI,遷移成本相對來說比較低,但因此也帶來了一些因框架不成熟導致的問題。甚至,我們也必須要知道即使是優秀的開源專案也會存在這樣那樣的問題。從這方面來看,瞭解開源框架本身的優點和缺點,對於如何正確使用來說非常重要。為了讓開源框架能為我所用,我們需要知道如何正確駕馭開源專案的原始碼。
接下來,筆者將結合自己的專案經驗,為讀者介紹如何有效駕馭開源專案中的原始碼。
原始碼駕馭技巧
如果一個古代的人穿越到現代,當他看到各式各樣的科技產品時,一定會驚呼它們為魔法。然而,他們不知道的是,這些所謂的 “魔法” 都是利用科學技術創造出來的,它們背後的原理是通過科學實驗總結出來的,並不是什麼神祕的巫術。這個道理同樣適用於軟體行業。
程式設計師用程式碼創造了魔法般的網際網路社會,這對於非技術人員來說非常神祕。他們知道那不是真正的魔法,但他們完全不知道這些是如何實現的,背後的原理是什麼。對於程式設計師來說,利用一些優秀的開源專案,可以快速讓他們開發出產品原型、驗證產品可行性、甚至開發出生產可用的系統。不過,如果程式設計師只是把開源產品當作工具,完全不理解裡面的原理是什麼,那就等於是在使用魔法,而你也不知道它為何會生效。這樣的做法會給你帶來麻煩。要正確使用一個開源框架,你必須掌握它;要掌握它,你必須理解其工作原理;要理解其工作原理,你必須閱讀其原始碼;要讓原始碼聽命於你,你必須學會如何駕馭它,也就是如何修改和優化原始碼。
接下來筆者將介紹幾個有效方式來幫助你駕馭原始碼。
克服挫敗情緒
讀者可能會驚訝於這個看上去沒什麼用的建議。但是,我看到很多朋友想學習一個開源專案時,經常因為專案過於複雜而中途放棄了,包括很多年前的自己。其中就是挫敗感在作祟。我承認,要短期理解一個大型開源專案是不太現實的。這導致了很多朋友被玲琅滿目的技術難點給嚇跑了,留下一句 “只要會用就行”,然後繼續搬磚擰螺絲。其實,他們都太低估自己的潛力了。筆者並不反對程式設計師的實用主義,但要注意過於實用主義會導致功利主義,最終原地踏步的將是自己。
開源框架最棒的一點就是它對來說完全是透明的,任何人都可以看到其中的程式碼,包括底層實現邏輯、程式碼組織結構、專案部署方式等等。這不是提升自己駕馭原始碼能力的絕佳機會麼?通過閱讀開源框架原始碼,你不僅可以瞭解其中的工作原理,還可以學習更好的程式設計方式,從而提升自己的職業素養。我真的不提倡 “從入門到放棄系列” 這句戲言,你至少應該這樣暗示自己,“我目前看不懂是因為我的基礎知識不足,等我提升基礎實力之後一定能駕馭它”。筆者在硬啃完一些複雜的開源框架原始碼之後,發現閱讀原始碼其實也沒那麼難。就像鍛鍊身體一樣,撐過這一個山坡、這一個衝刺,之後就非常輕鬆了,而且身體素質也提高了。
因此,克服挫敗情緒對於駕馭開源專案原始碼來說非常重要,而且你要暗示自己閱讀原始碼並不難。
自己寫開源專案
人們不願意閱讀原始碼的其中一個原因就是不瞭解。就像你不瞭解一個心儀的的時候,你會認為她是完美無瑕的女神;但當你真正追求她,跟她一起約會交流生活的時候,你會發現她不過是一個普通女孩兒。因此,你可以嘗試動手開始自己寫一個開源專案。這對你理解開源專案作者的思路會非常有幫助。這同樣不是一個有任何實際操作性的建議,但如果你真的開始動手做,就一定會提升得很快。俗話說:Learn by doing!
當然,要開始寫開源專案並不是一件容易的事情。你首先可能就會陷入寫什麼的困難中。這裡筆者推薦之前寫的關於開發開源專案的兩篇文章《如何打造一個上千Star的Github專案》以及《收穫人生第一個 5k Star 開源專案,經驗教訓分享給大家》,裡面詳細講解了筆者開發、維護、推廣開源爬蟲管理平臺 Crawlab 的心得體會,會介紹如何定位痛點、調研使用者、推廣產品、專案管理等等。有興趣的讀者可以深入閱讀。
定位入口檔案
終於,這一條是一個稍微有點乾貨的建議了。在閱讀原始碼的時候,你必須首先找到該專案的入口檔案(Entry File)。所謂的入口檔案,就是這個專案模組或系統暴露給外部系統的一個公共通道,整個專案的執行、呼叫、執行都是從這個檔案開始的。 而其他實現具體邏輯的程式碼就在入口檔案中被引用,或者在其他檔案中被引用。對於前端專案,一般就是 main.ts;Python 專案一般是 app.py;Golang 專案一般就是 main.go,等等。如果把看似不可駕馭的大型開源專案比做戰無不克的戰神阿喀琉斯,那麼入口檔案就是他的致命之踵。
其實,不光是閱讀開源專案,就是我們閱讀工作中其他同事寫的程式碼時,也是先從入口檔案開始研究。掌握了入口檔案的內部邏輯,你可以看到它引用的其他子模組,然後就可以順藤摸瓜找到其他核心模組,接著繼續下去。當整個專案通過入口檔案遍歷一遍之後,你會更容易理解程式碼的邏輯結構,從而幫助你深入理解具體的工作原理。
在之前解決 Element Plus 表格問題的例子中,筆者定位到了 table.vue 這個入口檔案,然後進一步找到了列資料的關鍵位置,最後輕鬆的解決了問題。看!駕馭原始碼其實並不是那麼難,對麼?
使用全域性搜尋
這又是一個技術性建議。由於大型專案通常來說要拆分成很多檔案,因此對於這種情況來看,你如果窮舉式的遍歷所有檔案會花大量的時間。因此,你可以嘗試全域性搜尋可能的關鍵字,從而快速定位到核心程式碼部分。全域性搜尋在主流 IDE 中功能非常強大,通常支援正則表示式、模糊匹配、精確匹配、大小寫等等。下圖是 JetBrains 旗下 WebStorm IDE 中的全域性搜尋結果截圖。
對於像 TS 這樣的靜態型別語言來說,還可以像 Java、C#、Golang 那樣查詢方法或變數的呼叫位置,在 WebStorms 裡是 “Find Usages”,在其他 IDE 中可能名稱不一樣,但它們都是一個意思。這些技巧都能有效幫助你閱讀理解原始碼。
閱讀技術文件
好的文件包括專案的架構、原理、概念等,可以幫助開發者快速理解框架的程式碼結構。不過,並不是所有開源框架都有詳細的文件,這種方式可遇不可求。不過,如果開源專案中一旦包含了技術文件,請一定花時間瀏覽一下。
當然,你也可以設法聯絡到開源專案作者。NPM 專案資訊中通常會包含作者的郵箱地址;而且如果你看的是 Github 上的開源專案,還可以主動向其提 Issue,在 Issue 中表達你的疑問。
總結
本篇文章通過介紹筆者折騰開源 Vue 3 UI 框架 Element Plus 的開發經歷,討論了開源專案的優勢和劣勢,得出優秀開源框架也存在問題的結論。然後,筆者結合自己的專案經驗提出了 5 個駕馭開源專案的技巧和方法,包括克服挫敗情緒、自己寫開源專案、定位入口檔案、使用全域性搜尋以及閱讀技術文件。現在的軟體開發離不開優秀的開源專案,但是我們也必須意識到開源專案並不是專門為你自己服務的,因此它存在一定的侷限性。為了能更好的使用開源框架,讓它能相容你的複雜需求,很多情況下你必須仔細閱讀它的原始碼,這就少不了會產生一定的學習成本。
我們通常說程式設計師要提升自己的技術實力,要會 “寫” 出好程式碼;但是,在團隊合作越來越重要的趨勢下,筆者認為程式設計師更重要的能力是要會 “讀” 別人的程式碼,不管好與壞。在編寫程式碼的過程中保證正確合理的命名規範,儘量新增上註釋以方便他人理解,這是寫程式碼的能力;跟他人合作時,仔細閱讀別人寫的程式碼,儘可能理解他們的思路,思考實現方式是否正確,等等,這是讀程式碼的能力。當然,寫和讀只是非常基礎的部分,駕馭程式碼的能力還不止 “寫” 和 “讀”,應該還包括思維、邏輯、規劃等能力,例如系統架構、演算法原理、可擴充套件性設計等等。
因此,當我們在抱怨同事程式碼寫得差的時候,千萬別懊惱或生氣,因為那可能是因為你閱讀程式碼的能力不夠,沒有理解清楚同事的思路;同樣,可能在同事看來,我們的程式碼也糟糕透了;或者,這是傳說中的五十步笑百步?
點選關注,第一時間瞭解華為雲新鮮技術~