Skip to content

Latest commit

 

History

History
663 lines (548 loc) · 24.2 KB

README.cn.md

File metadata and controls

663 lines (548 loc) · 24.2 KB

property-manager npm

Join the chat at https://gitter.im/snowyu/property-manager.js

Build Status Test Coverage Code Climate downloads license

使得业务对象/类的属性更容易管理.

适用于需要围绕业务对象/类来维护管理数据的场合. 我们首先需要定义该业务对象有哪些数据类型.

能够快速对该业务对象的公开属性进行赋值,合并,导出(JsonObject)以及校验.

功能:

  • 定义的属性可跟随业务类继承
  • 业务对象/类赋值属性从纯对象(JSON Object).
  • 克隆业务对象.
  • 比较业务对象是否相同(所有属性值相同).
  • 导出属性到纯对象(JSON Object).
  • 定义/声明属性类型及默认值
    • 支持 arrayOf 带类型的数组类型
    • 支持模板属性template(属性值由模板内容确定):
      • template {string | (this) => string}:
        • 字符串(模板), 如, '${author}-${uuid()}'
        • 或模板函数, 如, function() {return this.author + '-' + uuid()}
      • 导入的函数供模板使用 imports: {Object} the optional functions could be used in the template string.
      • 注意: 默认模板属性为只读,不过也可以将它设置为可写. 一旦新值被写入后,模板就不再有用,除非新值是nullundefined

我们经常需要管理一个对象的属性,考虑以下几点:

  • 创建业务对象时将选项设置为对象的属性
    • var myObj = new MyObject({opt1:value1, opt2:value2})
  • 赋值(设置)来自另一个对象的属性
    • myObj.assign({opt1:v1, opt2:v2})
  • 克隆业务对象(属性值完全相同):
    • var newObj = myObj.clone()
  • 通过分配的属性比较两个对象是否相同。
    • myObj.isSame(anotherObj)
  • 将属性导出为普通对象或 JSON,以便将来更轻松地重新创建对象。
    • 有一些内部属性不应该被导出。
    • 不应导出属性的空值或默认值。
    • 应该导出并分配有意义的(非英语)名称。
    • myObj.toObject() and myObj.toJSON()
    • JSON.stringify(myObj)
  1. Problem: 如何为对象属性赋值?
    • replace the standard assignPropertyTo() method.
    • define the attribute's assign(value, dest, src, name) method on the $attributes.
      • the custom attribute's assign the value. return the changed value.
  2. Problem: 如何决定应该为也许对象中的哪些属性赋值或获取属性的默认值?
    1. 在此对象上预先定义所有属性,即使值为 null。
      • 不支持默认值
    2. 定义一个简单的 $attributes 属性来管理属性及默认值:
      • {attrName: {value:'defaultValue'}, ...}
    3. 定义一个复杂的 $attributes(使用 Properties 类)来管理属性

所以我们有了相应的属性管理类:SimplePropertyManagerNormalPropertyManagerAdvancePropertyManager

首先是属性规则:

  • 导出的属性意味着它们是 JSON.stringify(aObj) 属性。
  • 不可枚举的属性不能被导出(export)和赋值(assign)。
  • 无法导出以"$"开头的可枚举属性,但可以被赋值(assign)。
  • undefined 值无法导出。
  • 只读(可写writable为假)属性不能赋值。
  • 属性的赋值顺序就是定义的属性的顺序。

你可以继承它

  • SimplePropertyManager: /lib/simple
    • 直接使用对象的属性描述符。
    • 所以不支持默认值。
    • 不支持对象赋值钩子函数。
    • 不支持有意义的(非英文)名称(别名)。
  • NormalPropertyManager: /lib/normal
    • 使用 $attributes 普通对象来保存声明属性
    • 支持默认值
    • 支持对象赋值钩子函数
    • 支持有意义的(非英文)名称(别名)
  • AdvancePropertyManager: /lib/advance
    • 使用 $attributes 来保存声明属性
    • $attributesProperties 类的实例。
    • 因此您可以自定义继承自的“Properties”类。
    • 支持默认值
    • 支持对象赋值钩子函数
    • 支持有意义的(非英文)名称(别名)
    • 如果可能,支持类型检查

$attributes 包含对象的所有属性(如属性描述符),其中key 是属性名称。 而value值是属性描述符对象.

属性描述符对象:

  • name (String): 要导出的非英文名称,默认为 key 名称。
  • value: 属性的默认值(如果存在)。 默认为 undefined
  • type (String): 属性的类型名称。 默认为 undefined
  • enumerable (Boolean): defaults to true
    • 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
  • configurable (Boolean): defaults to true
    • 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
  • writable (Boolean): defaults to true
    • 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。
  • assign (Function(value, dest, src, name)): defaults to undefined.
    • 自定义属性赋值函数。只需“返回”更改后的值。如果返回 undefined 就不要赋值
    • 注意: 它只用于从另一个对象赋值时。
    • 如果单独对属性赋值,则无效。 请使用属性描述符 set 来做到这一点。
    • 将其封装为智能赋值功能(仅支持 Advance Property Manager ):
      • 自动添加带前缀的隐藏内部属性
      • 自动添加描述符 get 函数以读取属性
      • 自动添加描述符 set 函数来写入属性(调用 assign 描述符)
  • 仅适用于 Normal 和 Advance Property Manager:
    • assigned (Boolean): 是否属性可被赋值. defaults: undefined
      • 如果 undefined 那么是否可被赋值由此决定: enumerable isn't false and (writable isn't false or isFunction(set))
    • 智能赋值支持(Smart Assign Support) 仅适用于 AdvancePropertyManager
      • assigned (Boolean|String): 当“assigned”值为字符串时启用智能赋值支持
      • 如果字符串值为空(""),那么它使用 nonExported1stChar+[name] 作为内部属性名称
      • 当字符串值非空,那么该assigned值作为内部属性名
      • 根据属性的可写性,自动创建对应的get/set描述符.
    • exported (Boolean): 是否该属性可被导出. defaults: undefined
      • 如果 undefined 那么是否可被导出由此决定: enumerable isn't false and the first char isn't "$"
    • alias (String|ArrayOf String): 该属性的别名. 用于从其它纯对象选项中赋值时
    • clone (Boolean): 初始化时如果值为对象,是否克隆默认属性值。defaults to true.
    • skipDefault (Boolean): 是否在导出的时候跳过默认值。defaults to true.
'$attributes': {
  'attrName': {
    name: 'exportedName',
    value: 123,
    enumerable: false,
    type: 'String',
    configurable: true,
    writable: true,
    assign: function(value, dest, src, name)
    get: ...,
    set: ....
  }
}

或者作为能力注入

当作为能力注入时,以下方法将被添加(或替换):

  • initialize(options) : overwrite this for assign options from constructor?
    • 如果可能,将属性的初始化值应用于对象。
    • 然后调用赋值(assign) 方法.
  • assign(options) : assign the options' attributes to this object.
    • how to decide which attribute should be assign?
    • I need an attributes manage class? or just a simple attributes list?
    • or define all attributes even the value is null when initialize
    • this must be an optional feature.
  • assignPropertyTo(dest, options, attributeName, value): assign an attribute to dest.
    • you can override it to determine howto assign an object value.
  • assignProperty(options, attributeName, value): assign an atrribute. called by assign
  • assignTo(dest) : assign the attributes to this dest object.
  • mergeTo(dest, options): 将this对象的属性值写入到 dest object, 如果options对象存在,并且options中的属性值存在,那么options中的属性值将覆盖原值.
    • do not overwrite the already exist attributes of the dest.
  • isSame(obj): compare the obj's attributes whether is the same value with itself.
  • clone(options): 克隆对象, 如果options对象存在,并且options中的属性值存在,那么options中的属性值将覆盖原值.
  • toObject(options): convert it as plain object.
    • do not export the non-enumerable attributes or beginning with '$'
    • do not export the attribute's value is null
    • do not export the attribute's value is default value.
      • where to get the default value?
  • toJSON(): this will call the toObject() to return.

注意: 如果构造函数的第一个参数不是属性选项,那么你需要指定属性选项参数位置.

用法

让你的类能够管理(导入/导出)属性

有下列三种方式使你的类可以管理属性:

  • 继承方式:让你的类继承自属性管理器
    • inherits from PropertyManager directly.
  • 通过将属性能力注入到你的类
    • 你需要确保下列的成员名称没有被类使用.
      1. The $attributes 成员用于 normal and advance PropertyManager.
      2. nonExported1stChar 成员用于改变非导出(non-exported)属性首字符约定,默认为 '$'.
      • 前四种方法必须存在。其他是可选的。但要注意它们的依赖性。
        1. assign
        2. assignPropertyTo
        3. getProperties
        4. defineProperties
        5. clone (optional)
          • mergeTo
        6. initialize (optional)
          • getProperties
          • assign
        7. assignProperty (optional)
          • assignPropertyTo
        8. mergeTo (optional)
          • getProperties
          • assignPropertyTo
        9. exportTo (optional)
          • mergeTo
        10. assignTo (optional)
          • getProperties
          • assignPropertyTo
        11. toObject (optional)
          • exportTo
        12. toJSON (optional)
          • toObject
        13. isSame (optional)
          • mergeTo
  • 修饰器: property-manager-decorator

Class Inherits

有三类属性管理器可供选择, 默认是NormalPropertyManager,详细介绍参见前述.

  • SimplePropertyManager: 最简单的属性管理器,直接使用JS的对象属性描述符,因此无法在类上定义属性
  • NormalPropertyManager: 常规属性管理器,通过在类上的prototype$attributes纯对象管理定义的属性
  • AdvancePropertyManager:高级属性管理器,通过在类上的prototype$attributes Properties对象管理定义的属性
// var inherits = require('inherits-ex/lib/inherits');
// var PropertyManager = require('property-manager');
// var PropertyManager = require('property-manager/lib/normal');
// var SimplePropertyManager = require('property-manager/lib/simple');
// var AdvancePropertyManager = require('property-manager/lib/advance');
import {inherits} from 'inherits-ex'
import {SimplePropertyManager, NormaPropertyManager, AdvancePropertyManager} from 'property-manager'

const ProperManager = NormaPropertyManager

//# Only for Normal or Advance PropertyManager
var defineProperties = ProperManager.defineProperties

class MyClass extends ProperManager {
  constructor(name, options) {
    super()
    this.name = name

    // if you use the SimplePropertyManager
    // you should define your properties here:
    //this.defineProperties({
    //  'attr1': {value:123}
    //  'hidden': {value:1, enumerable: false},
    //  '$dontExport': {value:3, enumerable: true}
    //})

    // initialize ProperManager
    this.initialize(options)
  }
}

/*
function MyClass(name, options) {
  this.name = name;
  // if you use the SimplePropertyManager
  // you should define your properties here:
  //this.defineProperties({
  //  'attr1': {value:123}
  //  'hidden': {value:1, enumerable: false},
  //  '$dontExport': {value:3, enumerable: true}
  //})
  PropertyManager.call(this, options);
}

inherits(MyClass, PropertyManager);
*/

// Only for normal and advance property manager
defineProperties(MyClass, {
    'attr1': {value:123},
    'hidden': {value:1, enumerable: false},
    '$dontExport': {value:3, enumerable: true},
    'custom': {
      value: {},
      assign: function(value, dest, src, name) {
        if (value == null) {
          value = {};
        }
        value.exta = 123;
        return value;
      }
    }
});

/*
function MyClassEx() {
  MyClassEx.__super__.constructor.apply(this, arguments)
}
inherits(MyClassEx, MyClass);
*/

class MyClassEx extends MyClass {}

// Inherited properties from MyClass
defineProperties(MyClassEx, {'extra': {value: 'extra'}});

灵活的属性能力注入到任意类中

通过PropertyAbility函数将属性能力注入到任意类中.

PropertyAbility(target:Function|Object, options?) 如果没有参数那么就是默认的normal属性管理器.

options:

  • name: {'simple' | 'advance' | 'normal' | 'abstract'} 选定属性管理器, 默认为 normal
  • optionsPosition: {number}, 可选的属性选项参数位置,在构造函数中需要导入Json对象属性时使用.
  • exclude: {string[]} 不需要注入的属性能力方法名称列表,默认为空。
// var PropertyAbility = require('property-manager/ability');
import {PropertyAbility} from 'property-manager'

class MyClass {
  constructor(name, options) {
    // if you use the SimplePropertyManager
    // you should define your properties here:
    //this.defineProperties({
    //  'attr1': {value:123}
    //  'hidden': {value:1, enumerable: false},
    //  '$dontExport': {value:3, enumerable: false}
    //})
    this.name = name;
    // initialize PropertyManager
    this.initialize.apply(this, arguments);
  }
}

/*
function MyClass(name, options) {
  // if you use the SimplePropertyManager
  // you should define your properties here:
  //this.defineProperties({
  //  'attr1': {value:123}
  //  'hidden': {value:1, enumerable: false},
  //  '$dontExport': {value:3, enumerable: false}
  //})
  this.name = name;
  this.initialize.apply(this, arguments);
}
*/

// add the property manager ability to MyClass
// the default is normal property manager
//   PropertyAbility(MyClass)
// you can specified the property manager 'simple', 'advance', 'normal':
//   PropertyAbility(MyClass, 'simple')
//   PropertyAbility(MyClass, {name: 'simple'})
// and you can specified the options position in the arguments
//  the first argument(arguments[0]) is `name`, and the second(arguments[1]) is the options
PropertyAbility(MyClass, {optionsPosition: 1});
// you can exclude some non-core methods:
//PropertyAbility(MyClass, {optionsPosition:1, exclude: ['assignTo', ...]})

// You can define your properties here to:
var defineProperties = MyClass.defineProperties;

//only for normal, advance property manager
defineProperties(MyClass, {
  'attr1': {value: 123},
  'hidden': {value: 1, enumerable: false},
  '$dontExport': {value: 3, enumerable: true},
  'date': {
    assign(value, dest, src, name, {isExported}) {
      let result;
      if (isExported) {
        result = value.toISOString()
      } else if (!(value instanceof Date)) {
        result = new Date(value)
      }
      return result;
    }
  }
  'custom': {
    value: {},
    assign: function(value, dest, src, name, opts) {
      if (value == null) {
        value = {};
      }
      value.exta = 123;
      return value;
    }
  }
});

class MyClassEx extends MyClass {}

/*
function MyClassEx() {
  MyClassEx.__super__.constructor.apply(this, arguments)
}
inherits(MyClassEx, MyClass);
*/

// Inherited properties from MyClass
defineProperties(MyClassEx, {'extra': {value: 'extra'}});

使用方式

使用方式非常简单,和平常的对象属性使用没啥区别. 定义属性也和Object.defineProperties类似.

现在 MyClass 类上有了下面几个属性:

  • attr1: 可导出及赋值
  • hidden: 不能导出及赋值
  • date: the date 可导出及赋值
  • $dontExport: 可赋值, 但不可导出
  • custom: 可导出及赋值, 赋值将被属性描述符中的 assign 函数改变.

MyClassEx 派生至 MyClass (注: 仅支持normal 或 advance property manager)

  • extra: 可导出及赋值
  • 其它属性继承至 MyClass
var assert = require('assert');

var my = new MyClass('aName', {
  attr1: 3,
  hidden: 11222,
  $dontExport: 1,
  custom: {
    b: 12
  }
});

assert.deepEqual(my.mergeTo(), {
  attr1: 3,
  $dontExport: 1,
  custom: {
    b: 12,
    exta: 123
  }
});

// the `hidden` can not be assigned and exported
assert.equal(my.hidden, 1);

// the `$dontExport` can not be exported
assert.deepEqual(my.toObject(), {
  attr1: 3
});

assert.equal(JSON.stringify(my), '{"attr1":3,"custom":{"b":12,"exta":123}}');

var obj = my.clone();

// compare each assigned properties.
assert.ok(obj.isSame(my));

assert.deepEqual(obj.mergeTo(), {
  attr1: 3,
  $dontExport: 1,
  custom: {
    b: 12,
    exta: 123
  }
});

var myEx = new MyClassEx('theClassEx', {attr1: 3, hidden:11222, $dontExport: 1, custom:{b:12}})
assert.deepEqual(myEx.mergeTo(), {extra:'extra', attr1:3, $dontExport:1, custom:{b:12, exta: 123}})

API

Changes

More recent changes see: CHANGELOG.md

v2

  • ES6 Class
  • ESM support
  • NodeJS >= 8

v1.4.0

  • feat: add the readonly to smart assigned property
@Properties
class Phone extends AdvancePropertyManager {
  @Prop({
    writable: false,
    exported: true,
    assigned: '',
  }) id!: string;
}
  • BROKEN CHANGE: DO NOT EXPORT the readonly property by default unless exported is true.
@Properties
class Phone extends AdvancePropertyManager {
  @Prop({
    writable: false,
    exported: true,
  }) id!: string;
}

v1.0.0

  • add the array with type supports.
import { arrayOf } from 'property-manager/lib/array';
import AdvancePropertyManager from 'property-manager/lib/advance';
import { PropertyManager as Properties, Property as Prop } from 'property-manager-decorator';

@Properties
class Phone extends AdvancePropertyManager {
  @Prop() value!: string;
  @Prop() codeNum!: string;
  @Prop() kind!: string;
  constructor(initValue?) {
    super(initValue);
  }
}

@Properties
class Contact extends AdvancePropertyManager {
  @Prop({type: String}) name!: string;
  @Prop({type: arrayOf(Phone)}) phones!: Phone[];
  constructor(initValue?) {
    super(initValue);
  }
}
  • BROKEN change toObject method params to (options?: IMergeOptions)
  • BROKEN change assign method params to (src, options?: IMergeOptions)
  • BROKEN change assignTo method params to (dest, options?: IMergeOptions)
  • BROKEN change assignPropertyTo and assignProperty method params to (dest, src, name: string, value, attrs?, options?: IMergeOptions)
  • BROKEN change exportTo method params to (dest, options?: IExportOptions)
  • BROKEN change mergeTo method params to (dest, options?: IMergeOptions)
    • add skipNull and skipUndefined option to IExportOptions and IMergeOptions
  • add the extends(attrs: Object, nonExported1stChar) method to the Properties
    • return a new Properties instance to extends properties from current instance.
  • add the inherited properties supports for AdvancePropertyManager.defineProperties
  • change the recreate argument default value of defineProperties to false for AdvancePropertyManager and NormalPropertyManager
  • set all methods and non-properties of Properties to be non-enumerable.

v0.13.0

  • add typed property for AdvancePropertyManager and NormalPropertyManager
function CustomType(value) {
  if (!(this instanceof CustomType)) return new CustomType(value)
  try {
    value = JSON.parse(value)
  } catch(err) {
    this.value = value
  }
}

const attrs = {
  prop1: {type: CustomType, value: 111}
}

class TypedPM extends AdvancePropertyManager {
  constructor(opts) {
    super(opts)
  }
}
TypedPM.defineProperties(attrs)

const obj = new TypedPM()
console.log(obj.prop1 instanceof CustomType)

v0.11.0

  • add the skipExists option to the Properties.assignTo and Properties.assignPropertyTo
  • the options to the Properties.assignTo(dest, src, options)
    • exclude(String|Array)
    • skipDefault(Boolean)
    • skipExists(Boolean)
    • skipReadOnly(Boolean)
    • exported(Boolean)

v0.10.0

  • add the alias property descriptor(Normal&Advance):
    • You can define one or more aliases to assign from other object(options)
    • alias (String|ArrayOf String)
  • Smart assignment property supports(AdvancePropertyManager):
    • broken: SMART_ASSIGN constant deprecated.
    • assigned descriptor (Boolean|String):
      • String means SMART_ASSIGN.
      • it's the internal property name of the smart assignment if it's string
      • the internal property name is the property name with prefix(nonExported1stChar) if it's an empty string
  • broken: remove attrsName property(fixed to '$attributes')
  • add the helper function: properties/define-properties.

v0.9.0

  • clone default property value if the value is an object when initializing
    • the object instances will share the same one of property value if the default value of property is an object.
      • howto create a new object instance when initializing default value.
      • Solution 1: the value descriptor could be a function to create new object instance:
        • Problem1: it will be only available for normal and advance property manager
          • value: function (){return Object.create()}
        • Problem2: the value can not be a function now.
      • Solution 2: check the value whether is object. if so, clone it when initializing.
        • use this solution. but if someone wish all instance share the same value.
        • add a descriptor to control whethe enable this. but simple can not support the custom descriptor.
          • clone (Boolean): defaults to true.
  • Smart assignment property supports:
    • assign property descriptor (Function(value, dest, src, name)):
      • It only used to assign the options from another object.
      • It's no effect if the assign the property individually. should use the property descriptor set to do so.
      • maybe I should wrap it:
        • add a hidden internal property with prefix(nonExported1stChar)
        • add descriptor get function to read the property
        • add descriptor set function to assign the property(call the assign descriptor).
      • need a descriptor to control whethe enable this.
        • assigned: AdvancePropertyManager::SMART_ASSIGN = 2
        • enabled: !get and !set and assigned is AdvancePropertyManager::SMART_ASSIGN
      • only available for advance property manager.
      • note: only value argument is passed into assign descriptor when assignment the property individually.

v0.8.0

  • add the property writable check: do not assign the readonly property.
  • Normal, Advance
    • add the assigned, exported (Boolean) to property descriptor directly.
      • assigned: enumerable isnt false and (writable isnt false or isFunction(set)).
      • exported: enumerable isnt false and the first char isnt "$"
  • PropertyManager::nonExported1stChar (Char), defaults to '$'
    • note: the exported descriptor is higher prior than nonExported1stChar.
  • nonExported1stChar option to the property manager ability.

v0.7.0

  • broken the arguments order of assign function in property descriptor are changed:
    • attr.assign(value, dest, src, name, opts) instead of assign(dest, src, value, name)

License

MIT