iOS开发-javaScript交互

来源:互联网 发布:推女郎吧最新域名 编辑:程序博客网 时间:2024/05/01 21:00

前言


当前混合开发模式迎来了前所未有的发展,跨平台开发、热更新等优点决定了这种模式的重要地位。虽然前端界面在交互、动效等多方面距离原生应用还有差距,但毫无疑问混合开发只会被越来越多的公司接受。在iOS中,混合开发模式被分为两个时代,分别是iOS7之前的坑爹时代与之后的黄金时代,其分割代表为JavaScriptCore框架



坑爹时代


作为完美避开iOS7之前版本的幸运儿,我只能从某位前辈的口中得知那悲惨的岁月。作为那个年代唯一能与前端界面交互的手段就是UIWebView,先不说它自身的内存泄露缺陷,下面是一段前辈写过的代码:


-(BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType

{

    NSString * address = request.URL.absoluteString;

    for(NSString * blackin_blackList){

        if([address containsString: black]){

            returnNO;

        }

    }

    for(NSString * eventin_eventList){

        if([address containsString: event]){

            SELcallback = NSSelectorFromString(_callbacks[event]);

            [self performSelector: callback];

            return[event containsString:@"shouldOpen=1"];

        }

    }

    returnYES;

}


在那个年代,前辈的小伙伴们把前端事件的触发条件设置为链接跳转,然后通过链接中的关键字符来判断处理操作。为此,需要定义好些个数据集合来存储这些关键字符的处理操作。如果遇到应用和前端交换交互数据的时候,那一长串的参数字符全部拼接在请求地址里,想想也是醉了。另外的交互方法就是通过stringByEvaluatingJavaScriptFromString方法来执行js代码。


JavaScriptCore


JavaScriptCore是一套用来对JS代码进行解析和提供执行环境的开源框架,极大的简化了我们的交互过程。下面从项目和JS代码相互调用的两个不同操作介绍其中相对应的方法


项目调用JS代码


  • JSContext

    一个JSContext对象是JavaScript运行的全局环境对象,它提供了代码运行和注册方法接口的服务。下面的代码就创建了一个JSContext对象,并且定义了一部分的JS代码加入到执行环境中


letcontext = JSContext()

context.evaluateScript(" var age = 22 ")

context.evaluateScript(" var name = 'SindriLin' ")

context.evaluateScript(" var birth = 1993-01-01 ")

context.evaluateScript(" var createPerson =

    function(age, name, birth)

    {

        return {'age': age, 'name': name, 'birth': birth}

    } ")

context.evaluateScript(" var codeDescription = 'The code create three value and a function to create a dictionary stored person information' ")


此外,在JS代码执行过程中,可能会出现语法错误等多种错误,通过下面的代码可以对这些错误进行处理


context?.exceptionHandler = {context,exceptionin

      print("Java Script Run Error: \(exception)")

}


  • JSValue

    JSValue是所有JSContext操作后返回的值,包装了几乎所有的数据类型,包括错误和IMP指针等。在JSValue类结构中存在多个toXXXX命名的方法转换成iOS数据类型以及call方法来调用方法。下面的代码从JSContext环境中获取已存在的部分变量,并且执行创建一个存储person信息的字典


letage = context?.objectForKeyedSubscript("age")

letname = context?.objectForKeyedSubscript("name")

letbirth = context?.objectForKeyedSubscript("birth")

letcreateFunction = context?.objectForKeyedSubscript("createPerson")

letcodeDescription = context?.objectForKeyedSubscript("codeDescription")

letperson = createFunction.call(withArguments:[age.toInt32(),name.toString(),birth.toString()])

 

letpersonInfo = "name:\(person["name"]) age: \(person["age"]andbirth:\(person["birth"])"

print("The javaScript code description: \(codeDescription.toString())")

print("The created person \(personInfo) ")


通过上面的例子,我们可以看到,只要了解到JS代码中我们需要调用的方法信息,通过JSContext + JSValue的方式我们就能轻松的在项目中调用前端界面的方法,而不再需要拼接长串参数字符通过链接地址传递给前端界面


JS调用项目代码


JavaScript访问我们代码中的对象以及方法有两种方式:Blocks和JSExport。


  • Blocks

    自定义的block代码可以通过JSContext转换成JS代码中的函数指针调用,这里存在一个坑就是Swift中的闭包无法完成这样的类型转换,因此这种方式的操作流程在Swift中是这样的:Closure -> block -> function pointer。在闭包转成block的这一过程中,需要使用一个重要的关键符@convention


let stringConvert:@convention(block)(String)->String = {

    letpinyin = NSMutableString(string:$0)asCFMutableString

    CFStringTransform(pinyin,nil,kCFStringTransformToLatin,false)

    CFStringTransform(pinyin,nil,kCFStringTransformStripCombiningMarks,false)

    returnpinyin as String

}      

 

letconvertObjc = unsafeBitCast(stringConvert, to: AnyObject.self)

context?.setObject(convertObjc, forKeyedSubscript:"convertFunc")

letconvertFunc = context?.objectForKeyedSubscript("convertFunc")

print("林欣达的拼音是\(convertFunc.call(withArguments:["林欣达"]).toString())")


这时候,只要前端在JS的按钮点击代码中调用convertFunc()这句代码就会执行这个closure中的代码。使用这种方式要注意由于闭包的捕获特性,有可能会导致你的JSContext对象被引用而无法被释放,使用JSContext.current()获取当前上下文来解决引用问题


  • JSExport

    在JS中调用iOS方法的时候,通过调用JSExport的派生协议方法来实现。所有派生协议的方法会自动提供给JavaScript代码使用,这个在下面的demo中可以看到


实战


在本文demo中我写了一段JS代码,下面放出这段代码以及运行效果。其中要注意的是按钮的onclik表示按钮点击的响应事件:


<!DOCTYPE html>

<html>

    <head>

        <metacharset="UTF-8">

    </head>

    <body>

        <divstyle="margin-top: 20px">

            <h2align="center"style="color:#ff0000">JS与iOS交互</h2>

            <inputtype="button"value="点击后切换控制器的背景颜色"onclick="sindrilin.call()">

        </div>

        <divstyle="color:#7BBDE5">

            <br />

            <br />

            账户:

            <inputid="account"type="text">

            <br />

            密码:

            <inputid="password"type="password">

        </div>

        <div>

            <inputtype="button"value="登录"onclick="login()">

        </div>

 

        <script>

 

            varlogin = function()

            {

                account = document.getElementById("account")

                password = document.getElementById("password")

                varaccountInfo = JSON.stringify({"account":account.value,"password":password.value});

                sindrilin.login(accountInfo);

            }

 

            varalertFromIOS = function(message)

            {

                    alert(message)

            }

 

        </script>

    </body>

</html>



首先我们需要加载这个HTML文件,然后获取代码运行的全局环境对象。基本上在所有的HTML格式文件中,获取环境对象的keyPath都是一样的:


letjsPath = Bundle.main().pathForResource("interaction", ofType:"html")

webView.loadRequest(URLRequest(url: URL(fileURLWithPath: jsPath!)))

interactionContext = webView.value(forKeyPath:"documentView.webView.mainFrame.javaScriptContext")as?JSContext

interactionContext?.exceptionHandler = {

    print("Interaction Error: \($1?.toString())")

}


对照HTML代码,最上面的按钮点击之后会调用一个sindrilin.call()的方法,这个方法最终要由我们的控制器来进行处理。我们可以把这个字符串分成类似Target-Action机制的两部分,前者sindrilin表示响应者,后面call()表示响应事件。其中Target的设置方式如下


interactionContext?.setObject(self,forKeyedSubscript:"sindrilin")


响应者已经有了,那么响应事件也要我们实现代码,这里就需要用到JSExport协议了。所有这种类似Target-Action的事件触发都会通过这个协议获取方法实现,因此我们需要自定义响应协议以及响应事件。对于有参数的方法我们需要用@objc(name)的方式给方法起OC式的方法名,才能保证能被正确调用响应:


@objcprotocol LXDInteractionExport: JSExport{

    func call()                                    ///响应sindrilin.call()

    @objc(login:)func login(accountInfo: String)  ///响应sindrilin.login(accountInfo)

}

 

extension ViewController: LXDInteractionExport{

    func call(){

        print("call from html button clicked")

        view.backgroundColor = UIColor(red: CGFloat(arc4random() % 256) / 255, green: CGFloat(arc4random() % 256) / 255, blue: CGFloat(arc4random() % 256) / 255, alpha:1)

    }

 

    func login(accountInfo: String){

        do{

            iflet JSON:[String: String] = tryJSONSerialization.jsonObject(with: accountInfo.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions())as?[String: String]{

                print("JSON:\(JSON)")

                letalert = interactionContext?.objectForKeyedSubscript("alertFromIOS")

                letmessage = "The alert from javascript call\naccount: \(JSON["account"]) and password: \(JSON["password"])"

                _ = alert?.call(withArguments:[message])

            }

        }catch{

            print("Error:\(error)")

        }      

    }

}


用户在前端界面输入账户和密码信息之后点击登录就会调用login(accountInfo: String)方法,将用户名和密码拼凑成JSON字符串传递过来。在响应方法中我解析获取对应字段的用户信息,并且组转成新的字符串调用JS的弹窗函数弹出响应。

demo下载:https://github.com/JustKeepRunning/LXDJavaScriptCoreDemo


0 0