vue異步組件使用及加載失敗異常處理
引言
在構(gòu)建大型單頁應(yīng)用時,組件的按需加載和延遲加載對于性能優(yōu)化至關(guān)重要。Vue.js 提供了一種實(shí)現(xiàn)這個需求的方式,那就是異步組件。異步組件允許我們將組件的加載延遲到實(shí)際需要的時候,而不是一開始就全部加載。這不僅可以減少首屏加載時間,還可以提高應(yīng)用的響應(yīng)速度。本文將介紹Vue2中異步組件的概念、使用方法以及一些技巧,重點(diǎn)介紹一下高階異步組件及高階異步組件加載失敗后,如何實(shí)現(xiàn)重新加載。
什么是異步組件
在Vue中,通常我們使用import語句來導(dǎo)入組件,然后在components選項(xiàng)中注冊它們。這種方式會導(dǎo)致所有組件在應(yīng)用程序初始化時都被加載,可能會影響應(yīng)用的初始加載性能。異步組件的概念就是延遲加載組件,只有在需要時才進(jìn)行加載,從而提高了應(yīng)用的加載速度。
如何區(qū)別一個組件是同步組件還是異步組件呢?我們?nèi)粘i_發(fā)過程中,常見的那些組件引用哪些是異步組件,哪些不是呢?
下面我們來舉例說明:
// Welcome.vue import HelloWorld from './HelloWorld.vue' export default { components: {HelloWorld}, template: '<HelloWorld/>' }
在上面的例子中我們定義了一個組件Welcome.vue
,在該組件中引入了局部組件HelloWorld
,對于Welcome
而言,此處的HelloWorld
組件是作為同步組件引入的。
如果想使用異步組件的形式引入,該如何修改上面的代碼呢?
// Welcome.vue //import HelloWorld from './HelloWorld.vue' const HellowWold = ()=>import('./HelloWorld.vue') export default { components: {HelloWorld}, template: '<HelloWorld/>' }
將組件改成上面的形式注冊局部組件,此時就是注冊了一個異步組件。組件自身的實(shí)現(xiàn)并沒有所謂的同步異步一說,關(guān)鍵在于如何引入組件。
上面異步組件的引入形式,常見于vue-router中注冊路由表時。這樣能能讓不同的路由獨(dú)立打包,按需加載。
異步組件的幾種書寫形式
普通異步組件,通過vue內(nèi)部曝露出的resolve方法返回對應(yīng)的組件
Vue.component('async-component', function(resolve, reject) { import('./AsyncComponent.vue').then((module) => { resolve(module.default); }).catch((error) => { reject(error); }); });
promise異步組件
Vue.component('async-component',()=>import('./AsyncComponent.vue'))
高階異步組件
// 高級異步組件 const AsyncComponent = () => ({ // 需要加載的組件 (應(yīng)該是一個 `Promise` 對象) component: import('./MyComponent.vue'), // 異步組件加載時使用的組件 loading: LoadingComponent, // 加載失敗時使用的組件 error: ErrorComponent, // 展示加載時組件的延時時間。默認(rèn)值是 200 (毫秒) delay: 200, // 如果提供了超時時間且組件加載也超時了, // 則使用加載失敗時使用的組件。默認(rèn)值是:`Infinity` timeout: 3000 })
異步組件的應(yīng)用場景
路由懶加載
在 Vue Router 中,我們可以使用異步組件來實(shí)現(xiàn)路由懶加載。懶加載是一種優(yōu)化策略,它只加載當(dāng)前需要顯示的組件,而不是一次性加載所有的組件。這樣就可以減少首屏加載時間,提高應(yīng)用的響應(yīng)速度。
我們可以將每個路由配置中的 component 選項(xiàng)替換為一個返回 Promise 的函數(shù),這樣 Vue Router 就會自動將這個組件注冊為異步組件,并在需要的時候進(jìn)行加載。例如:
const router = new VueRouter({ routes: [ { path: '/foo', component: () => import('./Foo.vue') } ] })
加載聯(lián)邦模塊
模塊聯(lián)邦:(Module Federation)是一種軟件架構(gòu)模式,主要用于解決在復(fù)雜分布式系統(tǒng)中,不同模塊間的依賴管理和共享問題。這種模式通過將大型應(yīng)用拆分并獨(dú)立開發(fā)、構(gòu)建和部署各個模塊,使得它們可以在運(yùn)行時動態(tài)地加載和共享。
日常開發(fā)中,開發(fā)人員可能將A應(yīng)用中的某一組件獨(dú)立打包js,以中間件或者SDK的形式供其他應(yīng)用使用。而B作為引用方,如何加載遠(yuǎn)程js,引入對應(yīng)的組件呢?
下面以我在開發(fā)中的一個實(shí)際應(yīng)用舉例,介紹兩種常規(guī)解決方法:
項(xiàng)目組中B應(yīng)用有一個遠(yuǎn)程服務(wù)的js文件,其中打包了我們需要引入的組件。在我們自身項(xiàng)目中需引入該組件,之所以采用加載js,而非npm包的形式,主要是為了解耦,后續(xù)該組件的迭代更新都不影響我們自身應(yīng)用,自身應(yīng)用也無需發(fā)版即可使用最新版本的組件。
首先,為方便后續(xù)使用,我們先封裝一個異步方法,用于加載和執(zhí)行對應(yīng)的js文件。加載執(zhí)行js的方式大致有兩種:
方法一:通過fetch請求獲取遠(yuǎn)程js內(nèi)容,然后通過eval函數(shù)執(zhí)行對應(yīng)的js文件。這種方式要考慮跨域問題和eval安全執(zhí)行策略問題,有些瀏覽器禁止在非安全環(huán)境下執(zhí)行eval函數(shù)。
fetch('path/to/remote/js/file.js') .then(response => response.text()) .then(jsCode => { // 執(zhí)行js new Function('return ' + jsCode)(); }) .catch(error => { console.error('Error loading remote component:', error); });
方法二:通過動態(tài)創(chuàng)建script標(biāo)簽來執(zhí)行對應(yīng)的js文件。這種方式比較穩(wěn)妥,無需考慮跨域問題。
const script = document.createElement('script') script.type = 'text/javascript' script.onload = () => resolve(void 0) script.onerror = (err) => { console.error(`資源加載失?。篳, src) reject(err) } script.src = src document.head.appendChild(script)
這里我們項(xiàng)目中采用第二種動態(tài)標(biāo)簽的形式,封裝了一個類,主要用于加載遠(yuǎn)程js,便于后續(xù)使用。其代碼簡寫如下:
import { Vue } from 'vue-property-decorator' export class AsyncComponentLoader { static cache: Record<string, any> = {} static async loadAsyncComponent(src: string, cachable: boolean = false) { if (!src) throw new Error('無法加載遠(yuǎn)程組件:未傳入遠(yuǎn)程組件加載地址') const name = 'remote_sdk_key' // 引入緩存,避免重復(fù)加載 if (cachable && AsyncComponentLoader.cache[name]) { ///遠(yuǎn)程組件js執(zhí)行后,在window上掛載,后續(xù)的邏輯有賴于遠(yuǎn)程js如何打包的,具體問題具體分析 Vue.component('RemoteComp',window[name]?.default) return } else { await AsyncComponentLoader.loadScript(src) Vue.component('RemoteComp',window[name]?.default) AsyncComponentLoader.cache[name] = window[name] } } static async loadScript(src) { // 加載遠(yuǎn)程js并執(zhí)行 return new Promise((resolve, reject) => { const script = document.createElement('script') script.type = 'text/javascript' script.onload = () => resolve(void 0) script.onerror = (err) => { console.error(`資源加載失?。篳, src) reject(err) } script.src = src document.head.appendChild(script) }) } }
同步引入
創(chuàng)建一個新的Vue組件文件AsyncComponent,在AsyncComponent文件中,你可以編寫一個方法來請求遠(yuǎn)程JS文件,并將其動態(tài)導(dǎo)入為組件。例如:
import { Vue, Component, Prop } from 'vue-property-decorator' import { AsyncComponentLoader } from './AsyncComponentLoader' @Component export default class extends Vue { @Prop() src!: string // sdk加載成功標(biāo)識 sdkLoaded = false // sdk加載loading showLoading = false async created() { //加載組件js this.loadComponent() } async loadComponent() { this.showLoading = true AsyncComponentLoader.loadAsyncComponent(this.src, true) .then(() => { this.sdkLoaded = true this.$emit('onload') //強(qiáng)制父組件重新渲染 this.$parent?.$forceUpdate() }) .catch(() => { this.$emit('onerror') }) .finally(() => { this.showLoading = false }) } genError() { const h = this.$createElement const style = { height: '150px', cursor: 'pointer', backgroundColor: '#fff' } return ( ///加載失敗時顯示 <div style={style} onClick={this.loadComponent}> 加載失敗,點(diǎn)擊重新加載 </div> ) } render() { const h = this.$createElement if (this.showLoading) { //加載loading const directives = [{ name: 'loading', value: this.showLoading }] return <div {...{ directives }} style="width: 100%;height: 150px"></div> } if (!this.sdkLoaded) { return this.genError() } return this.$slots.default } }
在上面代碼中定義了一個組件,該組件在created
鉤子函數(shù)中去加載遠(yuǎn)程js組件。在加載過程中,顯示loading,加載成功后顯示默認(rèn)插槽內(nèi)容,在插槽里,可以直接使用注冊的遠(yuǎn)程組件,如果js加載失敗,則顯示對應(yīng)的error,并支持點(diǎn)擊重新加載。
使用方式:
<async-load src="path/to/remote/js/file.js"> <RemoteComp :env="env" /> </async-load>
異步引入
通過直接注冊一個高階異步組件的形式,來引入遠(yuǎn)程js組件
Vue.component('async-component', function() { // 顯示loading狀態(tài) const loadingComponent = { template: '<div>Loading...</div>' }; // 顯示error狀態(tài) const errorComponent = { template: '<div>Error! Failed to load component.</div>' }; return { loading: loadingComponent, error:errorComponent, component: new Promise((resolve,reject)=>{ // 加載遠(yuǎn)程組件的js文件 const script = document.createElement('script'); script.src = 'path_to_remote_component.js'; script.onload = () => { // 注冊遠(yuǎn)程組件 resolve({ template: '<div>遠(yuǎn)程組件的模板</div>', // 遠(yuǎn)程組件的其他配置 }); }; script.onerror = (error) => { reject(error) }; document.head.appendChild(script); }), delay: 0, timeout: 3000 } });
注冊的異步組件可以直接使用,該組件使用方式:
<async-component></async-component>
無論成功還是失敗,異步組件的加載只執(zhí)行一次,之后就一直保留該狀態(tài)。這也就意味著,當(dāng)異步組件作為局部組件引入時,一旦加載失敗,后續(xù)無論路由如何跳轉(zhuǎn),該異步組件也一直渲染的是errorComp。除非刷新整個頁面,重新加載響應(yīng)異步組件。但是很多時候,作為局部組件加載失敗,我們只想重新加載失敗的那部分,局部刷新,該如何做呢
異步組件加載失敗異常處理
通常,異步組件作為路由組件,一個路由就是一個頁面,這個時候加載失敗時,我們一般都是刷新整個瀏覽器頁面就行。但是針對上面我們說的,當(dāng)一個異步組件作為頁面的一部分渲染時,如果加載失敗,如何只加載這部分,而不需要刷新整個瀏覽器頁面也是很有必要的。
在vue的issue中,也有人提出了類似的問題: https://github.com/vuejs/vue/issues/8524
在這個問題中,vue的作者尤大給出的解決方案是強(qiáng)制父組件重新渲染。
根據(jù)以上回答,我做了以下嘗試,將上面異步組件的errorComponent重新改了下,增加了刷新方法,調(diào)用強(qiáng)制刷新方法$forceUpdate
,強(qiáng)制父組件刷新。
@Component class ErrorComp extends Vue { async refresh() { // 強(qiáng)制父組件刷新,用以重新加載異步組件 this.$parent?.$forceUpdate() } render() { const h = this.$createElement return ( <div style="min-height:150px;padding: 10px" onClick={this.refresh}> 加載失??!請刷新重試 </div> ) } }
然而,經(jīng)實(shí)際代碼試驗(yàn),這種直接調(diào)用父實(shí)例刷新方法,并不能重新加載異步組件,對應(yīng)的異步組件依然渲染為errorComponent。此時,我們就需要執(zhí)行下調(diào)試,看看強(qiáng)制刷新后,為什么沒有重新加載異步組件。
首先,代碼執(zhí)行到vue源碼中的createComponent中,這是vue在解析組件類型標(biāo)簽中很重要的一個函數(shù),不了解的可以看下相關(guān)源碼。源碼中的Ctor就是上面我們注冊異步組件時Vue.component('async-component', function() {/**bula bula/})
的第二個函數(shù)入?yún)ⅰ?/p>
function createComponent ( Ctor, data, context, children, tag ) { if (isUndef(Ctor)) { return } var baseCtor = context.$options._base; // plain options object: turn it into a constructor //異步組件這里Ctor是個函數(shù),所以不走extend方法 if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(("Invalid Component definition: " + (String(Ctor))), context); } return } // async component var asyncFactory; //由于沒有走extend方法,自然沒有對應(yīng)的cid,異步組件可以執(zhí)行到這個里面 if (isUndef(Ctor.cid)) { asyncFactory = Ctor; //在異步組件加載失敗以后,可以在此處打上斷點(diǎn),然后點(diǎn)擊重新加載,觀察后續(xù)執(zhí)行過程 //這里重點(diǎn)要進(jìn)入這個方法,這個方法決定了異步組件渲染的是loading組件還是遠(yuǎn)程組件或者error組件 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context); if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } //后續(xù)代碼省略不看 …… }
在上述代碼中我們重點(diǎn)關(guān)注resolveAsyncComponent
方法的執(zhí)行,在下面代碼中,我們可以看到,方法在最開始的時候就對factory.error
進(jìn)行了判斷。在我們首次執(zhí)行異步組件加載時,由于加載失敗,此時會將factory.error置為true。所以后續(xù)我們強(qiáng)制父組件重新渲染時,在這個函數(shù)里,第一行判斷成功就直接返回了factory.errorComp組件,所以我們?nèi)绻叵虻谝淮文菢又匦录虞d該遠(yuǎn)程組件,我們需要把函數(shù)里面所有的提前返回都杜絕調(diào),以便于執(zhí)行到最終的加載邏輯中。
function resolveAsyncComponent ( factory, baseCtor ) { //此處加載失敗,直接返回了errorComp,如重新加載,將對應(yīng)的factory.error設(shè)為false if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } //由于是失敗重新加載,factory.resolved無值,無需處理 if (isDef(factory.resolved)) { return factory.resolved } var owner = currentRenderingInstance; if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { // already pending factory.owners.push(owner); } // 失敗重新時,將loading重新置為false if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } //為了進(jìn)入這個判斷里面,這里將factory.owners置為null if (owner && !isDef(factory.owners)) { var owners = factory.owners = [owner]; var sync = true; var timerLoading = null; var timerTimeout = null ;(owner).$on('hook:destroyed', function () { return remove(owners, owner); }); var forceRender = function (renderCompleted) { for (var i = 0, l = owners.length; i < l; i++) { (owners[i]).$forceUpdate(); } if (renderCompleted) { owners.length = 0; if (timerLoading !== null) { clearTimeout(timerLoading); timerLoading = null; } if (timerTimeout !== null) { clearTimeout(timerTimeout); timerTimeout = null; } } }; var resolve = once(function (res) { // cache resolved factory.resolved = ensureCtor(res, baseCtor); // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender(true); } else { owners.length = 0; } }); var reject = once(function (reason) { warn( "Failed to resolve async component: " + (String(factory)) + (reason ? ("\nReason: " + reason) : '') ); if (isDef(factory.errorComp)) { //執(zhí)行失敗時,將factory.error設(shè)為true factory.error = true; forceRender(true); } }); //這里傳入上面定義的resolve,reject方法 var res = factory(resolve, reject); if (isObject(res)) { if (isPromise(res)) { // () => Promise if (isUndef(factory.resolved)) { res.then(resolve, reject); } } else if (isPromise(res.component)) { res.component.then(resolve, reject); if (isDef(res.error)) { //此處初始化factory.errorComp,這里的res.error就是我們先前定義高階異步組件時,函數(shù)返回對象中的error factory.errorComp = ensureCtor(res.error, baseCtor); } if (isDef(res.loading)) { factory.loadingComp = ensureCtor(res.loading, baseCtor); if (res.delay === 0) { factory.loading = true; } else { timerLoading = setTimeout(function () { timerLoading = null; if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true; forceRender(false); } }, res.delay || 200); } } if (isDef(res.timeout)) { timerTimeout = setTimeout(function () { timerTimeout = null; if (isUndef(factory.resolved)) { reject( "timeout (" + (res.timeout) + "ms)" ); } }, res.timeout); } } } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
根據(jù)上面源碼中代碼執(zhí)行結(jié)果,重新調(diào)整刷新方法如下:
@Component class ErrorComp extends Vue { async refresh() { // 異步組件加載失敗后,在不刷新頁面的情況下重新加載遠(yuǎn)程組件 // 該方法hack了vue內(nèi)部實(shí)現(xiàn),非必要不使用,且依賴于vue源碼中resolveAsyncComponent方法,需注意vue版本 // @ts-ignore const asyncFactory: any = this.constructor.asyncFactory //注意這里的asyncFactory不是本身就有的,需要手動掛載。這里是為了統(tǒng)一封裝errorComp組件,便于其他異步組件也可以使用該errorComp if (!asyncFactory) return window.location.reload() // 異步組件加載失敗后,該標(biāo)識為true,返回之前設(shè)置的error component,如需重新加載,需重設(shè)error asyncFactory.error = false // 重設(shè)loading,否則重新加載后返回的是loading component asyncFactory.loading = false // 重設(shè)實(shí)例,否則無法進(jìn)入異步組件加載邏輯,不同版本的變量命名不同,應(yīng)用里用的是2.6.10版本,變量為owners,2.5中為contexts asyncFactory.contexts = null asyncFactory.owners = null // 強(qiáng)制父組件刷新,用以重新加載異步組件 this.$parent?.$forceUpdate() } render() { const h = this.$createElement return ( <div style="min-height:150px;padding: 10px"> 加載失敗!請 <span style="cursor: pointer;color: #3693ff" onClick={this.refresh}> 刷新 </span> 重試 </div> ) } }
在上面代碼中,在$forceUpdate
方法之前執(zhí)行了一些重置操作,用于清空異步組件的加載狀態(tài)。這里需要強(qiáng)調(diào)一下,代碼中的constructor.asyncFactory
不是本身就有的,需要手動掛載。這里是為了統(tǒng)一封裝errorComp組件,便于其他異步組件也可以使用該errorComp。至于后面的owners和contexts是由于不同版本的vue,在此處的實(shí)現(xiàn)略有不同,為了兼容性,這里兩個變量值都重置了一下。至于如何掛載asyncFactory,以下面?zhèn)未a為例:
import ErrorComp from 'ErrorComp' Vue.component('async-component', function asyncComponent() { const error = ErrorComp.extend() //這里掛載一下,便于后面失敗時重新加載引用 error.asyncFactory = asyncComponent return { loading: loadingComp, error:error, component: new Promise((resolve,reject)=>{ // …… }) } })
經(jīng)試驗(yàn),改造后的代碼可以實(shí)現(xiàn)異步組件加載失敗重新加載。如果有遇到相似問題的,可以參考下該方案。不過由于該方案有較強(qiáng)的侵入性,依賴于vue源碼的內(nèi)部實(shí)現(xiàn),不同vue版本,在這方面的實(shí)現(xiàn)策略不同,可能會導(dǎo)致不同的執(zhí)行效果,所以請注意使用的vue版本及其在這一塊的實(shí)現(xiàn)細(xì)節(jié)。本文中對應(yīng)的代碼適應(yīng)于vue 2.5-2.6,我并沒有查詢其他版本的vue源碼,有興趣的可以自行參考此方案做響應(yīng)調(diào)整。
結(jié)語
通過使用Vue2中的異步組件,我們可以優(yōu)化應(yīng)用程序的性能和加載速度,提升用戶體驗(yàn)。同時,合理地處理加載狀態(tài)和錯誤情況,以及靈活地使用高階異步組件和按需加載,可以讓我們更好地利用異步組件的優(yōu)勢,為用戶提供更好的應(yīng)用體驗(yàn)。
到此這篇關(guān)于vue異步組件使用及加載失敗異常處理的文章就介紹到這了,更多相關(guān)vue異步組件加載內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue前端登錄token信息驗(yàn)證功能實(shí)現(xiàn)
最近公司新啟動了個項(xiàng)目,用的是vue框架在做,下面這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)token登錄驗(yàn)證的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12Vue3+vite路由配置優(yōu)化(自動化導(dǎo)入)
這篇文章主要介紹了Vue3+vite路由配置優(yōu)化(自動化導(dǎo)入),需要的朋友可以參考下2023-09-09vue3語法中使用vscode打開滿屏紅線報(bào)錯的完美解決方法
這篇文章主要介紹了vue3語法中使用vscode打開滿屏紅線報(bào)錯的完美解決方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06vue使用video插件vue-video-player的示例
這篇文章主要介紹了vue使用video插件vue-video-player的示例,幫助大家更好的理解和使用vue插件,感興趣的朋友可以了解下2020-10-10Vue中使用crypto-js AES對稱加密算法實(shí)現(xiàn)加密解密
?在數(shù)字加密算法中,通過可劃分為對稱加密和非對稱加密,本文主要介紹了Vue中使用crypto-js AES對稱加密算法實(shí)現(xiàn)加密解密,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03