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

     

      

  • 相关阅读:
    LeetCode 1275. 找出井字棋的获胜者 Find Winner on a Tic Tac Toe Game
    LeetCode 307. 区域和检索
    LeetCode 1271 十六进制魔术数字 Hexspeak
    秋实大哥与花 线段树模板
    AcWing 835. Trie字符串统计
    Leetcode 216. 组合总和 III
    Mybatis 示例之 复杂(complex)属性(property)
    Mybatis 示例之 复杂(complex)属性(property)
    Mybatis 高级结果映射 ResultMap Association Collection
    Mybatis 高级结果映射 ResultMap Association Collection
  • 原文地址:https://www.cnblogs.com/nildog/p/5536081.html
Copyright © 2011-2022 走看看