阅读类APP涉及的技术

本文出自: http://mokai.me/read-app-knowledge.html

飞地是一款诗歌轻阅读产品,在技术选型时内容的载体采用了HTML,这样内容可以适用于全平台显示。

轻阅读是从技术角度分析的,因为没有像微信读书这类应用有长篇文字的书籍,需要实现各种PDF和ePub格式解析以及排版,我们只需要用UIWebView即可解决。

首先内容body中的一段HTML,通过接口拿到文章的内容后替换到完整的HTML模板中

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" >
        {css}
    </head>    
    <body id='articleCon'>
        {body}
    </body>
</html>

{css}是内容的样式,如标题、段落、脚注等,articleCon是为了样式选择器。

css来源有二种情况,启动时加载服务器最新的,如果失败则使用本地的备份css

最后使用loadHTMLString来加载替换后的HTML。

页面结构

飞地有几个内容模块都是基于HTML来做为内容的载体,但页面一般不只是纯内容,会有一些其它元数据,这些使用原生视图显示。

如上图文章详情页,整个页面的容器是UITableView,封面图、作者、日期、内容WebView都是tableHeaderView,评论列表为Cell。

tableHeaderView的高度我们需要自己计算,而WebView的高度可以在webViewDidFinishLoad后获取,并重新设置tableHeaderView的高度。

//原生代码

var contentHeight = webView.scrollView.contentSize.height
let fitHeight = webView.sizeThatFits(CGSize(width: 1.0, height: 1.0)).height
if fitHeight > contentHeight {
    contentHeight = fitHeight
}
if let documentHeight = jsBridge.getContentHeight(),
    documentHeight > contentHeight {
    contentHeight = documentHeight
}

jsBridge.getContentHeight()是执行JS层的代码document.body.scrollHeight * window.scale获取高度

Tip:直接设置tableView.tableHeaderView.frame.height时可能不会生效,需要重新tableView.tableHeaderView = tableHeaderView渲染一次。

rem

文章有各种各样的样式,移动设备碎片化,使用px明显已经不满足需求了,所以我们使用rem。

//JS代码

window.scale = 1.0; //�标志当前viewport使用的scale
!function(e){function t(a){if(i[a])return i[a].exports;var n=i[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=window;t["default"]=i.flex=function(normal,e,t){var a=e||100,n=t||1,r=i.document,o=navigator.userAgent,d=o.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i),l=o.match(/U3\/((\d+|\.){5,})/i),c=l&&parseInt(l[1].split(".").join(""),10)>=80,p=navigator.appVersion.match(/(iphone|ipad|ipod)/gi),s=i.devicePixelRatio||1;p||d&&d[1]>534||c||(s=1);var u=normal?1:1/s,m=r.querySelector('meta[name="viewport"]');m||(m=r.createElement("meta"),m.setAttribute("name","viewport"),r.head.appendChild(m)),m.setAttribute("content","width=device-width,user-scalable=no,initial-scale="+u+",maximum-scale="+u+",minimum-scale="+u),i.scale=u,r.documentElement.style.fontSize=normal?"50px": a/2*s*n+"px"},e.exports=t["default"]}]); flex(false,100, 1);
html {
    font-size: 62.5%;
}

上述代码分别加在文章内容HTML模板中与文章css中,而飞地的设计图输出是375pt * 667pt,所以我们只需要把设计上的pt/50转换成rem就行了(50是设备缩放基准值),如设计图上的正文字体是17pt,那么对应css的rem应该是 17pt /50 = 0.34rem

#articleCon n p {
    font-size: 0.34rem;
}

缓存

由于有离线阅读需求,app启动时会提前缓存文章,其实也就是存储文章的封面图、内容HTML等,但html中也有图片,所以我们需要用正则拿到所有img.src,然后缓存在本地,并将文章标识为已缓存。

<img\\s[\\s\\S]*?src\\s*?=\\s*?['\"](.*?)['\"][\\s\\S]*?>*

前期我们采用的方式是将所有img.src保持相对路径,loadHTMLString时如果文章标识已缓存则baseURL使用本地Path,否则使用线上URL。

优化后统一换成URLProtocol处理,提前缓存文章时用第三方图片加载库下载好图片,等阅读文章时利用URLProtocol机制拦截,如果是WebView的图片,判断该图片是否缓存在第三方图片加载库中,否则手动加载图片Data并且保存在第三方图片加载库,下次再拦截到此图片的请求直接从第三方图片加载库缓存中取。

URLProtocol是全局拦截,判断请求是否为WebView的图片可在shouldStartLoadWith时附加自定义Header,在URLProtocol识别Header就行

原生与JS交互

有二种方式,原生提供的JavaScriptCore、JS层通过iFrame加载URI(URI包括scheme与参数)原生在shouldStartLoadWith中拦截,飞地使用了第一种。

//原生代码

/// 原生JavaScriptCore暴露给JS层的对象
@objc protocol ContentWebViewJavaScriptBridgeProtocol: JSExport {
    
    /// 图片点击回调
    func onImageClick(_ currentImageIndex: Int, _ images: [String])
    
}

/// 原生与JS桥接
class ContentWebViewJavaScriptBridge: NSObject, ContentWebViewJavaScriptBridgeProtocol {
    //原生暴露给JS层的对象名
    static let name = "EnclaveNative"
    fileprivate var jsContext: JSContext?
    fileprivate weak var webView: UIWebView?
    
    var imageClickCallback: ((_ currentImageIndex: Int, _ images: [String])->())?
    
    convenience init(webView: UIWebView) {
        self.init()
        guard let jsContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext else { return }
        self.jsContext = jsContext
        self.webView = webView
        jsContext.setObject(self, forKeyedSubscript: ContentWebViewJavaScriptBridge.name as NSCopying & NSObjectProtocol)
        
        jsContext.exceptionHandler = { (ctx, value) in
            L.debug(value?.description ?? "exception")
        }
    }
    
    func onImageClick(_ currentImageIndex: Int, _ images: [String]) {
        //回调在UI线程
        DispatchQueue.main.async {
            self.imageClickCallback?(currentImageIndex, images)
        }
    }
}

//MARK: - Public
extension ContentWebViewJavaScriptBridge {
    
    /// 获取html中所有图片地址
    func getImages() -> [String]? {
        guard let jsContext = jsContext else { return nil }
        
        guard let jsValue = jsContext.evaluateScript("getImageSrcs()") else { return nil }
        return jsValue.toArray() as? [String]
    }
    
    /// 获取内容高度
    func getContentHeight() -> CGFloat? {
        if let heightString = webView?.stringByEvaluatingJavaScript(from: "Enclave.getContentHeight()"),
            let height = Float(heightString) {
            return CGFloat(height)
        }
        return nil
    }
    
    /// 切换主题
    func switchTheme() {
        if ELThemeManager.shared.style == .night {
            switchToNightMode()
        } else {
            switchToLightMode()
        }
    }
    
    /// 切换至夜间模式
    fileprivate func switchToNightMode() {
        webView?.stringByEvaluatingJavaScript(from: "Enclave.switchToNightMode()")
    }
    
    /// 切换至日间模式
    fileprivate func switchToLightMode() {
        webView?.stringByEvaluatingJavaScript(from: "Enclave.switchToLightMode()")
    }
}

  • JS -> 原生
EnclaveNative.onImageClick(currentImageIndex, srcs)
  • 原生 -> JS
webView.stringByEvaluatingJavaScript(from: "xxx()")

图片查看

点击内容HTML中的图片,需要在原生端显示查看。
首先在DOM加载完毕后为所有的有效img注册click事件,在事件触发时拿到所有img.src与当前img的index传到原生端并显示。

//JS代码

function getImageSrcs() {
    var srcs = []
    var imgs = document.getElementsByTagName('img')
    for (var i = 0; i < imgs.length; i++) {
        if(imgs[i].src.indexOf('data:') == 0 || imgs[i].parentNode.nodeName.toLocaleLowerCase() == 'a') {
            continue
        }
        srcs.push(imgs[i].src)
    }
    return srcs
}

function onImageClick(currentImageIndex) {
    var srcs = getImageSrcs()
    //原生回调
    EnclaveNative.onImageClick(currentImageIndex, srcs)
}

function didload() {
    var imgs = document.getElementsByTagName('img')
    //有效图片index,因为�可能会存在可跳转的图片
    var index = 0
    for (var i = 0; i < imgs.length; i++) {
        //加载失败时默认图,且不可点击
        if(imgs[i].naturalWidth == "undefined" || imgs[i].naturalWidth == 0) {
            imgs[i].src = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBwcHDQwNGBAQGBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIADIAZAMBEQACEQEDEQH/xABLAAEBAAAAAAAAAAAAAAAAAAAACBABAAAAAAAAAAAAAAAAAAAAAAEBAAAAAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//Z'
        }
        //并图片本身包含链接时也不可点击
        if(imgs[i].parentNode.nodeName.toLocaleLowerCase() == 'a') {
            continue
        }
        imgs[i].imageIndex = index++ //给img元素设置一个index
        imgs[i].onclick = function(e) {
            onImageClick(e.target.imageIndex) //拿当前事件的元素index然后回调
        }
    }
}

window.addEventListener('load', function() {
    didload()
}, false)

夜间模式

关于原生iOS端实现夜间模式可查看这里,这里主要讲述web页面实现。
由于css样式优先级的机制,最新的样式可覆盖旧的样式,所以我们只需要为每种样式添加一种夜间模式样式就行。

/*夜间模式样式*/
.night-mode {
    background-color: #333333;
}
.night-mode #articleCon p,
.night-mode #articleCon ol li,
.night-mode #articleCon ul li {
    color: #CDCDCD;
}

在原生端切换样式时,通过JS函数把夜间模式的css附加上去就行了,切换回默认主题删除样式即可。

//JS代码

//切换至夜间模式
Enclave.switchToNightMode = function() {
    document.querySelector('html').classList.add('night-mode')
}

//切换至白天模式
Enclave.switchToLightMode = function() {
    document.querySelector('html').classList.remove('night-mode')
}

参考

使用Flexible实现手淘H5页面的终端适配

文中有何错误还望指教~

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

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,725评论 1 92
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,391评论 25 707
  • 原创/卢卢 (诗)入水云间, (人)挥美绪弦。 (你)知天地远, (好)作丽世界。 (天)地你我情, (诗)育山水...
    AB774卢卢阅读 1,299评论 15 38