virtualDom+Promise實現動態影象之響應式頁面優化

fengyangyang發表於2019-03-28

promise

Promise

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。 —— ECMAScript 6 入門 阮一峰

場景

最近正在做的一個專案,包含"前臺"與"後臺", 前臺是對資料的展示,後臺是對使用者等相關許可權的管理,大到頁面級別的許可權,小到資料介面與選單許可權。整體是基於Oauth2.0做的一整套許可權系統。

virtualDom+Promise實現動態影象之響應式頁面優化

使用者登入成功後,返回許可權列表,一級許可權包含當前使用者可以訪問到的子選單:如下圖:

virtualDom+Promise實現動態影象之響應式頁面優化

管理員許可權下,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
複製程式碼

正常的公司網速下,頁面載入效果如下圖:

virtualDom+Promise實現動態影象之響應式頁面優化

因為是動態獲取子系統圖示,並且頁面響應式,也就是任何螢幕都會一屏顯示,所以未知影象的width,height,寬高會自動計算。

谷歌開發工具調製3G網速下,仔細看:

virtualDom+Promise實現動態影象之響應式頁面優化

為了模擬網速慢的情況,使用谷歌瀏覽器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物件組成的新陣列:

virtualDom+Promise實現動態影象之響應式頁面優化

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結果是這樣的:

virtualDom+Promise實現動態影象之響應式頁面優化

接著上面的程式碼

//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結構只是一個例子,實際情況要根據自己的結構編寫。

看實際專案效果:

virtualDom+Promise實現動態影象之響應式頁面優化

選單動態獲取,container並沒有高度被撐開的情況,頁面載入,container為空,後臺返回資料後,container呈現

參考文章:

Vue-節點、樹以及虛擬-DOM

vue核心之虛擬DOM(vdom)

相關文章