内容简介:MJExtensionA fast, convenient and nonintrusive conversion framework between JSON and model.转换速度快、使用简单方便的字典转模型框架
MJExtension
A fast, convenient and nonintrusive conversion framework between JSON and model.
转换速度快、使用简单方便的字典转模型框架
我们经常需要从网络上拉取json数据,然后将json数据转化为自己的模型数据,将json数据转化为我们自己的模型数据经常使用的框架有YYModel和MJExtension,所以现在也是打算花一些时间看一下MJExtension的源码,并且写一篇博客记录一下,因为不记录下来的话感觉很容易忘,学习效果不佳。
使用MJExtension
1.pod 'MJExtension'
2.#import "MJExtension.h"
3.开始使用
最简单的使用
模型:
//User.h @interface User : NSObject @property (nonatomic, copy)NSString *name; @property (nonatomic, copy)NSString *icon; @property (nonatomic, assign)unsigned int age; @property (nonatomic, copy)NSString *height; @property (nonatomic, strong)NSNumber *money; @end
字典转模型:
//ViewController.m NSDictionary *dict = @{ @"name" : @"Jack", @"icon" : @"lufy.png", @"age" : @20, @"height" : @"1.55", @"money" : @100.9 }; // JSON -> User User *user = [User mj_objectWithKeyValues:dict]; NSLog(@"name=%@, icon=%@, age=%u, height=%@, money=%@", user.name, user.icon, user.age, user.height, user.money);
打印结果:
name=Jack, icon=lufy.png, age=20, height=1.55, money=100.9
通过一句简单的代码,就把字典数据转化为了模型数据,非常方便简洁。
复杂一点的应用
很多时候json转模型都不是这样简单。有时候会出现模型中嵌套模型或者模型中的属性名和json数据中的key不一致的情况。
下面看一下一个Student类的模型:
//Student.h @interface Student : NSObject @property (nonatomic, copy)NSString *ID; @property (nonatomic, copy)NSString *desc; @property (nonatomic, copy)NSString *nowName; @property (nonatomic, copy)NSString *oldName; @property (nonatomic, copy)NSString *nameChangedTime; @property (nonatomic, strong)Bag *bag; @end
我们看到Student模型中嵌套着Bag这个模型:
//Bag.h @interface Bag : NSObject @property (nonatomic, copy)NSString *name; @property ( nonatomic, assign)double *price; @end
然后我们再看一下json数据:
NSDictionary *dict = @{ @"id" : @"20", @"description" : @"kids", @"name" : @{ @"newName" : @"lufy", @"oldName" : @"kitty", @"info" : @[ @"test-data", @{ @"nameChangedTime" : @"2013-08" } ] }, @"other" : @{ @"bag" : @{ @"name" : @"a red bag", @"price" : @100.7 } } };
可以看到字典数据中是id,而模型中是ID,同样也有desc和description。模型中有newName和oldName这些属性,而字典中这些属性在name字段下面。bag属性也是一样的道理,那么怎么办呢?
我们只需要实现MJExtension中的 + (NSDictionary *)mj_replacedKeyFromPropertyName
方法,在Student.m中 #import
然后实现 + (NSDictionary *)mj_replacedKeyFromPropertyName
方法:
//Student.m + (NSDictionary *)mj_replacedKeyFromPropertyName { return @{ @"ID" : @"id", @"desc" : @"description", @"oldName" : @"name.oldName", @"nowName" : @"name.newName", @"nameChangedTime" : @"name.info[1].nameChangedTime", @"bag" : @"other.bag" }; }
这个方法的作用就是在给模型赋值的时候,把右边字段的值赋给模型中左边字段的属性。
转化一下试试:
// JSON -> Student Student *stu = [Student mj_objectWithKeyValues:dict]; // Printing NSLog(@"ID=%@, desc=%@, oldName=%@, nowName=%@, nameChangedTime=%@", stu.ID, stu.desc, stu.oldName, stu.nowName, stu.nameChangedTime); // ID=20, desc=kids, oldName=kitty, nowName=lufy, nameChangedTime=2013-08 NSLog(@"bagName=%@, bagPrice=%d", stu.bag.name, stu.bag.price); // bagName=a red bag, bagPrice=100.700000
这个地方需要关注一个地方就是模型中的nameChangedTime这个属性,在字典中去取值的时候是取 name.info[1].nameChangedTime
这个字段的值,这个在后面我们讲核心源码的时候会用到。后面讲源码也会以上面这个为例子来讲,这样比较好理解。
MJExtension核心类简介
MJFoundation
-
这个类中只有一个方法,就是
+ (BOOL)isClassFromFoundation:(Class)c
,这个方法用来判断一个类是否是foundation类及其子类。
MJProperty
这个类非常重要,这个类是对我们类中属性的再封装。
首先会通过runtime的方法去遍历类中的属性:
unsigned int count; objc_property_t *propertyList = class_copyPropertyList([Student class], &count); for (int i = 0; i < count; i++) { objc_property_t property = propertyList[i]; const char *propertyName = property_getName(property); const char *attris = property_getAttributes(property); NSLog(@"%s %s", propertyName, attris); } free(propertyList);
打印结果:
ID T@"NSString",C,N,V_ID desc T@"NSString",C,N,V_desc nowName T@"NSString",C,N,V_nowName oldName T@"NSString",C,N,V_oldName nameChangedTime T@"NSString",C,N,V_nameChangedTime bag T@"Bag",&,N,V_bag
通过char类型的attris字符串我们可以看到,它中间有一个串是表示它是属于哪一个类的,比如NSString,Bag。
通过遍历类的属性,我们得到了objc_property_t类型的属性对象,然后使用这个objc_property_t对象来创建一个对应的MJProperty对象,我们看看MJ大神是怎么做的:
#pragma mark - 缓存 + (instancetype)cachedPropertyWithProperty:(objc_property_t)property { MJExtensionSemaphoreCreate MJExtensionSemaphoreWait MJProperty *propertyObj = objc_getAssociatedObject(self, property); if (propertyObj == nil) { propertyObj = [[self alloc] init]; propertyObj.property = property; objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } MJExtensionSemaphoreSignal return propertyObj; }
首先MJ大神通过objc_property_t对象这个key去缓存中取,如果缓存中取不到,那么就根据objc_property_t来创建一个MJProperty对象,并且把这个MJProperty对象通过property这个key与MJProperty类对象关联起来。那么下次如果再从缓存中取同一个objc_property_t对应的MJProperty对象就能取到了,就不用再创建了。这也是MJ大神使用缓存的一个地方。
上面代码块中 propertyObj.property = property;
这行代码触发了MJProperty对象的set方法:
MJProperty有一个type属性,这个属性是MJPropertyType类的,就是表示MJProperty对象的property属性是属于什么类型的。
另外每一个MJProperty对象还持有着两个字典,一个是 propertyKeysDict
,一个是 objectClassInArrayDict
。
-
propertyKeysDict
这个字典的key是
NSStringFromClass(class)
,值是一个数组,比如在复杂一点的应用
中,给模型中的nameChangedTime这个属性赋值的时候,在字典中去取值的时候要对应name.info[1].nameChangedTime这个字段的值。 那么就要把name,info,1,nameChangedTim,这个四个字段分别封装为一个MJPropertyKey,加入一个数组中,作为value。这个数组在最终取值的时候会用到。 -
objectClassInArrayDict
这个字典的key也是
NSStringFromClass(class)
,值是一个类对象,表示如果这个MJProperty对象的类型是数组,并且数组中的元素类型是模型,那么这个个字典的value就是模型的类对象。
MJPropertyKey
上面说过,给模型中的nameChangedTime这个属性赋值的时候,在字典中取值的时候要对应name.info[1].nameChangedTime这个字段的值,那么就要把name,info,1,nameCHangedTime这四个字段分别封装成一个MJPropertyKey。
它有两个属性,一个属性是name,也就是name,info,1这种,还有一个就是type它是自定义的 MJPropertyKeyType
类型的枚举值,这个枚举值有两种类型,即 MJPropertyKeyTypeDictionary
和 MJPropertyKeyTypeArray
,像name,info这种就属于 MJPropertyKeyTypeDictionary
类型的,1就属于 MJPropertyKeyTypeArray
类型的。这个也是在取值的时候用的,类型是 MJPropertyKeyTypeDictionary
就是从字典中取值,类型是 MJPropertyKeyTypeArray
就是从数组中取值。
MJPropertyType
MJProperty类有一个属性是type,这个属性是MJPropertyType类的,这个type属性就是表征这个MJProperty对象它的property属性属于什么类,NSString类或者NSNumber类等等。
MJProperty对象的type是通过截取property的attributes得到code然后初始化为MJPropertyType对象得到的:
_type = [MJPropertyType cachedTypeWithCode:code];
核心源码分析
我们就从 复杂一点的应用
这个例子去看一下MJExtension的核心源码。
沿着 + (instancetype)mj_objectWithKeyValues:(id)keyValues
这个方法一直往下查找就能找到其核心代码:
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context { // 获得JSON对象 keyValues = [keyValues mj_JSONObject]; MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典"); Class clazz = [self class]; NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames]; NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames]; //通过封装的方法回调一个通过运行时编写的,用于返回属性列表的方法。 [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) { @try { // 0.检测是否被忽略 if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return; if ([ignoredPropertyNames containsObject:property.name]) return; // 1.取出属性值 id value; NSArray *propertyKeyses = [property propertyKeysForClass:clazz]; for (NSArray *propertyKeys in propertyKeyses) { value = keyValues; for (MJPropertyKey *propertyKey in propertyKeys) { value = [propertyKey valueInObject:value]; } if (value) break; } // 值的过滤 id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property]; if (newValue != value) { // 有过滤后的新值 [property setValue:newValue forObject:self]; return; } // 如果没有值,就直接返回 if (!value || value == [NSNull null]) return; // 2.复杂处理 MJPropertyType *type = property.type; Class propertyClass = type.typeClass; Class objectClass = [property objectClassInArrayForClass:[self class]];//模型数组中对象的类 // 不可变 -> 可变处理 if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) { value = [NSMutableArray arrayWithArray:value]; } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) { value = [NSMutableDictionary dictionaryWithDictionary:value]; } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) { value = [NSMutableString stringWithString:value]; } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) { value = [NSMutableData dataWithData:value]; } if (!type.isFromFoundation && propertyClass) { // 模型属性 value = [propertyClass mj_objectWithKeyValues:value context:context]; } else if (objectClass) { if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) { // string array -> url array NSMutableArray *urlArray = [NSMutableArray array]; for (NSString *string in value) { if (![string isKindOfClass:[NSString class]]) continue; [urlArray addObject:string.mj_url]; } value = urlArray; } else { // 字典数组-->模型数组 value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context]; } } else { if (propertyClass == [NSString class]) { if ([value isKindOfClass:[NSNumber class]]) { // NSNumber -> NSString value = [value description]; } else if ([value isKindOfClass:[NSURL class]]) { // NSURL -> NSString value = [value absoluteString]; } } else if ([value isKindOfClass:[NSString class]]) { if (propertyClass == [NSURL class]) { // NSString -> NSURL // 字符串转码 value = [value mj_url]; } else if (type.isNumberType) { NSString *oldValue = value; // NSString -> NSNumber if (type.typeClass == [NSDecimalNumber class]) { value = [NSDecimalNumber decimalNumberWithString:oldValue]; } else { value = [numberFormatter_ numberFromString:oldValue]; } // 如果是BOOL if (type.isBoolType) { // 字符串转BOOL(字符串没有charValue方法) // 系统会调用字符串的charValue转为BOOL类型 NSString *lower = [oldValue lowercaseString]; if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) { value = @YES; } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) { value = @NO; } } } } // value和property类型不匹配 if (propertyClass && ![value isKindOfClass:propertyClass]) { value = nil; } } // 3.赋值 [property setValue:value forObject:self]; } @catch (NSException *exception) { MJExtensionBuildError([self class], exception.reason); MJExtensionLog(@"%@", exception); } }]; // 转换完毕 if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) { [self mj_keyValuesDidFinishConvertingToObject]; } return self; }
这一部分代码很长,我们一部分一部分来看:
1.将json数据转化为foundation类型:
// 获得JSON对象 keyValues = [keyValues mj_JSONObject]; MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues参数不是一个字典"); Class clazz = [self class]; NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames]; NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
allowedPropertyNames是允许进行字典和模型转换的属性名数组,ignoredPropertyNames是不允许进行字典和模型转换额属性名数组,这两个数组一般都是自己在模型类的.m文件中去设置的。
2.遍历整个类的属性:
+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration { // 获得成员变量 NSArray *cachedProperties = [self properties]; // 遍历成员变量 BOOL stop = NO; for (MJProperty *property in cachedProperties) { enumeration(property, &stop); if (stop) break; } }
再看一下 + (NSMutableArray *)properties
方法,其核心部分如下:
[self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
// 1.获得所有的成员变量
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList(c, &outCount);
// 2.遍历每一个成员变量
for (unsigned int i = 0; i
MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
// 过滤掉Foundation框架类里面的属性
if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;
property.srcClass = c;
[property setOriginKey:[ self propertyKey:property.name] forClass: self];
[property setObjectClassInArray:[ self propertyObjectClassInArray:property.name] forClass: self];
[cachedProperties addObject:property];
}
// 3.释放内存
free(properties);
}];
首先通过 + (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration
这个方法去遍历当前模型类及其父类,当追溯到Foundation类型的类时就停止遍历。
有一点需要注意的是,比如有一个Person类,其有两个属性name和sex,有一个Student类是继承自Person类的,这个Student类自己有一个school属性。那么当我们使用runtime的方法读取Student类的属性列表时,只能读取到一个自己声明的属性school。但是实际上name和sex也是它的属性,所以这个时候就要遍历其父类,拿到所有的属性。
当我们拿到模型类的objc_property_t类型的属性时,就将其封装成MJProperty对象:
MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property
方法先尝试从关联属性中通过property对象这个key来取出MJProperty对象,如果取不到就创建一个MJProperty对象,并通过property这个key将其与MJProperty的类对象关联起来,这样下次就可以直接通过关联属性来得到MJProperty的值了:
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property { MJExtensionSemaphoreCreate MJExtensionSemaphoreWait MJProperty *propertyObj = objc_getAssociatedObject(self, property); if (propertyObj == nil) { propertyObj = [[self alloc] init]; propertyObj.property = property; objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } MJExtensionSemaphoreSignal return propertyObj; }
propertyObj.property = property;
这行代码触发set方法,在set方法里面为MJProperty对象的name属性和type属性赋值,其中type属性就是和MJProperty对象关联的property属于什么类,是NSNumber类还是BOOL类等等:
- (void)setProperty:(objc_property_t)property { _property = property; MJExtensionAssertParamNotNil(property); // 1.属性名 _name = @(property_getName(property)); // 2.成员类型 NSString *attrs = @(property_getAttributes(property)); NSUInteger dotLoc = [attrs rangeOfString:@","].location; NSString *code = nil; NSUInteger loc = 1; if (dotLoc == NSNotFound) { // 没有, code = [attrs substringFromIndex:loc]; } else { code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)]; } _type = [MJPropertyType cachedTypeWithCode:code]; }
下面两行代码非常重要:
[property setOriginKey:[self propertyKey:property.name] forClass:self]; [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
对于第一行代码:
+ (id)propertyKey:(NSString *)propertyName
这个方法是获取模型的属性名在字典中对应的key,什么意思呢?还是拿第二个例子来说,它有一个nameChangedTime属性,由于我们在模型类中实现了 + (NSDictionary *)mj_replacedKeyFromPropertyName
这个方法,且这个方法中与nameChangedTime相对应的是 name.info[1].nameChangedTime
,所以 + (id)propertyKey:(NSString *)propertyName
返回的就是 name.info[1].nameChangedTime
这个字符串。
对于 - (void)setOriginKey:(id)originKey forClass:(Class)c
方法,这个方法会把 name.info[1].nameChangedTime
这个字符串拆解成一段一段,并封装成一个个MJPropertyKey对象,组成数组,赋值给MJProperty的propertyKeysDict这个字典:
对于第二行代码
如果模型中有数组类型的属性,并且数组中的元素也是模型类,那么就需要在模型类中实现 mj_objectClassInArray
方法,就像下面这样:
模型类中有一个数组类型的属性statuses,数组中的元素类型是模型,模型类是Status;另一个数组类型的属性是ads,数组中的元素类型是模型,模型类是Ad。
+ (NSDictionary *)mj_objectClassInArray { return @{ @"statuses" : @"Status", @"ads" : @"Ad" }; }
这时如果在 + (Class)propertyObjectClassInArray:(NSString *)propertyName
方法中传入statuses属性,那么返回的就是Status类。
然后 - (void)setObjectClassInArray:(Class)objectClass forClass:(Class)c
方法将这个Status类对象赋值给MJProperty对象的 objectClassInArrayDict
字典。
到这里遍历类的所有属性就结束了,这样获得了整个类的所有属性,每个属性被封装成了一个MJProperty对象,MJProperty对象有一个property属性,还有type属性来表征这个属性属于什么类。此外MJProperty对象还保存着两个字典 propertyKeysDict
和 objectClassInArrayDict
,这两个字典的key都是 NSStringFromClass(c)
,前者的value是一个数组,这个数组里面的元素是MJPropertyKey类型的,主要是用来取值用的,后者的value是一个类对象,如果属性是一个数组类型的属性,且数组元素是模型类型,那么这个值就是模型的类对象。
3.对模型进行赋值
首先如果这个属性不在属性白名单里或者在属性黑名单里,那么就返回,不对属性赋值:
if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return; if ([ignoredPropertyNames containsObject:property.name]) return;
然后从每个属性的propertyKeysDict字典中取出propertyKeys数组,根据propertyKeys数组来取值:
id value; NSArray *propertyKeyses = [property propertyKeysForClass:clazz]; for (NSArray *propertyKeys in propertyKeyses) { value = keyValues; for (MJPropertyKey *propertyKey in propertyKeys) { value = [propertyKey valueInObject:value]; } if (value) break; }
我们看一下 - (id)valueInObject:(id)object
这个方法是怎么操作的:
如果属性的类型是可变的类型,而取出的value是不可变的类型,那么就要把不可变类型变换为可变的类型:
MJPropertyType *type = property.type; Class propertyClass = type.typeClass; Class objectClass = [property objectClassInArrayForClass:[self class]];//模型数组中对象的类 // 不可变 -> 可变处理 if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) { value = [NSMutableArray arrayWithArray:value]; } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) { value = [NSMutableDictionary dictionaryWithDictionary:value]; } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) { value = [NSMutableString stringWithString:value]; } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) { value = [NSMutableData dataWithData:value]; }
上面就是完成了对属性的第一步赋值,但是这还不够,如果这个属性是模型类型,那么还要对这个模型再进行一次字典转模型操作。如果这个属性是数组类型且数组元素是模型类型,那么还要进行字典数组转模型数组的操作。或者属性是NSURL类型,value是NSString类型,这样也要进行一下转换:
这样整个模型赋值的过程也就完成了。
MJExtension中的一部分缓存操作
MJExtension中进行了大量的缓存操作来优化性能,下面讲几个比较重要的缓存,理解了这些缓存也有助于更深入的理解整个框架。
1.
NSObject+MJProperty
这个分类中保存着一个字典 cachedPropertiesDict
,这个字典的 key
是 NSStringFromClass(class)
,值就是一个数组,这个数组里面存放着一个类的所有属性。这样当我们下一次还要对同一个类进行模型赋值操作,就可以直接从这个字典里面取出这个类的一个包含所有属性的数组了。
2.
MJProperty
这个类中,通过runtime的动态关联属性的方法,关联每一个 objc_property_t
,注意是与类对象相关联。value是MJProperty对象:
MJProperty *propertyObj = objc_getAssociatedObject(self, property); if (propertyObj == nil) { propertyObj = [[self alloc] init]; propertyObj.property = property; objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
想象一种情况,Teacher和Student都继承自Person,所以Teacher和Student都有Person的属性,当我们先给Teacher模型赋值的时候,Person类的每一个属性已经调用了上面的代码块封装成了MJProperty对象,并与MJProperty类对象相关联。那么当我们再给Student模型赋值的时候,也会遍历Person类的属性,但是这个时候通过 MJProperty *propertyObj = objc_getAssociatedObject(self, property);
已经能得到MJProperty对象了,不用去创建。
3.
在 MJPropertyType
中有一个types字典,这个字典是在单例中初始化的,types字典的key是code,value是MJPropertyType对象,每次有新的code,就添加到这个字典里面去,这样的好处就是如果code一致,就可以直接从字典中取MJPropertyType。
4.
每一个 MJProperty
对象都有一个 propertyKeysDict
字典,这个字典的key是 NSStringFromClass(class)
,值是一个数组,比如一个MJProperty的名字是name.info[1].text,那么这个数组就会包括4个MJPropertyKey对象,分别表示name,info,1,text,这些key是在取值的时候用的。那么问题来了,为什么要设计字典来存储呢 ,直接用一个数组来存储不就好了吗?
其实这个问题和2相似,因为我们在第二次遍历Person类中的属性的时候不用去创建一个MJProperty对象,直接通过关联属性去取值就好了,但是Student模型和Teacher模型它们的propertyKeys是有可能不一样的,所以这里需要一个key来加以区分。
由于个人水平非常有限,这篇博客也只是我自己的理解,因此一定会有理解有误的地方,还请各位不吝指教。
这篇文章在简书的地址: MJExtension源码解读
作者:雪山飞狐_91ae
链接:https://www.jianshu.com/p/1d7029224ed1
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Redux 源码解读 —— 从源码开始学 Redux
- AQS源码详细解读
- SDWebImage源码解读《一》
- axios 核心源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
WebWork in Action
Jason Carreira、Patrick Lightbody / Manning / 01 September, 2005 / $44.95
WebWork helps developers build well-designed applications quickly by creating re-usable, modular, web-based applications. "WebWork in Action" is the first book to focus entirely on WebWork. Like a tru......一起来看看 《WebWork in Action》 这本书的介绍吧!