起因

  开玩笑说“iOS搞不动了”,另外一方面iOS组的哥哥们给力,少一个我也妥妥的。又听闻web前端组来了一个不得了的人物,“老司机,带带我”这种机会不能错过,1个多月前就申请转web前端了。开始是苦涩的,学习CSS、JS...... 自生自灭、自我生长。自己不懂、老司机也忙根本飞不起来。

  转机是后来老司机从业务中解脱了出来,计划自己搞一套Hybird开发框架,过程中会需要Native的同事协助,我也懂了那么一点点点点H5(也许是即将懂),So强势插入。

遇到的几个问题

  Dog_Hybird这个名字,是我瞎起的。同事并没有制止我。

  挑几个比较具体的问题讲讲,也就是在开发过程中思考得比较多的几个点,比较零散。

 JS和Native交互模式选择

  拦截url变化

  之前项目中也有简单的js交互,通过jsbridge实现。在UIWebView的shouldStartLoadWithRequest方法里面捕获url的变化,解析出需要的参数,然后传给一个统一的处理方法。

  这里主要约定的参数有:

  function      ---> 需要触发的事件名

  args           ---> 上面方法需传入的参数

  callBackId   ---> 执行方法后的回调ID,会作为回传参数的一部分

  统一处理方法:

  handleEvent ---> 根据function判断需要触发的事件

     func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if let requestStr = request.URL?.absoluteString {
if requestStr.hasPrefix("hybrid://") { let function = XXX
let args = XXX
let callBackId = XXX self.handleEvent(function, args: args, callbackID: callBackId)
}
}
return true
}

  方法注入

  拦截url的方式其实能满足绝大部分需求,至少我还没遇到不能满足的。low是low,好用。但是有个问题,如上面所说 有一个 handleEvent 方法去判断需要触发的事件,比如web页面想触发一个log方法,需要向Native端传递一串参数,解析出来 function 为log,然后去触发本地的log方法。随着事件的丰富,此方法体积必然爆炸,就算你分模块写、分文件写,也只是看上去好看一些而已,代码量在那里。

  那么怎样不low?舆论一致认为“方法注入”不low,高端、优雅。

  引入JavaScriptCore(需iOS7及以上)。最简单的例子,在UIWebView的webViewDidStartLoad或者webViewDidFinishLoad方法里面创建/更新JSContext,将方法注入到此context中。

  

     func webViewDidFinishLoad(webView: UIWebView) {
self.context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
self.context.setObject(unsafeBitCast(##需注入的方法##, AnyObject.self), forKeyedSubscript: ##注入方法对应名称##)
}

  方法以block的形式注入,另外还有通过实现协议的方式, 此处注入方法为一个特殊的类型:

  @convention(block) String -> Bool

  转换方法也有一个unsafeBitCast,让人多少有一些不安,还是OC写起来看着稳妥些,有兴趣的可以查一下OC的写法。

  这样简单的注入后JS代码便可直接调用注入的方法,没有了之前的一个参数转换事件的过程,臃肿的handleEvent方法直接不需要了。是不是很高端很优雅?不过在后来完善框架的过程中,遇到个问题:

  注入时机。如果你在webViewDidFinishLoad方法里面注入,那么如果是加载过程中就需要执行的方法怎么办?聪明的同学想到了”那就在webViewDidStartLoad方法里面注入啊“,确实这样能满足刚提出的需求。页面刷新之后又蒙逼了,注入方法失效了,又必须在webViewDidFinishLoad里面重新注入一次。关于页面刷新注入方法失效这个问题,网上很多人提出了,但是翻了很多页都没有很机智的解决办法。所以说啊老司机还是老司机,文章开头提到的老司机想到了一个办法。

     func webViewDidFinishLoad(webView: UIWebView) {
self.context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
self.context.setObject(unsafeBitCast(##需注入的方法##, AnyObject.self), forKeyedSubscript: ##注入方法对应名称##)
self.myWebView.stringByEvaluatingJavaScriptFromString("Hybrid.ready();")
}

  交互是双向的嘛!在方法注入后调用一个JS方法 Hybird.ready() 告诉web页面”我准备好了“。这样web页会在方法注入完毕后再去执行一些setting方法。缺点就是比普通的方法要慢上几十毫秒,毕竟要等待方法注入完毕。

 请求、回调方式

  在之前的项目中使用JSBridge,通过给web端注入session的方式来传递用户信息,web端拿着session自己去请求信息。现采用web端下达指令让Native去请求然后将数据回传的方式,如下:

     func demoApi(args: [String: AnyObject], callbackID: String) {
self.callBack(args, errno: , msg: "success", callback: callbackID)
} func callBack(data:AnyObject, errno: Int, msg: String, callback: String) {
let data = ["data": data,
"errno": errno,
"msg": msg,
"callback": callback]
let dataString = self.toJSONString(data)
self.myWebView.stringByEvaluatingJavaScriptFromString(self.HybirdEvent + "(\(dataString));")
}

  参数解释:

  data       ---> 网络请求结果、本地数据等回传信息

  errno      ---> 错误码,需要将Native端的错误码映射为和web端约定好的错误码

  msg        ---> 描述

  callback   ---> 回调ID,web端通过此参数才知道是从哪个方法回来的

  

 本地资源路径

  在项目目录中创建文件夹Group并不影响资源文件读取时的路径,想要有特定的路径,在引入文件时记得如下图选择。

  加载本地文件的方法比较简单:

             if let htmlPath = NSBundle.mainBundle().pathForResource(##本地文件路径##, ofType: "html") {
let url = NSURL(fileURLWithPath: htmlPath)
let request = NSURLRequest(URL: url)
self.webView.loadRequest(request)
}

  

  

 页面跳转

  iOS本身的push操作跳转到新页面后,前面的页面会保留在内存中,后退时便能pop到之前的页面,然后根据pop前的操作更新当前展示页面。然后web的所谓后退到前一页面其实都是通过 forward 指令下达的,都是新开一个UIWebView。但是这里需要达到和iOS本身pop一样的体验,所以需要自定义push动画,将web”回退“操作的push动画做成和原生的pop动画一致,让用户察觉不到逻辑上的差异。

  因为我们从iOS7开始支持,所以自定义动画可以用 UIViewControllerAnimatedTransitioning 轻松完成,相关的代码很容易搜到。

  这里要说的一点就是定义一个AnimateType对应用户感知到的push或pop(这里定义的pop实际上是push,只是自定义动画为pop)

 enum AnimateType {
case Normal
case Push
case Pop
} func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == UINavigationControllerOperation.Push {
if self.animateType == .Pop {
return HybirdTransionPush()//这个为自定义的push动画,表现为pop样式
}
else {
return nil
}
} else {
return nil
}
}

  

加载本地资源

要加载本地资源,就要拦截请求来判断选择加载逻辑。

通过NSURLProtocol拦截请求并处理

  这里用到NSURLProtocol中2个比较重要的方法。

  判断请求是否为需要拦截的请求

     override class func canInitWithRequest(request: NSURLRequest) -> Bool {
//如果被标记为已处理 直接跳过
if let hasHandled = NSURLProtocol.propertyForKey(DogHybirdURLProtocolHandled, inRequest: request) as? Bool where hasHandled == true {
print("重复的url == \(request.URL?.absoluteString)")
return false
}
if let url = request.URL?.absoluteString {
if url.hasPrefix(webAppBaseUrl) {
//从请求中解析出path 和 type 然后在 NSBundle.mainBundle() 和 NSSearchPathDirectory.DocumentDirectory 中查找
if let zipPath = NSBundle.mainBundle().pathForResource(path, ofType: type) {
if types.contains(type) {
//为需要处理的类型 types = ["html","js","css","jpg","png"]
return true
}
}
else {
let documentPaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentPath = documentPaths[]
let newPath = ##在document目录的路径##
let fileData = NSFileManager.defaultManager().contentsAtPath(documentPath + "/\(newPath).\(type)")
if fileData?.length > {
return true
}
}
}
}
return false
}

  对需要拦截的请求进行处理

     override func startLoading() {
//标记请求 防止重复处理
let mutableReqeust: NSMutableURLRequest = self.request.mutableCopy() as! NSMutableURLRequest
NSURLProtocol.setProperty(true, forKey: DogHybirdURLProtocolHandled, inRequest: mutableReqeust)
dispatch_async(dispatch_get_main_queue()) {
if let url = self.request.URL?.absoluteString {
if url.hasPrefix(webAppBaseUrl) {
let path = ##请求path##
let type = ##请求type##
let client: NSURLProtocolClient = self.client! var typeString = ""
switch type {
case "html":
typeString = "text/html"
break
case "js":
typeString = "application/javascript"
break
case "css":
typeString = "text/css"
break
case "jpg":
typeString = "image/jpeg"
break
case "png":
typeString = "image/png"
break
default:
break
}
let localUrl = ##先在DocumentDirectory中查找,如果不存在再在NSBundle.mainBundle()中查找##
let fileData = NSData(contentsOfFile: localUrl)
let url = NSURL(fileURLWithPath: localUrl)
let dataLength = fileData?.length ??
let response = NSURLResponse(URL: url, MIMEType: typeString, expectedContentLength: dataLength, textEncodingName: "UTF-8")
client.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
client.URLProtocol(self, didLoadData: fileData!)
client.URLProtocolDidFinishLoading(self) }
else {
print(">>>>> url不符合规则 <<<<<")
}
}
else {
print(">>>>> url字符串获取失败 <<<<<")
}
}
}

  需要注意一些细节。拦截请求后只对特定的类型替换为本地缓存。比如更新静态资源请求是下载zip包,如果本地也存在此zip包,那么更新请求会被拦截导致更新失败。还有一点先在DocumentDirectory中查找缓存文件,如果不存在再在NSBundle.mainBundle()中查找。因为NSBundle.mainBundle()是应用打包时就打入app中的资源,而DocumentDirectory是后来下载的资源,所以优先使用Document路径下的资源。前几天工作强度比较大,头有点晕,最开始把更新资源也写入到NSBundle.mainBundle()中,文件一直读不到。这里还是提醒一下这个基础知识点,NSBundle.mainBundle()路径是没有权限操作的哟!还有新建请求的MIMEType,写错了资源会以纯文本的形式读取出来。刚入前端坑伤不起啊。

 

  

Dog_Hybird的诞生的更多相关文章

  1. JSONP的诞生、原理及应用实例

    问题: 页面中有一个按钮,点击之后会更新网页中的一个盒子的内容. Ajax可以很容易的满足这种无须刷新整个页面就可以实现数据变换的需求. 但是,Ajax有一个缺点,就是他不允许跨域请求资源. 如果我的 ...

  2. Lambda表达式的诞生过程

    这是一篇很经典的文章,解决了工作中一些使用过但是又不太明白的知识点,今天终于弄明白了.花了一晚上重新整的,坚决要分享出来!!! 那得从很久很久以前说起了,记得那个时候... 懵懂的记得从前有个叫委托的 ...

  3. git的诞生

    Git的诞生   很多人都知道,Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了. Linus虽然创建了Linux,但Linux的壮大是靠全世 ...

  4. Selenium2学习-042-Selenium3启动Firefox Version 48.x浏览器(ff 原生 geckodriver 诞生)

    今天又被坑了一把,不知谁把 Slave 机的火狐浏览器版本升级为了 48 的版本,导致网页自动化测试脚本无法启动火狐的浏览器,相关的网页自动化脚本全线飘红(可惜不是股票,哈哈哈...),报版本不兼容的 ...

  5. 3.Git的诞生和其分布式的优点

    Git的诞生 省略了,喜欢的可以看百度. 分布式的优点 先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完 ...

  6. [转载]jQuery诞生记-原理与机制

    by zhangxinxu from http://www.zhangxinxu.com本文地址:http://www.zhangxinxu.com/wordpress/?p=3520 一.看似偶然的 ...

  7. Zygote进程【3】——SystemServer的诞生

    在ZygoteInit的main()方法中做了几件大事,其中一件便是启动Systemserver进程,代码如下: @/frameworks/base/core/Java/com/Android/int ...

  8. 【Android测试】【随笔】性能采集工具——小松鼠诞生记

    ◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/4945066.html 起因 去年刚加入TX的时候,我便接手 ...

  9. jQuery诞生记-原理与机制

    一.看似偶然的东西实际是必然会发生的 我大学时候在图书馆翻过一本很破旧的书,讲生物理论的,主要内容就是探讨生命的产生是偶然还是必然.里面很多亚里士多德都看不懂的公式计算什么的,还有模拟原始地球环境出现 ...

随机推荐

  1. PHP面向对象笔记

    一.构造函数.析构函数(1)构造函数:__construct()说明:对象被实例化时调用,可带参数例: $obj = new A($a,$b); (2)析构函数:_destruct()说明:页面执行结 ...

  2. sql期末复习(二)

    1.概念模式是对dba所看到的全局数据逻辑结构和特征的描述 概念模式是对数据整体的逻辑结构的描述 2.数据库网状模型应满足的条件是允许一个以上的结点无父结点,其余结点都只有一个父结点 3.sql语言中 ...

  3. C++构造函数和析构函数

    构造函数简介 在上一个章节我们在创建好类的对象之后,首先对它的每一个成员属性赋值之后再对它们进行输出操作,如果不赋值就输出,这些值就会是垃圾值.而为了代码的简介,一次性为所有成员属性初始化,C++的类 ...

  4. Socket实现仿QQ聊天(可部署于广域网)附源码(4)-加入数据库系统搭建完成

    1.前言 这是本系列的第四篇文章,上一篇我们讲到实现了客户端对客户端的抖屏与收发各种类型文件,本篇文章我们加入SQLServer数据库实现登录与好友的添加等功能,并对界面做了美化处理.向往常一样我会把 ...

  5. SQL Server 创建数据库邮件

    一. 背景 数据库发邮件通知数据库的运行状态(状态可以通过JOB形式获取)和信息,达到预警的效果. 二. 基础知识 msdb系统数据库保存有关Job,Database Mail,Nodifyicati ...

  6. PinnedHeaderListView实现删除

    项目中用到四个List集合展示一个页面,并且每个页面都会有一个标题栏.找了半天资料决定用PinnedHeaderListView开源项目.最后需求又来了,需要一个删除的功能,又去网上找资料,发现没有实 ...

  7. jQuery UI Datepicker使用介绍

    本博客使用Markdown编辑器编写 在企业级web开发过程中,日历控件和图表控件是使用最多的2中第三方组件.jQuery UI带的Datepicker,日历控件能满足大多数场景开发需要.本文就主要讨 ...

  8. canvas学习和面向对象(二)

    Canvas 学习(二) 上一篇Canvas 学习(一)中我是用canvas绘制了一些基本和组合的图形. 现在开始绘制图片和动画帧,以及面向对象的升级版本. 还是一样,看代码,所有的代码都托管在git ...

  9. Hive Tutorial(上)(Hive 入门指导)

    用户指导 Hive 指导 Hive指导 概念 Hive是什么 Hive不是什么 获得和开始 数据单元 类型系统 内置操作符和方法 语言性能 用法和例子(在<下>里面) 概念 Hive是什么 ...

  10. unbuntu14.04 安装nginx配置

    记录一下linux下安装nginx的所需要的配置. 首先从 nginx官网 下载所需要的版本,复制链接,执行 wget http://nginx.org/download/nginx-1.8.0.ta ...