为什么会有这样一个想法?
- 一个人做项目的时间有点久了,有时候为了修复一个小BUG 或者为更新一点内容就得去app store 审核,这个过程太漫长了,觉得烦躁了。
- 再就是有时候服务器的更新不及时,或者想自己控制app 内容。
- 考虑过引入ReactNative,但是这个东西,我自己觉得太过笨重了吧。
- 用现有的方式来写Native 要方便控制,方便更新,容易编写,考虑使用HTML,CSS,JS。
新的开发方式
为了解决以上问题,算是独辟蹊径,实现了一个新颖,并且可能容易被接受的构建iOS 原生app 的方式,这个方式有以下特点:
1. 不需要专门的服务器!!! 2. 非常方便进行app 的更新,随时更改app 的功能!!! 3. 容易扩展新的组件,实现自己的解析方式或者兼容现有的HTML 标准!!! 4. 使用HTML,CSS,JS来编写原生功能,Flex布局。复制代码
在讲述如何构建这样一种新颖的开发方式之前,上两张图,用这种方式实现的原生功能
开始搭建框架
要想制作这样一个框架,必须做到下面这些:
- 解析HTML,生成一个DOM 树
- 根据HTML 的相应标签,下载CSS,JS文件
- 解析CSS,把样式表合并到相应的Node上
- 根据DOM 树使用OC 或者Swift 创建视图
- 布局系统使用前端的Flex 布局,Facebook 出的yoga 可以帮助我们
- 想要交互必须得执行JS,这样需要JS 和Native 通信的能力
具体的实现源码可以查看
Step 1 - 解析HTML
推荐用苹果原生的NSXMLParser,但是NSXMLParser有一些坑
- 不能解析非闭合标签比如
<meta>
,应该是<meta>/<meta>
- 当扫描到标签内部的文本的时候,如果文本太长,可能一次扫描不完,需要自己做记录(不算是坑)
为了避开上面的非闭合标签的坑,你得寻找所有的非闭合标签,并补完全,使其成为闭合标签。 这里需要用到正则表达式 下面是我寻找所有的自闭和标签并补全的代码
-(void)parserHTML:(NSString *)html{ dispatch_async(tokenXMLParserQueue(), ^{ NSString *closedHTML = [self handleSimeClosedTagWithTagNameArray:@[@"meta",@"input"] html:html]; NSData *data = [closedHTML dataUsingEncoding:NSUTF8StringEncoding]; _parser = [[NSXMLParser alloc] initWithData:data]; _parser.delegate = self; [_parser parse]; });}-(NSString *)handleSimeClosedTagWithTagNameArray:(NSArray *)tagNameArray html:(NSString *)html{ __block NSString *temp = html; for (NSString *tagName in tagNameArray) { NSString *testString = @"<".token_append(tagName); NSString *closedString = [NSString stringWithFormat:@" ",tagName]; if ([html containsString:testString]) { //检测是否闭合 NSString *pattern = [NSString stringWithFormat:@"<%@(.*?)>",tagName]; NSRegularExpression *exp = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; NSArray*results = [exp matchesInString:html options:0 range:NSMakeRange(0, html.length)]; [results enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *matchString = [html substringWithRange:obj.range]; NSString *nextString = [html substringWithRange:NSMakeRange(obj.range.length+obj.range.location, tagName.length+3)]; if (![nextString isEqualToString:closedString]) { temp = temp.token_replace(matchString,matchString.token_append(closedString)); } }]; } } return temp;}复制代码
HTML 解析的同时,如果有<script>,<style>,<link>
等标签,需要启动下载器去下载相应的文件 下面只展示下载CSS文件
你要做到如下:
- HTML 解析完毕,你才能合并CSS 到CSS 选择器匹配的Node上
- 以及如何匹配CSS 选择器到Node 上
- 根据DOM 树构建相应的
UIView
层次结构 - 有可能涉及到线程同步的问题
[nodes enumerateObjectsUsingBlock:^(TokenXMLNode * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *linkURL = obj.innerAttributes[@"href"]; if (linkURL == nil || linkURL.length == 0) return; NSString *absoluteLinkURL = [NSString token_completeRelativeURLString:linkURL withAbsoluteURLString:_document.sourceURL]; HybridLog(@"开始下载CSS文件"); TokenNetworking.networking() .sendRequest(^NSURLRequest *(TokenNetworking *netWorking) { return NSMutableURLRequest.token_requestWithURL(absoluteLinkURL) .token_setPolicy(NSURLRequestReloadIgnoringLocalCacheData); }).transform(^id(TokenNetworking *netWorking, id responsedObj) { HybridLog(@"CSS文件下载完成"); NSString *cssText = [netWorking HTMLTextSerializeWithData:responsedObj]; NSDictionary *rules = [TokenCSSParser parserCSSWithString:cssText]; if (rules.allKeys.count) { [_document addCSSRuels:rules]; } self.styleAndLinkNodeCount -= 1; return cssText; }).finish(nil, ^(TokenNetworking *netWorkingObj, NSError *error) { self.styleAndLinkNodeCount -= 1; HybridLog(@"CSS文件下载错误: %@",error); [_document addFailedCSSURL:absoluteLinkURL]; }); }];复制代码
Step 2 - 解析CSS
Step 2.1 -将CSS 解析为 NSDictionary
如果你可以解析CSS,那么你可以自己实现一些诸如CSS里面的函数calc()等,是不是非常激动。你得做到以下两点
- 计算字符串数学表达式
- 去掉CSS 里面的注释
计算NSString 数学表达式NSString *mathExp = @"7+8*3";NSExpression *expression = [NSExpression expressionWithFormat:mathExp];id value = [expression expressionValueWithObject:nil context:nil];value 就是一个NSNumber 值为31复制代码
下面是去掉注释并解析为NSDictionary
的代码
//我为NSString 增加的正则表达式方法 下面的cssString.token_replaceWithRegExp(commentRegExp,@"")-(TokenStringReplaceWithRegExpBlock)token_replaceWithRegExp{ return ^NSString *(NSString *regExp,NSString *newString) { __block NSString *temp = [self copy]; NSRegularExpression *exp = [NSRegularExpression regularExpressionWithPattern:regExp options:0 error:nil]; NSArray*result = [exp matchesInString:temp options:0 range:NSMakeRange(0, temp.length)]; [result enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *stringWillBeReplaced = [self substringWithRange:obj.range]; temp = [temp stringByReplacingOccurrencesOfString:stringWillBeReplaced withString:newString]; }]; return temp; };}//参考了DTCoreText+(NSDictionary *)parserCSSWithString:(NSString *)cssString{ if (cssString == nil) return @{}; NSMutableDictionary *styleSheets = @{}.mutableCopy; NSString *commentRegExp = @"(?
调用 -parserCSSWithString
就会将CSS 文件解析为一个 NSDictionary
如下
body { --> { backgroundColor: rgb(120,120,120); @"backgroundColor":@"rgb(120,120,120)", width:120px; @"width":@"120px"} }复制代码
Step 2.2 - 匹配CSS 选择器 支持id选择器,class 选择器,简单的组合选择器
匹配相应的CSS 选择器到DOM 上相应的Nodes 匹配的时候你得从选择器字符串的右边匹配到左边,这样会加快匹配的速度,想想为啥?
+(NSSet*)matchNodesWithRootNode:(TokenXMLNode *)node selector:(NSString *)selector{ //去掉两端空格 if ([selector hasPrefix:@" "]) { selector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } //用空格分割 NSMutableArray *selectors = NSMutableArray.token_arrayWithArray(selector.token_separator(@" ")); if ([selectors containsObject:@""]) { [selectors removeObject:@""]; } NSMutableSet *matchNodeSet = [NSMutableSet set]; //先产生一个基本集合 [TokenXMLNode enumerateTreeFromRootToChildWithNode:node block:^(TokenXMLNode *node ,BOOL *stop) { [matchNodeSet addObject:node]; }]; //对selector 从右往左开始匹配 for (NSInteger i = selectors.count - 1 ; i>= 0; i--) { NSString *selector = selectors[i]; NSMutableSet *matchNodeSetCopy = [NSMutableSet setWithSet:matchNodeSet]; [matchNodeSet enumerateObjectsUsingBlock:^(TokenXMLNode * node, BOOL * _Nonnull stop) { //id 选择器 if ([selector hasPrefix:@"#"]) { if (![node.innerAttributes[@"id"] isEqualToString:[selector substringWithRange:NSMakeRange(1, selector.length-1)]]) { [matchNodeSetCopy removeObject:node]; } } else if ([selector hasPrefix:@"."]) { NSString *nodeClass = node.innerAttributes[@"class"]; NSString *selectorToBeMatched = [selector substringWithRange:NSMakeRange(1, selector.length-1)]; if ([nodeClass containsString:@" "]) {//包含多个类 NSArray *nodeClassArray = [nodeClass componentsSeparatedByString:@" "]; if (![nodeClassArray containsObject:selectorToBeMatched]) { [matchNodeSetCopy removeObject:node]; } } else { //不包含多个类 if (![nodeClass isEqualToString:[selector substringWithRange:NSMakeRange(1, selector.length-1)]]) { [matchNodeSetCopy removeObject:node]; } } } else { if (i == selectors.count-1) { if (![node.name isEqualToString:selector]) { [matchNodeSetCopy removeObject:node]; } } else { BOOL nodeMatchd = NO; //开始向上匹配父节点 TokenXMLNode *currentNode = node; while (currentNode.parentNode) { //匹配到父节点 if ([currentNode.name isEqualToString:selector]) { nodeMatchd = YES; break; } currentNode = currentNode.parentNode; } if (!nodeMatchd) { [matchNodeSetCopy removeObject:node]; } } } }]; matchNodeSet = matchNodeSetCopy; } return matchNodeSet;}复制代码
Step 3 - 根据DOM 树构建UIView 的层次结构
当NSXMLParser 解析到下面这两个方法的时候可以构建视图层次 因为HTML 标签内部的结构和UIView 的层次结构正好对应,都有父子关系,其实就是一颗多叉树,使用Stack层次遍历即可。
#pragma mark - XMLParserDelegate-(void)parserDidStart{ //新建一个栈 _viewStack = [[TokenHybridStack alloc] init];}-(void)parser:(TokenXMLParser *)parser didStartNodeWithinBodyNode:(TokenPureNode *)node{ //根据相应的node 创建相应的Native 组件 TokenPureComponent *view = [UIView token_produceViewWithNode:node]; if (view == nil) { view = [[TokenPureComponent alloc] init]; } view.associatedNode = node; node.associatedView = view; [_viewStack push:view];}-(void)parser:(TokenXMLParser *)parser didEndNodeWithinBodyNode:(TokenXMLNode *)node{ //在End调整UIView层次结构 UIView *currentView = [_viewStack pop]; UIView *parentView = [_viewStack top]; [parentView addSubview:currentView];}复制代码
Step 4 - 设置UIView 的相应的属性
如何设置,其实很简单 因为上文中,生成的UIView
都持有一个Node
,根据Node
的里面解析的数据就可以设置,你可以写总结的方法,推荐你为UIView
写一个 Category
增加一个方法专门设置Node
属性到UIView
属性的方法。里面可能遇到很多if-else
,本人水平有限,希望有人能帮助简化if-else
下面是我写的方法
//// UIView+Attributes.m// TokenHybrid//// Created by 陈雄 on 2017/11/9.// Copyright © 2017年 com.feelings. All rights reserved.//@implementation UIView (Attributes)...-(void)token_updateAppearanceWithNormalDictionary:(NSDictionary *)dictionary{ NSDictionary *d = dictionary; if(d[@"borderRadius"]) { self.layer.cornerRadius = [d[@"borderRadius"] floatValue];} if(d[@"zIndex"]) { self.layer.zPosition = [d[@"zIndex"] floatValue];} if(d[@"borderWidth"]) { self.layer.borderWidth = [d[@"borderWidth"] floatValue];} if(d[@"borderColor"]) { self.layer.borderColor = [UIColor ss_colorWithString:d[@"borderColor"]].CGColor;} if(d[@"backgroundColor"]) { self.backgroundColor = [UIColor ss_colorWithString:d[@"backgroundColor"]];} NSString *hidden = d[@"hidden"]; if(hidden) {self.hidden = hidden.token_turnBoolStringToBoolValue(); }}@end复制代码
Step 5 - JS 和OC/Swift 的交互
我说说我的做法 模型:TokenDomcument,TokenXMLNode,TokenTool
工具类:TokenViewBuilder,TokenJSContext
TokenViewBuilder
用来作为XMLParser
的delegate,并且构建DOM 树,下载JS,CSS,生成渲染树TokenDomcument
用来模仿浏览器的document,里面包含整个DOM 树,并且使用JSExport 导给JS使用TokenXMLNode
节点的父类,也遵循JSExport 协议,导给JS使用,并且通过它控制Native 组件TokenTool
用来给JS 提供各种Native API 如:定位,获取照片,弹出提示框,等等TokenJSContext
提供给JS 额外注入,并且执行JS 的环境- 并且如何交互的基础,请看
我自己根据这样一个思路做了一份源码希望大家能多给一点意见!