一个基于vue2的h5项目,被嵌套在安卓APP和iOS APP中使用。因为某些原因无法在webview中使用缓存,也无法使用cdn方式。
在经过分包、按需加载、代码清理、压缩资源文件、减少并发请求等常规手段优化后,网络较差的情况下较长的白屏时间仍然很尴尬。于是在index.html中加上了默认loading动画效果,这样用户在加载完入口文件后就可以直接看到动画,待vue初始化完成时再关闭loading动画,体验更佳。
辣么问题来了,在PC和Android设备上进行测试,都能达到预期效果。但是在iOS设备上,不管是 Safari 还是基于 iOS的WebView中,index.html的动画都无法显示,仍然是较长时间的白屏,然后直接出现了项目内部的动画。
查阅相关资料,在某乎的一篇文章(详见文末的“相关资料”)中有如下结论:
当前所有版本的 iOS Safari(包括所有基于 iOS WebView 的浏览器)都存在一个 bug,下载中的脚本会阻塞页面的显示,无论脚本是否在页面底部或是否有 defer 或 async 属性。
如果动态创建 script 标签并在异步回调中进行脚本加载,可以避免阻塞,大幅度改善 Mobile Safari 的首屏渲染时间,经统计可能会快 240 毫秒以上。
当前的的最佳实践是,在 rAF 后再推迟以进行脚本加载。
经过验证确实如此,于是有了以下相对perfect的方案:
index.html文件:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> <!-- 默认loading样式 --> <style> #app,body,html{width:100%;height:100%;padding:0;margin:0} .app-loading{position:fixed;z-index:999999999;top:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;} .app-loading .loading-wrap{display:flex;align-items:center;justify-content:center;padding:98px} .app-loading .dot{position:relative;box-sizing:border-box;display:inline-block;width:32px;height:32px;font-size:32px;transform:rotate(45deg);animation:app-loading-rotate 1.2s infinite linear} .app-loading .dot i{position:absolute;display:block;width:14px;height:14px;background-color:#409eff;border-radius:100%;opacity:.3;transform:scale(.75);transform-origin:50% 50%;animation:app-loading-spin-move 1s infinite linear alternate} .app-loading .dot i:nth-child(1){top:0;left:0} .app-loading .dot i:nth-child(2){top:0;right:0;animation-delay:.4s} .app-loading .dot i:nth-child(3){right:0;bottom:0;animation-delay:.8s} .app-loading .dot i:nth-child(4){bottom:0;left:0;animation-delay:1.2s} @keyframes app-loading-rotate{to{transform:rotate(405deg)}} @keyframes app-loading-spin-move{to{opacity:1}} </style> <!-- 打包时注入 --> <% if (htmlWebpackPlugin.options.isBuild) { %> <% for (var i in htmlWebpackPlugin.files.css) { %> <link href="<%= htmlWebpackPlugin.files.css[i] %>" rel="stylesheet"> <% } %> <% for (var i in htmlWebpackPlugin.files.js) { %> <link href="<%= htmlWebpackPlugin.files.js[i] %>" rel="preload" as="script" /> <% } %> <% } %> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- 默认loading --> <div class="app-loading"> <div class="loading-wrap"> <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span> </div> </div> <!-- 打包时注入 --> <% if (htmlWebpackPlugin.options.isBuild) { %> <script> { const loadScript = () => { let jsArr = '<%= htmlWebpackPlugin.files.js %>'.split(','); for(let item of jsArr) { let script = document.createElement('script') script.src = item document.body.appendChild(script) } } // 使用一个非 microtask 回调,使回调执行在下一个 event loop 中 const scheduleNextTick = callback => { const el = new Image() el.src = 'data:,' el.onload = el.onerror = callback } const isPageVisible = (document.visibilityState || document.webkitVisibilityState) === 'visible'; if (isPageVisible) { // raf 后推迟,不在 rendering 阶段执行可能阻塞的操作 requestAnimationFrame(() => scheduleNextTick(loadScript)) } else { loadScript() } } </script> <% } %> </body> </html>
vue.config.js中:
module.exports = { // ... chainWebpack(config) { // 移除 preload 插件 config.plugins.delete('preload') // 移除 prefetch 插件 config.plugins.delete('prefetch') // 当打包时才执行 config.when(process.env.NODE_ENV === 'production', (config) => { config.plugin('html').tap((args) => { // 关闭自动注入 args[0].inject = false // 自定义一个开启手动注入的标识 args[0].isBuild = true return args }) }) }, // ... }
在vue-router的全局后置钩子中关闭loading
router.afterEach((to, from) => { document.querySelector('.app-loading').style.display = 'none' })
有朋自远方来...评论一下呗O(∩_∩)O