Json的數據映射(基於LitJson)


  最近用到 Echarts 之類的圖表顯示插件, 它基於不同的 Json 結構可以得到不同的圖表, 我們從 http 請求來的數據一般就是 Json 的, 肯定就想通過圖形式的數據映射來完成圖表的顯示了, 不過首先C#它不是JS這種對 Json 結構友好的語言, 要對一個Json節點進行更新的時候, 需要把Json轉換成數據結構, 然后找到相應點, 再用新的Json進行節點讀取, 生成新的結構, 然后再從根節點讀取Json的字符串出來, 才能完成數據映射......

  然后就是復雜結構映射, 比如一個Object和Array進行反復嵌套的情況, 要篩選出來某些數據或者結構, 再映射到其它結構中去的話, 會非常困難, 如果考慮用UE的藍圖的話, 有可能可以做到, Unity這個光是自己做可視化界面以及無限展開的映射選項, 就已經要命了......

 

  先說一個數據映射的過程, 因為使用的是LitJson, 它的擴展性比較強, 數據映射的函數正是用到了 JsonData 的 ReadValue 的方式, 不過我們稍微改動了一下 : 

    /// <summary>
    /// 核心數據映射邏輯, 跟LitJson中的有差別, 簡單數據通過強制轉換得到, 並保留原有數據類型不變
    /// </summary>
    /// <param name="jsonData"></param>
    /// <param name="reader"></param>
    /// <param name="json"></param>
    /// <returns></returns>
    private static IJsonWrapper ReadValue(JsonData jsonData, JsonReader reader, string json = null)
    {
        try
        {
            reader.Read();      // the json maybe error with read while read prime data

            if(reader.Token == JsonToken.ArrayEnd ||
                reader.Token == JsonToken.Null)
                return null;

            IJsonWrapper instance = jsonData;

            if(reader.Token == JsonToken.String)
            {
                instance.SetString((string)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Double)
            {
                instance.SetDouble((double)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Int)
            {
                instance.SetInt((int)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Long)
            {
                instance.SetLong((long)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Boolean)
            {
                instance.SetBoolean((bool)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.ArrayStart)
            {
                instance.SetJsonType(JsonType.Array);

                while(true)
                {
                    IJsonWrapper item = ReadValue(new JsonData(), reader);
                    if(item == null && reader.Token == JsonToken.ArrayEnd)
                        break;

                    ((IList)instance).Add(item);
                }
            }
            else if(reader.Token == JsonToken.ObjectStart)
            {
                instance.SetJsonType(JsonType.Object);

                while(true)
                {
                    reader.Read();

                    if(reader.Token == JsonToken.ObjectEnd)
                        break;

                    string property = (string)reader.Value;

                    ((IDictionary)instance)[property] = ReadValue(new JsonData(), reader);
                }
            }
        }
        catch { }

        if(reader.Token == JsonToken.None)
        {
            JsonDataSetPrime(jsonData, json);
        }

        return jsonData;
    }
    
    public static string ToRawString(string str)
    {
        if(string.IsNullOrEmpty(str))
        {
            return string.Empty;
        }
        var rawStr = str;
        if(rawStr.StartsWith("\"") && rawStr.EndsWith("\""))
        {
            rawStr = rawStr.Substring(1, rawStr.Length - 2);
        }
        return rawStr;
    }

    public static void JsonDataSetPrime(JsonData jsonData, string json)
    {
        var rawType = jsonData.GetJsonType();
        DataTable dataTable = json;
        DataTable rawData = ToRawString((string)dataTable);
        switch(rawType)
        {
            case JsonType.Int: { ((IJsonWrapper)jsonData).SetInt((int)rawData); } break;
            case JsonType.Long: { ((IJsonWrapper)jsonData).SetLong((long)rawData); } break;
            case JsonType.Double: { ((IJsonWrapper)jsonData).SetDouble((double)rawData); } break;
            case JsonType.Boolean: { ((IJsonWrapper)jsonData).SetBoolean((bool)rawData); } break;
            case JsonType.String:
                {
                    ((IJsonWrapper)jsonData).SetString((string)rawData);
                }
                break;
        }
    }

  這個 ReadValue 就是置換 JsonData 里的內容的, 使用 try catch 是因為它轉換的邏輯是從上往下的, 節點的類型是由前置讀取的類型給出的, 所以如果是一個Json對象, 它的字符串是合法的就能正確讀取, 比如:

{"aa":100}

  可是如果修改對象是普通值類型, 比如要修改的是 100 這個對應的JsonData, 傳入的Json可能是這樣的:

"200"

  它就不是一個合法的Json, 是無法正常讀取的, 所以會拋出異常, 並且導致 JsonReader 的類型是 None, 所以 JsonDataSetPrime() 方法就強制給JsonData賦值了. 這樣就封裝了節點數據置換的方法了.

  然后做一個數據映射的路徑記錄, 就能把數據映射到模板里面去了, 比如下圖, 左邊是數據, 右邊是一個 Echarts 圖表的模板 : 

  獲取它的路徑就很簡單, 得到 From:xAxis/type To:title/text 這樣的結果, 那么只需要獲取到左邊的節點, 然后ToJson, 用右邊相應節點的JsonData來讀取一遍即可.

  然后就是有些復雜的映射, 比如 Object(Dictionary) 到 Array 的映射, Array 中嵌套的 Object/Array 對應的結構的映射, 可以從下面的截圖看出來 : 

  想要把 yAxis 這個 Object 映射到 data 這個 Array 里面去, 可以映射 Key 或者 Value 的方式 : 

 

  這里是把 yAxis 的 Keys 映射成 Array了. 或者也可以用 Values 映射, 只要結構能夠對的上就行了. 代碼邏輯跟下面相同, 只是把對應的數值取出來罷了:

    JsonData fromTag;
    var list = new List<string>(fromTag.Keys);
    var json = LitJson.JsonMapper.ToJson(list);

  因為 Object 的 Key 一定是 string 類型的, 所以可以簡單的創建 List 對象, 可是如果是 Value 這種復雜對象, 就不能簡單構建了!!! 這些映射可以通過簡單的數據結構就能保存, 比如上面的我的結構是這樣的 : 

    public enum MappingLogic
    {
        Replace,
        ObjectToArray_Keys,
        ObjectToArray_Values
    }
    public class MappingInfo
    {
        public string from;
        public string to;
        public MappingLogic mappingLogic = MappingLogic.Replace;
        public int order = 100;      // 數值越小越先執行
    }
    
    // Json 文件
    [{"from":"yAxis","to":"xAxis/data","mappingLogic":1,"order":100}]

  [ 從 yAxis 節點獲取 Keys 映射到 xAxis/data 節點中去 ]

 

  嵌套式的映射 : 

   要將左邊的 data (Array) 隊列中的 Object 結構中的 xAxis 映射到右邊的 data(Array)結構中去, 得到下圖 : 

  其實可以看出, 復雜結構就復雜在 Array 的操作上, Object 的每個對象都是有 Key 的, 而 Array 需要通過自己獲得 index 來進行映射(普通映射), 如果作為結構映射, 又需要通過 Foreach 進行結構厲遍, 看一下怎樣通過代碼完成映射結構的 : 

先看生成的映射文件 

[{
    "from": "series/[array:0]/markLine/data/[array:Foreach]/xAxis",
    "to": "series/[array:0]/data",
    "mappingLogic": 0,
    "order": 100
}]

  from 的結構已經不是簡單的節點路徑了, 到 data/[array:Foreach] 這個節點, 表示的就是對於 data 這個節點的 array 需要進行厲遍, 然后再獲取每個厲遍的 xAxis 節點的值, 映射到右邊的 data 節點去, 

  代碼結構 : 

    public const string Array = "array";
    public const string Foreach = "Foreach";
    
    
    public static void DoMap(JsonData from, JsonData to, MappingInfo info)
    {
        var fromTag = GetHierarchy(from, info.from, true);
        var json = "{}";
        switch(info.mappingLogic)
        {
            case MappingLogic.ObjectToArray_Keys:
                {
                    var list = new List<string>(fromTag.Keys);
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.ObjectToArray_Values:
                {
                    var list = new List<string>();
                    fromTag.ForeachDictionary((_key, _data) =>
                    {
                        list.Add(_data.ToString());
                    });
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            default: { json = fromTag.ToJson(); } break;
        }
        WrapJsonData(to, info.to, json, true, false);
    }

    // it must be root json node
    public static void WrapJsonData(JsonData jsonData, string hierarchy, string json, bool clear = true, bool keepRawData = true)
    {
        jsonData = GetHierarchy(jsonData, hierarchy, keepRawData);
        WrapJsonData(jsonData, json, clear);
    }
    public static void WrapJsonData(JsonData jsonData, string json, bool clear = true)
    {
        JsonReader reader = new JsonReader(json);
        if(clear)
        {
            jsonData.Clear();
            jsonData.ClearJsonCache();
        }
        ReadValue(jsonData, reader, json);
    }
    public static void WrapJsonData(JsonData jsonData, string json, bool clear = true)
    {
        JsonReader reader = new JsonReader(json);
        if(clear)
        {
            jsonData.Clear();
            jsonData.ClearJsonCache();
        }
        ReadValue(jsonData, reader, json);
    }
    public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true)
    {
        var node = root;
        if(string.IsNullOrEmpty(hierarchy) == false)
        {
            var layers = hierarchy.Split('/');
            if(layers != null && layers.Length > 0)
            {
                for(int i = 0; i < layers.Length; i++)
                {
                    var target = layers[i];
                    int index = -1;
                    if(node.IsArray && IsArrayIndex(target, ref index))
                    {
                        node = node[index];
                    }
                    else if(node.IsArray && IsArrayForeach(target, ref index))
                    {
                        var tempNode = new JsonData();
                        foreach(JsonData data in node)
                        {
                            var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1);
                            tempNode.Add(GetHierarchy(data, lastHierarchy, true));
                        }
                        if(keepRawData)
                        {
                            node = tempNode;
                        }
                        else
                        {
                            WrapJsonData(node, tempNode.ToJson(), true);
                        }
                        break;
                    }
                    else
                    {
                        node = node[target];
                    }
                }
            }
        }
        return node;
    }
    public static bool IsArrayIndex(string pattern, ref int index)
    {
        string element = "";
        if(IsArrayElement(pattern, ref element))
        {
            if(int.TryParse(element, out index))
            {
                return true;
            }
        }
        return false;
    }
    public static bool IsArrayForeach(string pattern, ref int index)
    {
        string element = "";
        if(IsArrayElement(pattern, ref element))
        {
            if(string.Equals(element, Foreach, System.StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }
    public static bool IsArrayElement(string pattern, ref string element)
    {
        if(string.IsNullOrEmpty(pattern) == false)
        {
            if(pattern.StartsWith("[") && pattern.EndsWith("]"))
            {
                var sp = pattern.Substring(1, pattern.Length - 2).Split(':');
                if(sp != null && sp.Length > 1)
                {
                    if(string.Equals(sp[0], Array, System.StringComparison.OrdinalIgnoreCase))
                    {
                        element = sp[1];
                        return true;
                    }
                }
            }
        }
        return false;
    }

 

  現在主要的就是這些了, 有了半圖形化的界面方式, 點擊連線就能生成映射數據了, 並且從普通數據的類型保持, 到簡單嵌套類型的數據映射, 它基本上能完成90%的需求了, 並且很貼心的添加了代碼生成:

  減少了大量的工作量, 未來可期...

 

  對了, 在 EditorWindow 下怎樣實現畫線, 這里用了一個取巧的方法, 每個元素做成一個Toggle, 當左邊的某個元素被點擊之后, 就設定當前鼠標位置為起始位置(它們都在Scroll里, 需要偏移位置) : 

 leftPoint = (UnityEngine.Event.current.mousePosition) + new Vector2(0, _jsonStringScroll1.y);

  然后通過跟當前鼠標位置做連線(有個 Drawing 的腳本, wiki 可找到) : 

    if(_selectedLeft != null)
    {
        Drawing.DrawArrow(leftPoint - new Vector2(0, _jsonStringScroll1.y), UnityEngine.Event.current.mousePosition, Color.gray, 2, 10);
    }

  

  

---------------------------------------------------------------------

今天發現對於結構映射沒有很好的方法, 比如下面這樣的 : 

  右邊給出了結構, 左邊給出了數據, 因為它們的命名不同, 所以需要根據右邊的結構來對左邊的數據進行映射, 就不能簡單地把左邊替換到右邊去了.

  只能額外添加一個映射功能了, 添加了一個 MappingLogic.StructureMapping 的映射邏輯, 這里 默認為模板中的第一條數據為結構, 從數據中對它進行映射:

    public static void DoMap(JsonData from, JsonData to, MappingInfo info)
    {
        var fromTag = info.mappingLogic != MappingLogic.Write ? GetHierarchy(from, info.from, true) : null;
        var json = "{}";
        switch(info.mappingLogic)
        {
            case MappingLogic.ObjectToArray_Keys:
                {
                    var list = new List<string>(fromTag.Keys);
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.ObjectToArray_Values:
                {
                    var list = new List<string>();
                    fromTag.ForeachDictionary((_key, _data) =>
                    {
                        list.Add(_data.ToString());
                    });
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.StructureMapping:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    var structure = targetNode[0];

                    var fromVar = GetVarName(info.from);
                    var toVar = GetVarName(info.to);
                    for(int i = 0, imax = fromTag.Count; i < imax; i++)
                    {
                        JsonData sourceData = fromTag[i];
                        JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))];
                        destData[toVar] = sourceData[fromVar];
                    }
                    return;
                }
                break;
            case MappingLogic.Write:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    WrapJsonData(targetNode, info.writeData, true);
                    return;
                }
                break;
            default: { json = fromTag.ToJson(); } break;
        }
        WrapJsonData(to, info.to, json, true, false);
    }

  而在獲取目標節點的時候直接對結構節點返回了:

    public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true)
    {
        var node = root;
        if(string.IsNullOrEmpty(hierarchy) == false)
        {
            var layers = hierarchy.Split('/');
            if(layers != null && layers.Length > 0)
            {
                for(int i = 0; i < layers.Length; i++)
                {
                    var target = layers[i];
                    int index = -1;
                    if(node.IsArray && IsArrayIndex(target, ref index))
                    {
                        node = node[index];
                    }
                    else if(node.IsArray && IsArrayForeach(target, ref index))
                    {
                        if(IsStructureMapping(target))
                        {
                            return node;  // 返回了
                        }
                        var tempNode = new JsonData();
                        foreach(JsonData data in node)
                        {
                            var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1);
                            tempNode.Add(GetHierarchy(data, lastHierarchy, true));
                        }
                        if(keepRawData)
                        {
                            node = tempNode;
                        }
                        else
                        {
                            WrapJsonData(node, tempNode.ToJson(), true);
                        }
                        break;
                    }
                    else
                    {
                        node = node[target];
                    }
                }
            }
        }
        return node;
    }

  當然這個必然是在 Array 節點下才需要的映射功能, 要不然直接映射就行了.

  映射的代碼比較簡單:

            case MappingLogic.StructureMapping:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    var structure = targetNode[0];

                    var fromVar = GetVarName(info.from);
                    var toVar = GetVarName(info.to);
                    for(int i = 0, imax = fromTag.Count; i < imax; i++)
                    {
                        JsonData sourceData = fromTag[i];
                        JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))];
                        destData[toVar] = sourceData[fromVar];
                    }
                    return;
                }
                break;

  這里只需要把兩個相關根節點找出來, 然后把模板節點進行克隆, 然后根據映射關系設置即可.

  這樣的映射沒有改變原有結構, 所以即使進行多層映射也是沒有問題的

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM