前端進階: 如何用javascript存儲函數(shù)?
任何一家Saas企業(yè)都需要有自己的低代碼平臺.在可視化低代碼的前端研發(fā)過程中, 發(fā)現(xiàn)了很多有意思的技術(shù)需求, 在解決這些需求的過程中, 往往也會給自己帶來很多收獲, 今天就來分享一下在研發(fā)Dooring過程中遇到的前端技術(shù)問題——javascript函數(shù)存儲.
背景介紹
我們都知道要想搭建一個前端頁面基本需要如下3個要素:
元素(UI) 數(shù)據(jù)(Data) 事件/交互(Event)
在?數(shù)據(jù)驅(qū)動視圖?的時代, 這三個要素的關(guān)系往往如下圖所示:

可視化搭建平臺的設(shè)計思路往往也是基于上面的過程展開的, 我們需要提供編輯器環(huán)境給用戶來創(chuàng)建視圖和交互, 最終用戶保存的產(chǎn)物可能是這樣的:
{
????"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序列化的方式), 但是如何將函數(shù)也一起保存呢? 保存好了函數(shù)如何在頁面渲染的時候能正常讓?js?運行這個函數(shù)呢?
實現(xiàn)方案思考

我們都知道將?js?對象轉(zhuǎn)化為json?可以用?JSON.stringify?來實現(xiàn), 但是它也會有局限性, 比如:
轉(zhuǎn)換值如果有 toJSON() 方法,那么由 toJson() 定義什么值將被序列化 非數(shù)組對象的屬性不能保證以特定的順序出現(xiàn)在序列化后的字符串中 布爾值、數(shù)字、字符串的包裝對象在序列化過程中會自動轉(zhuǎn)換成對應(yīng)的原始值 undefined、任意的函數(shù)以及 symbol 值,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成?null(出現(xiàn)在數(shù)組中時)。函數(shù)、undefined 被單獨轉(zhuǎn)換時,會返回 undefined,如JSON.stringify(function(){})?or?JSON.stringify(undefined)所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便? replacer?參數(shù)中強制指定包含了它們Date 日期調(diào)用了 toJSON() 將其轉(zhuǎn)換為了 string 字符串(同Date.toISOString()),因此會被當(dāng)做字符串處理 NaN 和 Infinity 格式的數(shù)值及 null 都會被當(dāng)做 null 其他類型的對象,包括 Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性
我們可以看到第4條, 如果我們序列化的對象中有函數(shù), 它將會被忽略! 所以常理上我們使用JSON.stringify?是無法保存函數(shù)的, 那還有其他辦法嗎?
也許大家會想到先將函數(shù)轉(zhuǎn)換成字符串, 再用?JSON.stringify?序列化后保存到后端, 最后在組件使用的時候再用?eval?或者?Function?將字符串轉(zhuǎn)換成函數(shù). 大致流程如下:

不錯, 理想很美好, 但是現(xiàn)實很_______.
接下來我們就一起分析一下關(guān)鍵環(huán)節(jié)?func2string?和?string2func?如何實現(xiàn)的.
js存儲函數(shù)方案設(shè)計
熟悉?JSON?API 的朋友可能會知道?JSON.stringify?支持3個參數(shù), 第二個參數(shù)?replacer?可以是一個函數(shù)或者一個數(shù)組。作為函數(shù),它有兩個參數(shù),鍵(key)和值(value),它們都會被序列化。?函數(shù)需要返回?JSON?字符串中的?value,?如下所示:
如果返回一個? Number, 轉(zhuǎn)換成相應(yīng)的字符串作為屬性值被添加入 JSON 字符串如果返回一個? String, 該字符串作為屬性值被添加入 JSON 字符串如果返回一個? Boolean, 則 "true" 或者?"false" 作為屬性值被添加入 JSON 字符串如果返回任何其他對象,該對象遞歸地序列化成 JSON 字符串,對每個屬性調(diào)用 replacer 方法。除非該對象是一個函數(shù),這種情況將不會被序列化成 JSON 字符 如果返回 undefined,該屬性值不會在 JSON 字符串中輸出
所以我們可以在第二個函數(shù)參數(shù)里對 value類型為函數(shù)的數(shù)據(jù)進行轉(zhuǎn)換。如下:
const?stringify?=?(obj)?=>?{
????return?JSON.stringify(obj,?(k,?v)?=>?{
??????if(typeof?v?===?'function')?{
??????????return?`${v}`
??????}
??????return?v
????})
}
這樣我們看似就能把函數(shù)保存到后端了. 接下來我們看看如何反序列化帶函數(shù)字符串的?json.
因為我們將函數(shù)轉(zhuǎn)換為字符串了, 我們在反解析時就需要知道哪些字符串是需要轉(zhuǎn)換成函數(shù)的, 如果不對函數(shù)做任何處理我們可能需要人肉識別.
人肉識別的缺點在于我們需要用正則把具有函數(shù)特征的字符串提取出來, 但是函數(shù)寫法有很多, 我們要考慮很多情況, 也不能保證具有函數(shù)特征的字符串一定是函數(shù).
所以我換了一種簡單的方式, 可以不用寫復(fù)雜正則就能將函數(shù)提取出來, 方法就是在函數(shù)序列化的時候注入標(biāo)識符, 這樣我們就能知道那些字符串是需要解析為函數(shù)了, 如下:
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?就是我們定義的標(biāo)識符, 這樣我們在用?JSON.parse?的時候就能快速解析函數(shù)了.?JSON.parse?也支持第二個參數(shù), 他的用法和?JSON.stringify?的第二個參數(shù)類似, 我們可以對它進行轉(zhuǎn)換, 如下:
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?可以把字符串轉(zhuǎn)換成 js 函數(shù), 它只接受字符串參數(shù),其可選參數(shù)為方法的入?yún)ⅲ靥顓?shù)為方法體內(nèi)容, 一個形象的例子:

我們上述的代碼中函數(shù)體的內(nèi)容:
new?Function(`return?${value.replace(this.FUNC_PREFIX,?'')}`)()
之所以要?return?是為了把原函數(shù)原封不動的還原, 大家也可以用?eval?, 但是出于輿論還是謹(jǐn)慎使用.
以上方案已經(jīng)能實現(xiàn)前端存儲函數(shù)的功能了, 但是為了更工程化和健壯性還需要做很多額外的處理和優(yōu)化, 這樣才能讓更多人開箱即用的使用你的庫.
最后
為了讓更多人能直接使用這個功能, 我將完整版?json?序列化方案封裝成了類庫, 支持功能如下:
stringify 在原生 JSON.stringify?的基礎(chǔ)上支持序列化函數(shù),錯誤回調(diào)parse 在原生 JSON.parse?的基礎(chǔ)上支持反序列化函數(shù),錯誤回調(diào)funcParse 將js對象中的函數(shù)一鍵序列化, 并保持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);
?//?調(diào)用方法
?obj.b();
更多推薦
點個在看你最
