05月21日
后庭里 WebApp( PWA) 实践经验分享

全网难寻,解密 WebApp 从构建到开发到部署的各种细节

大家好,我是 132,好久不见啦~好像刚见过 2333

简介

后庭里 App 是后庭花和里世界两个 PC 端网站的官方 App,后庭花+里世界=后庭里

技术选型

  • Vue2.5
  • webpack4
  • koa
  • PHP

重点

1.整体架构是怎样的,以及如何做到一个 App 两套接口共存?

里世界是六年前的 MVC 架构的 PHP 网站,前后端不分离,后庭花是 koa 写的 API,所以为了能造 App ,还需要给里世界写一套接口

2.通过 Hbuilder 打包后如何继续实现原生交互?

通过 HTML5+ 继续监听原生的 API ,实现后退按钮的监听和沉浸式设计?

3.PWA?

PWA 的简单实践?

4.移动端滚动机制?

WebApp 首当其冲的 dom 性能问题,以及具体的滚动方案?

5.总结

1.后庭里整体架构

如题,里世界由于原有架构(历史包袱)的锅,是没有办法做 App 的,之前虽然用了响应式的排版,但是体验并不好,所以为了能够给里世界造 App ,我单独用 PHP 给它做了一套 API
所以后庭里就可以一个 App 共存两套 API ,整体架构变成了这样:
图片

我不得不单独给里世界重新造一套 PHP 的 API ,说真的还是蛮坑的,我都很多年没写过 PHP 了 emmm

(p.s.暑假实习可能后端也是 PHP ,到时候有机会写一篇 PHP 做 API 的文章吧,不过现阶段,node 肯定是收割了新生代年轻人呐)

在具体的代码中,前端需要同时管理两套接口的,这里我用的是 axios 作为请求库,因为是两套不同 URL 接口,所以可以在 create 的时候区分开

axios.create({
  baseURL: 'https://www.idanmu.cc'//or https://www.uraban.me
})

这样子同时管理两个不同的 baseURL 的两套接口,就可以很好地划分

然后在开发中,区分两个大组件,分别对应写逻辑就可以啦 √

2.HTML5+

Vue 开发 spa 的业务方面其实没啥可说的,很快业务就写完了,我差不多两天吧(⊙o⊙)…
写完业务逻辑要想变成原生的 App 就需要打包了……一般我们写个 demo 啥的都会考虑 Hbuilder 或者 ApiCloud 直接打包完事儿了但是正式线上的 App 这样子是不行的,很多交互需要原生配合来完成

通常在公司里都是安卓小哥哥给 h5 暴露方法,这被称为 hybrid 开发,但是很明显,我是一个人,我不会安卓我不得不借助 HTML5+ 提供的 API 来完成这些事情:

如图:
图片

首先是沉浸式设计……
这里就有一个有史以来的大坑啦!

我需要在外部监听 Vue 的路由变化,进而操作 dom,什么意思呢……
就是我需要等 Vue 打包完成后,然后额外加载一个 JS,里面是原生 API 的操作

但是 Vue 的路由变化是通过 push 路由来实现的,所以 hashchange、popstate 事件都是无法被触发的……

这就比较惨了……网上给出的解决方案要么定时器轮询,要么拦截路由,而 Vue 内部的导航守卫就是通过后者但是这两种方案都不好,前者性能差,后者实现难

没办法……只能监听 dom 变化了,dom 发生变化说明组件进行了切换,但是这样也会触发多余的事件,但是相比延时好太多了

监听 dom 变化用的是 MutationObserver ,大概逻辑如下

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
    const option = {
        childList: true,
        subtree: true
    };
    const observer = new MutationObserver(function () {
        //to do
    });

    observer.observe(app, option);

to do 里面就是对应的 dom 操作了,HTML5+ 的沉浸式操作是将导航栏的 top 值加一个状态栏的高度即可,简单的 dom 操作

然后除了沉浸式设计,还需要监听原生的返回键

    const webview = plus.webview.currentWebview();
    let first = null
    plus.key.addEventListener('backbutton', ()=> {
        webview.canBack(e=> {
            if (e.canBack) {
                webview.back();
            } else {
                if (!first) {
                    document.getElementById('toast').style.display = 'block';
                    first = new Date().getTime();
                    setTimeout(() => {
                        document.getElementById('toast').style.display = 'none';
                        first = null
                    }, 1000)
                } else {
                    if (new Date().getTime() - first < 1000) {
                        plus.runtime.quit();
                    }
                }

            }
        });
    });

上述代码主要是实现了对返回键的监听,点击第一次出现提示,一秒内点两次就直接退出,然后操作 dom 就是控制一个 div 是显示隐藏,这个 div 就是……
图片
就是它

这样一来,利用 HTML5+ 的 API ,就实现了安卓的沉浸式设计和返回按钮的监听……

但是 iOS 该怎么办呢?iOS 是必须传到 APP store 里的,我又买不起开发者权限,所以……

3. PWA

不幸中的万幸,iOS 11.3+ 已经支持了 PWA,所以我们可以通过 将网站改造成 PWA 的方式,然后通过 Safari 添加到主屏幕,就能拥有和原生 App 几乎一致的体验
PWA 主要有两点:1. manifest 2. serviceworker

我们可以新建一个 manifest.json ,如下:

{
    "name": "后庭里",
    "short_name": "后庭里",
    "description": "后庭花+里世界=后庭里",
    "start_url": "/",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#ff677d",
    "theme_color": "#ff677d",
    "icons": [
        {
            "src": "./icon/72x72.png",
            "sizes": "72x72",
            "type": "image/png"
        },
}

其中 background_color 和 theme_color 和 icons,iOS 都是不支持的,color 我們可以不管它,但是 icon 还是要加的,方法是通过 <link rel="apple-touch-icon" href="icon.jpg"> 来加上 icon ,注意尺寸一定要小,加载速度要很快,如果慢了的话,iOS 会默认将截图作为图标的

至此,iOS 已经成功搞定了 PWA,是不是超级简单但是安卓的浏览器还是不行的……安卓浏览器要想实现出现“添加到桌面”的提示,需要满足拥有 manifest 和 serviceworker 两者同时存在,而且用户对页面还有一定的粘性(要多访问几次才会出现提示)

所以,我们需要注册 serviceworker,代码如下:

if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register("sw.js")
    .then(registration => {
      console.log("service worker 注册成功")
    })
    .catch(err => {
      console.log("servcie worker 注册失败")
    })
}

至于具体的 serviceworker 的各种相关,这里先把一一赘述了,只需要创建一个空的 sw.js,浏览器就能检测到
但是上篇文章我说过, serviceworker 可以用来离线首屏,实现一触即发 √
其实是一个抓取首页的 index.html ,然后写入缓存的一个过程

const cacheStorageKey = 'idanmu-pwa-1'

const cacheList = [
  '/',
  "index.html"
]

self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(cacheStorageKey)
    .then(cache => cache.addAll(cacheList))
  )
})

这样一来,一个 Vue 的 WebApp,实现了异步以后每个文件都很小的情况下,又能离线首屏,就是真正意义上的首屏一触即发啦可惜,这种一触即发仅仅适用于 PWA ,和支持 serviceworker 特性的浏览器,好在国内主流浏览器是支持的

如果是在安卓的 WebView 里,可以想办法通过类似的离线机制完成同样的事情 √

好了,这样一来,iOS 的 App 也通过 PWA 的方式搞定啦!

4.移动端的滚动机制

然后我们回归 App 的本身,在开发 WebApp 的过程中,我发现滚动机制是个大坑,市面上的滚动库,iscroll 、 better-scroll 、 alloy-touch 、mescroll
以上是这些库是相同的原理,都是通过定位和 touch 事件改变 translate 的值和一定的曲线算法来实现惯性和回弹

但是我发现,安卓上面,这个方式的性能,很差,无独有偶,上面的库全都全都卡的不要不要的 o(╥﹏╥)o

我也很绝望……
我研究了很久,没找到原理,iOS 和 PC 端都是性能 ok 的,然而,安卓就是卡,我深度怀疑是安卓天生就是对 Web 有偏见

最终我们没有使用滚动库,直接用原生的 overflow-scrolling: touch ,但是这个仅对 iOS 有效也是绝望了

大家有好的方案请务必告知我!据我所知,滚动性能好的 App ,都不是 WebApp,所以!

人生重来一次,我不写 WebApp 啦

但是人生没重来 o(╥﹏╥)o 虽然最终的解决方案没搞好,但是,过程中的解决思路可以分享下

首先,我一开始是认为,滚动的性能不好是因为图片的加载问题,所以我去监听了图片的 onload 事件

let imgs = [].slice.call(document.querySelectorAll(".content img"))
if (imgs) {
    imgs.forEach((i) => {
        i.onload =  ()=> {
            this.scroll.refresh()
        }

而且用了懒加载,就是 vue-lazyload 这个插件
使用方法很简单,就是全局引入它,然后将 :src 改成 v-lazy
但是它有个缺点,就是只能对 Vue 的 template 能使用 v-lazy 这个指令,但是我们有时候渲染内容的时候,图片是 Markdown 编辑器插入的图片,这个时候我们就要原生操作 dom 将原来的内容改成以下这个结构
html <div v-lazy-container="{ selector: 'img' }"> <img data-src="//domain.com/img1.jpg"> <img data-src="//domain.com/img2.jpg"> <img data-src="//domain.com/img3.jpg"> </div>
其实很简单的啦,就是给父级元素 setAttribute 一个 v-lazy-container 属性,然后将 src 替换成 data-img

这样虽然能够通过遍历图片的 onload 事件,保证撑开高度……但是仿佛和性能没什么干系但是我发现如果是原生 JS 渲染 dom 的话,其实并不会卡,最终实在没理由了,只好怪 Vue 咯?

好吧,最终还是怪 Vue ,嗯,没毛病,哈哈

  • 上拉加载
    啊对啦,关于滚动,有很多经典的场景,比如上拉加载,这个的原理很简单,就是监听是否到达底部,如果是,发下一页的请求即可
window.addEventListener('scroll', () => {
    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
    const innerHeight = window.innerHeight
    const offsetHeight = document.documentElement.offsetHeight || document.body.offsetHeight
    const t = scrollTop + innerHeight - offsetHeight
    console.log(t)
    if (t === 0) {
        if (this.sw) {
                this.$emit('getMore')
            }
        }
    })

很简单,就是通过监听 scroll 事件,然后计算 scrollTop + innerHeight - offsetHeight 的值,如果等于0说明到底了,然后派发一个事件,事件内发请求
可以看到,有个 sw ,是个开关,默认是打开,然后发请求之前马上将开关关闭,等请求完成,再次打开,然后继续往下滑
这样就可以保证每次只往下加载一页

更新:以上代码会导致浏览器不断重排,极大的影响性能,仅作为思路参考……不好意思哈,我刚发现,后面我考虑写一个滚动库,来应对安卓卡顿问题,到时候再来分享!

至于下拉刷新,同样的道理,监听 touch 事件,做一个动画,然后 touchend 的时候发请求即可

5.总结

这样一来,一个基本的 WebApp 就搞定了o(╥﹏╥)o,过程虽然不艰辛,但是坑也不少,市面上的 WebApp 都是浏览器内跑起来就可以了,很少有考虑原生交互、PWA 的

经过这一个真正的线上项目,我对 WebApp 有了更新的认知,现在我认为 native 其实好就好在他没有浏览器的坑,没有 dom 性能的锅

希望我的这个教训也能给很多做 h5 开发的架构师,在某些时候考虑更换技术选型

好啦,望天,又写了一次作文o(╥﹏╥)o

后庭花网站 API 接口端、PC 端、管理系统端、移动端,全部都初具雏形了,剩下的就是完善 feature 和修复 bug 啦

暑假如果能找到实习,会带来新的实践分享,敬请期待!

2条评论

我们公司招php,会前端更好啦

纤纤7 个月前回复

沙发~

伊撒尔7 个月前回复