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诞生记-原理与机制
一.看似偶然的东西实际是必然会发生的 我大学时候在图书馆翻过一本很破旧的书,讲生物理论的,主要内容就是探讨生命的产生是偶然还是必然.里面很多亚里士多德都看不懂的公式计算什么的,还有模拟原始地球环境出现 ...
随机推荐
- IOS系列swift语言之课时二
今天我们要讲的就是函数[对于函数,在最后面还有几道题,喜欢的博友可以看了自己做一下,和我交流一下] 当然这与我们的c语言还是有一定的共同之处的,对于有一些c语言或者是java基础的童鞋,我觉得是很容易 ...
- Tips for newbie to read source code
This post is first posted on my WeChat public account: GeekArtT Reading source code is always one bi ...
- myeclipse转到函数定义的方法去
转到函数的定义CTRl+鼠标左击 myeclipse自动补全的快捷键 alt+/
- 字典 Key值转换为数组
public static string[] GetCategories() { Dictionary<string, int> itemMap = new Dictionary<s ...
- Windows phone重写返回键
protected override void OnBackKeyPress(System.ComponentModel.CancelEventArgs e) {//需要设置这个属性 e.Cancel ...
- 深入学习jQuery元素尺寸和位置操作
× 目录 [1]尺寸设置 [2]位置设置 前面的话 对于javascript来说,元素尺寸有scroll.offset.client三大属性,以及一个强大的getBoundingClientRect( ...
- Android的PopWindow动画实现
转载博客:http://www.open-open.com/lib/view/open1423626956186.html 1.实现步骤 1.主布局activity_main.xml <Rela ...
- SubSonic3.0.0.4.2源码包与调用Dll
================================================================ 名 称:SubSonic插件版 本:3.0.0.4.2最后 ...
- JavaScript框架设计(四) 字符串选择器(选择器模块结束)
JavaScript框架设计(四) 字符串选择器(选择器模块结束) 经过前面JavaScript框架设计(三) push兼容性和选择器上下文的铺垫,实现了在某一元素下寻找,现在终于进入了字符串选择器 ...
- MySQL的多存储引擎架构
支持多种存储引擎是众所周知的MySQL特性,也是MySQL架构的关键优势之一.如果能够理解MySQL Server与存储引擎之间是怎样通过API交互的,将大大有利于理解MySQL的核心基础架构.本文将 ...