iOS 开发中图文混排除了用 CoreText 等方案以外,这里演示另外一种比较主流方案。用 WebView + HTML + JavaScript + JSExport。本文将使用这一方案来完成一个新闻客户端的详情页。
HTML 和 CSS 在界面布局和呈现上深耕多年,Android 也是借鉴 HTML 的那套方案。相比使用 Native 的方案,这样的好处显而易见。一套布局代码,相同的体验,全平台通用。每个平台都有自己的浏览器核心,WebKit 对 HTML 的解析速度也很不错。最重要的,HTML 来处理这种布局的代码量少了很多。很多跨平台的解决方案都是采用 HTML + CSS。
但这也并不是没有缺点。CoreText 占用的内存更少,渲染速度快,可以在后台线程渲染。而 WebView 的内容只能在主线程渲染。基于 CoreText 可以做更细腻的原生交互效果。而 WebView 的交互效果都是用 JavaScript 来实现的,一个简单的按钮按下效果都会有一定程度的卡顿。这也使得新浪微博等主流 App 都放弃使用这种方案。但是对于内容展示页面这种没有交互或者交互较少页面,这才是最佳的方案。
本文将以这个思路制作一个图文混排的新闻详情页 Demo。它将支持显示新闻内容、图片、相关新闻和*字体大小调整、*夜间模式等功能。你可以在 这里 找到本文的 Demo。
实现思路大概分为五步。
- 使用 HTML 写好布局。
- 使用 JavaScript 写好注入数据的方法。
- 将文字和图片拼接为 HTML 代码。
- 使用 WebView 加载 HTML 界面并执行 JavaScript 方法注入数据。
- 实现 JavaScript 点击回调来跳转 Native 界面。
1. 使用 HTML 写好布局
<div class="wrap">
<h1 id="title"></h1>
<div class="info">
<span id="source"></span>
<span id="time"></span>
</div>
<div id="content"></div>
<div id="correlationHeader"></div>
<div id="correlation"></div>
这里将各模块的 div 和 span 布局写好,然后使用 JavaScript 根据 ID 找到对应 div 并对其 innerHTML 赋值来实现数据注入。同样,通过 JavaScript 来切换写好的 CSS 样式实现字体大小的更改。CSS 代码太多不贴,你可以在 Demo 自己查看。
2. 使用 JavaScript 写好注入数据的方法。
根据效果图和上文的 HTML 的布局可以看出。title(新闻标题)、source(新闻来源)、time(时间)、correlationHeader(相关新闻 Header) 都为纯文字。所以可以通过相同的方法来注入数据。
function addData(type, data) {
document.querySelector('#' + type).innerHTML = data;
}
调用此方法通过 querySelector 找到对应的 div 然后将 data 赋值给 innerHTML 就可以将对应的 type 显示出来了。
[_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"addData(\"correlationHeader\",\"%@\");", NSLocalizedString(@"相关新闻", nil)]];
同样的,content(新闻内容) 为文字和图片拼接的 HTML 字符串。correlation(相关新闻)为文字、图片和分割线拼接的 HTML 字符串。如果他们使用 Objective-C 将数据转换成 HTML 字符串就也可以用上述方法去显示。
在 Demo 中 content 是 Objective-C 拼接好 HTML 然后同样调用 addData 去显示。这样更具有灵活性。而 correlation 则是直接将 Json 通过 JavaScript 注入,由 JavaScript 实现拼接。这样本地不用做过多的处理,显得优雅了许多。
function addCorrelationData(data, index) {
var banner ='<HR class="banner" width="100%" color=#cdcdcd SIZE=1>';
var title='<div class="list-title">'+data['title']+'</div>';
var img='<img src="'+data['img']+'" width="108px" height="74px" class="list-image">';
var info ='<div class="list-info"><span>'+data['author']+'</span><span class="list-info-right">'+data['date']+'</span></div>';
var correlation=document.querySelector('#correlation');
var div = document.createElement('div');
if (index == 0) {
div.innerHTML = banner+"<div class='list-padding'>"+img+title+info+"</div>"+banner;
} else {
div.innerHTML = "<div class='list-padding'>"+img+title+info+"</div>"+banner;
}
correlation.appendChild(div);
div.addEventListener('click',function(){mxNewsContext.onClick(index);});
}
此方法调用几次将会添加几条相关新闻。代码很简单,分别为添加分割线、新闻标题、图片、新闻源和时间,最后添加 div 的点击事件,通过 JSExport 调用本地代码实现跳转。最后你可以再添加一些便于使用的接口,比如 showLoading、showError 等。
function showLoading(loadingString) {
clearHTML();
document.querySelector('#content').innerHTML = '<div style=\"margin:100px auto;width:18em\"><p style=\"color:#969595;font:bolder 17.5px HelveticaNeue;text-align:center\">' + loadingString +'</p></div>';
}
function showError(imagePath){
clearHTML();
document.querySelector('#content').innerHTML = '<img src="'+imagePath+'">';
}
3. 将文字和图片拼接为 HTML 代码
假设这个页面的数据源直接就是从网页上抓取好的 HTML。那直接调用 addData 显示就可以。如果是约定好的 Json 数据,也可以自己来拼接。假设定义了这样一个数据片段的 Model。
typedef NS_ENUM(NSInteger, XFContentFragmentType) {
XFContentFragmentTypeText,
XFContentFragmentTypeImage
};
@interface XFContentFragmentModel : NSObject
@property (nonatomic, assign) XFContentFragmentType type;
@property (nonatomic, copy) NSString *value;
@end
我们可以自己实现一个类来拼接他们。Demo 中的 XFHTMLConfigurator 做了简单的演示。
+ (NSString *)connectToHTMLStringWith:(NSArray<XFContentFragmentModel *> *)fragmentModels {
NSMutableString *htmlString = [NSMutableString string];
for (XFContentFragmentModel *model in fragmentModels) {
switch (model.type) {
case XFContentFragmentTypeText: {
[htmlString appendFormat:@"<p>%@</p>", model.value];
break;
} case XFContentFragmentTypeImage: {
[htmlString appendFormat:@"<img src = '%@'>", model.value];
break;
} default: {
break;
}
}
}
return htmlString;
}
你可以定义更复杂的 Model 来标记图片悬浮位置、边框、字体大小、颜色等更多的属性,或者添加表格、分割线等更多的片段类型。这只需在 Configurator 中添加几个简单的 HTML 标记即可支持这些复杂的排版。
4. 使用 WebView 加载 HTML 界面并执行 JavaScript 方法注入数据
使用 WebView 来加载 HTML 和 JavaScript 并没有什么好说的。
// 加载 HTML 示例
[_webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"XFNewsContent" withExtension:@"html"]]];
// 加载 JavaScript 示例
[_webView stringByEvaluatingJavaScriptFromString:@"addData(\"title\",\"this is title\")];
5. 实现 JavaScript 点击回调来跳转 Native 界面
使用 JSExport 实现 JavaScript 调用 Objective-C 代码需要先定义继承自 JSExport 的协议。协议中的方法就可以在 JavaScript 直接调用。
@protocol XFCorrelationNewsJSExport <JSExport>
- (void)onClick:(NSInteger)index;
@end
当然你还需要拿到 WebView 的 JSContext,并将实现 JSExport 的对象传给 JSContext。
_jsContext = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
_jsContext[@"xfNewsContext"] = _jsExport;
然后你就可以在 JavaScript 直接像这样来调用 onClick 方法了。
xfNewsContext.onClick(index)
由于 JavaScript 是垃圾回收机制,所有的对象都是强引用。所以我们的如果用 self 来实现协议,将 _jsContext[@"xfNewsContext"] = self,这时 self 也强引用了 _jsContext,就造成了循环引用。更多避免这个问题的方法可以参考 这里。
参考资料
[简书] JavaScript和Objective-C交互的那些事(续)
[唐巧] 谈谈 React Native
[Div] 我对 React Native 的理解和看法
[唐巧] 基于 CoreText 的排版引擎:基础