优化由来
开发中或多或少都会碰到网页中js与原生代码的交互内容,当然处理的方式也有很多种,比如拦截、JavaScriptCore
等进行内容的交互,而这次想记录的主要就通过JavaScriptCore
进行交互的方案。
如上图所示,通常通过JavaScriptCore
进行内容的交互主要分为三步:
- 继承
JSExport
协议,将需要交互的方法声明在协议里- 创建
JSContext
所对应的交互对象,该对象实现第一步中的协议方法- 通过
JSContext
进行对象的注入,完成JS调用方法的映射关系
所以实现下来就变成了如下的样子:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JSExportDelegate<JSExport>
- (void)requestData;
//分享到盆友圈
JSExportAs(shareWX,- (void)shareWX:(NSString *)imgurl withLink:(NSString *)link title:(NSString *)title desc:(NSString *)desc);
//分享到微信
JSExportAs(shareWXCircle,- (void)shareWXCircle:(NSString *)imgurl withLink:(NSString *)link title:(NSString *)title desc:(NSString *)desc);
//是否隐藏分享按钮
JSExportAs(hideShare, - (void)hideShare:(NSInteger)status);
//分享图片,保存至本地
JSExportAs(shareImage, - (void)shareImage:(NSString *)backGroundImg headerImg:(NSString *)headerImg nickName:(NSString *)nickName QRLink:(NSString *)qrLink);
@end
@interface JSExportApi : NSObject<JSExportDelegate>
@property (nonatomic,weak) JSContext *context;
@property (nonatomic,weak) UIWebView *webView;
@property (nonatomic,weak) GCCommonController *vc;
//请求的正文内容的ID
@property (nonatomic,copy) NSString *requestID;
//请求的正文的api接口
@property (nonatomic,copy) NSString *webContentApi;
//数据请求成功
@property (nonatomic,copy) void (^requestSuccessBlock)();
- (void)requestData;
@end
紧接着,在使用的时候通过如下的方式进行注入
- (void)fillJSContent{
//注入 js内容
JSContext *context = [self valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSExportApi *jsObject = [JSExportApi new];
jsObject.context = context;
jsObject.webView = self;
[context setObject:jsObject forKeyedSubscript:@"app"];
}
这样基本也就基本满足了相关的需求内容,如果按照这样的形式开发下去,会有很多问题,如下:
- 随着业务的增加,继承自
JSExport
的协议中声明的方法会越来越多,有些业务压根不需要的一些功能也一并加入到了交互中,臃肿问题很明显 - 由于业务代码的入侵,会使当前的交互模块与各个业务模块之间变得高度耦合,随着耦合性的增加,后期维护起来也会愈发麻烦
- 深受
js
方法定义的影响,后续各个方法的改动以及增加的成本变得很高
优化方案
显然这些问题会产生一个严重的后果,随着js
交互相关业务的增加,业务代码的入侵造成各个业务模块对交互模块的高度耦合,让项目的后续迭代以及维护变得愈发困难。对此,我们有必要设计一个关于js
与本地代码交互的通用模块,保证后续的交互的正常进行,并且当前的交互模块不与任何的业务模块存在耦合关系,有着类似于依赖倒置的设计原则(业务模块依赖js
交互模块,而js
交互模块与任何业务模块不产生任何依赖关系),同时要把后续迭代中的代码改动成本降到最低。基于这些特点,就有了js
交互的优化方案的方向。
如图所示,按照优化思想,需要将各个业务模块内容分离出将要设计的js
交互模块,对于模块的设计有着如下特点:
- 具有封装性,不受后续新增交互内容引入的影响
- 具有业务分流作用,将业务的处理分离到各个业务模块中处理
- 具有独立性,不存在业务模块的依赖关系
为了保证其封装性以及后续js
内容的引入让其不受影响,需要去设计一个统一的入口来处理各种交互内容的添加。如图:
如果能在js
的交互中统一的使用一个方法进行处理,那么实际上第一个封装性特点就满足了,所以此时有两种方案去设计这样一个入口处理方法:
- 在后台设计
js
交互的过程中让他们统一一个调用接口方法- 如果第一步协商不了的话,只能通过自己本地
js
代码注入转换来实现了
对于第二种方案,我们需要进行本地js
方法包装了,在设计方法包装的时候,我们需要注意以下几点:
- 对于js原始方法的调用转换成对统一入口处理方法
postMsg()
的调用 - 转换的最终的目的运行环境仍是js的运行环境,即
context
环境下 - js包装完成之后的注入时机
对于第二点需要强调一点的是,context
是js
运行的环境,也就是说在js
运行的时候,是通过context
环境中的代码进行执行的,所以注入的js代码实际上是之前js
定义的某些方法内容的实现的补充(context
中可能只存在方法的调用,但是没有方法实体,此时注入方法实体之后,context
就能够正常调用了)。所以对于第三点一定要保证js调用某个方法之前已经将该方法的实现注入到context
中才能够保证其调用的正常。
在设计方法包装的时候,我们需要注意以下几点
- 原始方法的名字的包装
- 原始方法的参数的包装
/*
例如JS调用一个app.showMessage()方法
本地设计的统一的入口方法为postMessage(methodName,parameter)
通过包装转换到统一的入口方法进行调用就变成了如下内容:
func showMessage(){
app.postMessage(methodName:'showMessage',parameter:{"":""});
}
所以重点放在了包装的过程上
*/
- (NSMutableString *)setupSignalJsMethod:(NSString *)kMethodName parameter:(NSString *)kParameter{
//在此拿到对应的参数来组装实现对应的js方法内容
NSMutableString *signalJsMethod = [@"" mutableCopy];
NSMutableString *parameterTransfrom = [@"" mutableCopy];
if (parameter.length > 0) {
NSArray *parameterArray = [kParameter componentsSeparatedByString:@","];
NSLog(@"参数数组:%@",parameterArray);
[parameterTransfrom appendString:@"{"];
NSLog(@"处理:%@",parameterArray);
for (NSString *tempParameter in parameterArray) {
NSLog(@"Linshi:%@",tempParameter);
[parameterTransfrom appendString:[NSString stringWithFormat:@"'%@':%@,",tempParameter,tempParameter]];
}
[parameterTransfrom replaceCharactersInRange:NSMakeRange(parameterTransfrom.length-1 , 1) withString:@""];
[parameterTransfrom appendString:@"}"];
NSLog(@"获取的参数:%@",parameterTransfrom);
}else{
parameterTransfrom = [@"{}" mutableCopy];
}
[signalJsMethod appendString:[NSString stringWithFormat:@"app.%@ = function(%@){\n",kMethodName,kParameter]];
[signalJsMethod appendString:[NSString stringWithFormat:@"\t app.postJSMessage({%@:'%@', %@:%@});",kJsMethodName,kMethodName,kJsMethodParameter,parameterTransfrom]];
[signalJsMethod appendString:@"\t}\n\n"];
return signalJsMethod;
}
由于正常函数的调用是通过函数名字与参数的内容进行调用的,所以针对于这两个必要内容,可以在设计JSExportApiObj
的时候添加对于原始context
环境中调用方法的包装、解析、转换js
的调用到统一的入口postmessage()
来进行处理(该设计思想用到了设计模式中的外观模式)。
如此一来,JSExportManager
就变成了:
对于方法的添加以及定义我们可以新建一个管理类用于管理所有的js交互方法以及内容,如下所示:
这里声明了两个字典属性主要目的是为了存储js交互中所添加的方法实现、以及方法参数等函数信息。postMsg(method,parameter)
统一入口则用来处理需要调用的方法,类似于一个方法路由,通过字典来找到对应存储的执行体进行方法的调用。
为了方便注入我们可以为UIWebView
添加一个分类,来保证通用功能的封装以及功能接口的提供。
所以不知不觉就添加了三个类用于支撑js交互相关的内容,分别是
JSManager
、JSExportObj
、UIWebView + JSBridge
。它们之间的关系如下图所示:
分类JSBridge
负责提供js交互方法的声明以及js代码的注入,而JSManager
类主要负责由js
调用方法的包装,包装完成之后通过unitAllInjectMethod()
返回包装后的js
实现代码给bridge
,目的是让bridge
将包装后的js实现代码放到context
环境中,从而保证js
的方法调用转换为统一的postMsg()
的调用,进而统一的交由JSEXportApi
中的PostMsg()
方法去进行处理。最后的方法的本地执行实际上是通过JSExportApiObj
中的PostMsg()
来进行方法路由索引最终执行的。
概要代码实现
/*
*js交互方法用于处理所有的关于js交互的内容
*/
- (void)postJSMessage:(id)parameter{
if ([parameter isKindOfClass:[NSDictionary class]]) {
NSString *methodName = [parameter objectForKey:kJsMethodName];
NSDictionary *methodParameter = [parameter objectForKey:kJsMethodParameter];
NSLog(@"执行交互:%@",methodParameter);
if (methodName) {
GC_JsMethodBlock localBlock = [self.manager.jsMethodDic objectForKey:methodName];
if (localBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
localBlock(methodParameter);
});
}
}
}
}
/*
在此组装js方法,目的是将当前的js方法转换成统一的一个js入口方法进行处理
app.showToast(message) 组装成
app.showToast = showToast(message){
app.postJsMessage(methodName:'showToast', parameter:'message');
}
*/
- (NSMutableString *)transfromToInjectJSCode{
NSLog(@"包装开始");
NSMutableString *jsImplementMethod = [@"" mutableCopy];
NSDictionary *allMethodKeys = [[self.manager.jsMethodDic allKeys] copy];
for (NSString *kMethodName in allMethodKeys) {
NSLog(@"当前的keyName:%@",kMethodName);
[jsImplementMethod appendString:[self setupSignalJsMethod:kMethodName]];
}
NSLog(@"result:%@-----",jsImplementMethod);
return jsImplementMethod;
}
- (NSMutableString *)setupSignalJsMethod:(NSString *)kMethodName{
//在此拿到对应的参数来组装实现对应的js方法内容
NSMutableString *signalJsMethod = [@"" mutableCopy];
NSMutableString *parameterTransfrom = [@"" mutableCopy];
NSString *parameter = [self.manager.jsParameterDic objectForKey:kMethodName];
if (parameter.length > 0) {
NSArray *parameterArray = [parameter componentsSeparatedByString:@","];
NSLog(@"参数数组:%@",parameterArray);
[parameterTransfrom appendString:@"{"];
NSLog(@"处理:%@",parameterArray);
for (NSString *tempParameter in parameterArray) {
NSLog(@"Linshi:%@",tempParameter);
[parameterTransfrom appendString:[NSString stringWithFormat:@"'%@':%@,",tempParameter,tempParameter]];
}
[parameterTransfrom replaceCharactersInRange:NSMakeRange(parameterTransfrom.length-1 , 1) withString:@""];
[parameterTransfrom appendString:@"}"];
NSLog(@"获取的参数:%@",parameterTransfrom);
}else{
parameterTransfrom = [@"{}" mutableCopy];
}
if (parameter) {
[signalJsMethod appendString:[NSString stringWithFormat:@"app.%@ = function(%@){\n",kMethodName,parameter]];
[signalJsMethod appendString:[NSString stringWithFormat:@"\t app.postJSMessage({%@:'%@', %@:%@});",kJsMethodName,kMethodName,kJsMethodParameter,parameterTransfrom]];
[signalJsMethod appendString:@"\t}\n\n"];
}
NSLog(@"transFormResult:%@",signalJsMethod);
return signalJsMethod;
}
/*
*js交互方法用于处理所有的关于js交互的内容
*/
- (void)postJSMessage:(id)parameter{
if ([parameter isKindOfClass:[NSDictionary class]]) {
NSString *methodName = [parameter objectForKey:kJsMethodName];
NSDictionary *methodParameter = [parameter objectForKey:kJsMethodParameter];
NSLog(@"执行交互:%@",methodParameter);
if (methodName) {
GC_JsMethodBlock localBlock = [self.manager.jsMethodDic objectForKey:methodName];
if (localBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
localBlock(methodParameter);
});
}
}
}
}
- (void)cfgJsMethod:(NSString *)kJsMethodName withParameterFormatter:(NSString *)parameterStr withMethodBlock:(GC_JsMethodBlock)block{
[self.jsMethodDic setObject:block forKey:kJsMethodName];
[self.jsParameterDic setObject:parameterStr forKey:kJsMethodName];
}
最后
这样设计最终的效果是:
[self.webView cfgJsMethod:@"shareWX" withParameterFormatter:@"imgurl,link,title,desc" withMethodBlock:^(NSDictionary *parameter) {
NSString *imgurl = parameter[@"imgurl"];
NSString *link = parameter[@"link"];
NSString *title = parameter[@"title"];
NSString *desc = parameter[@"desc"];
//完成业务逻辑的执行内容
}];
其实在设计Manager
的时候我们可以将一些通用能力的功能提前加入到js交互中,比如系统弹窗
、系统提示
等等内容。而对于各个业务板块的逻辑的内容就放在各个业务模块中进行处理以避免js交互层
的业务代码的入侵。
最后该JS交互方案的思想还要感谢博主@翻炒吧蛋滚饭的提示与建议,由衷感谢。