最近學習H5遊戲編程,Phaser代碼裏面有以下的代碼, 就能加載圖片。react
this.load.image("phaser-logo-i", "../images/logo.png");
複製代碼
也有專門的資源加載庫PreloadJSios
二者的原理都是相同的,請求到資源,而後利用URL.createObjectURL
生成url地址,以便以後複用。git
Phaser資源加載器和PreloadJS都能進行資源記載,並經過一個key來使用。 看起來很美好,可是當頁面再次刷新的時候,仍是會發出請求,再次加載文件。github
這裏就延伸到瀏覽器緩存了,比較理想的就是Service Worker啦,合理配置以後,能夠不發出請求,離線依舊可使用。web
我這裏就是就是用的另一種比較常見的緩存方案,indexedDB。ajax
const resourcesInfo = [{
pre: ["promise"],
key: "axios",
ver: "1.2",
url: "https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"
},{
key: "mqtt",
ver: "1.0",
url: "https://cdnjs.cloudflare.com/ajax/libs/mqtt/4.2.6/mqtt.min.js"
},{
key: "lottie",
url: "https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.7.8/lottie.min.js"
},{
key: "flv",
url: "https://cdnjs.cloudflare.com/ajax/libs/flv.js/1.5.0/flv.min.js"
},
{
key: "promise",
url: "https://cdnjs.cloudflare.com/ajax/libs/promise-polyfill/8.2.0/polyfill.min.js"
}];
let startTime;
const rl = new ResourceLoader(resourcesInfo, idb);
rl.on("progress", (progress, info)=>{
console.log("progress:", progress, info);
});
rl.on("completed", (datas)=>{
console.log("completed event:", datas);
console.log("total time:", Date.now() - startTime)
});
rl.on("loaded", (datas)=>{
console.log("loaded event:", datas);
console.log("total time:", Date.now() - startTime)
});
rl.on("error", (error, info)=>{
console.log("error event:", error.message, info);
});
複製代碼
這裏先看看幾個資源屬性,axios
咱們來過一下流程:promise
檢查資源是否加載完畢
若是記載完畢,直接觸發completed事件瀏覽器
從indexedDB載入緩存
檢查是否有可加載的資源
若是有,進入4
若是沒有,檢查是否均加載完畢, 加載完畢觸發complete事件
若是沒有,進入5
若是有,進入3
若是有,更新內部狀態,觸發progress事件,若是加載完畢,直接觸發completed事件, 反之進入3
若是沒有,發起網絡請求, 下載資源,存入indexedDB,更新內部狀態。 觸發progress事件,若是加載完畢,直接觸發completed事件, 反之進入3
備註:
工欲善其事,必先利其器。 先擼工具方法。
咱們須要網路請求,對象複製(不破壞傳入的參數),參數的合併,檢查key值等。
function fetchResource(url){
return fetch(url, {
method: "get",
responseType: 'blob'
}).then(res=>{
if(res.status >= 400){
throw new Error(res.status + "," + res.statusText);
}
return res;
}).then(res=> res.blob());
}
複製代碼
function compareVersion(v1 = "", v2 = "") {
if(v1 == v2){
return 0;
}
const version1 = v1.split('.')
const version2 = v2.split('.')
const len = Math.max(version1.length, version2.length);
while (version1.length < len) {
version1.push('0')
}
while (version2.length < len) {
version2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(version1[i]) || 0;
const num2 = parseInt(version2[i]) || 0;
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
複製代碼
function copyObject(obj){
return JSON.parse(JSON.stringify(obj));
}
複製代碼
function generateBlobUrl(blob){
return URL.createObjectURL(blob);
}
複製代碼
function validateKey(resources){
let failed = false;
// 空key檢查
const emptyKeys = resources.filter(r=> r.key == undefined || r.key == "");
if(emptyKeys.length > 0){
failed = true;
console.error("ResourceLoader validateKey: 資源都必須有key");
return failed;
}
// 資源重複檢查
const results = Object.create(null);
resources.forEach(r=>{
(results[r.key] = results[r.key] || []).push(r);
});
Object.keys(results).forEach(k=>{
if(results[k].length > 1){
console.error("key " + k + " 重複了," + results[k].map(r=>r.url).join(","));
failed = true;
}
});
return failed;
}
複製代碼
咱們的資源加載是要有進度通知,錯誤通知,完畢通知的。這就是一個典型的消息通知。
class Emitter {
constructor() {
this._events = Object.create(null);
}
emit(type, ...args) {
const events = this._events[type];
if (!Array.isArray(events) || events.length === 0) {
return;
}
events.forEach(event => event.apply(null, args));
}
on(type, fn) {
const events = this._events[type] || (this._events[type] = []);
events.push(fn)
}
off(type, fn) {
const events = this._events[type] || (this._events[type] = []);
const index = events.find(f => f === fn);
if (index < -1) {
return;
}
events.splice(index, 1);
}
}
複製代碼
咱們爲了方便擴展,對緩存管理進行一次抽象。
CacheManager的傳入參數storage真正負責存儲的, 我這裏使用一個極其輕量級的庫idb-keyval。更多的庫能夠參見IndexedDB。
class CacheManager {
constructor(storage) {
this.storage = storage;
this._cached = Object.create(null);
}
load(keys) {
const cached = this._cached;
return this.storage.getMany(keys).then(results => {
results.forEach((value, index) => {
if (value !== undefined) {
cached[keys[index]] = value;
}
});
return cached;
})
}
get datas() {
return this._cached;
}
get(key) {
return this._cached[key]
}
isCached(key) {
return this._cached[key] != undefined;
}
set(key, value) {
return this.storage.set(key, value);
}
clear() {
this._cached = Object.create(null);
// return this.storage.clear();
}
del(key){
delete this._cached[key];
// return this.storage.del();
}
}
複製代碼
Loader繼承Emitter,本身就是一個消息中心了。
/ status: undefined loading loaded error
class ResourceLoader extends Emitter {
constructor(resourcesInfo, storage = defaultStorage) {
}
// 重置
reset() {
}
// 檢查是否加載完畢
isCompleted() {
}
// 當某個資源加載完畢後的回調
onResourceLoaded = (info, data, isCached) => {
}
// 某個資源加載失敗後的回調
onResourceLoadError(err, info) {
}
// 進行下一次的Load, onResourceLoadError和onResourceLoaded均會調用次方法
nextLoad() {
}
// 獲取下載進度
getProgress() {
}
// 獲取地址url
get(key) {
}
// 得到緩存信息
getCacheData(key) {
}
// 請求資源
fetchResource(rInfo) {
}
// 某個資源記載失敗後,設置依賴他的資源的狀態
setFactorErrors(info) {
}
// 檢查依賴項是否是都被加載了
isPreLoaded(pre) {
}
// 查找能夠加載的資源
findCanLoadResource() {
}
// 獲取資源
fetchResources() {
}
// 準備,加載緩存
prepare() {
}
// 開始加載
startLoad() {
}
}
複製代碼
簡單先看一下骨架, startLoad爲入口。 其他每一個方法都加上了備註。很好理解。 Loader類的所有代碼
const defaultStorage = {
get: noop,
getMany: noop,
set: noop,
del: noop,
clear: noop
};
// status: undefined loading loaded error
class ResourceLoader extends Emitter {
constructor(resourcesInfo, storage = defaultStorage) {
super();
this._originResourcesInfo = resourcesInfo;
this._cacheManager = new CacheManager(storage);
this.reset();
}
reset() {
const resourcesInfo = this._originResourcesInfo;
this.resourcesInfo = resourcesInfo.map(r => copyObject(r));
this.resourcesInfoObj = resourcesInfo.reduce((obj, cur) => {
obj[cur.key] = cur;
return obj;
}, {});
// 已緩存, 緩存不等已加載,只有調用URL.createObjectURL以後,纔會變爲loaded
this._loaded = Object.create(null);
this._cacheManager.clear();
}
isCompleted() {
return this.resourcesInfo.every(r => r.status === "loaded" || r.status === "error");
}
onResourceLoaded = (info, data, isCached) => {
console.log(`${info.key} is loaded`);
const rInfo = this.resourcesInfo.find(r => r.key === info.key);
rInfo.status = "loaded";
this._loaded[rInfo.key] = {
key: rInfo.key,
url: generateBlobUrl(data)
};
this.emit("progress", this.getProgress(), rInfo);
if (!isCached) {
const info = {
data,
key: rInfo.key,
url: rInfo.url,
ver: rInfo.ver || ""
};
this._cacheManager.set(info.key, info);
}
this.nextLoad();
}
nextLoad() {
if (!this.isCompleted()) {
return this.fetchResources()
}
this.emit("completed", this._loaded);
// 所有正常加載,才觸發loaded事件
if (this.resourcesInfo.every(r => r.status === "loaded")) {
this.emit("loaded", this._loaded);
}
}
getProgress() {
const total = this.resourcesInfo.length;
const loaded = Object.keys(this._loaded).length;
return {
total,
loaded,
percent: total === 0 ? 0 : + ((loaded / total) * 100).toFixed(2)
}
}
get(key) {
return (this._loaded[key] || this.resourcesInfoObj[key]).url;
}
getCacheData(key) {
return this._cacheManager.get(key)
}
fetchResource(rInfo) {
return fetchResource(`${rInfo.url}?ver=${rInfo.ver}`)
.then(blob => this.onResourceLoaded(rInfo, blob))
.catch(error => this.onResourceLoadError(error, rInfo));
}
onResourceLoadError(err, info) {
const rInfo = this.resourcesInfo.find(r => r.key === info.key);
rInfo.status = "error";
console.error(`${info.key}(${info.url}) load error: ${err.message}`);
this.emit("error", err, info);
// 因被依賴,會致使其餘依賴他的資源爲失敗
this.setFactorErrors(info);
this.nextLoad();
}
setFactorErrors(info) {
// 未開始,pre包含info.key
const rs = this.resourcesInfo.filter(r => !r.status && r.pre && r.pre.indexOf(info.key) >= 0);
if (rs.length < 0) {
return;
}
rs.forEach(r => {
console.warn(`mark ${r.key}(${r.url}) as error because it's pre failed to load`);
r.status = "error"
});
}
isPreLoaded(pre) {
const preArray = Array.isArray(pre) ? pre : [pre]
return preArray.every(p => this._loaded[p] !== undefined);
}
findCanLoadResource() {
const info = this.resourcesInfo.find(r => r.status == undefined && (r.pre == undefined || this.isPreLoaded(r.pre)));
return info;
}
fetchResources() {
let info = this.findCanLoadResource();
while (info) {
const cache = this._cacheManager.get(info.key);
// 有緩存
if (cache) {
const isOlder = compareVersion(cache.ver, info.ver || "") < 0;
// 緩存過時
if (isOlder) {
console.warn(`${info.key} is cached, but is older version, cache:${cache.ver} request: ${info.ver}`);
} else {
console.log(`${info.key} is cached, load from db, pre`, info.pre);
this.onResourceLoaded(info, cache.data, true);
info = this.findCanLoadResource();
continue;
}
}
console.log(`${info.key} load from network ${info.url}, pre`, info.pre);
info.status = "loading";
this.fetchResource(info);
info = this.findCanLoadResource();
}
}
prepare() {
const keys = this.resourcesInfo.map(r => r.key);
return this._cacheManager.load(keys);
}
startLoad() {
const failed = validateKey(this.resourcesInfo);
if (failed) {
return;
}
if (this.isCompleted()) {
this.emit("completed", this._cacheManager.datas);
}
this.prepare()
.then(() => this.fetchResources())
.catch(err=> this.emit("error", err));
}
}
複製代碼