博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
新的iOS开发方式,无需服务器,做自己的前端转原生iOS app的框架
阅读量:5845 次
发布时间:2019-06-18

本文共 11370 字,大约阅读时间需要 37 分钟。

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

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

新的开发方式

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

1. 不需要专门的服务器!!!    2. 非常方便进行app 的更新,随时更改app 的功能!!!    3. 容易扩展新的组件,实现自己的解析方式或者兼容现有的HTML 标准!!!    4. 使用HTML,CSS,JS来编写原生功能,Flex布局。复制代码

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

开始搭建框架

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

  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
*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
*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

  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. 并且如何交互的基础,请看

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

转载于:https://juejin.im/post/5a27db5df265da43144094ab

你可能感兴趣的文章
前端面试大全
查看>>
node js、npm、homebrew、cocoapod、git、hexo
查看>>
Canvas基本知识点三:线条操作
查看>>
wepack到底是什么(使用篇)
查看>>
TreeMap
查看>>
开发数字货币场外OTC交易所,币币现货交易所搭建开发
查看>>
CSS欺骗
查看>>
原来 ArrayList 内部原理这么简单
查看>>
linux基础之进程管理与服务
查看>>
Java进阶面试题:如何设计一个高并发系统?
查看>>
Kotlin 3 属性和函数
查看>>
Android-仪表盘控件仿芝麻信用
查看>>
git 常用命令
查看>>
关于java 1.8的Lambda表达式详解
查看>>
缅怀那些正渐行渐远的编程语言
查看>>
各个网站的CSS清除代码
查看>>
TableView的集合
查看>>
element-ui源码阅读笔记(radio篇)
查看>>
Objective-C混淆之方法名混淆
查看>>
【FB官方活动】Facebook Audience Network广告变现进阶训练营深圳线下活动
查看>>