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判断需要触发的事件

 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: 0, 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[0]
19                     let newPath = ##在document目录的路径##
20                     let fileData = NSFileManager.defaultManager().contentsAtPath(documentPath + "/\(newPath).\(type)")
21                     if fileData?.length > 0 {
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 ?? 0
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,写错了资源会以纯文本的形式读取出来。刚入前端坑伤不起啊。

 

 

  

 

posted on 2016-05-27 21:25  Nil_Cy  阅读(4884)  评论(4编辑  收藏  举报