起因

  开玩笑说“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判断需要触发的事件

  1. func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
  2. if let requestStr = request.URL?.absoluteString {
  3. if requestStr.hasPrefix("hybrid://") {
  4.  
  5. let function XXX
  6. let args = XXX
  7. let callBackId = XXX
  8.  
  9. self.handleEvent(function, args: args, callbackID: callBackId)
  10. }
  11. }
  12. return true
  13. }

  方法注入

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

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

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

  

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

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

  @convention(block) String -> Bool

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

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

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

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

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

 请求、回调方式

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

  1. func demoApi(args: [String: AnyObject], callbackID: String) {
  2. self.callBack(args, errno: , msg: "success", callback: callbackID)
  3. }
  4.  
  5. func callBack(data:AnyObject, errno: Int, msg: String, callback: String) {
  6. let data = ["data": data,
  7. "errno": errno,
  8. "msg": msg,
  9. "callback": callback]
  10. let dataString = self.toJSONString(data)
  11. self.myWebView.stringByEvaluatingJavaScriptFromString(self.HybirdEvent + "(\(dataString));")
  12. }

  参数解释:

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

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

  msg        ---> 描述

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

  

 本地资源路径

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

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

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

  

  

 页面跳转

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

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

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

  1. enum AnimateType {
  2. case Normal
  3. case Push
  4. case Pop
  5. }
  6.  
  7. func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  8. if operation == UINavigationControllerOperation.Push {
  9. if self.animateType == .Pop {
  10. return HybirdTransionPush()//这个为自定义的push动画,表现为pop样式
  11. }
  12. else {
  13. return nil
  14. }
  15. } else {
  16. return nil
  17. }
  18. }

  

加载本地资源

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

通过NSURLProtocol拦截请求并处理

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

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

  1. override class func canInitWithRequest(request: NSURLRequest) -> Bool {
  2. //如果被标记为已处理 直接跳过
  3. if let hasHandled = NSURLProtocol.propertyForKey(DogHybirdURLProtocolHandled, inRequest: request) as? Bool where hasHandled == true {
  4. print("重复的url == \(request.URL?.absoluteString)")
  5. return false
  6. }
  7. if let url = request.URL?.absoluteString {
  8. if url.hasPrefix(webAppBaseUrl) {
  9. //从请求中解析出path 和 type 然后在 NSBundle.mainBundle() 和 NSSearchPathDirectory.DocumentDirectory 中查找
  10. if let zipPath = NSBundle.mainBundle().pathForResource(path, ofType: type) {
  11. if types.contains(type) {
  12. //为需要处理的类型 types = ["html","js","css","jpg","png"]
  13. return true
  14. }
  15. }
  16. else {
  17. let documentPaths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
  18. let documentPath = documentPaths[]
  19. let newPath = ##在document目录的路径##
  20. let fileData = NSFileManager.defaultManager().contentsAtPath(documentPath + "/\(newPath).\(type)")
  21. if fileData?.length > {
  22. return true
  23. }
  24. }
  25. }
  26. }
  27. return false
  28. }

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

  1. override func startLoading() {
  2. //标记请求 防止重复处理
  3. let mutableReqeust: NSMutableURLRequest = self.request.mutableCopy() as! NSMutableURLRequest
  4. NSURLProtocol.setProperty(true, forKey: DogHybirdURLProtocolHandled, inRequest: mutableReqeust)
  5. dispatch_async(dispatch_get_main_queue()) {
  6. if let url = self.request.URL?.absoluteString {
  7. if url.hasPrefix(webAppBaseUrl) {
  8. let path = ##请求path##
  9. let type = ##请求type##
  10. let client: NSURLProtocolClient = self.client!
  11.  
  12. var typeString = ""
  13. switch type {
  14. case "html":
  15. typeString = "text/html"
  16. break
  17. case "js":
  18. typeString = "application/javascript"
  19. break
  20. case "css":
  21. typeString = "text/css"
  22. break
  23. case "jpg":
  24. typeString = "image/jpeg"
  25. break
  26. case "png":
  27. typeString = "image/png"
  28. break
  29. default:
  30. break
  31. }
  32. let localUrl = ##先在DocumentDirectory中查找,如果不存在再在NSBundle.mainBundle()中查找##
  33. let fileData = NSData(contentsOfFile: localUrl)
  34. let url = NSURL(fileURLWithPath: localUrl)
  35. let dataLength = fileData?.length ??
  36. let response = NSURLResponse(URL: url, MIMEType: typeString, expectedContentLength: dataLength, textEncodingName: "UTF-8")
  37. client.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
  38. client.URLProtocol(self, didLoadData: fileData!)
  39. client.URLProtocolDidFinishLoading(self)
  40.  
  41. }
  42. else {
  43. print(">>>>> url不符合规则 <<<<<")
  44. }
  45. }
  46. else {
  47. print(">>>>> url字符串获取失败 <<<<<")
  48. }
  49. }
  50. }

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

  1.  

  

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. bootstrap与Select2使用小结

    这个select2组件的功能确实很强大,可以将图片放入到select里面随着文字一起显示. 组件的下载地址以及API说明地址: 1.Select2使用示例地址:https://select2.gith ...

  2. Apk去掉签名以及重新签名的方法

    Android开发中很重要的一部就是用自己的密钥给Apk文件签名,不经过签名的Apk文件一般是无法安装的,就算装了最后也是失败. 网上流传的"勾选允许安装未知来源的应用"其实跟签不 ...

  3. 【hadoop2.2(yarn)】基于yarn成功执行分布式map-reduce,记录问题解决过程。

    hadoop2.x改进了hadoop1.x的架构, 具体yarn如何工作以及改进了什么可以在网上学, 这里仅记录我个人搭建的问题和理解,希望能帮助遇到困难的朋友. 在开始前,必须了解yarn版本的ma ...

  4. SQL Server 堆表行存储大小(Record Size)

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 堆表行记录存储格式(Heap) 案例分析(Case) 参考文献(References) 二.背 ...

  5. sizzle分析记录:分解流程

    <form> <label>Name:</label> <input name="name" /> <fieldset> ...

  6. error: failed to push some refs to '......'解决方案

    由于是初学者,又因为最近项目需要,只好边学边用吧. 在使用  “git push origin master” 时出现了以下问题 网上搜到的解决方案,可用: 先输入: git stash(用于暂存当前 ...

  7. TextView跑马灯效果

    转载:http://www.2cto.com/kf/201409/330658.html 一.只想让TextView显示一行,但是文字超过TextView的长度怎么办?在开头显示省略号 android ...

  8. VS 2015 Enterprise第二大坑

    前言 继上篇文章之后,你会继续跌进大坑,这个坑困扰我一上午,同时也会让你大跌眼镜,如果你遇到了,那么恭喜你提升自身能力和解决能力的时机到了,当然你可以通过本文继续少走不必要的弯路[我也是无意中发现的捷 ...

  9. 关于SubSonic3.0查询或更新时出现System.NullReferenceException异常的处理

    在调试程序时,同事发现添加记录时,出现了System.NullReferenceException异常 DictBase dict = new DictBase();    dict.DictCode ...

  10. iOS开发之使用CocoaPods更新第三方出现“target overrides the `OTHER_LDFLAGS`……”问题解决方案

    今天在自己的项目中用CocoaPods引入第三方SDWebImage的时候,出现了问题.当更新完毕后,在终端没太注意这个问题的提示,就直接使用SDWebImage了,在使用的时候一些方法的提示和头文件 ...