美文网首页
阅读类APP涉及的技术

阅读类APP涉及的技术

作者: _GKK_ | 来源:发表于2017-12-23 19:17 被阅读67次

    本文出自: 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页面的终端适配

    文中有何错误还望指教~

    相关文章

      网友评论

          本文标题:阅读类APP涉及的技术

          本文链接:https://www.haomeiwen.com/subject/iybqgxtx.html