virtualDom+Promise實(shí)現(xiàn)動(dòng)態(tài)圖像之響應(yīng)式頁面優(yōu)化

promise

Promise

所謂Promise,簡單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。
—— ECMAScript 6 入門 阮一峰

場景

最近正在做的一個(gè)項(xiàng)目,包含"前臺(tái)"與"后臺(tái)", 前臺(tái)是對(duì)數(shù)據(jù)的展示,后臺(tái)是對(duì)用戶等相關(guān)權(quán)限的管理,大到頁面級(jí)別的權(quán)限,小到數(shù)據(jù)接口與菜單權(quán)限。整體是基于Oauth2.0做的一整套權(quán)限系統(tǒng)。

image

用戶登錄成功后,返回權(quán)限列表,一級(jí)權(quán)限包含當(dāng)前用戶可以訪問到的子菜單:如下圖:

image

管理員權(quán)限下,7個(gè)子系統(tǒng)圖標(biāo)會(huì)全部返回,實(shí)際情況返回多少菜單取決于當(dāng)前用戶權(quán)限。例如普通用戶只能看到查看兩個(gè)子系統(tǒng),因?yàn)槭莿?dòng)態(tài)可配置的,考慮到后期可能會(huì)增加子系統(tǒng),所以子系統(tǒng)路由、名稱、以及圖像全部都來源于后臺(tái),通過后臺(tái)管理進(jìn)行配置。

按照正常的思路,后臺(tái)獲取菜單信息,然后循環(huán)渲染

//Home.vue
data() {
    return: {
        list:[]
    }
},
mounted() {
    this.GetMenuList();
},
methods:{
    GetMenuList() {
        ...
        this.list = res.result //后臺(tái)獲取菜單列表
    }
}
<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

正常的公司網(wǎng)速下,頁面加載效果如下圖:

image

因?yàn)槭莿?dòng)態(tài)獲取子系統(tǒng)圖標(biāo),并且頁面響應(yīng)式,也就是任何屏幕都會(huì)一屏顯示,所以未知圖像的width,height,寬高會(huì)自動(dòng)計(jì)算。

谷歌開發(fā)工具調(diào)制3G網(wǎng)速下,仔細(xì)看:

image

為了模擬網(wǎng)速慢的情況,使用谷歌瀏覽器network設(shè)置了3G網(wǎng)速,可以看到頁面加載時(shí),背景的container是一個(gè)最小高度(動(dòng)態(tài)計(jì)算),當(dāng)圖片加載成功并且循環(huán)顯示后,塊的高度撐搞,并且圖片也是從0高度到實(shí)際高度,在弱網(wǎng)絡(luò)情況下,體驗(yàn)一般。

考慮過通過固定寬高來解決,但是用不能滿足響應(yīng)式的一屏幕顯示。

問題原因

因?yàn)閠emplate循環(huán)里,動(dòng)態(tài)賦值圖片src,所以此時(shí)的邏輯是先請(qǐng)求所有的數(shù)據(jù)列表(其中包含圖像src字段),然后動(dòng)態(tài)綁定:src,然后圖片根據(jù)src才能加載,也就造成了弱網(wǎng)下的“延遲”問題。

根據(jù)原因分析,container容器預(yù)先寫在template標(biāo)簽里,高度由圖像撐開,container內(nèi)部循環(huán)完成后,基本的骨架已經(jīng)渲染完成,然后圖像加載完成,container被撐高。

因此,如果先加載完成全部圖片,再進(jìn)行渲染是不是可以解決?

制定方案:

  • 1.通過image對(duì)象,用代碼初始化load所有圖像。
  • 2.通過appedChild一次性將內(nèi)容插入container容器內(nèi)

1.利用Promise,異步加載圖像

修改代碼如下:

//Home.vue
<script>
export default {
  data () {
    return {
      list: [], //存放后臺(tái)返回的數(shù)據(jù)
    }
  },
  mounted() {
    this.GetMenuList();
  },
  methods:{
    GetMenuList() {
        ...
        this.list = res.result //后臺(tái)獲取菜單列表,不負(fù)責(zé)渲染,只存儲(chǔ)數(shù)據(jù)
        this.loadImages();
    },
    //圖像加載方法
    loadItemImage(img) {
      return new Promise(resolve => {
        const image = new Image();//通過new Image對(duì)象 加載圖像,本質(zhì)是一個(gè)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 = () => {      //添加跳轉(zhuǎn)點(diǎn)擊事件
          this.routerLink(img.url, img.name, img.id);//跳轉(zhuǎn)函數(shù)
        };
        image.className = "home_container_img_item";  //添加class屬性
      });
    },
    //圖像處理函數(shù)
    loadImages() {
      Promise.all(
        this.list.map(img => {
          return this.loadItemImage(img);
        })
      ).then(imgs => { 
         //imgs是list.map后生成的新數(shù)組,其中包含了n個(gè)image對(duì)象
         //得到已經(jīng)加載完成的image數(shù)組,準(zhǔn)備append到container容器
      });
    }
  }
}
</script>

解讀代碼:首先后臺(tái)返回?cái)?shù)據(jù)后調(diào)用loadImages,對(duì)list進(jìn)行map操作,每一項(xiàng)執(zhí)行l(wèi)oadItemImage方法。

loadItemImage方法:return 了一個(gè)Promise實(shí)例,實(shí)例內(nèi)部通過代碼new Image,創(chuàng)建了一個(gè)image實(shí)例,其中src、onclick、onload、className是image原型上的屬性和事件,因?yàn)閷?shí)際需要點(diǎn)擊圖像跳轉(zhuǎn),所以在image上新增了一些自定義屬性,供跳轉(zhuǎn)使用。當(dāng)圖像load成功后resolve。循環(huán)操作后,map返回一個(gè)由image對(duì)象組成的新數(shù)組:

image

Promise.all當(dāng)所有的圖像加載完成后,準(zhǔn)備進(jìn)行append到container節(jié)點(diǎn)。

如何append?

參考了vue的思想,通過虛擬Dom操作映射到真實(shí)的Dom下,避免直接循環(huán)append操作dom,數(shù)據(jù)驅(qū)動(dòng)視圖:

新建virtualDom.js

//聲明Dom類,用工廠方法進(jìn)行封裝:
class Dom {
    constructor(tags, attribute, children, event) {
      this.tags = tags;           // html標(biāo)簽字段,div p input..
      this.attribute = attribute; //class style 等html屬性
      this.children = children;   //子節(jié)點(diǎn)數(shù)組
      this.event = event;         //事件對(duì)象
      }
};
// 創(chuàng)建虛擬DOM,返回虛擬節(jié)點(diǎn)(object)
export function createElement(tags, attribute, children, event = {}) {
   return new DOM(tags, attribute, children, event);
   //少數(shù)dom元素可能存在事件,如點(diǎn)擊事件等,應(yīng)對(duì)多數(shù)情況,設(shè)置event默認(rèn)值{}
}

新建render.js :


// render方法虛擬DOM映射到真實(shí)DOM
export function render(dom) {
  // 根據(jù)標(biāo)簽創(chuàng)建元素
  let el = document.createElement(dom.tags);

  // 遍歷添加屬性
  for (let key in dom.attribute) {
    // 設(shè)置屬性的方法
    setAttr(el, key, dom.attribute[key]);
  }
  //添加事件
  for (let key in dom.event) {
    // 添加事件的方法
    AddEvent(el, key, dom.event[key]);
  }
  
  //針對(duì)于與大多數(shù)情況做了判斷,本項(xiàng)目的上下文環(huán)境中有三種情況
  dom.children.forEach(child => {
    if (child instanceof Dom)          //如果子節(jié)點(diǎn)是Dom類,那么就繼續(xù)向下遞歸
      child = render(child)
    else if (typeof child == 'string') //如果是文本那么就是文本節(jié)點(diǎn)
      child = document.createTextNode(child);
    else
      child = child;                   //其他html元素,本項(xiàng)目中是<img>元素
    // 添加到對(duì)應(yīng)元素內(nèi)
    el.appendChild(child);             //插入元素
  });
  return el;
}

// 設(shè)置屬性
export function setAttr(node, key, value) {
  switch (key) {
    case 'value':
      // node是一個(gè)input或者textarea就直接設(shè)置其value即可
      if (node.tagName.toLowerCase() === 'input' ||
        node.tagName.toLowerCase() === 'textarea') {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    case 'style':
      // 直接賦值行內(nèi)樣式
      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;
    //...根據(jù)情況添加
  }
}
// 將元素插入到頁面內(nèi)
export function renderDom(el, target) {
  target.appendChild(el);
}

準(zhǔn)備映射虛擬dom:

映射: Object to dom.

或者說是:
js to html.

本項(xiàng)目中的dom結(jié)果是這樣的:

image

接著上面的代碼

//Home.vue
import { createElement} from "common/utils/virtualDom.js";
import { render, renderDom} from "common/utils/render.js";

...
//圖像處理函數(shù)
loadImages() {
  Promise.all(
    this.list.map(img => {
      return this.loadItemImage(img);
    })
  ).then(imgs => { //imgs是list.map后生成的新數(shù)組,其中包含了n個(gè)image對(duì)象
     //得到已經(jīng)加載完成的image數(shù)組,準(zhǔn)備append到container容器
    let element = []
    for(let img of imgs) {
      element.push(createElement('div', {class: 'home_container_col_item'},
      [ //創(chuàng)建div,子元素img就是每一個(gè)img對(duì)象
        createElement('div',{class: 'home_container_content_img'},[img]), 
        createElement(//創(chuàng)建文本字,添加點(diǎn)擊事件
        'div',
        {class:'home_container_content_title'},
        [img.descritpion],
        {click:()=>{this.routerLink(img.url, img.name, img.id)}}),
      ]
      ))
    }
    //createElement的四個(gè)參數(shù)依次寫入
    //上邊的樹就是下面這種結(jié)構(gòu)
      // {
      //   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子系統(tǒng)']
      //     },
      //   ]
      // };
    //然后循環(huán)push到數(shù)組里。
    //...接上
    //container
    let virtualDom = createElement(
      "div",
      { class: "home_container_menu_row" },
      element
     );
    let el = render(virtualDom); // 渲染虛擬DOM得到真實(shí)的DOM結(jié)構(gòu)
    renderDom(el, document.getElementById("home_container_center"));//掛載dom
  });
}

上面的dom結(jié)構(gòu)只是一個(gè)例子,實(shí)際情況要根據(jù)自己的結(jié)構(gòu)編寫。

看實(shí)際項(xiàng)目效果:

image

菜單動(dòng)態(tài)獲取,container并沒有高度被撐開的情況,頁面加載,container為空,后臺(tái)返回?cái)?shù)據(jù)后,container呈現(xiàn)

參考文章:

Vue-節(jié)點(diǎn)、樹以及虛擬-DOM

vue核心之虛擬DOM(vdom)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容