最近在封装一个组件,有一个字符串枚举类型, 发现不支持, 想探探原因,看看能否hook解决这个问题!  因为数字枚举类型编辑器是支持的。

cocos creator支持的所有属性类型分别是:

[
  "string",
  "number",
  "boolean",
  "array",
  "object",
  "enum",
  "color",
  "vec2",
  "vec3",
  "String",
  "Float",
  "Boolean",
  "Object",
  "Integer",
  "Enum",
  "asset",
  "cc.Asset",
  "cc.Node",
  "cc.Vec2",
  "cc.Vec3",
  "cc.Size",
  "cc.Color",
  "cc.Rect",
  "cc.Vec4"
]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

所有类型的显示模版如下:

string
template(t) {
    let i;
    return (i = t.multiline
      ? '\n        <ui-text-area class="flex-1" resize-v></ui-text-area>\n      '
      : '\n        <ui-input class="flex-1"></ui-input>\n      ');
  }
===================
number
template(t) {
      let i;
      return (i = t.slide
        ? '\n        <ui-slider class="flex-1"></ui-slider>\n      '
        : '\n        <ui-num-input class="flex-1"></ui-num-input>\n      ');
    }
===================
boolean

    <ui-checkbox class="flex-1"></ui-checkbox>
  
===================
array

    <ui-num-input class="flex-1"></ui-num-input>
    <div slot="child"></div>
  
===================
object

    <div class="child" slot="child"></div>
  
===================
enum

    <ui-select class="flex-1"></ui-select>
  
===================
color

    <ui-color class="flex-1"></ui-color>
  
===================
vec2

    <ui-prop name="X" id="x-comp" slidable class="fixed-label red flex-1">
      <ui-num-input class="flex-1"></ui-num-input>
    </ui-prop>
    <ui-prop name="Y" id="y-comp" slidable class="fixed-label green flex-1">
      <ui-num-input class="flex-1"></ui-num-input>
    </ui-prop>
  
===================
vec3

    <ui-prop name="X" id="x-comp" slidable class="fixed-label red flex-1">
      <ui-num-input class="flex-1"></ui-num-input>
    </ui-prop>
    <ui-prop name="Y" id="y-comp" slidable class="fixed-label green flex-1">
      <ui-num-input class="flex-1"></ui-num-input>
    </ui-prop>
    <ui-prop name="Z" id="z-comp" slidable class="fixed-label blue flex-1">
      <ui-num-input class="flex-1"></ui-num-input>
    </ui-prop>
  
===================
String
template(t) {
    let i;
    return (i = t.multiline
      ? '\n        <ui-text-area class="flex-1" resize-v></ui-text-area>\n      '
      : '\n        <ui-input class="flex-1"></ui-input>\n      ');
  }
===================
Float
template(t) {
      let i;
      return (i = t.slide
        ? '\n        <ui-slider class="flex-1"></ui-slider>\n      '
        : '\n        <ui-num-input class="flex-1"></ui-num-input>\n      ');
    }
===================
Boolean

    <ui-checkbox class="flex-1"></ui-checkbox>
  
===================
Object

    <div class="child" slot="child"></div>
  
===================
Integer
template(t) {
      let i;
      return (i = t.slide
        ? '\n        <ui-slider class="flex-1"></ui-slider>\n        '
        : '\n        <ui-num-input class="flex-1" type="int"></ui-num-input>\n        ');
    }
===================
Enum

      <ui-select class="flex-1"></ui-select>
    
===================
asset
(t) =>
      `\n        <ui-asset class="flex-1" type="${t.assetType}"></ui-asset>\n      `
===================
cc.Asset
(t) =>
      `\n        <ui-asset class="flex-1" type="${t.assetType}"></ui-asset>\n      `
===================
cc.Node
(t) =>
      `\n        <ui-node class="flex-1"\n          type="${t.typeid}"\n          typename="${t.typename}"\n        ></ui-node>\n      `
===================
cc.Vec2

        <ui-prop name="X" id="x-comp" subset slidable class="fixed-label flex-1">
          <ui-num-input class="flex-1"></ui-num-input>
        </ui-prop>
      
        <ui-prop name="Y" id="y-comp" subset slidable class="fixed-label flex-1">
          <ui-num-input class="flex-1"></ui-num-input>
        </ui-prop>
      
===================
cc.Vec3

        <ui-prop name="X" id="x-comp" subset slidable class="fixed-label flex-1">
          <ui-num-input class="flex-1"></ui-num-input>
        </ui-prop>
      
        <ui-prop name="Y" id="y-comp" subset slidable class="fixed-label flex-1">
          <ui-num-input class="flex-1"></ui-num-input>
        </ui-prop>
      
        <ui-prop name="Z" id="z-comp" subset slidable class="fixed-label flex-1">
          <ui-num-input class="flex-1"></ui-num-input>
        </ui-prop>
      
===================
cc.Size

      <ui-prop name="W" id="w-comp" subset slidable class="fixed-label flex-1">
        <ui-num-input class="flex-1"></ui-num-input>
      </ui-prop>
      <ui-prop name="H" id="h-comp" subset slidable class="fixed-label flex-1">
        <ui-num-input class="flex-1"></ui-num-input>
      </ui-prop>
    
===================
cc.Color

      <ui-color class="flex-1"></ui-color>
    
===================
cc.Rect

      <div class="vertical flex-1">
        <div class="layout horizontal">
          <ui-prop subset slidable name="X" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;">
            <ui-num-input id="x-input" class="flex-1"></ui-num-input>
          </ui-prop>
          <ui-prop subset slidable name="Y" class="fixed-label flex-1" style="min-width: 0;">
            <ui-num-input id="y-input" class="flex-1"></ui-num-input>
          </ui-prop>
        </div>
        <div class="layout horizontal">
          <ui-prop subset slidable name="W" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;">
            <ui-num-input id="w-input" class="flex-1"></ui-num-input>
          </ui-prop>
          <ui-prop subset slidable name="H" class="fixed-label flex-1" style="min-width: 0;">
            <ui-num-input id="h-input" class="flex-1"></ui-num-input>
          </ui-prop>
        </div>
      </div>
    
===================
cc.Vec4

      <div class="vertical flex-1">
        <div class="layout horizontal">
          <ui-prop name="X" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;">
            <ui-num-input id="x-input" class="flex-1"></ui-num-input>
          </ui-prop>
          <ui-prop name="Y" class="fixed-label flex-1" style="min-width: 0;">
            <ui-num-input id="y-input" class="flex-1"></ui-num-input>
          </ui-prop>
        </div>
        <div class="layout horizontal">
          <ui-prop name="Z" class="fixed-label flex-1" style="min-width: 0; margin-right: 10px;">
            <ui-num-input id="z-input" class="flex-1"></ui-num-input>
          </ui-prop>
          <ui-prop name="W" class="fixed-label flex-1" style="min-width: 0;">
            <ui-num-input id="w-input" class="flex-1"></ui-num-input>
          </ui-prop>
        </div>
      </div>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.

有一个关键算法, 它是获取所有属性源头:

function getClassAttrs(ctor) {
  return ctor.hasOwnProperty('__attrs__') && ctor.__attrs__ || createAttrs(ctor);
}

function attr(ctor, propName, newAttrs) {
  var attrs = getClassAttrs(ctor);

  if (!CC_DEV || typeof newAttrs === 'undefined') {
    // get
    var prefix = propName + DELIMETER;
    var ret = {};

    for (var key in attrs) {
      if (key.startsWith(prefix)) {
        ret[key.slice(prefix.length)] = attrs[key];
      }
    }

    return ret;
  } else if (CC_DEV && typeof newAttrs === 'object') {
    // set
    cc.warn("`cc.Class.attr(obj, prop, { key: value });` is deprecated, use `cc.Class.Attr.setClassAttr(obj, prop, 'key', value);` instead please.");

    for (var _key in newAttrs) {
      attrs[propName + DELIMETER + _key] = newAttrs[_key];
    }
  }
} // returns a readonly meta object
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

creator编辑器自己有一个运行时, 所有的对象存放cc.engine.attachedObjsForEditor:

cocos craetor2.4.5 逆向之属性面板显示原理_ide

如果是数字枚举, 结果如下:

cocos craetor2.4.5 逆向之属性面板显示原理_ide_02

如果是字符串枚举, 结果如下:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_03

这个就是问题所在了,类型是Enum,默认值string类型,enumList 为空。 

enumList 元素对象类型是{name:xxx, value: v}

enumList 的数据来源算法是:

cc.Enum.getList = function (enumDef) {
  if (enumDef.__enums__) return enumDef.__enums__;
  var enums = enumDef.__enums__ = [];

  for (var name in enumDef) {
    var value = enumDef[name];

    if (Number.isInteger(value)) {
      enums.push({
        name: name,
        value: value
      });
    }
  }

  enums.sort(function (a, b) {
    return a.value - b.value;
  });
  return enums;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

从这个代码就可以知道enumList为啥空了, 因为只处理了interger. 

写一个插件看看, 强行让它也可以处理string类型看看

cocos craetor2.4.5 逆向之属性面板显示原理_ide_04

 现在插件运行正确,确实能赋值给enumList了, 但是还是显示类型错误

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_05

那么就需要从显示逻辑着手了,为啥显示Type Error:

 这是错误组件cc-type-error-prop:

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_06

 看看什么时候会用cc-type-error-prop组件:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_07

通过断点找到了异常的地方, 大概的意思值类型String和Enum不是一个类型

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_08

 这个数据本身没问题, 因为字符串枚举, 底层本身就是string。 

enum CmpIconGroup {

  ABC = 'abc',

  DEF = 'def',

}

从初步分析结果来看,数字枚举应该也会显示Type Error才对, 因为number类似不等于Enum,  再次分析正确的情况:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_09

 非常好,这就是为啥不显示的Type Error的原因了, value是2,但是o却是Enum.  并没有识别出是Number

为啥value=2能识别出是Enum, 而value='def'确是string?

继续往前跟踪,  发现之前忽略一点,只关注了prop属性值, 没有关心value值:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_10

 继续往前,看看为啥数字2为啥可以识别出Enum:

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_11

这个数字2,源头就是Enum, 再看看字符串枚举, 这个源头是啥:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_12

 数据源头也是Enum, 但是经过Editor.getNodeDump(node) 之后变成了String:

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_13

继续挖掘, 找到了根源:

cocos craetor2.4.5 逆向之属性面板显示原理_字符串_14

如果原始数据是Enum, 值是Number类型时,强制用Enum, 而忽略了string类型。  这也是为啥Type Error的真正原因。

知道原因就好处理了, 直接hook Editor.getNodeDump方法, 解决编辑器不支持字符串枚举的方法是写一个插件,代码如下:

if(CC_EDITOR){
        cc.Enum.getList = function (enumDef) {
            if (enumDef.__enums__) return enumDef.__enums__;
            var enums = enumDef.__enums__ = [];
          
            let isStr = false;
            for (var name in enumDef) {
              var value = enumDef[name];
          
              isStr = typeof value === 'string';
              enums.push({name: name,value: value});
            }
          
            !isStr && enums.sort(function (a, b) {
              return a.value - b.value;
            });
            return enums;
        };

        const oldDump = Editor.getNodeDump;
        Editor.getNodeDump = function(node){
            const ret = oldDump(node);
            let {types, value} = ret;
            if(value.__comps__){
                value.__comps__.forEach((comp)=>{
                    const cmpType = types[comp.type];
                    const properties = cmpType.properties;
                    for(let propKey in properties){
                        const prop = properties[propKey];
                        if(prop.type === 'Enum'){// 真实数据是Enum类型
                            comp.value[propKey].type = 'Enum';
                        }
                    }
                });
            }
            return ret;
        }

        Editor.UI.getProperty('Enum').inputValue = function(){
            const ty = this.get_real_type();
            if(ty == 'string'){
                return this.$input.value;
            }
            return Number(this.$input.value);
        }

        Editor.UI.getProperty('Enum').get_real_type = function(){
            if(this.__ty){
                return this.__ty;
            }
            let ty = 'string';
            try{
                ty = typeof this._attrs.enumList[0].value;
            }catch(e){
                // pass
            }
            this.__ty = ty;
            return ty;
        }

        const oldEidtorWarn = Editor.warn;
        Editor.warn = function(s){// hook 枚举类型保存错误的警告!因为已经支持string类型了
            if(typeof s == 'string' && s.startsWith('Expecting number type of value for')){
                return;
            }
            return oldEidtorWarn.apply(Editor, arguments);
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

附上最终的效果图:

cocos craetor2.4.5 逆向之属性面板显示原理_string类_15

碰到字符串枚举类型时编辑器再也不显示Type Error了, 哈哈,完美收工!!!

经过实际测试已经支持 string float, int 的枚举类型了。

比如:

export const CmpIconSize = {
SCALE_0_3: 0.3,
SCALE_0_5: 0.5,
SCALE_0_8: 0.8,
SCALE_1 : 1,
SCALE_1_2: 1.2,
SCALE_1_5: 1.5,
SCALE_2 : 2,
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

后续属性设置可以直接 

@property({
type: cc.Enum(CmpIconType),
tooltip: '图标类型',
})
iconSize = CmpIconType.SCALE_1_2;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.