Vue2.x项目骨架屏注入(下)

1,127 阅读3分钟

骨架屏注入实践二:dps(draw-page-structure)

前提背景:

vue-skeleton-webpack-plugin通过预渲染手动书写的代码生成相应的骨架屏,通过 vueSSR 结合 webpack 在构建时渲染写好的 vue 骨架屏组件,将预渲染生成的 DOM 节点和相关样式插入到最终输出的 html 中,然后预渲染生成骨架屏所需的 DOM 节点,但由于该方案不够灵活、可控;因此针对Vue2.x项目骨架屏注入(上)的方案进行优化——网页骨架屏自动生成(dps)。

dps 具体操作步骤

1、安装dps

npm i draw-page-structure -D

2、在具体某个页面中需要生成骨架屏的页面添加:

mounted() {
    // 注意:生成骨架屏后,把骨架屏代码贴到vue文件后,下面代码需要注释掉,否则每次进来又生成html插入页面
    setTimeout(() => {
        createSkeletonHTML({
        // 此段代码实际无效
            background: 'red',
            animation: 'opacity 1s linear infinite;'
        }).then((skeletonHTML) => {
        // 此处输出来的就是骨架屏节点,可以自定义骨架屏节点内容
            console.log(skeletonHTML);
        }).catch((e) => {
            console.error(e);
        });
    }, 5000);
},

3、npm run dev 在浏览器中运行后,可在控制台输出当前页面骨架屏节点,复制添加到应用页面,如:

src/commercialActivity/index.vue

<template lang="html">
    <div class="commercialActivity">
    <!--为了交互好看就用了transition-->
        <transition name="skeleton">
            <div v-if="showSkeleton">
            <!--骨架屏节点-->
                <div class="_ __" style="height:100%;z-index:990;background:#fff"></div><div class="_" style="height:2.099%;top:3.748%;left:6.667%;width:73.667%"></div><div class="_" style="height:2.099%;top:3.748%;left:81.067%;width:12.267%"></div><div class="_" style="height:1.799%;top:8.096%;left:6.667%;width:34.492%"></div><div class="_" style="height:1.799%;top:8.096%;left:74.133%;width:19.200%"></div><div class="_" style="height:4.498%;top:14.768%;left:9.333%;width:7.467%"></div><div class="_" style="height:2.249%;top:15.967%;left:26.975%;width:40.000%"></div><div class="_" style="height:2.249%;top:15.892%;left:69.333%;width:4.000%;border-radius:50%"></div><div class="_" style="height:2.099%;top:15.967%;left:76.000%;width:16.000%"></div><div class="_" style="height:2.249%;top:27.361%;left:1.867%;width:3.950%"></div><div class="_" style="height:10.195%;top:23.388%;left:8.483%;width:17.908%"></div><div class="_" style="height:2.249%;top:23.763%;left:29.058%;width:66.938%"></div><div class="_" style="height:2.399%;top:26.912%;left:30.125%;width:6.400%;border-radius:2px"></div><div class="_" style="height:1.799%;top:27.211%;left:74.246%;width:21.750%"></div><div class="_" style="height:2.249%;top:30.960%;left:29.058%;width:66.938%"></div><div class="_" style="height:2.399%;top:37.031%;left:40.858%;width:12.800%"></div><div class="_" style="height:1.150%;top:37.830%;left:55.942%;width:3.200%"></div><div class="_" style="height:2.849%;top:42.429%;left:35.067%;width:29.867%"></div><div class="_" style="height:2.249%;top:95.610%;left:1.867%;width:4.000%"></div><div class="_" style="height:6.447%;top:93.403%;left:6.975%;width:6.400%"></div><div class="_" style="height:6.447%;top:93.403%;left:73.333%;width:26.667%"></div>
            </div>
        </transition>
....
    </div>
</template>
<style>
._{
position:fixed
z-index:999;
<!--由于createSkeletonHTML方法中,background: 'red',里边不生效,粘贴代码时手动修改一下样式,否则是undefined,看不到颜色及效果-->
background:#efefef; 
}
.__{
top:0%;
left:0%;
width:100%;
}
</style>

app.styl

// 骨架过渡
.skeleton-enter-active
  transition opacity .6s
.skeleton-enter
  opacity 0
.skeleton-leave-active
  transition opacity .6s
.skeleton-leave-to
  opacity 0
  

骨架是在页面没渲染完成,就是接口没返回之前展示,接口返回骨架隐藏。因此需要把mounted里边生成骨架屏的代码进行注释,否则每次页面运行完毕都会自动生成骨架屏节点,自动插入页面,导致多份骨架屏重叠的现象,也因此对骨架屏节点进行v-if="showSkeleton"控制:

src/commercialActivity/commercialActivity.js

data() {
        return {
            showSkeleton: true, // 是否显示骨架
        }
    }
methods: {
        // 滚动下拉下拉加载
        infinite(val) {
            return _.debounce(() => {
            // 请求接口的地方
                this.getProductInfo({
                    activity_id: this.activityId,
                    page: this.listQery.page,
                    ser: val ? val.search : ''
                }).then(() => {
                // 接口返回骨架隐藏
                    this.showSkeleton = false;
                    if (!this.errorType.type) {
                        this.$refs.infiniteLoading.stateChanger.loaded();
                    } else {
                        this.$refs.infiniteLoading.stateChanger.complete();
                    }
                    if (this.hasMore) {
                        this.distance = -Infinity;
                    } else {
                        this.$refs.infiniteLoading.stateChanger.complete();
                    }
                }).catch((err) => {
                // 接口返回骨架隐藏
                    this.showSkeleton = false;
                    console.log(err);
                });
                if (this.isAllChecked) {
                    this.isAllChecked = true;
                }
            }, 500);
        },
}

4、dps是把当前可视的页面生成骨架 dps设计的规则如下:

    1. 只遍历可见区域可见的 DOM 节点,包括:非隐藏元素、宽高大于 0 的元素、非透明元素、内容不是空格的元素、位于浏览窗口可见区域内的元素等;
    1. 针对(背景)图片、文字、表单项、音频视频、Canvas、自定义特征的块等区域来生成颜色块;
    1. 页面节点使用的样式不可控,所以不可取 style 的尺寸相关的值,可通过 getBoundingClientRect 获取节点宽、高、距离视口距离的绝对值,计算出与当前设备的宽高对应的百分比作为颜色块的单位,来适配不同设备

5、由于dps生成的页面骨架是运行放在页面节点中,而不是index.html中的首屏骨架,所以看到的交互是看到'白屏->页面骨架->正常页面',实际dps只是单纯的生成页面的骨架的的代码。所以在首屏index.html页面加入loading的交互(具体看项目是否需要首屏loading的交互):

<body>
    <div id="app">
        <!-- 样式写在该地方是为了能够当页面过来了这个app下的所有内容都给替换掉 -->
        <style>
            #app {
              height: 100%;
              margin: 0px;
              padding: 0px;
            }
            #loader-wrapper {
              position: fixed;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
            }
            #loader {
              display: block;
              position: relative;
              left: 50%;
              top: 50%;
              width: 150px;
              height: 150px;
              margin: -75px 0 0 -75px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: red;
              -webkit-animation: spin 2s linear infinite;
              -ms-animation: spin 2s linear infinite;
              -moz-animation: spin 2s linear infinite;
              -o-animation: spin 2s linear infinite;
              animation: spin 2s linear infinite;
            }
            #loader:before {
              content: "";
              position: absolute;
              top: 5px;
              left: 5px;
              right: 5px;
              bottom: 5px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: red;
              -webkit-animation: spin 3s linear infinite;
              -moz-animation: spin 3s linear infinite;
              -o-animation: spin 3s linear infinite;
              -ms-animation: spin 3s linear infinite;
              animation: spin 3s linear infinite;
            }
            #loader:after {
              content: "";
              position: absolute;
              top: 15px;
              left: 15px;
              right: 15px;
              bottom: 15px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: red;
              -moz-animation: spin 1.5s linear infinite;
              -o-animation: spin 1.5s linear infinite;
              -ms-animation: spin 1.5s linear infinite;
              -webkit-animation: spin 1.5s linear infinite;
              animation: spin 1.5s linear infinite;
            }
            #loader-wrapper .fengche-logo {
              display: block;
              position: absolute;
              left: 50%;
              top: 50%;
              width: 60px;
              height: 60px;
              margin: -30px 0 0 -30px;
            }
            #loader-wrapper .fengche-logo img {
              width: 100%;
            }
            @-webkit-keyframes spin {
              0% {
                -webkit-transform: rotate(0deg);
                -ms-transform: rotate(0deg);
                transform: rotate(0deg);
              }
              100% {
                -webkit-transform: rotate(360deg);
                -ms-transform: rotate(360deg);
                transform: rotate(360deg);
              }
            }
            @keyframes spin {
              0% {
                -webkit-transform: rotate(0deg);
                -ms-transform: rotate(0deg);
                transform: rotate(0deg);
              }
              100% {
                -webkit-transform: rotate(360deg);
                -ms-transform: rotate(360deg);
                transform: rotate(360deg);
              }
            }
</style>
        <div id="loader-wrapper">
        <div id="loader"></div>
        <div class="fengche-logo">
            <img src="data:image/gif;base64,R0lGODlhZABkAPcAAAAAAP///+YAFOQABOQAA+MABeMAA+EBBOEBAuQCBuQCBOQCA+ECBuMDCOMDBOIGCeILDOQOEuUSFuQUGOUWGuYZGuUaHeYdHuYfIuYiJOclJ+cnK+coKegrLuUrLOcvMek0Nug3OOY4Oek7Puo+QetAQ+lBQutFSOtGSOtKS+tLTepNTuxQUe1aW+pZWu1jZPBrbu9rbfF+gPGBgvGDhPKNj/avsPWztOUACOQABuMABuMBCeUDDOQDC+QDCeQECuIFDOQHDuYTGeUWHegoMOkuM+pCRu1LT+xNUu1XXu9kafFyd/B2efKChe+ChfKFiPSVmfSdofaprPi3uve2uPrP0fvc3eYAC+YADeYADuUAEOUADOUAC+UADuYIFOcNG+gVI+gbJuovOes4ROs8RetATOxBTexETOxHUO1QWu9fZ/BpcfKDivWip/Wmqvi9wfrT1f3p6uYAEeYAE+YAFeUAE+cDF+cPIecQIecUJekfL+kgMOojM+kjM+kjMeokM+klM+ooN+szQOs2Q/7w8ecAGucAGOcAF/vX2/3o6v3r7f719ugAHvzf4/zh5f74+f7t8f7y9f/2+f/5+//8/f/7/f/9/v/+//78+/718v76+f3y8P749/rU0f3w7/7y8f3z8vjJxuMbFutQTPWppvWxr/a1sva2tPa4tfe6t/a5tvfBv/nNy/rY1/zg3/zl5OQAAOMAAOEAAN8AAN0AANsAAOICAeEHA+EDA+EGBeEJBuMRDeITEeMaGuQhH+UoJOcxL+czM+k6N+k6Ouo9PehAP+hCQOlDQudBQelEQ+lGRetJRupUU+xYVutcW+1eXuxhYO5oZ+1qae5ta+5xbu5ubu9zcu94du98e/GFg/GJifKOjPKQjvCQjvKSkvOWlfOZl/SfnfOcnPWiofOiovWmpvWpqPWsq/Wurfaysva2tve5uPi8u/e9vffCwfnFxfnKyvrQz/nOzvrS0vvW1vzi4vvh4fzk5Pzm5v3r6/3u7v3w8P7z8//7+//+/v///yH5BAEAAP8ALAAAAABkAGQAAAj/AAMIHEiw4CR4pZiY6ZPnDp6HECNKnEixokWLd/L4KcPEBjxJBUOKHCmwETcjsWLBwtGljsuXMGPKnEmzps2XXXDASlmCmxWSQAtCUoJBViwBc7jkILAzpdOnUKNKnUq1qkoCObjMERBL1gUlkIKSpOTtQspCW4xOwEDihYwnTeLKnUu3rt27eO0+kfGCBIYJXbkU6mqhRiWxBeMgSUlHgKwGKmbYQ0y5smWC9WiseCBrDp2URxJVnuchFp1DBmJJY3e5tWvK7aalPPSZgzzEbyTEQiuLWKjXwIOTDFVMluBYEaYEtdJrdw5Z0x4Jn05doCZqsnIMroCIZCZgsgpx/9EVrrp56uN0HQeWSWQlNOFz3Dp3vr7wUrq0xzozKWQUWQKkVYNllYCSzyvz1ACPffV9YxxXUBRECAWxHCLLC5TZ4MQaKQQzxA87xJLLggyaF40sh8RCQVgDMRHLHDl8UJk4tcgCSw5YaOEZcveUaF4wOXC1xECOmFWILKdYNg4CORwiwJNIycKMj9WlIoshsVjQiEAOGiJLCq2BIwsOTj7Z2CzbUEndCuHJ4k0AkywmhyzluBaOcWUKQMecSaoZnDm2yBELGo/A8YMWsYAgnWviZNeYmTlk8ImfwPUTQixZ+AAHOyjGgiFwjRJwyFZbydIMpcBJU2EsqNSAqQJSCP+3JA6GbEWHFrJog6prNjSA6BNJEIDDEPpMZ4MuODxKBxe7TLarZfxYgMMAaYyBQw4YVEcFBKYhVcgAHYDyrGUY5ICDIIFsAa556kQAy6gCWPjpuIgVMQAWfPTRBQEknOcOLzoY8iQXuLRDL2JGENCFHoDsW0J98vAyQGN06NBLJwcHhYLCezRMwMP1ubOLaXMcKQwl0x1m3sZddOwwg6c8ELBnuQp3jgn4rMyxxyDbtwoEZAqQBZKv7SONLLMooXPLPPuoji4E0PEZBK60do4HAGaBQJ3Usezyx1Se84ABo8ryjGX6SHNAhQIYMkAs83S988tUppMLDoXQIQs4lKn/8ossWDwabyzKyM003VSqA0EBh1wBwStB8VONLAbQsRWUc1jIxHReN+1nO7yklEE+AvFjTTWEEKQOB7JoAS+U8SI6SxTCdY64mv/yEncA7/jSFTIC1SNNA0cJ/mTZsXggQ87B2Q42qlW0IhA9ocuhd5ou0GI97JmnKIsJ2VTnfM+72uPLzAIMMEEl8ljQrZ5z4NBVM6ScN/6zm2zT3OuIvhkKBAOgTUooMA2M1ed+lFoEOlg3MdjpLQQou4EsJoiBaHiiRAhUkzeEIYsGwk5PWIgFfQIgBWdcg3kYnNvzSoQJcIjAKLS53AePdLZdZbA+9xCH+2Lhug9+cFkSqBqq/25YHSt0gwLZkYMPlyiAQszCYENUIfmmUw9qXGAWQWPiEumQg35F8XArDA4l4MACC8DQclpk4meOEQdKEbE1k1jFMoAQiwHoKY1ptJwsfsEKP72xMovgxjGMokQ8GvJ4BNhFKdT0R7EoohofgOHrDonHORiiALJIk48aSZJWPGMIKYkXJUeJlEPgQBbY2KQUKbOIc0DDKDjYHin1dIhaolGLt5LF5hjEyYHkwxogmGC8ZDhKqQ1ggrEgQBbk4Bk1zkEWLrjgAVc5kkvUoxpFMY3A8nhLB84pGOdIhwyGEYEG7EQLjfEMDpIFP1kAIx7TBOMUCUKJYcwiB83Eo9Ri0f+kD87BAMzgxEA04Yl1sABr3fJCCIbQBYpxZRd9Eh81RyKPXWAhT7jUwiyaYQQuGA8LEBCNSCaxjWTIog65IocsCqknAsjCHEv72jwLcqcrYPSHAoBFzV4pODrgAAP+CMo13qYBK4DgfZnbApokKk+xTGOlS8zc0HzBGkls4H16gsURENOBrqxCHxDgQp4OkZZrGE6miHEBiohpSQPIIhqpC0AVIhA1KKXoCYh5RlfepI6V9hRXzhBX8yYKlH6AgGwfjIUoxkGQdcBiC7CzkCkQAw86nkA6J5KlAOrwzGD8BDi9JAg9RuYkLspiFEIcyDheFNlY9FEs/hhCDhLwE07/DEMWxttnL1iBMteEliDjmMUWLASBbQS1IGLC6BxiYcBH2OMe/REJNXT6mwB4IgMVkuE+GdBG3xJWLN+YhSyC8Y6ReANFUPJpBeoRgHsogxe9cMNI3GEUGQzkHbnIQp4sGQsjCNS7TbXMDLDBD5JgA73HiwUxQLINLM4ilSJJhFmIQJAZ+PV4A4BAj17z28tAg23HkwU0BHINWTBCFtYgyTVmIQHBCuQFs7CVRrkx2ABXRxIlAHG8UCmQVx6pGiQpx2NsUJDiHAKlQK4xWs2DjwzkQHAW+oZAWLAbWcSAJITowCxaUJB6XCAHsNDAImr3XeG0IghdcODeBKKCKid5/yTPoEUIQrKOCZaXzDamjjywKoA51ekSKYgFI2KRYpJIAQER6E5BsGFWzpU5OKzQsU8HYDBLfIAWsajFm0c6hFlIOYV5ng6n8kQHAlCAHgHwxw2ccA1tVCEoR5jFpuO55OrcidSx+EAkWpOOWRQhrvbpcGVmIOlYmMA1+dBBLEgU7EcDpwXvSq+UXIMJJNCCa80OtRiJUVczxaKGrTEHLZCACV462zWcyAAO1Azuy4SCn6TLdq2nwwkJaAVzsbiyawjxgVm8wdzaBo4n7A07Qc0AONbYMsDnLRw4RCDNUOoCLN70Gm7MIlsDWcSi8Mzw4EzBV7DjAgHk+5o3OCAWyv8JAD+Ud1bPUQcKsNAC7HAQhH+/xqqyFkgbagGDs/6hCwMYgXmYAAuWCiAHEvjsa4wwCxDsegmz2KVwErawdA1gA+bRKzFzMIF35OMTKrsMCroyWSXQQhv+YAY2LAGcDtwLEGS4Fsapg5LcaoEHGAABEmAwg2+YoxQ3aAc8EJGI6IZkBV15wSTUQAsoRCJL+kCEsy5TLhwMQg0E4MIEUBicRZTGeHqSAxfe1pUJIrMHEtgACVbwDGpc4xoz8MY4OqCDZ1oB6uKQRAQqcA8TjMDwlMkHBUSehG/EQg4GEAd1snF8NZ6mltDfk8idYnpkPvY0XBiGGDIZgAtYwBUaiMX/q5W0AEF54x1XisWpRGKJebCCFfKQRzzmMQ960KMV+He/NgzQhZseUmpSA30xpAVY4BmZ81g4QAzxYAEREA9msTuV4Qyx4CWrYAVecAWxwAH7IBLv0BVQMQA5sAM90AM7QHqaNUuk5BlboAHrRAxfwAXQ0A2VwQ9dtQVBgAiX8GGfoXwh4QYFgE7pNAdaMIREWAengYJI+CRCGAEc4AVCUAdc4ADzIhbh8CKx0AIo0wa4FQvDIBJSkAMQl14AKDVJWIZPogPsoA0ZwA5CIAvlwQlwGIeL8F8FMQKrQjsBAAnYdST1UxDokACuI4CCOIiEWIiGeIiCOARfYAdCYAda/0ACLGACJTCJkygMw+AOBWEO4RELGKAIA/EEK4UDFDApBNEO4lV9qJiKqriKrNiKrog0sxCLsjgLtEALIyQQnCAtc9IEBDEJYTCB00YQi9AOqaAKxniMyJiMqoAKqHAKppAO6VAK0ogO51CN1liN6JCN0lgK0JgOpvCNpxCOzciM5FiOqLAO6JiO63ADgbdrA8EM6RcGGycQb3B8czAL0ZAxqPJUciAoKVcQSjCBQ9No+ugjB4YFXqIGIxEJxHAluDINBVki2KEFXkIM7igSixBMh4ArzLAJEWkem9AMstB/4zVmQPEK4FEIjuEL5gB8HwlH5+ALADIYH8BeYvEJOc4mB4XwHMbwaS95Gd9gUtohKCQgTYhRCUuQEnLgJYniAuVglD8pEvtgDs5wKVciKITmkrhRBErJFbFgABaQdymwAipQlmZ5lmiZlmq5lmypliuQAiCAARZAen2WEkRgc6/BBnY4JreiFANAAIAZmII5mIRZmIZ5mIhJACDIBeh0SlzIBtXRBtYABkahEiCYA5iZmZq5mZzZmZ75maA5AE0hC2BgDW1wCfXRCO9QDkuABmMgCLAZm7I5m7RZm7Z5m7gpCGOABktQDu+wJZUREAA7" />
        </div>
        </div>
    </div>
    <!-- built files will be auto injected -->
</body>

运行后,页面的交互为:首屏loading->页面骨架屏->数据请求回来后的页面正常ui交互

6、当项目中使用的手淘lib-flexible或者px进行了rem的处理和使用一些ui框架,例如vant,那么使用dps的话,直接生成的%骨架屏,就不适用了,就需要自定义骨架屏,把%处理为rem,这样页面的骨架屏ui交互才正常

mounted() {
    setTimeout(() => {
    /* 此举为自定义骨架屏,处理%转为rem,项目的根字体大小=37.5px */
            createSkeletonHTML({}).then(skeletonHTML => {
                // 最好自定义高度比例较大的,兼容各种较长的屏幕,例如375 * 1000 (iphoneX - 375 * 812)
                const skeletonWidth = document.body.clientWidth // 屏幕宽度
                const skeletonHeight = document.body.clientHeight // 屏幕高度
                const skeletonTop = 0 // 骨架屏从哪截取(width:375px下top的px值,不包括header的44px)
                let cacheHtml = JSON.parse(JSON.stringify(skeletonHTML))
                cacheHtml = cacheHtml.replace(/<style>\S+<\/style>/, '') // 去掉style,提取到公共
                // 下面代码是将百分比转换为rem:height(px) = val * clientHeight/100
                cacheHtml = cacheHtml.replace(/height:\d*\.*\d*%/g, (val) => {
                    return `height:${(skeletonHeight * val.slice(7, -1) / 3750).toFixed(6)}rem`
                })
                cacheHtml = cacheHtml.replace(/top:\d*\.*\d*%/g, (val) => {
                    return `top:${(skeletonHeight * val.slice(4, -1) / 3750).toFixed(6)}rem`
                })
                cacheHtml = cacheHtml.replace(/left:\d*\.*\d*%/g, (val) => {
                    return `left:${(skeletonWidth * val.slice(5, -1) / 3750).toFixed(6)}rem`
                })
                cacheHtml = cacheHtml.replace(/width:\d*\.*\d*%/g, (val) => {
                    return `width:${(skeletonWidth * val.slice(6, -1) / 3750).toFixed(6)}rem`
                })
                console.log('首屏骨架:', cacheHtml)
                // 骨架屏截取,去掉header44px(屏幕宽度375px下)+自定义的skeletonTop
                const skeletonCutTop = (skeletonTop + 44) / 37.5
                cacheHtml = cacheHtml.replace(/<div class="_" style="\S+<\/div>/g, (val) => {
                    let isCut = true
                    val = val.replace(/top:\d*\.*\d*rem/g, (topRem) => {
                        if (topRem.slice(4, -3) < skeletonCutTop) {
                            isCut = false
                            return topRem
                        }
                        return `top:${(topRem.slice(4, -3) - (44 / 37.5)).toFixed(6)}rem`
                    })
                    return isCut ? val : ''
                })
                // 骨架屏背景截取+下移
                cacheHtml = cacheHtml.replace(/<div class="_ __" style="\S+<\/div>/g, (val) => {
                    return `<div class="_ __" style="height:${((skeletonHeight - skeletonTop) / 37.5).toFixed(6)}rem;top:${(skeletonTop / 37.5).toFixed(6)}rem;z-index:990;background:#fff"></div>`
                })
                // 微信环境下header隐藏的处理
                cacheHtml = `<div class="skeleton-box"><div :class="[$store.getters.isWx||$store.getters.isApp?'':'not-wx','skeleton-content']">${cacheHtml}</div></div>`
                console.log('页面骨架:', cacheHtml)
            }).catch(e => {
                console.error(e)
            })
        }, 8000)
}

注意事项1:

lib-flexible方案限制font-size最大54px;更好的兼容是改为跟项目的max-width:1280px一样;max-width:1280px应是固定不应转rem,px改为大写PX

function refreshRem(){
    var width = docEl.getBoundingClientRect().width;
    if (width / dpr > 1280) {
        width = 1280 * dpr;
    }
    var rem = width / 10;
    docEl.style.fontSize = rem + 'px';
    flexible.rem = win.rem = rem;
}

注意事项2:

顶部类似搜索框的vant组件可不用骨架遮挡

const skeletonTop = 48 // 骨架屏从哪截取(width:375px下不需截取的height,不包括header的44px)

注意事项3:

vant组件也需要对应转rem,postcss.config.js改为:

module.exports = ({ file }) => {
    let remUnit
    if (file && file.dirname && file.dirname.indexOf('vant') > -1) {
        remUnit = 37.5
    } else {
        remUnit = 75
    }
    return {
        plugins: {
            'postcss-px2rem-exclude': {
                remUnit,
            }
        }
    }
}

注意事项4:

一般静态页面是不加骨架屏的,因为没有接口请求,本身加载速度很快,没有必要骨架屏,而且骨架屏也是个静态,只是为了有请求的交互体验提高!