美文网首页
virtualDom+Promise实现动态图像之响应式页面优化

virtualDom+Promise实现动态图像之响应式页面优化

作者: 冯阳阳的博客 | 来源:发表于2019-03-28 18:30 被阅读0次
    promise

    Promise

    所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
    —— ECMAScript 6 入门 阮一峰

    场景

    最近正在做的一个项目,包含"前台"与"后台", 前台是对数据的展示,后台是对用户等相关权限的管理,大到页面级别的权限,小到数据接口与菜单权限。整体是基于Oauth2.0做的一整套权限系统。

    image

    用户登录成功后,返回权限列表,一级权限包含当前用户可以访问到的子菜单:如下图:

    image

    管理员权限下,7个子系统图标会全部返回,实际情况返回多少菜单取决于当前用户权限。例如普通用户只能看到查看两个子系统,因为是动态可配置的,考虑到后期可能会增加子系统,所以子系统路由、名称、以及图像全部都来源于后台,通过后台管理进行配置。

    按照正常的思路,后台获取菜单信息,然后循环渲染

    //Home.vue
    data() {
        return: {
            list:[]
        }
    },
    mounted() {
        this.GetMenuList();
    },
    methods:{
        GetMenuList() {
            ...
            this.list = res.result //后台获取菜单列表
        }
    }
    
    <div class='container'>
        <div class="col_item" v-for="(item,index) in list" :key="index">
          <div class="content_img">
            <img :src="item.src" alt class="img_item"
              @click="routerLink(item.url)"
            >
          </div>
          <div class="content_title">{{ item.descritpion }}</div>
         </div>
    </div
    

    正常的公司网速下,页面加载效果如下图:

    image

    因为是动态获取子系统图标,并且页面响应式,也就是任何屏幕都会一屏显示,所以未知图像的width,height,宽高会自动计算。

    谷歌开发工具调制3G网速下,仔细看:

    image

    为了模拟网速慢的情况,使用谷歌浏览器network设置了3G网速,可以看到页面加载时,背景的container是一个最小高度(动态计算),当图片加载成功并且循环显示后,块的高度撑搞,并且图片也是从0高度到实际高度,在弱网络情况下,体验一般。

    考虑过通过固定宽高来解决,但是用不能满足响应式的一屏幕显示。

    问题原因

    因为template循环里,动态赋值图片src,所以此时的逻辑是先请求所有的数据列表(其中包含图像src字段),然后动态绑定:src,然后图片根据src才能加载,也就造成了弱网下的“延迟”问题。

    根据原因分析,container容器预先写在template标签里,高度由图像撑开,container内部循环完成后,基本的骨架已经渲染完成,然后图像加载完成,container被撑高。

    因此,如果先加载完成全部图片,再进行渲染是不是可以解决?

    制定方案:

    • 1.通过image对象,用代码初始化load所有图像。
    • 2.通过appedChild一次性将内容插入container容器内

    1.利用Promise,异步加载图像

    修改代码如下:

    //Home.vue
    <script>
    export default {
      data () {
        return {
          list: [], //存放后台返回的数据
        }
      },
      mounted() {
        this.GetMenuList();
      },
      methods:{
        GetMenuList() {
            ...
            this.list = res.result //后台获取菜单列表,不负责渲染,只存储数据
            this.loadImages();
        },
        //图像加载方法
        loadItemImage(img) {
          return new Promise(resolve => {
            const image = new Image();//通过new Image对象 加载图像,本质是一个object
            image.src = img.imgUrl;   //指定src
            image.url = img.url;      //自定义添加一些字段暴漏到外部
            image.id = img.id;        //自定义添加一些字段暴漏到外部
            image.name = img.name;
            image.descritpion = img.descritpion;  //自定义添加一些字段暴漏到外部
            image.onload = () => resolve(image);  //加载图像
            image.onerror = () => resolve(image);
            image.onclick = () => {      //添加跳转点击事件
              this.routerLink(img.url, img.name, img.id);//跳转函数
            };
            image.className = "home_container_img_item";  //添加class属性
          });
        },
        //图像处理函数
        loadImages() {
          Promise.all(
            this.list.map(img => {
              return this.loadItemImage(img);
            })
          ).then(imgs => { 
             //imgs是list.map后生成的新数组,其中包含了n个image对象
             //得到已经加载完成的image数组,准备append到container容器
          });
        }
      }
    }
    </script>
    

    解读代码:首先后台返回数据后调用loadImages,对list进行map操作,每一项执行loadItemImage方法。

    loadItemImage方法:return 了一个Promise实例,实例内部通过代码new Image,创建了一个image实例,其中src、onclick、onload、className是image原型上的属性和事件,因为实际需要点击图像跳转,所以在image上新增了一些自定义属性,供跳转使用。当图像load成功后resolve。循环操作后,map返回一个由image对象组成的新数组:

    image

    Promise.all当所有的图像加载完成后,准备进行append到container节点。

    如何append?

    参考了vue的思想,通过虚拟Dom操作映射到真实的Dom下,避免直接循环append操作dom,数据驱动视图:

    新建virtualDom.js

    //声明Dom类,用工厂方法进行封装:
    class Dom {
        constructor(tags, attribute, children, event) {
          this.tags = tags;           // html标签字段,div p input..
          this.attribute = attribute; //class style 等html属性
          this.children = children;   //子节点数组
          this.event = event;         //事件对象
          }
    };
    // 创建虚拟DOM,返回虚拟节点(object)
    export function createElement(tags, attribute, children, event = {}) {
       return new DOM(tags, attribute, children, event);
       //少数dom元素可能存在事件,如点击事件等,应对多数情况,设置event默认值{}
    }
    

    新建render.js :

    
    // render方法虚拟DOM映射到真实DOM
    export function render(dom) {
      // 根据标签创建元素
      let el = document.createElement(dom.tags);
    
      // 遍历添加属性
      for (let key in dom.attribute) {
        // 设置属性的方法
        setAttr(el, key, dom.attribute[key]);
      }
      //添加事件
      for (let key in dom.event) {
        // 添加事件的方法
        AddEvent(el, key, dom.event[key]);
      }
      
      //针对于与大多数情况做了判断,本项目的上下文环境中有三种情况
      dom.children.forEach(child => {
        if (child instanceof Dom)          //如果子节点是Dom类,那么就继续向下递归
          child = render(child)
        else if (typeof child == 'string') //如果是文本那么就是文本节点
          child = document.createTextNode(child);
        else
          child = child;                   //其他html元素,本项目中是<img>元素
        // 添加到对应元素内
        el.appendChild(child);             //插入元素
      });
      return el;
    }
    
    // 设置属性
    export function setAttr(node, key, value) {
      switch (key) {
        case 'value':
          // node是一个input或者textarea就直接设置其value即可
          if (node.tagName.toLowerCase() === 'input' ||
            node.tagName.toLowerCase() === 'textarea') {
            node.value = value;
          } else {
            node.setAttribute(key, value);
          }
          break;
        case 'style':
          // 直接赋值行内样式
          node.style.cssText = value;
          break;
        default:
          node.setAttribute(key, value);
          break;
      }
    }
    //添加事件
    function AddEvent(el, key, funcEvent) {
      switch (key) {
        case 'click':     //单击
          el.onclick = funcEvent;
          break;
        case 'dbClick':
          //..
          break;
        //...根据情况添加
      }
    }
    // 将元素插入到页面内
    export function renderDom(el, target) {
      target.appendChild(el);
    }
    

    准备映射虚拟dom:

    映射: Object to dom.

    或者说是:
    js to html.

    本项目中的dom结果是这样的:

    image

    接着上面的代码

    //Home.vue
    import { createElement} from "common/utils/virtualDom.js";
    import { render, renderDom} from "common/utils/render.js";
    
    ...
    //图像处理函数
    loadImages() {
      Promise.all(
        this.list.map(img => {
          return this.loadItemImage(img);
        })
      ).then(imgs => { //imgs是list.map后生成的新数组,其中包含了n个image对象
         //得到已经加载完成的image数组,准备append到container容器
        let element = []
        for(let img of imgs) {
          element.push(createElement('div', {class: 'home_container_col_item'},
          [ //创建div,子元素img就是每一个img对象
            createElement('div',{class: 'home_container_content_img'},[img]), 
            createElement(//创建文本字,添加点击事件
            'div',
            {class:'home_container_content_title'},
            [img.descritpion],
            {click:()=>{this.routerLink(img.url, img.name, img.id)}}),
          ]
          ))
        }
        //createElement的四个参数依次写入
        //上边的树就是下面这种结构
          // {
          //   tags: "div",
          //   attribute: {
          //     class: "home_container_col_item"
          //   },
          //   children: [
          //     {
          //       tags: "div",
          //       attribute: {
          //         class: "home_container_content_img"
          //       },
          //       children: [image]
          //     },
          //     {
          //       tags: "div",
          //       attribute: {
          //         class: "home_container_content_title"
          //       },
          //       children: ['xxx子系统']
          //     },
          //   ]
          // };
        //然后循环push到数组里。
        //...接上
        //container
        let virtualDom = createElement(
          "div",
          { class: "home_container_menu_row" },
          element
         );
        let el = render(virtualDom); // 渲染虚拟DOM得到真实的DOM结构
        renderDom(el, document.getElementById("home_container_center"));//挂载dom
      });
    }
    

    上面的dom结构只是一个例子,实际情况要根据自己的结构编写。

    看实际项目效果:

    image

    菜单动态获取,container并没有高度被撑开的情况,页面加载,container为空,后台返回数据后,container呈现

    参考文章:

    Vue-节点、树以及虚拟-DOM

    vue核心之虚拟DOM(vdom)

    相关文章

      网友评论

          本文标题:virtualDom+Promise实现动态图像之响应式页面优化

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