所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。 —— ECMAScript 6 入門 阮一峯javascript
最近正在作的一個項目,包含"前臺"與"後臺", 前臺是對數據的展現,後臺是對用戶等相關權限的管理,大到頁面級別的權限,小到數據接口與菜單權限。總體是基於Oauth2.0作的一整套權限系統。css
用戶登陸成功後,返回權限列表,一級權限包含當前用戶能夠訪問到的子菜單:以下圖:html
管理員權限下,7個子系統圖標會所有返回,實際狀況返回多少菜單取決於當前用戶權限。例如普通用戶只能看到查看兩個子系統,由於是動態可配置的,考慮到後期可能會增長子系統,因此子系統路由、名稱、以及圖像所有都來源於後臺,經過後臺管理進行配置。vue
按照正常的思路,後臺獲取菜單信息,而後循環渲染:java
//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 複製代碼
正常的公司網速下,頁面加載效果以下圖:node
由於是動態獲取子系統圖標,而且頁面響應式,也就是任何屏幕都會一屏顯示,因此未知圖像的width,height,寬高會自動計算。數組
谷歌開發工具調製3G網速下,仔細看:promise
爲了模擬網速慢的狀況,使用谷歌瀏覽器network設置了3G網速,能夠看到頁面加載時,背景的container是一個最小高度(動態計算),當圖片加載成功而且循環顯示後,塊的高度撐搞,而且圖片也是從0高度到實際高度,在弱網絡狀況下,體驗通常。瀏覽器
考慮過經過固定寬高來解決,可是用不能知足響應式的一屏幕顯示。bash
由於template循環裏,動態賦值圖片src,因此此時的邏輯是先請求全部的數據列表(其中包含圖像src字段),而後動態綁定:src,而後圖片根據src才能加載,也就形成了弱網下的「延遲」問題。
根據緣由分析,container容器預先寫在template標籤裏,高度由圖像撐開,container內部循環完成後,基本的骨架已經渲染完成,而後圖像加載完成,container被撐高。
所以,若是先加載完成所有圖片,再進行渲染是否是能夠解決?
制定方案:
修改代碼以下:
//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對象組成的新數組:
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結果是這樣的:
接着上面的代碼
//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結構只是一個例子,實際狀況要根據本身的結構編寫。
看實際項目效果:
菜單動態獲取,container並無高度被撐開的狀況,頁面加載,container爲空,後臺返回數據後,container呈現。
參考文章: