首页

时间和精神的房子
壹只iOS程序员的修行世界,欢迎来访

如果文章对您有所帮助
将是我最大的荣幸

使用 WebView + HTML + JavaScript + JSExport 实现图文混排

iOS 开发中图文混排除了用 CoreText 等方案以外,这里演示另外一种比较主流方案。用 WebView + HTML + JavaScript + JSExport。本文将使用这一方案来完成一个新闻客户端的详情页。

HTML 和 CSS 在界面布局和呈现上深耕多年,Android 也是借鉴 HTML 的那套方案。相比使用 Native 的方案,这样的好处显而易见。一套布局代码,相同的体验,全平台通用。每个平台都有自己的浏览器核心,WebKit 对 HTML 的解析速度也很不错。最重要的,HTML 来处理这种布局的代码量少了很多。很多跨平台的解决方案都是采用 HTML + CSS。

但这也并不是没有缺点。CoreText 占用的内存更少,渲染速度快,可以在后台线程渲染。而 WebView 的内容只能在主线程渲染。基于 CoreText 可以做更细腻的原生交互效果。而 WebView 的交互效果都是用 JavaScript 来实现的,一个简单的按钮按下效果都会有一定程度的卡顿。这也使得新浪微博等主流 App 都放弃使用这种方案。但是对于内容展示页面这种没有交互或者交互较少页面,这才是最佳的方案。

本文将以这个思路制作一个图文混排的新闻详情页 Demo。它将支持显示新闻内容、图片、相关新闻和字体大小调整、夜间模式等功能。你可以在 这里 找到本文的 Demo。

实现思路大概分为五步。

  1. 使用 HTML 写好布局。
  2. 使用 JavaScript 写好注入数据的方法。
  3. 将文字和图片拼接为 HTML 代码。
  4. 使用 WebView 加载 HTML 界面并执行 JavaScript 方法注入数据。
  5. 实现 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 的排版引擎:基础

关注作者

分享本文

目录