如何在?Angular?中使用模塊聯(lián)合構(gòu)建微前端

如何在 Angular 中使用模塊聯(lián)合構(gòu)建微前端

對前端 Web 應(yīng)用程序的需求持續(xù)增長。作為消費者,我們希望我們的 Web 應(yīng)用程序功能豐富且高性能。作為開發(fā)人員,我們擔(dān)心如何在提供高質(zhì)量功能和性能的同時牢記良好的開發(fā)實踐和架構(gòu)。

進入微前端架構(gòu)。微前端以與微服務(wù)相同的概念建模,作為分解整體式前端的一種方式。您可以組合微型前端以形成功能齊全的 Web 應(yīng)用程序。由于每個微型前端都可以獨立開發(fā)和部署,因此您有一種強大的方法來擴展前端應(yīng)用程序。

那么微前端架構(gòu)是什么樣的呢?假設(shè)您有一個電子商務(wù)網(wǎng)站,看起來和這個網(wǎng)站一樣令人驚嘆:

銷售帶有標題的氣球、帳戶和購物車鏈接以及要購買的氣球圖像的示例電子商務(wù)網(wǎng)站的圖像

您可能有一個購物車、注冊用戶的帳戶信息、過去的訂單、付款方式等。您可以進一步將這些功能分類到域中,每個域都可以是一個單獨的微前端,也稱為 .微前端遙控器的集合位于另一個網(wǎng)站,即Web應(yīng)用程序。remotehost

因此,使用微前端分解不同功能的電子商務(wù)網(wǎng)站可能如下圖所示,其中購物車和帳戶功能位于單頁應(yīng)用程序 (SPA) 中的單獨路由中:

Image of the example balloon e-commerce site showing a breakdown of the views. The entire site is wrapped in a host named 'Shell', and micro-frontends for the 'Cart' and 'Account' links

你可能會說,“微前端聽起來很酷,但管理不同的前端和跨微前端的編排狀態(tài)聽起來也很復(fù)雜。你是對的。微前端的概念已經(jīng)存在了幾年,推出自己的微前端實現(xiàn)、共享狀態(tài)和支持它的工具是一項艱巨的任務(wù)。但是,微前端現(xiàn)在得到了 Webpack 5 和模塊聯(lián)合的良好支持。并非所有 Web 應(yīng)用程序都需要微前端架構(gòu),但對于那些已經(jīng)開始變得笨拙的大型、功能豐富的 Web 應(yīng)用程序,我們的 Web 工具中對微前端的一流支持絕對是一個加分項。

這篇文章是系列文章的第一部分,我們將使用 Angular 和微型前端構(gòu)建一個電子商務(wù)網(wǎng)站。我們將使用帶有模塊聯(lián)合的 Webpack 5 來支持將微前端連接在一起。然后,我們將演示在不同前端之間共享經(jīng)過身份驗證的狀態(tài),并將其全部部署到免費的云托管提供商。在第一篇文章中,我們將探索一個初學(xué)者項目,并了解不同應(yīng)用程序如何連接,使用Okta添加身份驗證,并添加共享身份驗證狀態(tài)的布線。最后,您將擁有一個看起來像這樣的應(yīng)用程序:

先決條件

Node: 這個項目是使用Node v16.14和npm v8.5開發(fā)的

Angular CLI

Okta CLI

使用Webpack 5和模塊聯(lián)合的微前端啟動器

使用OpenID Connect添加身份驗證

創(chuàng)建一個新的Angular應(yīng)用程序

適用于您的Angular應(yīng)用程序的模塊聯(lián)合

微前端狀態(tài)管理

下一步

了解Angular、OpenID Connect、微前端等

使用Webpack 5和模塊聯(lián)合的微前端啟動器

這個網(wǎng)絡(luò)應(yīng)用程序里有很多!我們將使用初學(xué)者代碼來確保我們專注于特定于微前端的代碼。如果您對使用起動器而不是從頭開始感到沮喪,請不要擔(dān)心。我將提供Angular CLI命令,以便在存儲庫的README.md上重新創(chuàng)建此初學(xué)者應(yīng)用程序的結(jié)構(gòu),以便您擁有所有說明。

按照以下步驟克隆Angular Micro Frontend示例GitHub repo,并在您最喜歡的IDE中打開repo。

git clone https://github.com/oktadev/okta-angular-microfrontend-example.git

cd okta-angular-microfrontend-example

npm ci

讓我們深入研究代碼!?/p>

我們有一個Angular項目,在src/projects目錄中有兩個應(yīng)用程序和一個庫。這兩個應(yīng)用程序被命名為shell和mfe-basket,庫被命名為shared。shell應(yīng)用程序是微前端主機,mfe-basket是微前端遠程應(yīng)用程序。shared庫包含我們想要在整個網(wǎng)站上共享的代碼和應(yīng)用程序狀態(tài)。當(dāng)您為該應(yīng)用程序應(yīng)用上面顯示的相同類型的圖表時,它看起來像這樣:

在這個項目中,我們使用@angular-architects/module-federation依賴項來幫助封裝配置Webpack和模塊聯(lián)合插件的一些復(fù)雜性。shell和mfe-basket應(yīng)用程序有自己單獨的webpack.config.js。打開shell或mfe-basket應(yīng)用程序的projects/shell/webpack.config.js文件,以查看整體結(jié)構(gòu)。這個文件是我們在模塊聯(lián)合插件中添加主機、遙控器、共享代碼和共享依賴項的布線的地方。如果您不使用@angular-architects/module-federation依賴項,結(jié)構(gòu)將有所不同,但配置的基本想法保持不變。

讓我們來探索一下這個配置文件的部分。

// ...imports here

const sharedMappings = new mf.SharedMappings();

sharedMappings.register(

? path.join(__dirname, '../../tsconfig.json'),

? [

? ? '@shared'

? ]);

module.exports = {

? // ...other very important config properties

? plugins: [

? ? new ModuleFederationPlugin({

? ? ? library: { type: "module" },

? ? ? // For remotes (please adjust)

? ? ? // name: "shell",

? ? ? // filename: "remoteEntry.js",

? ? ? // exposes: {

? ? ? //? ? './Component': './projects/shell/src/app/app.component.ts',

? ? ? // },? ? ? ?


? ? ? // For hosts (please adjust)

? ? ? remotes: {

? ? ? ? "mfeBasket": "http://localhost:4201/remoteEntry.js",

? ? ? },

? ? ? shared: share({

? ? ? ? // ...important external libraries to share

? ? ? ? ...sharedMappings.getDescriptors()

? ? ? })

? ? }),

? ? sharedMappings.getPlugin()

? ],

};

在mfe-basket的webpack.config.js中,您將在文件頂部看到@shared的路徑和配置,以確定在遠程應(yīng)用程序中要公開的內(nèi)容。

shell應(yīng)用程序在端口4200上服務(wù),mfe-basket應(yīng)用程序在端口4201上服務(wù)。我們可以打開兩個終端來運行每個應(yīng)用程序,或者我們可以使用示意圖為我們創(chuàng)建的以下npm腳本來add@angular-architects/module-federation:

npm run run:all

當(dāng)您這樣做時,您將看到兩個應(yīng)用程序在瀏覽器中打開,以及它們?nèi)绾卧诙丝?200上運行的shell應(yīng)用程序中組合在一起。單擊Baket按鈕導(dǎo)航到在mfe-basket應(yīng)用程序中顯示BasketModule的新路由。登錄按鈕還不能正常工作,但我們接下來會在這里運行。

注意-我本可以用于初學(xué)者的另一個選項是Nx工作區(qū)。Nx擁有強大的工具和內(nèi)置支持,用于使用Webpack和模塊聯(lián)合構(gòu)建微型前端。但我想在項目工具上變得簡約,這樣你就有機會沉浸在一些配置要求中。

@shared語法對你來說可能看起來有點不尋常。您可能期望看到通往圖書館的相對路徑。@shared語法是庫路徑的別名,該路徑在項目的tsconfig.json文件中定義。你不必這樣做。您可以使用相對路徑離開庫,但添加別名使您的代碼看起來更干凈,并有助于確保代碼架構(gòu)的最佳實踐。

由于除了webpack.config.js主機應(yīng)用程序不知道遠程應(yīng)用程序,我們通過在decl.d.ts中聲明遠程應(yīng)用程序來幫助TypeScript編譯器。您可以在此提交中查看為初學(xué)者所做的所有配置更改和源代碼。

使用OpenID Connect添加身份驗證

模塊聯(lián)合最有用的功能之一是管理共享代碼和狀態(tài)。讓我們通過向項目添加身份驗證來看看這一切是如何運作的。我們將在現(xiàn)有應(yīng)用程序和新的微前端中使用經(jīng)過身份驗證的狀態(tài)。

在開始之前,您需要一個免費的Okta開發(fā)人員帳戶。安裝Okta CLI并運行okta register以注冊新帳戶。如果您已經(jīng)有一個帳戶,請運行okta login。然后,運行okta apps create。選擇默認應(yīng)用程序名稱,或根據(jù)您認為合適的方式進行更改。選擇單頁應(yīng)用程序,然后按Enter鍵。

使用http://localhost:4200/login/callback進行重定向URI,并將注銷重定向URI設(shè)置為http://localhost:4200。

Okta CLI是做什么的?

The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200. You will see output like the following when it’s finished:

Okta application configuration:

Issuer:? ? https://dev-133337.okta.com/oauth2/default

Client ID: 0oab8eb55Kb9jdMIr5d6

NOTE: You can also use the Okta Admin Console to create your app. SeeCreate an Angular Appfor more information.

記下Issuer和Client ID。你很快就會需要這些價值觀。

我們將使用Okta AngularOkta Auth JS庫將我們的Angular應(yīng)用程序與Okta身份驗證連接起來。通過運行以下命令將它們添加到您的項目中。

npm install @okta/okta-angular@5.2 @okta/okta-auth-js@6.4

接下來,我們需要將OktaAuthModule導(dǎo)入shell項目的AppModule,并添加Okta配置。將以下代碼中的占位符替換為之前的Issuer和Client ID。

import { OKTA_CONFIG, OktaAuthModule } from '@okta/okta-angular';

import { OktaAuth } from '@okta/okta-auth-js';

const oktaAuth = new OktaAuth({

? issuer: 'https://{yourOktaDomain}/oauth2/default',

? clientId: '{yourClientID}',

? redirectUri: window.location.origin + '/login/callback',

? scopes: ['openid', 'profile', 'email']

});

@NgModule({

? ...

? imports: [

? ? ...,

? ? OktaAuthModule

? ],

? providers: [

? ? {provide: OKTA_CONFIG, useValue: { oktaAuth } }

? ],

? ...

})

使用Okta進行身份驗證后,我們需要設(shè)置登錄回調(diào)以完成登錄過程。在shell項目中打開app-routing.module.ts,并更新路由數(shù)組,如下所示。

import { OktaCallbackComponent } from '@okta/okta-angular';

const routes: Routes = [

? {path: '', component: ProductsComponent },

? {path: 'basket', loadChildren: () => import('mfeBasket/Module').then(m => m.BasketModule) },

? {path: 'login/callback', component: OktaCallbackComponent }

];

現(xiàn)在我們已經(jīng)在應(yīng)用程序中配置了Okta,我們可以添加代碼來登錄和注銷。在shell項目中打開app.component.ts。我們將添加使用Okta庫登錄和注銷的方法。我們還將更新兩個公共變量,以使用實際的身份驗證狀態(tài)。更新您的代碼以匹配以下代碼。

import { Component, Inject } from '@angular/core';

import { filter, map, Observable, shareReplay } from 'rxjs';

import { OKTA_AUTH, OktaAuthStateService } from '@okta/okta-angular';

import { OktaAuth } from '@okta/okta-auth-js';

@Component({

? selector: 'app-root',

? templateUrl: './app.component.html',

? styles: []

})

export class AppComponent {

? public isAuthenticated$: Observable<boolean> = this.oktaStateService.authState$

? ? ? .pipe(

? ? ? ? ? filter(authState => !!authState),

? ? ? ? ? map(authState => authState.isAuthenticated ?? false),

? ? ? ? ? shareReplay()

? ? ? );


? public name$: Observable<string> = this.oktaStateService.authState$

? ? ? .pipe(

? ? ? ? ? filter(authState => !!authState && !!authState.isAuthenticated),

? ? ? ? ? map(authState => authState.idToken?.claims.name ?? '')

? ? ? );

? constructor(private oktaStateService: OktaAuthStateService, @Inject(OKTA_AUTH) private oktaAuth: OktaAuth) { }

? public async signIn(): Promise<void> {

? ? await this.oktaAuth.signInWithRedirect();

? }

? public async signOut(): Promise<void> {

? ? await this.oktaAuth.signOut();

? }

}

我們需要為登錄和注銷按鈕添加點擊處理程序。在shell項目中打開app.component.html。更新登錄注銷按鈕的代碼,如圖所示。

<li>

? <button *ngIf="(isAuthenticated$ | async) === false; else logout"

? ? class="flex items-center transition ease-in delay-150 duration-300 h-10 px-4 rounded-lg hover:border hover:border-sky-400"

? ? (click)="signIn()"

? >

? ? <span class="material-icons-outlined text-gray-500">login</span>

? ? <span>&nbsp;Sign In</span>

? </button>

? ? <ng-template #logout>

? ? ? <button?

? ? ? ? class="flex items-center transition ease-in delay-150 duration-300 h-10 px-4 rounded-lg hover:border hover:border-sky-400"

? ? ? ? (click)="signOut()"

? ? ? >

? ? ? ? <span class="material-icons-outlined text-gray-500">logout</span>

? ? ? ? <span>&nbsp;Sign Out</span>

? ? ? </button>

? </ng-template>

</li>

嘗試使用npm run run:all運行項目。現(xiàn)在您將能夠登錄和退出。當(dāng)您登錄時,會顯示一個新的個人資料按鈕。當(dāng)您單擊它時,什么都不會發(fā)生,但我們將創(chuàng)建一個新的遙控器,將其連接到主機,然后在這里共享身份驗證狀態(tài)!

創(chuàng)建一個新的Angular應(yīng)用程序

現(xiàn)在,您將有機會通過創(chuàng)建顯示經(jīng)過身份驗證的用戶配置文件信息的微前端應(yīng)用程序,了解微前端遠程如何連接到主機。停止為項目提供服務(wù),并在終端中運行以下命令,以在項目中創(chuàng)建新的Angular應(yīng)用程序:

ng generate application mfe-profile --routing --style css --inline-style --skip-tests

有了這個Angular CLI命令,你

生成了一個名為mfe-profile的新應(yīng)用程序,其中包括一個模塊和一個組件

在應(yīng)用程序中添加了一個單獨的路由模塊

定義了在組件中內(nèi)聯(lián)的CSS樣式

跳過為初始組件創(chuàng)建相關(guān)測試文件

您現(xiàn)在將為默認路由創(chuàng)建一個組件,HomeComponent和一個用于容納微前端的模塊。我們可以連接微型前端,只使用一個組件而不是一個模塊。事實上,一個組件將滿足我們對配置文件視圖的需求,但我們將使用一個模塊,以便您可以看到每個微前端如何隨著項目的發(fā)展而增長。在終端中運行以下兩個命令:

ng generate component home --project mfe-profile

ng generate module profile --project mfe-profile --module app --routing --route profile

使用這兩個Angular CLI命令,您:

在mfe-profile應(yīng)用程序中創(chuàng)建了一個新組件HomeComponent

創(chuàng)建了一個新的模塊ProfileModule,帶有路由和默認組件ProfileComponent。您還使用AppModule的“/profile”路徑將ProfileModule添加為懶惰加載路由。

讓我們更新代碼。首先,我們將添加默認路線。打開projects/mfe-profile/src/app/app-routing.module.ts,并為HomeComponent添加新路由。您的路由數(shù)組應(yīng)與以下代碼匹配。

const routes: Routes = [

? {path: '', component: HomeComponent },

? {path: 'profile', loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule) }

];

接下來,我們將更新AppComponent和HomeComponent模板。Openprojectsprojects/mfe-profile/src/app/app.component.html并刪除其中的所有代碼。用以下內(nèi)容替換它:

<h1>Hey there! You're viewing the Profile MFE project! ??</h1>

<router-outlet></router-outlet>

打開projects/mfe-profile/src/app/home/home.component.html,并將文件中的所有代碼替換為:

<p>

? There's nothing to see here. ??<br/>

? The MFE is this way ??<a routerLink="/profile">Profile</a>

</p>

最后,我們可以更新配置文件的代碼。幸運的是,Angular CLI為我們處理了很多腳手架。因此,我們只需要更新組件的TypeScript文件和模板。

打開projects/mfe-profile/src/app/profile/profile.component.ts并編輯組件以添加兩個公共屬性,并在構(gòu)造函數(shù)中包含OktaAuthStateService:

import { Component, OnInit } from '@angular/core';

import { OktaAuthStateService } from '@okta/okta-angular';

import { filter, map } from 'rxjs';

@Component({

? selector: 'app-profile',

? templateUrl: './profile.component.html',

? styles: []

})

export class ProfileComponent {

? public profile$ = this.oktaStateService.authState$.pipe(

? ? filter(state => !!state && !!state.isAuthenticated),

? ? map(state => state.idToken?.claims)

? );

? public date$ = this.oktaStateService.authState$.pipe(

? ? filter(state => !!state && !!state.isAuthenticated),

? ? map(state => (state.idToken?.claims.auth_time as number) * 1000),

? ? map(epochTime => new Date(epochTime)),

? );

? constructor(private oktaStateService: OktaAuthStateService) { }

}

接下來,打開相應(yīng)的模板文件,并將現(xiàn)有代碼替換為以下內(nèi)容:

<h3 class="text-xl mb-6">Your Profile</h3>

<div *ngIf="profile$ | async as profile">

? <p>Name: <span class="font-semibold">{{profile.name}}</span></p>

? <p class="my-3">Email: <span class="font-semibold">{{profile.email}}</span></p>

? <p>Last signed in at <span class="font-semibold">{{date$ | async | date:'full'}}</span></p>

</div>

嘗試通過在終端中運行ng serve mfe-profile --open來自行運行mfe-profile應(yīng)用程序。請注意,當(dāng)我們導(dǎo)航到/profile路由時,我們看到一個控制臺錯誤。我們將Okta添加到shell應(yīng)用程序中,但現(xiàn)在我們需要將mfe-profile應(yīng)用程序轉(zhuǎn)換為微前端,并共享身份驗證狀態(tài)。停止提供應(yīng)用程序,以便我們?yōu)橄乱徊阶龊脺蕚洹?/p>

適用于您的Angular應(yīng)用程序的模塊聯(lián)合

我們希望使用@angular-architects/module-federation的原理圖將mfe-profile應(yīng)用程序轉(zhuǎn)換為微型前端,并添加必要的配置。我們將為此應(yīng)用程序使用端口4202。通過在終端中運行以下命令來添加原理圖:

ng add @angular-architects/module-federation --project mfe-profile --port 4202

此示意圖如下:

更新項目的angular.json配置文件,為應(yīng)用程序添加端口,并更新構(gòu)建器以使用自定義Webpack構(gòu)建器

創(chuàng)建webpack.config.js文件,并構(gòu)建模塊聯(lián)合的默認配置

首先,讓我們通過更新inprojectsprojects/mfe-profile/webpack.config.js的配置,將新的微前端添加到shell應(yīng)用程序中。在文件的中間,有一個帶有注釋代碼的plugins屬性。我們需要完成配置。由于此應(yīng)用程序是遠程的,我們將在注釋下更新代碼片段:

// For remotes (please adjust)

默認值大多是正確的,只是我們有一個模塊,而不是我們想要公開的組件。如果您想公開組件,您所要做的就是更新要公開的組件。通過匹配以下代碼片段,更新配置片段以公開ProfileModule:

// For remotes (please adjust)

name: "mfeProfile",

filename: "remoteEntry.js",

exposes: {

? './Module': './projects/mfe-profile/src/app/profile/profile.module.ts',

},

現(xiàn)在我們可以將微前端整合到shell應(yīng)用程序中。打開projects/shell/webpack.config.js。在這里,您將添加新的微前端,以便shell應(yīng)用程序知道如何訪問它。在文件中間,在plugins組中,有一個remotes屬性。初學(xué)者代碼mfeBasket中的微前端已添加到remotes對象中。您還將在那里添加mfeProfile的遙控器,遵循相同的模式,但將端口替換為4202。更新您的配置,使其看起來像這樣。

// For hosts (please adjust)

remotes: {

? "mfeBasket": "http://localhost:4201/remoteEntry.js",

? "mfeProfile": "http://localhost:4202/remoteEntry.js"

},

我們可以更新代碼以合并配置文件的微前端。打開projects/shell/src/app/app-routing.module.ts。使用路徑“配置文件”在路由數(shù)組中向配置文件微前端添加路徑。您的路由數(shù)組應(yīng)該看起來像這樣。

const routes: Routes = [

? {path: '', component: ProductsComponent },

? {path: 'basket', loadChildren: () => import('mfeBasket/Module').then(m => m.BasketModule) },

? {path: 'profile', loadChildren: () => import('mfeProfile/Module').then(m => m.ProfileModule)},

? {path: 'login/callback', component: OktaCallbackComponent }

];

這是什么???IDE將導(dǎo)入路徑標記為錯誤!shell應(yīng)用程序代碼不知道Profile模塊,TypeScript需要一點幫助。打開projects/shell/src/decl.d.ts并添加以下行代碼。

declare module 'mfeProfile/Module';

IDE現(xiàn)在應(yīng)該更快樂了。?/p>

接下來,更新shell應(yīng)用程序中Profile的導(dǎo)航按鈕,以路由到正確的路徑。Openprojectsprojects/shell/src/app/app.component.html,并找到配置文件按鈕的routerLink。它應(yīng)該大約在第38行。目前,routerLink配置是routerLink="/"但現(xiàn)在應(yīng)該是

<a routerLink="/profile">

這是我們將微前端遠程連接到主機應(yīng)用程序所需的一切,但我們也希望共享身份驗證狀態(tài)。模塊聯(lián)盟使共享狀態(tài)成為一塊(杯)蛋糕。

微前端狀態(tài)管理

要共享庫,您需要在webpack.config.js中配置庫。讓我們從shell開始。Openprojectsprojects/shell/src/webpack.config.js。

有兩個地方可以添加共享代碼。一個地方是項目內(nèi)的代碼實現(xiàn),一個地方是共享的外部庫。在這種情況下,我們可以共享Okta外部庫,因為我們沒有實現(xiàn)包裝Okta授權(quán)庫的服務(wù),但我將指出這兩個地方。

首先,我們將添加Okta庫。向下滾動到文件底部的shared屬性。您將遵循與列表中已有的@angular庫相同的模式,并添加兩個Okta庫的單例,如此片段所示:

shared: share({

? // other Angular libraries remain in the config. This is just a snippet

? "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },

? "@okta/okta-angular": { singleton: true, strictVersion: true, requiredVersion: 'auto' },

? "@okta/okta-auth-js": { singleton: true, strictVersion: true, requiredVersion: 'auto' },

? ...sharedMappings.getDescriptors()

})

當(dāng)您在此項目中創(chuàng)建庫時,如啟動代碼中的籃子服務(wù)和項目服務(wù),您將庫添加到webpack.config.js文件頂部的sharedMappings數(shù)組中。如果您創(chuàng)建一個新庫來包裝Okta的庫,這就是您將添加它的地方。

現(xiàn)在您已將Okta庫添加到微前端主機中,您還需要將它們添加到消耗依賴項的遙控器中。在我們的案例中,只有mfe-profile應(yīng)用程序使用Okta認證的狀態(tài)信息。Openprojectsprojects/mfe-profile/webpack.config.js。像對shell應(yīng)用程序一樣,將兩個Okta庫添加到shared屬性中。

現(xiàn)在,您應(yīng)該能夠使用npm run run:all運行該項目,紙杯蛋糕店面應(yīng)該允許您登錄,查看您的個人資料,退出,并將物品添加到您的紙杯蛋糕籃中!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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