zoukankan      html  css  js  c++  java
  • 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,写错了资源会以纯文本的形式读取出来。刚入前端坑伤不起啊。

     

      

  • 相关阅读:
    mysql系列:加深对脏读、脏写、可重复读、幻读的理解
    PHP命令空间namespace及use的用法实践总结
    欲望与自制力
    如何被动不费太大力气的提升自己
    经常用到的常识
    Html单选按钮自定义样式
    连连看算法实现 —— 分治实现
    ctime,atime,mtime
    C++实现二叉搜索树的插入,删除
    进程调度算法总结
  • 原文地址:https://www.cnblogs.com/nildog/p/5536081.html
Copyright © 2011-2022 走看看