前端進階: 如何用 javascript 存儲函數?
任何一家 Saas 企業都需要有自己的低代碼平臺. 在可視化低代碼的前端研發過程中, 發現了很多有意思的技術需求, 在解決這些需求的過程中, 往往也會給自己帶來很多收穫, 今天就來分享一下在研發 Dooring 過程中遇到的前端技術問題——javascript 函數存儲.
背景介紹
我們都知道要想搭建一個前端頁面基本需要如下 3 個要素:
-
元素 (UI)
-
數據 (Data)
-
事件 / 交互 (Event)
在 數據驅動視圖 的時代, 這三個要素的關係往往如下圖所示:
可視化搭建平臺的設計思路往往也是基於上面的過程展開的, 我們需要提供編輯器環境給用戶來創建視圖和交互, 最終用戶保存的產物可能是這樣的:
{
"name": "Dooring表單",
"bgColor": "#666",
"share_url": "http://xxx.cn",
"mount_event": [
{
"id": "123",
"func": () => {
// 初始化邏輯
GamepadHapticActuator();
},
"sourcedata": []
}
],
"body": [
{
"name": "header",
"event": [
{
"id": "123",
"type": "click",
"func": () => {
// 組件自定義交互邏輯
showModal();
}
}
]
}
]
}
那麼問題來了, json
字符串我們好保存 (可以通過JSON.stringify
序列化的方式), 但是如何將函數也一起保存呢? 保存好了函數如何在頁面渲染的時候能正常讓 js
運行這個函數呢?
實現方案思考
我們都知道將 js
對象轉化爲json
可以用 JSON.stringify
來實現, 但是它也會有侷限性, 比如:
-
轉換值如果有 toJSON() 方法,那麼由 toJson() 定義什麼值將被序列化
-
非數組對象的屬性不能保證以特定的順序出現在序列化後的字符串中
-
布爾值、數字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值
-
undefined
、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成null
(出現在數組中時)。函數、undefined 被單獨轉換時,會返回 undefined,如JSON.stringify(function(){})
orJSON.stringify(undefined)
-
所有以 symbol 爲屬性鍵的屬性都會被完全忽略掉,即便
replacer
參數中強制指定包含了它們 -
Date 日期調用了 toJSON() 將其轉換爲了 string 字符串(同 Date.toISOString()),因此會被當做字符串處理
-
NaN 和 Infinity 格式的數值及 null 都會被當做 null
-
其他類型的對象,包括 Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性
我們可以看到第 4 條, 如果我們序列化的對象中有函數, 它將會被忽略! 所以常理上我們使用JSON.stringify
是無法保存函數的, 那還有其他辦法嗎?
也許大家會想到先將函數轉換成字符串, 再用 JSON.stringify
序列化後保存到後端, 最後在組件使用的時候再用 eval
或者 Function
將字符串轉換成函數. 大致流程如下:
不錯, 理想很美好, 但是現實很_______.
接下來我們就一起分析一下關鍵環節 func2string
和 string2func
如何實現的.
js 存儲函數方案設計
熟悉 JSON
API 的朋友可能會知道 JSON.stringify
支持 3 個參數, 第二個參數 replacer
可以是一個函數或者一個數組。作爲函數,它有兩個參數,鍵(key)和值(value),它們都會被序列化。 函數需要返回 JSON
字符串中的 value
, 如下所示:
-
如果返回一個
Number
, 轉換成相應的字符串作爲屬性值被添加入 JSON 字符串 -
如果返回一個
String
, 該字符串作爲屬性值被添加入 JSON 字符串 -
如果返回一個
Boolean
, 則 "true" 或者 "false" 作爲屬性值被添加入 JSON 字符串 -
如果返回任何其他對象,該對象遞歸地序列化成 JSON 字符串,對每個屬性調用 replacer 方法。除非該對象是一個函數,這種情況將不會被序列化成 JSON 字符
-
如果返回 undefined,該屬性值不會在 JSON 字符串中輸出
所以我們可以在第二個函數參數裏對 value 類型爲函數的數據進行轉換。如下:
const stringify = (obj) => {
return JSON.stringify(obj, (k, v) => {
if(typeof v === 'function') {
return `${v}`
}
return v
})
}
這樣我們看似就能把函數保存到後端了. 接下來我們看看如何反序列化帶函數字符串的 json
.
因爲我們將函數轉換爲字符串了, 我們在反解析時就需要知道哪些字符串是需要轉換成函數的, 如果不對函數做任何處理我們可能需要人肉識別.
人肉識別的缺點在於我們需要用正則把具有函數特徵的字符串提取出來, 但是函數寫法有很多, 我們要考慮很多情況, 也不能保證具有函數特徵的字符串一定是函數.
所以我換了一種簡單的方式, 可以不用寫複雜正則就能將函數提取出來, 方法就是在函數序列化的時候注入標識符, 這樣我們就能知道那些字符串是需要解析爲函數了, 如下:
stringify: function(obj: any, space: number | string, error: (err: Error | unknown) => {}) {
try {
return JSON.stringify(obj, (k, v) => {
if(typeof v === 'function') {
return `${this.FUNC_PREFIX}${v}`
}
return v
}, space)
} catch(err) {
error && error(err)
}
}
this.FUNC_PREFIX
就是我們定義的標識符, 這樣我們在用 JSON.parse
的時候就能快速解析函數了. JSON.parse
也支持第二個參數, 他的用法和 JSON.stringify
的第二個參數類似, 我們可以對它進行轉換, 如下:
parse: function(jsonStr: string, error: (err: Error | unknown) => {}) {
try {
return JSON.parse(jsonStr, (key, value) => {
if(value && typeof value === 'string') {
return value.indexOf(this.FUNC_PREFIX) > -1 ? new Function(`return ${value.replace(this.FUNC_PREFIX, '')}`)() : value
}
return value
})
} catch(err) {
error && error(err)
}
}
new Function
可以把字符串轉換成 js 函數, 它只接受字符串參數,其可選參數爲方法的入參,必填參數爲方法體內容, 一個形象的例子:
我們上述的代碼中函數體的內容:
new Function(`return ${value.replace(this.FUNC_PREFIX, '')}`)()
之所以要 return
是爲了把原函數原封不動的還原, 大家也可以用 eval
, 但是出於輿論還是謹慎使用.
以上方案已經能實現前端存儲函數的功能了, 但是爲了更工程化和健壯性還需要做很多額外的處理和優化, 這樣才能讓更多人開箱即用的使用你的庫.
最後
爲了讓更多人能直接使用這個功能, 我將完整版 json
序列化方案封裝成了類庫, 支持功能如下:
-
stringify 在原生
JSON.stringify
的基礎上支持序列化函數, 錯誤回調 -
parse 在原生
JSON.parse
的基礎上支持反序列化函數, 錯誤回調 -
funcParse 將 js 對象中的函數一鍵序列化, 並保持 js 對象類型不變
安裝方式如下:
# or npm install xijs
yarn add xijs
使用:
import { parser } from 'xijs';
const a = {
x: 12,
b: function() {
alert(1)
}
}
const json = parser.stringify(a);
const obj = parser.parse(json);
// 調用方法
obj.b();
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/WCT7mYD52EU9_IF4sCB3Tw