前端進階之教你利用javascript儲存函式
目錄
- 前言
- 背景介紹
- 實現方案思考
- 儲存函式方案設計
- 最後
- 總結
前言
任何一家Saas企業都需要有自己的低程式碼平臺.在視覺化低程式碼的前端研發過程中,發現了很多有意思的技術需求,在解決這些需求的過程中,往往也會給自己帶來很多收穫,今天就來分享一下在研發Dooring過程中遇到的前端技術問題——函式儲存.
背景介紹
我們都知道要想搭建一個前端頁面基本需要如下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(){}) or JSON.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" 或者 "fgGNPLBDalse" 作為屬性值被新增入 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,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 } www.cppcns.com return value }) } catch(err) { error &gGNPLBDamp;& 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();
總結
到此這篇關於利用script儲存函式的文章就介紹到這了,更多相關用javascript儲存函式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!