当前位置: 首页 > 科技观察

在Swift中使用JavaScript的方法和技巧

时间:2023-03-17 12:31:05 科技观察

本文的作者NateCook是一名独立的Web和移动应用程序开发人员,是Mattt之后NSHipster的主要维护者,也是一位非常知名和活跃的Swift博主,同时也是支持自动生成Swift在线文档的SwiftDoc.org站点创建器。在这篇文章中,他介绍了在Swift中使用JavaScript的方法和技巧,对iOS和Web应用工程师有很大的实用价值。排名迅速飙升,从刚刚发布时的第68位跃升至第22位,Objective-C依然位居第10位,而JavaScript则凭借在iOS平台的原生体验成为了年度最热门的编程语言。早在2013年,苹果发布的OSXMavericks和iOS7两大系统就加入了JavaScriptCore框架,让开发者可以轻松、快速、安全地使用JavaScript语言编写应用程序。不管是好是坏,JavaScript的霸主地位已经成为事实。开发者趋之若鹜,JS工具资源层出不穷,OSX、iOS系统的高速虚拟机也百花齐放。JSContext/JSValueJSContext是JavaScript代码的运行环境。上下文是执行JavaScript代码的环境,也称为作用域。在浏览器中运行JavaScript代码时,JSContext相当于一个窗口,可以方便的执行创建变量、计算甚至定义函数等JavaScript代码://Objective-CJSContext*context=[[JSContextalloc]init];[contextevaluateScript:@"varnum=5+5"];[contextevaluateScript:@"varnames=['Grace','Ada','Margaret']"];[contextevaluateScript:@"vartriple=function(value){returnvalue*3}"];JSValue*tripleNum=[contextevaluateScript:@"triple(num)"];//Swiftletcontext=JSContext()context.evaluateScript("varnum=5+5")context.evaluateScript("varnames=['Grace','Ada','Margaret']")context.evaluateScript("vartriple=function(value){returnvalue*3}")lettripleNum:JSValue=context.evaluateScript("triple(num)")像JavaScript这样的动态语言需要动态类型(DynamicType),所以如第一行代码所示,不同的值在JSContext中被封装在JSValue对象中,包括字符串、数字、数组、函数等,甚至还有Error、null和undefined。JSValue包含一系列获取UnderlyingValue的方法,如下表所示:获取上例中的tripleNum值,使用对应的方法即可://Objective-CNSLog(@"Tripled:%d",[tripleNumtoInt32]);//Tripled:30//Swiftprintln("Tripled:\(tripleNum.toInt32())")//Tripled:30下标值(SubscriptingValues)通过在JSContext和JSValue实例中使用下标符号使得很容易获取上下文中已经存在的值。其中,JSContext只能将字符串下标放入对象和数组中,而JSValue可以是字符串或整数下标。//Objective-CJSValue*names=context[@"names"];JSValue*initialName=names[0];NSLog(@"Thefirstname:%@",[initialNametoString]);//Thefirstname:Grace//Swiftletnames=context.objectForKeyedSubscript("names")letinitialName=names.objectAtIndexedSubscript(0)println("Thefirstname:\(initialName.toString())")//Thefirstname:Grace毕竟Swift语言诞生不久,所以不能像Objective-C那样随意使用下标符号,目前Swift的方法只能实现objectAtKeyedSubscript()和objectAtIndexedSubscript()等下标。调用函数(CallingFunctions)我们可以使用Foundation类作为参数,直接从Objective-C/Swift代码中调用封装在JSValue中的JavaScript函数。在这里,JavaScriptCore再次起到了凝聚作用。//Objective-CJSValue*tripleFunction=context[@"triple"];JSValue*result=[tripleFunctioncallWithArguments:@[@5]];NSLog(@"Fivetripled:%d",[resulttoInt32]);//SwiftlettripleFunction=context.objectForKeyedSubscript("triple")letresult=tripleFunction.callWithArguments([5])println("Fivetripled:\(result.toInt32())")异常处理(ExceptionHandling)JSContext还有一个绝活,这是设置上下文环境中的exceptionHandler属性,可以检查和记录发生的语法、类型和运行时错误。exceptionHandler是一个回调处理器,主要接收JSContext的引用来处理异常。//Objective-Ccontext.exceptionHandler=^(JSContext*context,JSValue*exception){NSLog(@"JSError:%@",exception);};[contextevaluateScript:@"functionmultiply(value1,value2){returnvalue1*value2"];//JSError:SyntaxError:Unexpectedendofscript//Swiftcontext.exceptionHandler={context,exceptioninprintln("JSError:\(exception)")}context.evaluateScript("functionmultiply(value1,value2){returnvalue1*value2")//JSError:SyntaxError:UnexpectedendofscriptJavaScriptfunctioncall知道了如何从JavaScript环境中获取不同的值并调用函数,那么反过来,如何在JavaScript环境中获取Objective-C或Swift定义的自定义对象和方法呢?从JSContext获取本地客户端代码,主要有两种方式,Blocks和JSExport协议。Blocks(块)在JSContext中,如果给Objective-C代码块分配了标识符,JavaScriptCore会自动将其封装在一个JavaScript函数中,这样在JavaScript上使用Foundation和Cocoa类会更方便——这一点再次验证它实现了JavaScriptCore强大的连接功能。现在CFStringTransform也能在JavaScript上使用了,如下所示://Objective-Ccontext[@"simplifyString"]=^(NSString*input){NSMutableString*mutableString=[inputmutableCopy];CFStringTransform((__bridgeCFMutableStringRef)mutableString,NULL,kCFStringTransformToLatin,NO);CFStringTransform((__bridgeCFMutableStringRef)mutableString,NULL,kCFStringTransformStripCombiningMarks,NO);returnmutableString;};NSLog(@"%@",[contextevaluateScript:@"simplifyString('??????!')"]);//SwiftletsimplifyString:@objc_blockString->String={inputinvarmutableString=NSMutableString(string:input)asCFMutableStringRefCFStringTransform(mutableString,nil,kCFStringTransformToLatin,Boolean(0))CFStringTransform(mutableString,nil,kCFStringTransformStripCombiningMarks,Boolean(0))returnconmutableString}(unsafeBitCast(simplifyString,AnyObject.self),forKeyedSubscript:"simplifyString")println(context.evaluateScript("simplifyString('?????!')"))//annyeonghasaeyo!需要注意的是,Swift的speedbump只适用于Objective-C的blocks,对于Swift的闭包是没有用的。要在JSContext中使用闭包,有两个步骤:一个是使用@objc_block声明,另一个是使用Swift的关节白化unsafeBitCast()函数转换为AnyObject。内存管理(MemoryManagement)代码块可以捕获变量引用,JSContext所有变量的强引用都保存在JSContext中,注意避免循环强引用。此外,不要在代码块中捕获JSContext或任何JSValues。推荐使用[JSContextcurrentContext]获取当前的Context对象,根据具体需要将该值作为参数传入block。JSExport协议在JSExport协议的帮助下,自定义对象也可以在JavaScript上使用。在JSExport协议中声明的实例方法和类方法,无论属性如何,都可以自动与JavaScript交互。具体的实践过程将在文章后面介绍。JavaScriptCore实践我们可以通过一些示例更好地理解如何使用上述技术。首先定义一个符合JSExport子协议PersonJSExport的Person模型,然后使用JavaScript创建并填充JSON中的实例。有了整个JVM,NSJSONSerialization还做了什么?PersonJSExports和PersonPerson类实现的PersonJSExports协议指定了可用的JavaScript属性,在创建的时候,类方法是必不可少的,因为JavaScriptCore不适合初始化转换,我们不能像原生JavaScript类型那样使用varperson=newPerson()。//Objective-C//inPerson.h----------------@classPerson;@protocolPersonJSExports@property(nonatomic,copy)NSString*firstName;@property(nonatomic,copy)NSString*lastName;@propertyNSIntegerageToday;-(NSString*)getFullName;//createandreturnanewPersoninstancewith`firstName`and`lastName`+(instancetype)createWithFirstName:(NSString*)firstNamelastName:(NSString*)lastName;@end@interfacePerson:NSObject@property(nonatomic,copy)NSString*firstName;@property(nonatomic,copy)NSString*lastName;@propertyNSIntegerageToday;@end//inPerson.m----------------@implementationPerson-(NSString*)getFullName{return[NSStringstringWithFormat:@"%@%@",self.firstName,self.lastName];}+(instancetype)createWithFirstName:(NSString*)firstNamelastName:(NSString*)lastName{Person*person=[[Personalloc]init];person.firstName=firstName;person.lastName=lastName;returnperson;}@end//Swift//Customprotocolmustbedeclaredwith`@objc`@objcprotocolPersonJSExports:JSExport{varfirstName:String{getset}varlastName:String{getset}varbirthYear:NSNumber?{getset}funcgetFullName()->String///createandreturnanewPersoninstancewith`firstName`and`lastName`classfunccreateWithFirstName(firstName:String,lastName:String)->Person}//Customclassmustinheritfrom`NSObject`@objcclassPerson:NSObject,PersonJSExports{//propertiesmustbedeclaredas`dynamic`dynamicvarfirstName:StringdynamicvarlastName:StringdynamicvarbirthYear:NSNumber?init(firstName:String,lastName:String){self.firstName=firstNameself.lastName=NamelastName}ithfunccreateWlastName:String)->Person{returnPerson(firstName:firstName,lastName:lastName)}funcgetFullName()->String{return"\(firstName)\(lastName)"}}配置JSContext创建Person类后,需要先Export到JavaScript环境,还需要导入MustacheJS库,才能将模板应用到Person对象上//Objective-C//exportPersonclasscontext[@"Person"]=[Personclass];//loadMustache.jsNSString*mustacheJSString=[NSStringstringWithContentsOfFile:...encoding:NSUTF8StringEncodingerror:nil];[contextevaluateScript:mustacheJSString];//Swift//exportPersonclasscontext.setObject(Person.self,forKeyedSubscript:"Person")//loadMustache.jsifletmustacheJSString=String(contentsOfFile:...,encoding:NSUTF8StringEncoding,error:nil){context.evaluateScript(mustacheJSString)}JavaScriptdata&process下面简单列举一个JSON例子,使用JSON创建一个新的Person实例。注意:JavaScriptCore实现了Objective-C/Swift方法名和JavaScript代码的交互。因为JavaScript没有命名参数,所以任何额外的参数名称都是以驼峰命名法附加到函数名称。在此示例中,Objective-C方法createWithFirstName:lastName:在JavaScript中变为createWithFirstNameLastName()。//JSON[{"first":"Grace","last":"Hopper","year":1906},{"first":"Ada","last":"Lovelace","year":1815},{"first":"Margaret","last":"Hamilton","year":1936}]//JavaScriptvarloadPeopleFromJSON=function(jsonString){vardata=JSON.parse(jsonString);varpeople=[];for(i=0;i