Dog_Hybird的诞生
起因
开玩笑说“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的诞生的更多相关文章
- JSONP的诞生、原理及应用实例
问题: 页面中有一个按钮,点击之后会更新网页中的一个盒子的内容. Ajax可以很容易的满足这种无须刷新整个页面就可以实现数据变换的需求. 但是,Ajax有一个缺点,就是他不允许跨域请求资源. 如果我的 ...
- Lambda表达式的诞生过程
这是一篇很经典的文章,解决了工作中一些使用过但是又不太明白的知识点,今天终于弄明白了.花了一晚上重新整的,坚决要分享出来!!! 那得从很久很久以前说起了,记得那个时候... 懵懂的记得从前有个叫委托的 ...
- git的诞生
Git的诞生 很多人都知道,Linus在1991年创建了开源的Linux,从此,Linux系统不断发展,已经成为最大的服务器系统软件了. Linus虽然创建了Linux,但Linux的壮大是靠全世 ...
- Selenium2学习-042-Selenium3启动Firefox Version 48.x浏览器(ff 原生 geckodriver 诞生)
今天又被坑了一把,不知谁把 Slave 机的火狐浏览器版本升级为了 48 的版本,导致网页自动化测试脚本无法启动火狐的浏览器,相关的网页自动化脚本全线飘红(可惜不是股票,哈哈哈...),报版本不兼容的 ...
- 3.Git的诞生和其分布式的优点
Git的诞生 省略了,喜欢的可以看百度. 分布式的优点 先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完 ...
- [转载]jQuery诞生记-原理与机制
by zhangxinxu from http://www.zhangxinxu.com本文地址:http://www.zhangxinxu.com/wordpress/?p=3520 一.看似偶然的 ...
- Zygote进程【3】——SystemServer的诞生
在ZygoteInit的main()方法中做了几件大事,其中一件便是启动Systemserver进程,代码如下: @/frameworks/base/core/Java/com/Android/int ...
- 【Android测试】【随笔】性能采集工具——小松鼠诞生记
◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/4945066.html 起因 去年刚加入TX的时候,我便接手 ...
- jQuery诞生记-原理与机制
一.看似偶然的东西实际是必然会发生的 我大学时候在图书馆翻过一本很破旧的书,讲生物理论的,主要内容就是探讨生命的产生是偶然还是必然.里面很多亚里士多德都看不懂的公式计算什么的,还有模拟原始地球环境出现 ...
随机推荐
- JDK安装源码src和doc
(1)src 打开JDK的安装目录如(C:\Program Files\Java\jdk1.8.0_91)有一个src.zip的压缩文件,这个压缩文件里就是源码. mkdir src copy src ...
- XML实体引用
在 XML 中,一些字符拥有特殊的意义. 如果你把字符 "<" 放在 XML 元素中,会发生错误,这是因为解析器会把它当作新元素的开始. 这样会产生 XML 错误: < ...
- The Installation and Compilation of OpenCASCADE
OpenCASCADE的编译 The Installation and Compilation of OpenCASCADE eryar@163.com 一. 安装OpenCASCADE 可以从Ope ...
- Win10系统菜单打不开问题的解决,难道是Win10的一个Bug ?
Win10左下角菜单打不开,好痛苦,点击右下角的时间也没反应,各种不爽,折磨了我好几天,重装又不忍心,实在费劲,一堆开发环境要安装,上网找了很多方法都不适用.今天偶然解决了,仔细想了下,难道是Win1 ...
- Mac OS apache php配置
1.进入Apache配置文件sudo vi /etc/apache2/httpd.conf 找到#LoadModule php5_module libexec/apache2/libphp5.s ...
- BFC之清除浮动篇&clear
我们在日常代码生活中,或多或少会利用浮动来布局,例如导航布局,如下图所示: 但是,我们在实现的时候,经常会遇到父元素“塌陷”以及清除浮动问题.例如 <!DOCTYPE html> < ...
- 异步Promise实现
简介 异步回调的书写往往打乱了正常流的书写方式,在ECMAScript 6中实现了标准的Promise API,旨在 解决控制回调流程的问题. 简单的实现了Promise API: (function ...
- Service实现文件下载
首先在Activity中声明Intent对象,启动Service: //生成Intent对象 Intent intent = new Intent(); //将文件名对象存入到intent对象当中 i ...
- 使用OAuth打造webapi认证服务供自己的客户端使用
一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorization(授权),而不是Authentication(认证). ...
- Apache 创建虚拟主机目录和设置默认访问页面
虚拟主机 (Virtual Host) 是在同一台机器搭建属于不同域名或者基于不同 IP 的多个网站服务的技术. 可以为运行在同一物理机器上的各个网站指配不同的 IP 和端口, 也可让多个网站拥有不同 ...