新的iOS開發(fā)方式伦泥,無需服務(wù)器,做自己的前端轉(zhuǎn)原生iOS app的框架
為什么會有這樣一個想法锦溪?
- 一個人做項目的時間有點久了不脯,有時候為了修復(fù)一個小BUG 或者為更新一點內(nèi)容就得去app store 審核,這個過程太漫長了刻诊,覺得煩躁了防楷。
- 有時候一個H5頁面,用webView展示坏逢,首屏加載時間慢域帐,各種CSS,JS腳本都要加載。
- 再就是有時候服務(wù)器的更新不及時是整,或者想自己控制app 內(nèi)容肖揣。
- 考慮過引入ReactNative,但是這個東西浮入,我自己覺得太過笨重了吧龙优。
- 用現(xiàn)有的方式來寫Native 要方便控制,方便更新,容易編寫彤断,考慮使用HTML,CSS,JS野舶。
新的開發(fā)方式
為了解決以上問題,算是獨辟蹊徑宰衙,實現(xiàn)了一個新穎平道,并且可能容易被接受的構(gòu)建iOS 原生app 的方式,這個方式有以下特點:
1. 不需要專門的服務(wù)器!!!
2. 首屏加載速度快供炼,第二次以后均從緩存加載一屋,緩存甚至包括原生的視圖坐標(biāo)信息。
3. 非常方便進行app 的更新袋哼,隨時更改app 的功能!!!
4. 容易擴展新的組件冀墨,實現(xiàn)自己的解析方式或者兼容現(xiàn)有的HTML 標(biāo)準(zhǔn)!!!
5. 使用HTML,CSS,JS來編寫原生功能,F(xiàn)lex布局涛贯。
該想法已經(jīng)實現(xiàn)诽嘉,點擊github鏈接查看TokenHybrid源碼
目前我已將這種方式放進我們團隊的app -掌上理工大 (app store 可以搜索)里面使用啦。
在講述如何構(gòu)建這樣一種新穎的開發(fā)方式之前弟翘,上兩張圖虫腋,用這種方式實現(xiàn)的原生功能
開始搭建框架
要想制作這樣一個框架,必須做到下面這些:
- 解析HTML衅胀,生成一個DOM 樹
- 根據(jù)HTML 的相應(yīng)標(biāo)簽岔乔,下載CSS,JS文件
- 解析CSS滚躯,把樣式表合并到相應(yīng)的Node上
- 根據(jù)DOM 樹使用OC 或者Swift 創(chuàng)建視圖
- 布局系統(tǒng)使用前端的Flex 布局雏门,F(xiàn)acebook 出的yoga 可以幫助我們
- 想要交互必須得執(zhí)行JS,這樣需要JS 和Native 通信的能力
Step 1 - 解析HTML
推薦用蘋果原生的NSXMLParser掸掏,但是NSXMLParser有一些坑
- 不能解析非閉合標(biāo)簽比如
<meta>
茁影,應(yīng)該是<meta>/<meta>
- 當(dāng)掃描到標(biāo)簽內(nèi)部的文本的時候,如果文本太長丧凤,可能一次掃描不完募闲,需要自己做記錄(不算是坑)
為了避開上面的非閉合標(biāo)簽的坑,你得尋找所有的非閉合標(biāo)簽愿待,并補完全浩螺,使其成為閉合標(biāo)簽。
這里需要用到正則表達式
下面是我尋找所有的自閉和標(biāo)簽并補全的代碼
-(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<NSTextCheckingResult *> *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>
等標(biāo)簽要出,需要啟動下載器去下載相應(yīng)的文件
下面只展示下載CSS文件
你要做到如下:
- HTML 解析完畢,你才能合并CSS 到CSS 選擇器匹配的Node上
- 以及如何匹配CSS 選擇器到Node 上
- 根據(jù)DOM 樹構(gòu)建相應(yīng)的
UIView
層次結(jié)構(gòu) - 有可能涉及到線程同步的問題
[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农渊,那么你可以自己實現(xiàn)一些諸如CSS里面的函數(shù)calc()等患蹂,是不是非常激動。你得做到以下兩點
- 計算字符串?dāng)?shù)學(xué)表達式
- 去掉CSS 里面的注釋
計算NSString 數(shù)學(xué)表達式
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<NSTextCheckingResult *> *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 = @"(?<!:)\\/\\/.*|\\/\\*(\\s|.)*?\\*\\/";
//去掉CSS里面的評論
NSString *css = cssString.token_replaceWithRegExp(commentRegExp,@"")
.token_replace(@"\n",@"")
.token_replace(@"\r",@"");
int braceMarker = 0;
NSString *selector;
NSString *rule;
for (int i = 0; i < css.length; i ++) {
unichar c = [css characterAtIndex:i];
if (c == '{') {
selector = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
braceMarker = i + 1;
}
if (c == '}') {
rule = [css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
braceMarker = i + 1;
if (selector.length && rule.length) {
NSDictionary *dic = [self converAttrStringToDictionary:rule];
if ([selector hasPrefix:@" "] || [selector hasSuffix:@" "]) {
selector = [selector stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
[styleSheets setObject:dic forKey:selector];
}
}
}
return styleSheets;
}
調(diào)用 -parserCSSWithString
就會將CSS 文件解析為一個 NSDictionary
如下
body { --> {
backgroundColor: rgb(120,120,120); @"backgroundColor":@"rgb(120,120,120)",
width:120px; @"width":@"120px"
} }
Step 2.2 - 匹配CSS 選擇器 支持id選擇器,class 選擇器传于,簡單的組合選擇器
匹配相應(yīng)的CSS 選擇器到DOM 上相應(yīng)的Nodes
匹配的時候你得從選擇器字符串的右邊匹配到左邊囱挑,這樣會加快匹配的速度,想想為啥沼溜?
+(NSSet <TokenXMLNode *> *)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 <TokenXMLNode *> *matchNodeSet = [NSMutableSet set];
//先產(chǎn)生一個基本集合
[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;
//開始向上匹配父節(jié)點
TokenXMLNode *currentNode = node;
while (currentNode.parentNode) {
//匹配到父節(jié)點
if ([currentNode.name isEqualToString:selector]) {
nodeMatchd = YES;
break;
}
currentNode = currentNode.parentNode;
}
if (!nodeMatchd) {
[matchNodeSetCopy removeObject:node];
}
}
}
}];
matchNodeSet = matchNodeSetCopy;
}
return matchNodeSet;
}
Step 3 - 根據(jù)DOM 樹構(gòu)建UIView 的層次結(jié)構(gòu)
當(dāng)NSXMLParser 解析到下面這兩個方法的時候可以構(gòu)建視圖層次
因為HTML 標(biāo)簽內(nèi)部的結(jié)構(gòu)和UIView 的層次結(jié)構(gòu)正好對應(yīng)平挑,都有父子關(guān)系,其實就是一顆多叉樹系草,使用Stack層次遍歷即可弹惦。
#pragma mark - XMLParserDelegate
-(void)parserDidStart{
//新建一個棧
_viewStack = [[TokenHybridStack alloc] init];
}
-(void)parser:(TokenXMLParser *)parser didStartNodeWithinBodyNode:(TokenPureNode *)node{
//根據(jù)相應(yīng)的node 創(chuàng)建相應(yīng)的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調(diào)整UIView層次結(jié)構(gòu)
UIView *currentView = [_viewStack pop];
UIView *parentView = [_viewStack top];
[parentView addSubview:currentView];
}
Step 4 - 設(shè)置UIView 的相應(yīng)的屬性
如何設(shè)置,其實很簡單
因為上文中悄但,生成的UIView
都持有一個Node
,根據(jù)Node
的里面解析的數(shù)據(jù)就可以設(shè)置石抡,你可以寫總結(jié)的方法檐嚣,推薦你為UIView
寫一個 Category
增加一個方法專門設(shè)置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,并且構(gòu)建DOM 樹隐解,下載JS,CSS鞍帝,生成渲染樹 -
TokenDomcument
用來模仿瀏覽器的document,里面包含整個DOM 樹煞茫,并且使用JSExport 導(dǎo)給JS使用 -
TokenXMLNode
節(jié)點的父類帕涌,也遵循JSExport 協(xié)議,導(dǎo)給JS使用续徽,并且通過它控制Native 組件 -
TokenTool
用來給JS 提供各種Native API 如:定位蚓曼,獲取照片,彈出提示框钦扭,等等 -
TokenJSContext
提供給JS 額外注入纫版,并且執(zhí)行JS 的環(huán)境 - 并且如何交互的基礎(chǔ),請看非常容易懂得JS和OC交互
我自己根據(jù)這樣一個思路做了一份源碼TokenHybrid源碼希望大家能多給一點意見客情!