新的iOS開發(fā)方式,無需服務(wù)器喇完,做自己的前端轉(zhuǎn)原生iOS app的框架

新的iOS開發(fā)方式伦泥,無需服務(wù)器,做自己的前端轉(zhuǎn)原生iOS app的框架

為什么會有這樣一個想法锦溪?

  1. 一個人做項目的時間有點久了不脯,有時候為了修復(fù)一個小BUG 或者為更新一點內(nèi)容就得去app store 審核,這個過程太漫長了刻诊,覺得煩躁了防楷。
  2. 有時候一個H5頁面,用webView展示坏逢,首屏加載時間慢域帐,各種CSS,JS腳本都要加載。
  3. 再就是有時候服務(wù)器的更新不及時是整,或者想自己控制app 內(nèi)容肖揣。
  4. 考慮過引入ReactNative,但是這個東西浮入,我自己覺得太過笨重了吧龙优。
  5. 用現(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)的原生功能

GIF圖

image

開始搭建框架

要想制作這樣一個框架,必須做到下面這些:

  1. 解析HTML衅胀,生成一個DOM 樹
  2. 根據(jù)HTML 的相應(yīng)標(biāo)簽岔乔,下載CSS,JS文件
  3. 解析CSS滚躯,把樣式表合并到相應(yīng)的Node上
  4. 根據(jù)DOM 樹使用OC 或者Swift 創(chuàng)建視圖
  5. 布局系統(tǒng)使用前端的Flex 布局雏门,F(xiàn)acebook 出的yoga 可以幫助我們
  6. 想要交互必須得執(zhí)行JS,這樣需要JS 和Native 通信的能力

Step 1 - 解析HTML

推薦用蘋果原生的NSXMLParser掸掏,但是NSXMLParser有一些坑

  1. 不能解析非閉合標(biāo)簽比如 <meta>茁影,應(yīng)該是<meta>/<meta>
  2. 當(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文件

你要做到如下:

  1. HTML 解析完畢,你才能合并CSS 到CSS 選擇器匹配的Node上
  2. 以及如何匹配CSS 選擇器到Node 上
  3. 根據(jù)DOM 樹構(gòu)建相應(yīng)的UIView 層次結(jié)構(gòu)
  4. 有可能涉及到線程同步的問題
[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()等患蹂,是不是非常激動。你得做到以下兩點

  1. 計算字符串?dāng)?shù)學(xué)表達式
  2. 去掉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

  1. TokenViewBuilder 用來作為XMLParser的delegate,并且構(gòu)建DOM 樹隐解,下載JS,CSS鞍帝,生成渲染樹
  2. TokenDomcument 用來模仿瀏覽器的document,里面包含整個DOM 樹煞茫,并且使用JSExport 導(dǎo)給JS使用
  3. TokenXMLNode 節(jié)點的父類帕涌,也遵循JSExport 協(xié)議,導(dǎo)給JS使用续徽,并且通過它控制Native 組件
  4. TokenTool 用來給JS 提供各種Native API 如:定位蚓曼,獲取照片,彈出提示框钦扭,等等
  5. TokenJSContext 提供給JS 額外注入纫版,并且執(zhí)行JS 的環(huán)境
  6. 并且如何交互的基礎(chǔ),請看非常容易懂得JS和OC交互

我自己根據(jù)這樣一個思路做了一份源碼TokenHybrid源碼希望大家能多給一點意見客情!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末其弊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子膀斋,更是在濱河造成了極大的恐慌梭伐,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件概页,死亡現(xiàn)場離奇詭異籽御,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門技掏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铃将,“玉大人,你說我怎么就攤上這事哑梳【⒀郑” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵鸠真,是天一觀的道長悯仙。 經(jīng)常有香客問我,道長吠卷,這世上最難降的妖魔是什么锡垄? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮祭隔,結(jié)果婚禮上货岭,老公的妹妹穿的比我還像新娘。我一直安慰自己疾渴,他們只是感情好千贯,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搞坝,像睡著了一般搔谴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桩撮,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天敦第,我揣著相機與錄音,去河邊找鬼店量。 笑死申尼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的垫桂。 我是一名探鬼主播师幕,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诬滩!你這毒婦竟也來了霹粥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤疼鸟,失蹤者是張志新(化名)和其女友劉穎后控,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體空镜,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡浩淘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年捌朴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片张抄。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡砂蔽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出署惯,到底是詐尸還是另有隱情左驾,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布极谊,位于F島的核電站诡右,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏轻猖。R本人自食惡果不足惜帆吻,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咙边。 院中可真熱鬧桅锄,春花似錦、人聲如沸样眠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檐束。三九已至,卻和暖如春束倍,著一層夾襖步出監(jiān)牢的瞬間被丧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工绪妹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甥桂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓邮旷,卻偏偏與公主長得像黄选,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婶肩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345