新的iOS开发方式,无需服务器,做自己的前端转原生iOS app的框架

新的iOS开发方式,无需服务器,做自己的前端转原生iOS app的框架

为什么会有这样一个想法?

  1. 一个人做项目的时间有点久了,有时候为了修复一个小BUG 或者为更新一点内容就得去app store 审核,这个过程太漫长了,觉得烦躁了。
  2. 有时候一个H5页面,用webView展示,首屏加载时间慢,各种CSS,JS脚本都要加载。
  3. 再就是有时候服务器的更新不及时,或者想自己控制app 内容。
  4. 考虑过引入ReactNative,但是这个东西,我自己觉得太过笨重了吧。
  5. 用现有的方式来写Native 要方便控制,方便更新,容易编写,考虑使用HTML,CSS,JS。

新的开发方式

为了解决以上问题,算是独辟蹊径,实现了一个新颖,并且可能容易被接受的构建iOS 原生app 的方式,这个方式有以下特点:

    1. 不需要专门的服务器!!!
    2. 首屏加载速度快,第二次以后均从缓存加载,缓存甚至包括原生的视图坐标信息。
    3. 非常方便进行app 的更新,随时更改app 的功能!!!
    4. 容易扩展新的组件,实现自己的解析方式或者兼容现有的HTML 标准!!!
    5. 使用HTML,CSS,JS来编写原生功能,Flex布局。

该想法已经实现,点击github链接查看TokenHybrid源码

目前我已将这种方式放进我们团队的app -掌上理工大 (app store 可以搜索)里面使用啦。

在讲述如何构建这样一种新颖的开发方式之前,上两张图,用这种方式实现的原生功能

GIF图

image

开始搭建框架

要想制作这样一个框架,必须做到下面这些:

  1. 解析HTML,生成一个DOM 树
  2. 根据HTML 的相应标签,下载CSS,JS文件
  3. 解析CSS,把样式表合并到相应的Node上
  4. 根据DOM 树使用OC 或者Swift 创建视图
  5. 布局系统使用前端的Flex 布局,Facebook 出的yoga 可以帮助我们
  6. 想要交互必须得执行JS,这样需要JS 和Native 通信的能力

Step 1 - 解析HTML

推荐用苹果原生的NSXMLParser,但是NSXMLParser有一些坑

  1. 不能解析非闭合标签比如 <meta>,应该是<meta>/<meta>
  2. 当扫描到标签内部的文本的时候,如果文本太长,可能一次扫描不完,需要自己做记录(不算是坑)

为了避开上面的非闭合标签的坑,你得寻找所有的非闭合标签,并补完全,使其成为闭合标签。
这里需要用到正则表达式
下面是我寻找所有的自闭和标签并补全的代码


-(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>等标签,需要启动下载器去下载相应的文件
下面只展示下载CSS文件

你要做到如下:

  1. HTML 解析完毕,你才能合并CSS 到CSS 选择器匹配的Node上
  2. 以及如何匹配CSS 选择器到Node 上
  3. 根据DOM 树构建相应的UIView 层次结构
  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,那么你可以自己实现一些诸如CSS里面的函数calc()等,是不是非常激动。你得做到以下两点

  1. 计算字符串数学表达式
  2. 去掉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<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;
}

调用 -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 <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];
    //先产生一个基本集合
    [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

  1. TokenViewBuilder 用来作为XMLParser的delegate,并且构建DOM 树,下载JS,CSS,生成渲染树
  2. TokenDomcument 用来模仿浏览器的document,里面包含整个DOM 树,并且使用JSExport 导给JS使用
  3. TokenXMLNode 节点的父类,也遵循JSExport 协议,导给JS使用,并且通过它控制Native 组件
  4. TokenTool 用来给JS 提供各种Native API 如:定位,获取照片,弹出提示框,等等
  5. TokenJSContext 提供给JS 额外注入,并且执行JS 的环境
  6. 并且如何交互的基础,请看非常容易懂得JS和OC交互

我自己根据这样一个思路做了一份源码TokenHybrid源码希望大家能多给一点意见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266