原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)8——History對(duì)象

增加了頁(yè)面切換效果后,再配合手勢(shì)以及按鍵,讓單頁(yè)面app更像一個(gè)原生app, 然而卻引來(lái)了一個(gè)新的問(wèn)題, 通過(guò)瀏覽器自帶的前進(jìn)后退無(wú)法有選擇的采取動(dòng)畫(huà)效果。因?yàn)樵趐opstate的時(shí)候,都是采用一種動(dòng)畫(huà)。如果能夠在popstate事件中能夠知道當(dāng)前頁(yè)面以及要更換的頁(yè)面的位置,采取相應(yīng)的動(dòng)畫(huà)的,那就可以解決這個(gè)問(wèn)題。由于瀏覽器的history的棧無(wú)法讀取, 我們就創(chuàng)建一個(gè)History對(duì)象, 執(zhí)行的過(guò)程與瀏覽器的原生行為一致, 通過(guò)取當(dāng)前頁(yè)面在創(chuàng)建的History的位置, 以及它的前后的頁(yè)面與要切換的頁(yè)面進(jìn)行比較位置關(guān)系。
通過(guò)第六篇已經(jīng)了解瀏覽器原生的history棧的原理。這里創(chuàng)建一個(gè)History對(duì)象,構(gòu)造函數(shù)代碼如下

function History() {
    this.history = [];
    this.index = null;
}

history的數(shù)組代表瀏覽器中的頁(yè)面的url, index表示當(dāng)前頁(yè)面的url的索引值。
對(duì)應(yīng)的pushState方法如下

pushState: function (str) {
    if (this.index !== null) {
        var len = this.history.length;
        var nextIndex = this.index + 1;
        this.history.splice(nextIndex, this.len - nextIndex, str);
        this.index = nextIndex;
    }
    else {
        this.history.push(str);
        this.index = 0;
    }

    return str;
},

replaceState的方法如下

replaceState: function (str) {
    if (this.index) {
        this.history.splice(this.index, 1, str);
    }
    else {
        this.history.push(str);
        this.index = 0;
    }
    return str;
},

這兩個(gè)方法完全照瀏覽器的原理而創(chuàng)建的
對(duì)應(yīng)的還有back以及forward, 在單頁(yè)面中, 原生瀏覽器history的go方法不常用,因此不實(shí)現(xiàn), back方法和forward方法代碼如下:

back: function () {
    if (this.index === null) return "";
    return this.history[this.index === 0 ? 0 : --this.index];
},
forward: function () {
    if (this.index === null) return "";
    var len = this.history.length;
    return this.history[this.index === len - 1 ? len - 1 : ++this.index];
},

返回history索引值的url字符串

接著實(shí)現(xiàn)獲取當(dāng)前索引值前后位置的url值,代碼如下:

getSurround: function () {
    var len = this.history.length;
    if (this.index === null) {
        return {
            next: "",
            prev: ""
        };
    }
    else if (this.history.length === 1) {
        return {
            next: "",
            prev: ""
        };
    }
    else if (this.index === 0) {
        return {
            next: this.history[1],
            prev: ""
        };
    }
    else if (this.index === len - 1) {
        return {
            next: "",
            prev: this.history[len - 2]
        }
    }
    else {
        return {
            next: this.history[this.index + 1],
            prev: this.history[this.index - 1]
        }
    }
}

這里的情況比較多,不過(guò)思路很簡(jiǎn)單, 接著修改App對(duì)象,將History對(duì)象組合在App對(duì)象中,App對(duì)象的構(gòu)造函數(shù)加入一個(gè)history,放置History對(duì)象

function App(options) {
    options = options || {};
    App.extend(options, {
        appClass: "app",
        changeClass: "app-change",
        backClass: "app-back",
        changeState: "change-state",
        pageInReverse: "page-in-reverse",
        pageOutReverse: "page-out-reverse",
        pageIn: "page-in",
        pageOut: "page-out"
    });
    this.options = options;
    this.currentPage = null;
    this.staticPage = null;
    this.pageContainer = null;
    this.routeObj = {};
    this.history = new History();
}

然后修改_attachHistory方法,將對(duì)應(yīng)的方法在自定義的history對(duì)象中補(bǔ)上,代碼如下:

_attachHistory: function (page, isBack) {
    var newUrl =  page.url;

    if (isBack) {
        history.replaceState({data: newUrl}, "", newUrl);
        this.history.replaceState(newUrl);
    }
    else {
        history.pushState({data: newUrl}, "", newUrl);
        this.history.pushState(newUrl);
    }
},

接著修改initialize方法中的popstate事件,因?yàn)檫@里不僅有history操作,還要分別獲取當(dāng)前頁(yè)面的前后頁(yè)面的url,代碼如下:

window.addEventListener("popstate", function (ev) {
    if (ev.state && ev.state.data) {
        var url = ev.state.data;
        var page = that.routeObj[url];
        var urlObj = that.history.getSurround();
        if (urlObj.prev == url) {
            that.history.back();
            that.isRenderBack = true;
        }
        else if (urlObj.next === url) {
            that.history.forward();
            that.isRenderBack = false;
        }
        
        that._renderPage(page);
    }
}, false);

首先獲取當(dāng)前頁(yè)面的前后的url, 判斷下一個(gè)即將渲染的頁(yè)面,如果是后退操作, 自定義的history執(zhí)行back方法,反之執(zhí)行前進(jìn)方法, 然后設(shè)置不同的動(dòng)畫(huà)方式。
當(dāng)然這里有個(gè)特殊情況, 就是如果當(dāng)前頁(yè)面的前后頁(yè)面的url都相同, 然而這個(gè)情況在正常情況下可以完全被避免的。 假如在a頁(yè)面,pushstate到b頁(yè)面,然后在pushstate到a頁(yè)面,接著執(zhí)行history.back()操作。這一系列的操作方式是可以直接用a頁(yè)面, pushstate到b頁(yè)面,在執(zhí)行history.back(), 接著執(zhí)行history.forward()來(lái)代替。

除了前后切換的效果之外,還有一種就是直接替換完全不變,相當(dāng)于第二章那種的,html完全替換, 因此引入了renderInstance方法,因此為了修改代碼如下:

renderBack: function (page, isBack) {
    this.isRenderBack = true;
    this._render(page, isBack);
},
render: function (page, isBack) {
    this._render(page, isBack);
},
renderInstance: function (page, isBack) {
    this._render(page, isBack, true);
},
_render: function (page, isBack, instance) {
    if (typeof page === "string") page = this.routeObj[page];
    if (page === this.currentPage) return;
    this.routeObj[page.url] = page;
    this._attachHistory(page, isBack);
    this._renderPage(page, instance);
    this.pageContainer.scrollTop = 0;
},
_renderPage: function (page, isInstance) {
    if (this.currentPage) this.currentPage._dispose();
    this.currentPage = page;
    page.app = this;
    var that = this;

    document.title = page.title;
    
    page.render(function (html) {
        if (isInstance) {
            var changeDom = that.changeDom;
            changeDom.innerHTML = html;
            page._initialize(changeDom);
        }
        else {
            var backDom = that.backDom;
            backDom.innerHTML = html;
            that._replaceDom();
            page._initialize(backDom);
        }
    });
},

原理非常的的簡(jiǎn)單,通過(guò)一個(gè)布爾值來(lái)選擇那種切換形式。
這里還可以通過(guò)增加方法,或者在初始化中傳入不同的動(dòng)畫(huà)類(lèi)來(lái)切換動(dòng)畫(huà)類(lèi)型。讓動(dòng)畫(huà)變得非常的容易。

這一篇就是為多頁(yè)面切換的最后一篇, 做到了這篇后,做到了單頁(yè)面的基礎(chǔ)。頁(yè)面切換的動(dòng)態(tài)效果是單頁(yè)面和多頁(yè)面的最大區(qū)別,多頁(yè)面是無(wú)法做到的。雖然這里的代碼非常簡(jiǎn)單,App對(duì)象, History對(duì)象和Page對(duì)象加起來(lái)才500行代碼有余, 卻讓單調(diào)的多頁(yè)面改成交互優(yōu)雅的單頁(yè)面, 這里雖然只有4個(gè)簡(jiǎn)單的頁(yè)面, 但是這一套邏輯可以為轉(zhuǎn)化任意多個(gè)頁(yè)面的項(xiàng)目,把它轉(zhuǎn)化為你想要的單頁(yè)面。 現(xiàn)在正是你實(shí)踐的時(shí)候,選一個(gè)自己做過(guò)的多頁(yè)面,或者隨便想一個(gè)自己想做的, 可以按照1-8篇的的順序,把它們改造為單頁(yè)面。改造的效果會(huì)讓你得到真正的收獲。

總結(jié):?jiǎn)雾?yè)面的靈活性增加了開(kāi)發(fā)的無(wú)限玩法, 不變的是css, html和js基礎(chǔ), 不用多增加任何的其它的知識(shí)就能做一個(gè)優(yōu)質(zhì)的單頁(yè)面。如果你已經(jīng)通過(guò)上面的教程改編過(guò)了一個(gè)單頁(yè)面, 你覺(jué)得你自己可以把它引入項(xiàng)目中了, 事實(shí)告訴你, 離真正要做成一個(gè)項(xiàng)目, 現(xiàn)在才剛剛開(kāi)始,后面還有很多坑需要填補(bǔ)。在多頁(yè)面開(kāi)發(fā)時(shí),你不用關(guān)心一個(gè)頁(yè)面切換到另外一個(gè)頁(yè)面后, 之前的頁(yè)面怎么辦, 然而單頁(yè)面卻要異常關(guān)心,你需要把之前頁(yè)面的無(wú)關(guān)數(shù)據(jù)(包括組件)給清理了,防止垃圾累計(jì)導(dǎo)致頁(yè)面的卡頓以及移動(dòng)端電量的消耗, 另外就是異步的操作, 單頁(yè)面頻繁使用異步的操作, 比如ajax與indexdb操作, 一個(gè)微小的異步操作就會(huì)帶來(lái)整個(gè)應(yīng)用的崩潰。 因此這里推薦每個(gè)Page頁(yè)面都應(yīng)該是單獨(dú)的個(gè)體,它與其它頁(yè)面沒(méi)有任何關(guān)系,它只能通過(guò)App對(duì)象來(lái)選擇渲染與否。

后續(xù)更新:之前的篇章都是靜態(tài)的頁(yè)面,接下來(lái)就要轉(zhuǎn)化為動(dòng)態(tài)的,通過(guò)ajax與后端交互數(shù)據(jù),然后更新前端顯示的信息。在這之前, 需要給之前創(chuàng)建的服務(wù)器加上POST應(yīng)答的功能。

請(qǐng)用移動(dòng)設(shè)備打開(kāi)該案例
案例鏈接


原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)1——傳統(tǒng)頁(yè)面的開(kāi)發(fā)
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)2——Page對(duì)象
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)3——App對(duì)象
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)4——tap事件與slide事件
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)5——nodejs服務(wù)器的搭建
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)6——history api應(yīng)用
原生開(kāi)發(fā)移動(dòng)web單頁(yè)面(step by step)7——頁(yè)面切換動(dòng)畫(huà)

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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