起因

  开玩笑说“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. "org.eclipse.wst.validation" has been removed 导入maven 项目出错。

    在谷歌中找到解决方案: 右键关闭项目,在打开,将项目刷新,选中项目右键----->Maven4myeclipse------->Update maven project 错误消失. 若还有 ...

  2. VS2013常用快捷键你敢不会?

    F1 帮助文档 F5 运行 F12 跳转到定义 F11 单步调试 Shift+F5 停止调试 Ctrl+滚轮 放大缩小当前视图 Ctrl+L 删除当前行 Ctrl+K,Ctrl+C 注释选中代码 Ct ...

  3. LINQ系列:LINQ to SQL Select查询

    1. 查询全部字段 using (NorthwindContext context = new NorthwindContext()) { var expr = context.Products; f ...

  4. [转]自己写PHP扩展之创建一个类

    原文:http://www.imsiren.com/archives/572 比如我们要创建一个类..PHP代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...

  5. ASP.NET Core的配置(3): 将配置绑定为对象[上篇]

    出于编程上的便利,我们通常不会直接利用ConfigurationBuilder创建的Configuration对象读取某个单一配置项的值,而是倾向于将一组相关的配置绑定为一个对象,我们将后者称为Opt ...

  6. 在ubuntu server中安装和配置docker

    经过一段时间针对不同版本的学习,现在总结当前最新的安装配置过程(应该也是比较简单的) 如果不清楚什么是docker,请参考 https://www.docker.com/ 准备工作 建议在安装之前运行 ...

  7. [C#]浅析ref、out参数

    转载:http://www.cnblogs.com/vd630/p/4601919.html#top 按引用传递的参数算是C#与很多其他语言相比的一大特色,想要深入理解这一概念应该说不是一件容易的事, ...

  8. Stackoverflow/dapper的Dapper-Extensions用法(一)

    Dapper-Extensions Dapper Extensions is a small library that complements Dapper by adding basic CRUD ...

  9. 软件工程-构建之法 理解C#一小段程序

    一.前言 老师给出的要求: 阅读下面程序,请回答如下问题: 问题1:这个程序要找的是符合什么条件的数? 问题2:这样的数存在么?符合这一条件的最小的数是什么? 问题3:在电脑上运行这一程序,你估计多长 ...

  10. 机器学习 1 linear regression 作业(二)

    这个线性回归的作业需要上传到https://inclass.kaggle.com/c/ml2016-pm2-5-prediction 上面,这是一个kaggle比赛的网站.第一次接触听说这个东西,恰好 ...