怎么通过id渲染页面_「快页面」动态配置化页面渲染器原理介绍
引言
「快頁面」是知乎內部一個快速搭建后臺管理頁面的平臺,使用者僅用半小時即可將一個常規復雜度的后臺頁面開發完成。
「快頁面」平臺的基石是它的「渲染器」,一個能將 JSON 配置渲染成頁面的 React 組件。
這篇文章將會提供一種配置化渲染器實現思路。
不過在開始介紹原理之前,想先對這類工具的存在價值做一個簡單的分析評估,搞明白我們為什么要做它。
核心目標 - 提升開發效率
一些質疑
一開始產生做「快頁面」這個平臺的想法時,我也在懷疑這樣的東西真的能提高效率嗎? 它所帶來的學習成本難道不會實際上高過它帶來的收益嗎?
其實配置化頁面渲染是一個非常老舊的話題了,因為一般情況下,一個十幾人規模的前端團隊,只要不斷接到大量高度相似的管理后臺需求,內部都會催生出一個這樣的頁面配置化工具。
只是如今社區內也并沒有誕生出一個已經被廣泛使用的類似工具,大多只是作為各公司內部系統內部使用。
在「快頁面」平臺內部上線一年后的今天,我能確定它真的能提高開發效率。
許多項目一期的后臺需求都很簡單,一個表單用于創建和編輯,一個表格用來查詢,然后在表格上加個公開按鈕,這種需求使用快頁面開發,平均每個頁面用半小時,最多一小時就完成上線了。
不過同時無法忽視的一點是,為了抵消它所帶來的學習成本,必然需要做很多文檔,智能編輯器,版本管理等輔助性工作。 這將經歷一個比做出渲染器和專屬組件更為漫長和曲折的過程。
效率提升關鍵點
其實這類工具提升效率的關鍵點各不相同,「快頁面」則是通過以下三點提高效率:
約束需求范圍,約定優于配置
把頁面從用代碼表達改為用配置表達,相當于創建了一種 DSL,省去了 import 語句,對效率的提升有限。
提升效率的關鍵是分析高頻業務需求,簡化成固定流程,限制需求范圍,要放棄支持過于靈活的需求。
比如通用的表單需求,我們把它拆解成以下幾個部分:
「請求數據」→「設置初始值」→「指定 POST 地址」→「用戶與表單交互」→「校驗」→「提交」→「成功后跳轉」
其他細枝末節的比如「提交按鈕放哪」「提交按鈕文案」等低頻需求不考慮。
一些難以用配置表達的需求,比如「拿到請求數據后先處理下對象結構」「提交的時候發兩個接口」「提交的時候刪除一些字段」等等,它們其實是一種回調函數,變化多端,無窮無盡,除非是高頻需求,否則盡量放棄支持。
我們抽象了高頻需求中的公共部分,用一目了然的配置表達,放棄了靈活性,得到了效率的提升。 這就是「約定優于配置」。
省去構建部署環節,快速上線
我們的配置是以 JSON 的形式存在的,區別于 js 代碼,它的好處在于簡短,可通信,可存儲。
既然一份 JSON 對應一個頁面,如果把它存到數據庫中,用接口讀取和修改,再做一個在線編輯器,應該就能脫離項目的構建,部署流程,做到開發完成后立刻上線了。
「快頁面」省去了構建和部署流程,實際上,在本項目中,也就是省去了原本每次代碼合并后所需要的 10 分鐘以上的等待時間,間接省去了 git clone 代碼,安裝依賴,啟動項目等等開發前的必要工作。
非前端參與前端頁面開發成為可能
有了在線智能編輯器和文檔,后端也能照著其他頁面的配置樣例,快速開發一個常規復雜度的前端頁面了。
如果這個在線智能編輯器更強大一些,擺脫了對編輯 JSON 的依賴,轉為可視化交互,它就能成為一個草圖編輯器,從而使得更多的人參與到前端頁面的開發過程中去。
當后端能借助在線智能編輯器獨立完成前端頁面開發時,這其中的溝通聯調成本也就大大降低了。
配置樣例
這是一個簡化的表格查詢需求配置樣例
{如樣例所示,用 component 字段表示要使用的組件,組件嵌套組件形成一份能表達整個頁面內容的 JSON 配置。
頁面渲染結果注意到配置中有些值含有「雙花括號」,如 {{record.id}}: {{record.community_name}}
這種格式表示它們是動態變化的,讓組件具備隨狀態變化顯示不同 UI 的能力,支持表達式計算。 文章后面會詳細介紹這一功能。
這個樣例中的組件樹可簡化為下圖(僅顯示有 component 的部分)
組件樹其中 Layout 影響頁面的標題,邊距;
AutoTable 是強大的表格組件,負責發請求,表格分頁等邏輯;
Enter 是一個鏈接按鈕;
MapBadge 常用于顯示各種狀態或類型,在 UI 上比普通文字更醒目一些。
這份 JSON 很精煉地表達了一個頁面的內容,Layout(頁面布局) AutoTable(表格),Enter 和 MapBadge(表格中的兩列,一列是鏈接,一列是類型),比起原先 JSX 的寫法,代碼量大大減少了。
渲染流程
我們可以把渲染流程粗略地分為「React 組件渲染」和「雙花括號表達式渲染」
React 組件渲染
配置單元
仔細觀察配置結構可以發現,嵌套的關鍵是 component,與 component 同級的那些字段將會作為組件的屬性傳入,即
{我們把含有 component 的 Object 叫做一個「配置單元」,就像 React 組件可以自由作為其他組件的任意屬性傳入一樣,「配置單元」之間也可以作為對方的一個屬性形成嵌套。
那么對每一個配置單元的基本操作就是,調用 React.createElement() 將其實例化為 React Element。
自底向上
當我們對一個有兩層嵌套的配置單元嘗試 React.createElement() 時便會發現,我們好像需要確定一個渲染順序。
這個順序就是自底向上。 以上面的 Layout - AutoTable 為例:
假設是自頂向下,那就是
React其實 Layout 就是個簡單 UI 組件,沒有任何復雜邏輯,會把外界傳給它的 children 原封不動地傳給 React 的 API,這時毫無疑問會報錯。
回過頭來看,其實自底向上的順序理是所當然的,因為 JSX 轉譯出來的 JS 代碼本來就是自底向上的,想想「函數執行棧」就明白了。
因此渲染順序是: 自底向上。
深度優先遍歷
知道了渲染順序,知道了每一層都是在執行 React.createElement(),接下來寫一個深度優先遍歷就行了。 代碼簡化如下:
function常見的遞歸遍歷而已,通過 dfs 收集到一個遵循組件自底向上順序的數組,接下來對其中元素逐個執行 React.createElement() 并替換即可。
// config 是整個頁面的配置,paths 是深度優先遍歷時收集到的配置單元路徑其中 getComponentByName 是根據組件名找到組件的方法,也就是接下來要說的。
根據組件名找到組件類
先實現一個組件引用緩存管理器
// componentProvider接著注入所有組件
// injectComponents.js 文件根據組件名取用組件
import都是非常簡單直白的邏輯
到這里,一個基本的靜態配置渲染流程已經實現了,如果我們的頁面是像寫靜態 HTML 標簽一般沒有任何動態需求,這樣就足夠了。
但后臺需求不會這么簡單,實際使用后我們會發現,比起寫 JSX,這種 JSON 配置有一個致命的缺陷,那就是數據在被傳給 UI 組件前,我們連對它進行一點點計算都做不到,也沒法寫回調函數。 因此就需要下面這第二部分「雙花括號表達式渲染」」。
雙花括號表達式渲染
表達式扮演什么角色
首先要明白,「雙花括號表達式」在頁面配置中究竟扮演了一個什么樣的角色,我們能在傳統寫 JSX 的過程中,找到與之對應的角色嗎?
在本項目中,「雙花括號表達式」滿足了
- 對數據的計算處理的需要
- 實現部分的回調函數的需要
對數據的計算處理
最常見的例子,往往頁面中表單請求的 HTTP 接口地址,需要受頁面當前路由的影響
比如我們要在
https://example.xxx.com/projects/:id這個頁面中請求
https://api.xxx.com/projects/:id這個接口地址
很明顯接口地址中的參數 id 是從頁面路由中得到的
那么寫成「雙花括號表達式」就是
'https://api.xxx.com/projects/{{match.params.id}}'這類計算邏輯很常見,非常重要, 而「雙花括號表達式」就可以滿足這類需求。
部分的回調函數
JSON 配置中只能寫數字,字符串,布爾值這些簡單類型,不能寫函數。
那通過 eval 生成函數行不行呢? 在 JSON 中就以字符串的形式存在。
這個思路被我們放棄了,因為它過于復雜,過于靈活了。
我們依然是只針對高頻需求做支持
不過這意味著我們需要做一些特殊組件,將原本需要傳入回調函數才能實現的邏輯變成僅需一小段 JSON,比如點擊按鈕后彈框填寫表單,或要求用戶確認危險操作等等
表達式計算的實現
實現表達式計算靠 eval 生成一個立即執行函數就可以了,這里需注意幾個關鍵點:
- 屏蔽全局變量
- eval 生成的函數變量命名空間與全局變量可能有交集
- 全局變量中可能有變量名并不符合標識符命名規則
- 打印計算過程中的報錯
屏蔽全局變量
這里的全局變量其實指的就是 window 對象上的屬性,由于我們利用了立即執行函數的閉包特性,因此它在執行過程中會受到 window 對象上屬性的影響,導致奇怪的計算結果。
這種情況一旦發生,不容易發現原因,安全起見還是屏蔽掉的好。
屏蔽的方式就是循環枚舉出 window 上的屬性,然后執行。
leteval 生成的函數變量命名空間與全局變量可能有交集
表達式的數據源中可能與全局變量有同名屬性,就不能和上面一樣賦為 undefined 了,舉例:
// 表達式中系統預先定義了一個 prompt 變量,它和 window.prompt 重名了全局變量中可能有變量名并不符合標識符命名規則
某些第三方庫可能會在 window 上注入它自定義的標識變量,但卻沒有遵循變量命名規則,使用了諸如「減號 -」等特殊符號。
這種標識符可能會讓屏蔽全局變量的語句報錯,所以記得過濾下。
打印計算過程中的報錯
表達式計算是非常有可能失敗的,比如下面這個報錯大家肯定見的太多了。
TypeError: Cannot read property 'someProp' of undefined通過 try catch 捕獲到并打印出來,可以極大地幫助使用者調試。
計算表達式時的數據來源
我們的「雙括號表達式」要影響的是 UI,
而在 React 中,能夠即時影響 UI 的數據只有三種來源,state,props 和 context。
state
state 是組件的一些內部屬性,比如表格的分頁,是由表格內部自行管理的。
props
props 是我們給組件傳入的屬性,其實就是「配置單元」里寫死的。
context
借助一些狀態管理庫,如 redux + react redux,context 就變成了組件的 props。
這三種數據源只有在組件的 render 方法中可以全部拿到,并且還能隨數據的變化立即影響 UI。
自底向上的局限
仍是以上面展示的樣例為例,假設目前 JSON 配置中組件樹的結構有如下三層。
Layout|-- AutoTable|-- Enter ( href = https://example.xxx.com/resources/{{record.id}} ) // record 是表格任意一行的數據表達的意思很簡單,頁面中有個表格,表格中有一列要放個鏈接入口。
按照自底向上的順序,應當是先執行 createElement(Enter) ,再執行 createElement(AutoTable)
可我們給 Enter 傳入的 href 屬性是一個「雙花括號表達式」,表達式中依賴的 record 是自身所處表格那一整行的數據,屬于 AutoTable 組件私有的變量。
我們原先的自底向上流程無視了私有關系,在嘗試計算表達式時發現缺少了一些私有變量。
這就是原先自底向上的局限,看來,想要支持表達式計算,渲染流程還需要再改進。
自底向上流程之間的接力
既然那些變量是私有的,那就應該在遵循私有關系的前提下進行自底向上的渲染。
怎么遵循呢? 那就是在自底向上的過程中,忽略一些組件的子級配置,由該組件自己負責子級配置的自底向上渲染。
這樣一來,原本只有一次的自底向上渲染,由于 AutoTable 組件的存在,這個流程被分割成了兩次,好像兩次接力一般。
我們把那些類似 AutoTable 這種負責接力的組件稱作「接力組件」。
仍是以上面的 Layout - AutoTable - Enter 為例
在這個流程中,由于有一個「接力組件」AutoTable 存在,需要兩次自底向上的渲染
第一次自底向上把 AutoTable 及其所有子級字段視為一整個配置單元,這樣 AutoTable 便成了最底部的那個「配置單元」。
第二次自底向上由 AutoTable 接力,對其所有子級字段進行自底向上的渲染。
以此類推。 即使有更多「接力組件」,流程都是一樣的。
本文最后會有圖片形式的流程詳細介紹。
接力組件
哪些組件是接力組件
主要是那些需要提供私有變量給「雙花括號表達式」的組件,比如表格需要提供表格每一行的數據,表單需要提供表單當前值,等等其他有類似需求的組件。
渲染器怎樣知道當前組件是不是「接力組件」
白名單是個辦法,但這樣做的話,每新增一個「接力組件」,都需要更改白名單,渲染器和組件之間存在耦合。
所以更好的辦法是做個 HOC
我們把「遍歷計算并替換雙花括號表達式」「自底向上調用 React.createElement」兩個公共邏輯合并成一個方法抽象出來,就叫它 autoRender 吧。
做一個 HOC,它有兩個功能:
這樣一來,做一個「接力組件」就變得很簡單,只要拿這個 HOC 包裝一下,然后在被包裝的組件中隨自己想法調用 HOC 提供的 autoRender 方法即可。
渲染器和組件之間實現了解耦。
形如閉包的表達式變量作用域
既然「接力組件」擁有一些私有變量,那么符合直覺的作用域應該是:
父級不能讀取「接力組件」子級的變量,但「接力組件」可以使用父級的變量。
就像閉包的作用域一樣,當前函數可以使用外層函數的變量,外層函數卻不可以使用當前函數的變量。
這個的實現也不難, 一句話概括就是: 每個「接力組件」向它子級的所有「接力組件」注入數據。
所謂注入數據就是給子級的「接力組件」添加一個特定字段,比如 __injectedData
在本例中,就是要向 AutoTable 這個「接力組件」注入 __injectedData,內容是頁面的路由信息等數據。
{(假定頁面路由中參數 id 為 20) 注入后變為
{之后 AutoTable 使用 autoRender 方法時便會把這份被注入的數據和自身私有的數據合并,來渲染子級配置中的「雙花括號表達式」
流程圖解
上面純文字描述很不直觀,下面是一個圖片形式的完整流程。
在這個例子中,共存在兩個「接力組件」: Page 和 AutoTable
Page 可以為表達式提供頁面路由數據,包括參數匹配結果,即 match。
假設頁面路由中存在參數 id,值為 3,即 match.params.id = 3。
開始啟動渲染計算表達式,注入數據接力組件被視為一個整體createElement(AutoTable)開始接力(AutoRender 筆誤,是 AutoTable)
Text 組件的 children 屬性的值是一個表達式,表達式中使用了 record 和 match 兩個變量
record 是表格中每一行的數據,由 AutoTable 提供,假設 record.type = 'typeA'
match 是頁面路由參數匹配結果,顯然 AutoTable 本身無法提供 match 數據
但之前 Page 已向 AutoTable 注入了 injectedData,其中含有 match 變量
因此 Text 組件的 children 屬性表達式可以計算出結果
計算表達式createElement(Text)AutoTable 已被實例化,只剩 LayoutcreateElement(Layout),流程結束總結
本篇文章介紹了知乎內部一個后臺頁面搭建平臺「快頁面」,主要內容是渲染器的實現原理。
在介紹原理之前,首先對這類工具的存在意義做了一些初步的分析;
隨后以一份配置樣例為例,介紹了渲染器的實現原理,包括「React 組件渲染」和「雙花括號表達式渲染」兩部分。
每一個配置化工具應該都是深度結合了業務方向,項目基礎,團隊投入等實際情況得到的結果。
因此理論上,在業界,同類工具應該有很多,所以本文也只是一種實現思路。
歡迎對這類工具感興趣的小伙伴在評論區交流。
總結
以上是生活随笔為你收集整理的怎么通过id渲染页面_「快页面」动态配置化页面渲染器原理介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python爬虫搜特定内容的论文_pyt
- 下一篇: java接口构建英雄属性_Java开发学