上面11篇論述了主要的原理,作為最后一篇,我們主要論述單頁(yè)面相比于多頁(yè)面的靈活的部分,如何使用最原始的html,js,css發(fā)揮web的最大魅力。 動(dòng)畫(huà),過(guò)場(chǎng)動(dòng)畫(huà)單頁(yè)面要比多頁(yè)面靈活,擁有過(guò)場(chǎng)動(dòng)畫(huà)是它最直觀的表現(xiàn),并且頁(yè)面切換不會(huì)出現(xiàn)白屏的現(xiàn)象。 在底層ReplaceProto對(duì)象中,專門(mén)設(shè)置了兩個(gè)dom,一個(gè)dom作為放置當(dāng)前頁(yè)面,另一個(gè)dom放置切換頁(yè)面。在切換過(guò)程中,通過(guò)兩個(gè)dom的過(guò)渡產(chǎn)生過(guò)場(chǎng)動(dòng)畫(huà)。動(dòng)畫(huà)方式在css3中定義,然后根據(jù)情況進(jìn)行不同的動(dòng)畫(huà)切換, 同時(shí)完成后退和前進(jìn)兩個(gè)過(guò)場(chǎng)
它們共同組成了頁(yè)面的動(dòng)畫(huà)效果 默認(rèn)的動(dòng)畫(huà)由App對(duì)象里面定義,在附帶的app.css中定義行為 this.options = { changeClass: "app-change", backClass: "app-back", area: "change-state", in: { // 進(jìn)場(chǎng)動(dòng)畫(huà) back: "page-out", change: "page-in" }, out: { // 出場(chǎng)動(dòng)畫(huà) back: "page-in-reverse", change: "page-out-reverse" } }; 在切換頁(yè)面的時(shí)候,也可以通過(guò)傳入不同的類名,實(shí)現(xiàn)自定義動(dòng)畫(huà), 詳細(xì)見(jiàn)ReplaceProto的render方法。render: function (pagename, isReplace, option) _getReplaceClass: function (option) { var options = this.options; option = option || {}; return { backStaticClass: option.backClass || options.backClass, changeStaticClass: option.changeClass || options.changeClass, areaClass: option.area || options.area, backActiveClass: this.isRenderBack ? option.backClass || options.out.back : option.backClass || options.in.back, changeActiveClass: this.isRenderBack ? option.changeClass || options.out.change : option.changeClass || options.in.change } }, 類似的在PopUp對(duì)象中同樣有頁(yè)面切換動(dòng)畫(huà),PopUp對(duì)象還要有個(gè)彈出彈窗和關(guān)閉的動(dòng)畫(huà)。請(qǐng)看動(dòng)畫(huà)設(shè)置: this.options = { className: "popup", changeClass: "popup-change", backClass: "popup-back", area: "popup-state", currentIn: { // 顯示頁(yè)面上的入場(chǎng) backClass: "popup-active-out", changeClass: "popup-active-in" }, currentOut: { // 顯示頁(yè)面上的出場(chǎng) backClass: "popup-active-in-reverse", changeClass: "popup-active-out-reverse" }, staticIn: "popup-static-out", // 彈窗進(jìn)入頁(yè)面 staticOut: "popup-static-in" // 彈窗隱藏 }; 可以在PopUp配置或在show和hidden方法中設(shè)置不同的動(dòng)畫(huà)效果。 show: function (dom, config, target, isDismisBeforeShow) // config.staticIn hidden: function (option, bk) // option.staticOut或show方法傳入的config.staticOut 對(duì)于組件的頁(yè)面切換動(dòng)畫(huà)和App的動(dòng)畫(huà)切換是一致的。 初始化頁(yè)面歷史緩存如果用戶從首頁(yè)進(jìn)入網(wǎng)站,我們不用對(duì)history記錄做任何更改,這是一種常規(guī)情況。然而網(wǎng)站的入口是url,如果url不是進(jìn)入首頁(yè),而是從詳情頁(yè)或是付款頁(yè)進(jìn)入網(wǎng)站,或者通過(guò)其它手段(掃碼等)。 當(dāng)用戶在該頁(yè)面進(jìn)行了操作(如果不做任何操作,點(diǎn)擊后退應(yīng)該是退出網(wǎng)站),為了讓用戶有一個(gè)瀏覽流程,在詳情頁(yè)點(diǎn)擊后退應(yīng)該是返回到列表頁(yè)。 原理很簡(jiǎn)單,判斷進(jìn)入初始頁(yè),然后先pushState若干個(gè)頁(yè)面。然后渲染頁(yè)面。從App.initialize的方法來(lái)看,有一個(gè)_prevAttachHistory(prevHistory)操作,這就是為了該目的。這里的prevHistory是由開(kāi)發(fā)初始化定義的App實(shí)例對(duì)象的getInitHistory來(lái)得到,這是一個(gè)url數(shù)組。 例如我們初始化定義 app.getInitHistory = function (pathname) { if (pathname === "/detail") return [ "/home" ]; // 也可以帶參數(shù)的url } 通過(guò)這樣配置后,頁(yè)面直接打開(kāi)詳情頁(yè),在詳情頁(yè)操作后,點(diǎn)擊后退鍵回退到首頁(yè),而不是退出該網(wǎng)站。注意,如果進(jìn)入頁(yè)面后沒(méi)有任何操作,直接點(diǎn)擊退出,是不會(huì)退到首頁(yè)的,這是瀏覽器的一大特性,只有用戶與該頁(yè)面有交互,即使是touchstart,mousedown都能后退到首頁(yè)。后退到前頁(yè)面,因?yàn)闅v史記錄中存的不是頁(yè)面緩存,它是初始化一個(gè)新的Page對(duì)象,會(huì)走完P(guān)age的整個(gè)生命周期流程 加速加載優(yōu)化,service延遲加載從上面篇章提到,整個(gè)資源獲取都是按需加載的,即使組件中的小圖標(biāo)的svg片段也是如此。特別一個(gè)大的首頁(yè),里面包含著很多小圖標(biāo),引入很多組件js,片段html,需要等它們?nèi)考虞d完會(huì)耗費(fèi)很長(zhǎng)時(shí)間。因此我們可以對(duì)常見(jiàn)的資源進(jìn)行統(tǒng)一定義加載。
持續(xù)化數(shù)據(jù)和異步操作對(duì)于Component,Page, PopUp,App對(duì)象中,都定義一個(gè)data,這是一個(gè)放置臨時(shí)數(shù)據(jù)的對(duì)象。特別是Page,這個(gè)data格式有嚴(yán)格的要求,必須是可序列化的。 在Page執(zhí)行restore的時(shí)候,我們是無(wú)法獲取到在其他頁(yè)面改變了的全局?jǐn)?shù)據(jù),我們可以把數(shù)據(jù)存放在App實(shí)例對(duì)象的data里面,然后切換頁(yè)面的時(shí)候獲取App對(duì)象中的data數(shù)據(jù),進(jìn)行有效的局部刷新操作。這要比重新去后端獲取一次更合理。 如果在PopUp對(duì)象的Page對(duì)象,也可以用相同的方式放在PopUp的對(duì)象的data里面。統(tǒng)一放置方法可以用this.parent.data.key = value; 一個(gè)網(wǎng)站,很少使用大量的持久化數(shù)據(jù),對(duì)于webapp,使用持久化數(shù)據(jù)卻很常見(jiàn)。我們可以使用同步操作的localStorage或異步操作的IndexedDB數(shù)據(jù)庫(kù)。在使用IndexedDB的時(shí)候要特別的注意,異步操作時(shí)頁(yè)面突然切換導(dǎo)致回調(diào)函數(shù)執(zhí)行錯(cuò)誤,因此盡量在執(zhí)行完后再執(zhí)行跳轉(zhuǎn)。如果不可避免,可以仿照Ajax的封裝方式,跳轉(zhuǎn)頁(yè)面的時(shí)候讓獲取數(shù)據(jù)操作停止,存數(shù)據(jù)的回調(diào)中判斷是否當(dāng)前頁(yè)面是有效顯示頁(yè)。 瀏覽器緩存瀏覽器緩存可以提高頁(yè)面的加載速度,有時(shí)候卻成了我們更新項(xiàng)目的一大阻礙,特別在測(cè)試公眾號(hào)的時(shí)候。因此我們通過(guò)后綴名版本來(lái)解決問(wèn)題。比如我們?cè)趇ndex.html上header的新建script上加一句App.version = 2.0; 再把str.js和index.js版本號(hào)更改相同的版本號(hào)。接著框架內(nèi)部會(huì)把所有的引入的js以及獲取的靜態(tài)文件都會(huì)加上?v=2.0重置所有的版本號(hào) 內(nèi)存使用單頁(yè)面對(duì)于內(nèi)存的使用非常的苛刻。如果無(wú)限制的使用,會(huì)導(dǎo)致頁(yè)面奔潰或讓手機(jī)設(shè)備快速耗電。因此這里我們對(duì)每個(gè)模塊的引用都做了嚴(yán)格的處理。對(duì)于dom和事件,在頁(yè)面銷毀的時(shí)候都會(huì)自動(dòng)去銷毀。而且引用外庫(kù)的時(shí)候,我們建議在init初始化數(shù)據(jù),在dispose方法中進(jìn)行數(shù)據(jù)清理。對(duì)于引用沒(méi)有數(shù)據(jù)回收操作的外庫(kù)的時(shí)候要特別小心,不能無(wú)限制的新建對(duì)象,這樣會(huì)導(dǎo)致頁(yè)面堆積越多的內(nèi)存而無(wú)法銷毀。我們可以使用創(chuàng)建一個(gè)對(duì)象,然后進(jìn)行無(wú)限制的使用(單例模式)。 通過(guò)異步按需加載的好處在于,能讓內(nèi)存使用量盡可能的變少。在加載首頁(yè)的時(shí)候,我們的網(wǎng)頁(yè)的內(nèi)存使用量基本和純使用靜態(tài)頁(yè)面的網(wǎng)站持平的。隨著組件量以及頁(yè)面的增加,我們緩存了大量的js,靜態(tài)html,會(huì)讓內(nèi)存使用量增多,而且緩存在history的Page對(duì)象,也會(huì)提高內(nèi)存使用量。盡管如此,我們的內(nèi)存使用量也不會(huì)超過(guò)靜態(tài)頁(yè)面太多,在可以接受的范圍之內(nèi)。 本地文件打開(kāi)有時(shí)候我們需要本地直接打開(kāi),雖然用的很少,但還是會(huì)遇到的。比如原生App嵌入webview,在沒(méi)有網(wǎng)的情況下要打開(kāi)網(wǎng)站,這時(shí)候只能通過(guò)打開(kāi)本地頁(yè)面,雖然功能有點(diǎn)閹割,但是頁(yè)面布局還是可以復(fù)用原來(lái)的,我們需要做一下的調(diào)整:
雖然付出了一些努力,但是非常值得的,底層是支持本地文件打開(kāi)的,以下功能會(huì)受到限制:
支持SSRSSR對(duì)于單頁(yè)面相對(duì)多頁(yè)面是一個(gè)缺陷,盡管努力去彌補(bǔ),但總是無(wú)法盡善盡美。而且單純?cè)谇岸伺κ菬o(wú)法完成的。這里我們通過(guò)以下手段來(lái)實(shí)現(xiàn)SSR:
雖然通過(guò)上面拼接成的html可以在瀏覽器上直接打開(kāi),然而瀏覽器畢竟沒(méi)有直接渲染組件的功能, 因此渲染的結(jié)果不會(huì)太好。只能讓搜索引擎獲取到, 然后通過(guò)下面的方法進(jìn)行分別渲染: if (preload === "true") { // 通過(guò)更改html,渲染組件 var activeHtml = pageContainer.innerHTML; pageContainer.innerHTML = ""; var staticHtml = document.body.innerHTML; if (staticPage.preload) staticPage.preload(); staticPage.initialize(body, staticHtml, {}, function () { body.removeAttribute("data-preload"); that._initCurrentPage(staticPage, currentPage, prevHistory); if (currentPage.preload) currentPage.preload(); currentPage.initialize(that.changeDom, activeHtml); }); } else { // 常規(guī)手段 staticPage.render(function (html) { staticPage.initialize(body, html, {}, function () { that._initCurrentPage(staticPage, currentPage, prevHistory) currentPage.render(function (html) { currentPage.initialize(that.changeDom, html); }); }); }); } 超越web,支持electron等方式現(xiàn)在web在通過(guò)electron打包成桌面App,因?yàn)閑lectron使用了node技術(shù),所以在獲取文件或者資源的時(shí)候就不一樣了。我們可以更改fetch方法: if (typeof __dirname === "string") { require("fs").readFile(url, "utf-8", function (error, result) { if (error) console.log(error); else Http.cache.dispatch(url, result); }) } else { var obj = createRequest(this, url, undefined, function (result) { Http.cache.dispatch(url, result); }, { onabort: function (ev) { Http.cache.remove(url); } }); this.http.ajax(obj); } 更改獲取文件路徑方法 App.join = function (url) { if (typeof __dirname === "string") return require("path").join(__dirname, url); return url; }; 還需要更改Page獲取資源路徑方法 function getBaseUrl(urlStr) { if (typeof __dirname === "string") { urlStr = require("path").join(__dirname, urlStr); return urlStr.split("\\").slice(0, -1).join("\\") + "\\"; } return urlStr.split("/").slice(0, -1).join("/") + "/"; } 這樣子,就可以兼容electron的環(huán)境了。 使用pwa技術(shù)有幸于web的發(fā)展進(jìn)程都是圍繞了漸進(jìn)增強(qiáng)的路線,所以很容易讓webapp支持pwa的各種技術(shù)
總結(jié)這一篇作為完結(jié)篇,主要對(duì)常見(jiàn)的開(kāi)發(fā)問(wèn)題進(jìn)行了進(jìn)一步的擴(kuò)展。 |
|