這篇文章將為大家詳細(xì)講解有關(guān)如何解決vue spa應(yīng)用中的路由緩存問(wèn)題,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
專注于為中小企業(yè)提供網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)萬(wàn)年免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了近1000家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過(guò)網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
單頁(yè)面應(yīng)用中的路由緩存問(wèn)題
通常我們?cè)谶M(jìn)行頁(yè)面前后退時(shí),瀏覽器通常會(huì)幫我們記錄下之前滾動(dòng)的位置,這使得我們不會(huì)在每次后退的時(shí)候都丟失之前的瀏覽器記錄定位。但是在現(xiàn)在愈發(fā)流行的SPA(single page application 單頁(yè)面應(yīng)用)中,當(dāng)我們從父級(jí)頁(yè)面打開(kāi)子級(jí)頁(yè)面,或者從列表頁(yè)面進(jìn)入詳情頁(yè)面,此時(shí)如果回退頁(yè)面,會(huì)發(fā)現(xiàn)之前我們?yōu)g覽的滾動(dòng)記錄沒(méi)有了,頁(yè)面被置頂?shù)搅俗铐敳浚路鹗堑谝淮芜M(jìn)入這個(gè)頁(yè)面一樣。這是因?yàn)樵趕pa頁(yè)面中的url與路由容器頁(yè)面所對(duì)應(yīng),當(dāng)頁(yè)面路徑與其發(fā)生不匹配時(shí),該頁(yè)面組件就會(huì)被卸載,再次進(jìn)入頁(yè)面時(shí),整個(gè)組件的生命周期就會(huì)完全重新走一遍,包括一些數(shù)據(jù)的請(qǐng)求與渲染,所以之前的滾動(dòng)位置和渲染的數(shù)據(jù)內(nèi)容也都完全被重置了。
vue中的解決方式
vue.js最貼心的一點(diǎn)就是提供了非常多便捷的API,為開(kāi)發(fā)者考慮到很多的應(yīng)用場(chǎng)景。在vue中,如果想緩存路由,我們可以直接使用內(nèi)置的keep-alive組件,當(dāng)keep-alive包裹動(dòng)態(tài)組件時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例,而不是銷毀它們。
內(nèi)置組件keep alive
keep-alive是Vue.js的一個(gè)內(nèi)置組件。它主要用于保留組件狀態(tài)或避免重新渲染。
使用方法如下:
keep-alive組件會(huì)去匹配name名稱為 'a', 'b' 的子組件,在匹配到以后會(huì)幫助組件緩存優(yōu)化該項(xiàng)組件,以達(dá)到組件不會(huì)被銷毀的目的。
實(shí)現(xiàn)原理
先簡(jiǎn)要看下keep-alive組件內(nèi)部實(shí)現(xiàn)代碼,具體代碼可以見(jiàn)Vue GitHub
created () { this.cache = Object.create(null) this.keys = [] }
在created生命周期中會(huì)用Object.create方法創(chuàng)建一個(gè)cache對(duì)象,用來(lái)作為緩存容器,保存vnode節(jié)點(diǎn)。Tip: Object.create(null)創(chuàng)建的對(duì)象沒(méi)有原型鏈更加純凈
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern 檢查匹配是否為緩存組件,主要根據(jù)include傳入的name來(lái)對(duì)應(yīng) const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included 該判斷中判斷不被匹配,則直接返回當(dāng)前的vnode(虛擬dom) (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { //查看cache對(duì)象中已經(jīng)緩存了該組件,則vnode直接使用緩存中的組件實(shí)例 vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } else { //未緩存的則緩存實(shí)例 cache[key] = vnode keys.push(key) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } vnode.data.keepAlive = true } return vnode || (slot && slot[0]) }
上述代碼主要是在render函數(shù)中對(duì)是否是緩存渲染進(jìn)行判斷
vue keep-alive內(nèi)部實(shí)現(xiàn)的基本流程就是:
首先通過(guò)getFirstComponentChild獲取到內(nèi)部的子組件
然后拿到該組件的name與keep-alive組件上定義的include與exclude屬性進(jìn)行匹配,
如果不匹配就表示不緩存組件,就直接返回該組件的vnode(vnode就是一個(gè)虛擬的dom樹(shù)結(jié)構(gòu),由于原生dom上的屬性非常多,消耗巨大,使用這種模擬方式會(huì)減少很多dom操作的開(kāi)銷)
如果匹配到,則在cache對(duì)象中查看是否已經(jīng)緩存過(guò)該實(shí)例,如果有就直接將緩存的vnode的componentInstance(組件實(shí)例)覆蓋到目前的vnode上面,否則將vnode存儲(chǔ)在cache中。
React中的解決方案
在react中沒(méi)有提供類似于vue的keep-alive的解決方案,這意味這我們可能需要自己編寫(xiě)一些代碼或者通過(guò)一些第三方的模塊來(lái)解決。
在React項(xiàng)目GitHub的該issue中進(jìn)行了相關(guān)討論,開(kāi)發(fā)維護(hù)人員給出了兩種方式來(lái)解決:
將數(shù)據(jù)與組件分開(kāi)緩存。例如,你可以將state提升到一個(gè)不會(huì)被卸載的父級(jí)組件,或者像redux一樣將其放在一個(gè)側(cè)面緩存中。我們也正在為此開(kāi)發(fā)一類的API支持(context)。
不要去卸載你要“保持活動(dòng)”的視圖,只需使用style={{display:'none'}}屬性去隱藏它們。
1. 集中的狀態(tài)管理恢復(fù)快照方式
在React中通過(guò)redux或mobx集中的狀態(tài)管理來(lái)緩存頁(yè)面數(shù)據(jù)以及滾動(dòng)條等信息,以達(dá)到緩存頁(yè)面的效果。
componentDidMount() { const {app: {dataSoruce = [], scrollTop}, loadData} = this.props; if (dataSoruce.length) { //判斷redux中是否已經(jīng)有數(shù)據(jù)源 // 有數(shù)據(jù)則不再加載收據(jù),只恢復(fù)滾動(dòng)狀態(tài) window.scrollTo(0, scrollTop); } else { //沒(méi)有數(shù)據(jù)就去請(qǐng)求數(shù)據(jù)源 this.props.loadData(); // 在redux中定義的數(shù)據(jù)請(qǐng)求的action } } handleClik = () => { 在點(diǎn)擊進(jìn)入下一級(jí)頁(yè)面前先保存當(dāng)前的滾動(dòng)距離 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; const {saveScrollTop} = this.props; saveScrollTop(scrollTop); }
首先我們可以在redux中為頁(yè)面定義異步的action,將請(qǐng)求回來(lái)的數(shù)據(jù)放入集中的store中(redux的該相關(guān)具體用法不在細(xì)述)。在sotre里我們可以保存當(dāng)前頁(yè)面的數(shù)據(jù)源、滾動(dòng)條高度以及其他一些可能要用到的分頁(yè)數(shù)據(jù)等來(lái)幫助我們恢復(fù)狀態(tài)。
在componentDidMount生命周期里,首先根據(jù)redux里store中的對(duì)應(yīng)的字段,判斷是否已經(jīng)加載過(guò)數(shù)據(jù)源。如果已經(jīng)緩存過(guò)數(shù)據(jù)則不再去請(qǐng)求數(shù)據(jù)源,只去恢復(fù)一下store里的存儲(chǔ)過(guò)的一些滾動(dòng)條位置信息等。如果還未請(qǐng)求過(guò)數(shù)據(jù),就使用在redux中定義的異步action去請(qǐng)求數(shù)據(jù),在將數(shù)據(jù)在reducer里將數(shù)據(jù)存到store中。 在render函數(shù)里,我們只需要讀取redux里存儲(chǔ)的數(shù)據(jù)即可。
為了保留要緩存頁(yè)面的一些狀態(tài)信息,如滾動(dòng)條、分頁(yè)、操作狀態(tài),我們可以在進(jìn)行對(duì)應(yīng)操作時(shí)候?qū)⑦@些信息存入redux的store中,這樣當(dāng)我們恢復(fù)頁(yè)面時(shí),就可以將這些對(duì)應(yīng)狀態(tài)一一讀取并還原。
2. 使用display的屬性來(lái)切換顯示隱藏路由組件
想要display的屬性來(lái)切換顯示隱藏路由組件,首先要保證路由組件不會(huì)在url變化時(shí)候被卸載。在react-router中最使用的Route組件,它可以通過(guò)我們定義的path屬性來(lái)與頁(yè)面路徑來(lái)進(jìn)行匹配,并渲染對(duì)應(yīng)的組件,從而達(dá)到保持UI與URL同步變化的效果。
首先簡(jiǎn)要看下Route組件的實(shí)現(xiàn) GitHub Route.js
return ({children && !isEmptyChildren(children) ? children : props.match // props.match 屬性來(lái)確定是否要渲染組件 ? component ? React.createElement(component, props) : render ? render(props) : null : null} );
上述代碼出現(xiàn)在關(guān)鍵的render方法最后的return中
Route組件會(huì)根據(jù)props對(duì)象中的match屬性來(lái)確定是否要渲染組件,如果match匹配到了就使用Route組件上傳遞的component或者render屬性來(lái)渲染對(duì)應(yīng)組件,否則就返回null。
然后溯源而上,我們找到了props對(duì)象中關(guān)于match的定義:
const location = this.props.location || context.location; const match = this.props.computedMatch ? this.props.computedMatch //already computed the match for us : this.props.path ? matchPath(location.pathname, this.props) : context.match; const props = { ...context, location, match };
上述代碼顯示,match首先會(huì)從組件的this.props中的computedMatch屬性來(lái)判斷:如果this.props中存在computedMatch則直接使用定義好的computedMatch屬性賦值給match,否則如果this.props.path存在,就會(huì)使用matchPath方法來(lái)根據(jù)當(dāng)前的location.pathname來(lái)判斷是否匹配。
然而在react router的Route組件API文檔中我們似乎沒(méi)有看到過(guò)有關(guān)于computedMatch的介紹,不過(guò)在源碼中有一行這樣的注釋
//already computed the match for us
該注釋說(shuō)在
接下來(lái)我們?cè)偃チ私庖幌耂witch組件:
Switch組件只會(huì)渲染第一個(gè)被location匹配到的并且作為子元素的
我們翻開(kāi)Switch組件的實(shí)現(xiàn)源碼:
let element, match; // 定義最后返回的組件元素,和match匹配變量 React.Children.forEach(this.props.children, child => { if (match == null && React.isValidElement(child)) { // 如果match沒(méi)有內(nèi)容則進(jìn)入該判斷 element = child; const path = child.props.path || child.props.from; match = path // 該三元表達(dá)式只有在匹配到后會(huì)給match賦值一個(gè)對(duì)象,否則match一直為null ? matchPath(location.pathname, { ...child.props, path }) : context.match; } }); return match ? React.cloneElement(element, { location, computedMatch: match }) : null;
首先我們找到computedMatch屬性是在React.cloneElement方法中,cloneElement方法會(huì)將追加定義的屬性合并到該clone組件元素上,并返回clone后的React組件,等于就是將新的props屬性傳入組件并返回新組件。
在上文中找到computedMatch的值match也是根據(jù)matchPath來(lái)判斷是否匹配的,matchPath是react router中的一個(gè)API,該方法會(huì)根據(jù)你傳入的第一個(gè)參數(shù)pathname與第二個(gè)要匹配的props屬性參數(shù)來(lái)判斷是否匹配。如果匹配就返一個(gè)對(duì)象類型并包含相關(guān)的屬性,否則返回null。
在React.Children.forEach循環(huán)子元素的方法中,matchPath方法判斷當(dāng)前pathname是否匹配,如果匹配就給定義的match變量進(jìn)行賦值,所以當(dāng)match被賦值以后,后續(xù)的循環(huán)就也不會(huì)再進(jìn)行匹配賦值,因?yàn)镾witch組件只會(huì)渲染第一次與之匹配的組件。
3. 實(shí)現(xiàn)一個(gè)路由緩存組件
我們知道Switch組件只會(huì)渲染第一項(xiàng)匹配的子組件,如果可以將匹配到的組件都渲染出來(lái),然后只用display的block和none來(lái)切換是否顯示,這也就實(shí)現(xiàn)了第二種解決方案。
參照Switch組件來(lái)封裝一個(gè)RouteCache組件:
import React from 'react'; import PropTypes from 'prop-types'; import {matchPath} from 'react-router'; import {Route} from 'react-router-dom'; class RouteCache extends React.Component { static propTypes = { include: PropTypes.oneOfType([ PropTypes.bool, PropTypes.array ]) }; cache = {}; //緩存已加載過(guò)的組件 render() { const {children, include = []} = this.props; return React.Children.map(children, child => { if (React.isValidElement(child)) { // 驗(yàn)證是否為是react element const {path} = child.props; const match = matchPath(location.pathname, {...child.props, path}); if (match && (include === true || include.includes(path))) { //如果匹配,則將對(duì)應(yīng)path的computedMatch屬性加入cache對(duì)象里 //當(dāng)include為true時(shí),緩存全部組件,當(dāng)include為數(shù)組時(shí)緩存對(duì)應(yīng)組件 this.cache[path] = {computedMatch: match}; } //可以在computedMatch里追加入一個(gè)display屬性,可以在路由組件的props.match拿到 const cloneProps = this.cache[path] && Object.assign(this.cache[path].computedMatch, {display: match ? 'block' : 'none'}); return{React.cloneElement(child, {computedMatch: cloneProps})}; } return null; }); } } // 使用
在閱讀了源碼后,我們知道Route組件會(huì)根據(jù)它的this.props.computedMatch來(lái)判斷是否要渲染該組件。
我們?cè)诮M件內(nèi)部創(chuàng)建一個(gè)cache對(duì)象,將已經(jīng)匹配到的組件的computedMatch屬性寫(xiě)入該緩存對(duì)象中。這樣即使當(dāng)url不再匹配時(shí),也能通過(guò)讀取cache對(duì)象中該路徑的值,并使用React .cloneElement方法將computedMatch屬性賦值給組件的props。這樣已緩存過(guò)的路由組件就會(huì)被一直渲染出來(lái),組件就不會(huì)被卸載掉。
因?yàn)榻M件內(nèi)部可能會(huì)包裹多個(gè)路由組件,所以使用React.Children.map方法將內(nèi)部包含的子組件都循環(huán)返回。
為了UI與路由對(duì)應(yīng)顯示正確,我們通過(guò)當(dāng)前的計(jì)算得出的match屬性,來(lái)隱藏掉不匹配的組件,只為我們展示匹配的組件即可。如果你不想在組件外再套一層div,也可以在組件內(nèi)部通過(guò)this.props.match中的display屬性來(lái)切換顯示組件。
仿照vue keep alive的形式,設(shè)置一個(gè) include 參數(shù)API。當(dāng)參數(shù)為true時(shí)緩存內(nèi)部的所有子組件,當(dāng)參數(shù)為數(shù)組時(shí)則緩存對(duì)應(yīng)的path路徑組件。
使用效果
在最初時(shí),從未被url匹配過(guò)的組件不會(huì)被渲染,里面的dom結(jié)構(gòu)是空的。
當(dāng)切換到對(duì)應(yīng)組件時(shí),當(dāng)前的組件被渲染,而之前已匹配的組件不會(huì)被卸載,只是被隱藏
在輸出日志中可以看到,當(dāng)我們不停的來(lái)回切換時(shí),componentDidMount生命周期也只執(zhí)行一次,在props.match中我們可以獲取到當(dāng)前的display值。
4. 另外的也可以采用一些第三方組件模塊來(lái)實(shí)習(xí)緩存機(jī)制:
react-keeper
react-router-cache-route
react-live-route
關(guān)于“如何解決vue spa應(yīng)用中的路由緩存問(wèn)題”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。