使用 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 的排版引擎:基础

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容