簡體   English   中英

模塊聯合 Angular 來自同一遠程主機的多個微前端 inject() 必須從注入上下文中調用 MatCommonModule_Factory

[英]Module Federation Angular multiple microfrontends from same remote host inject() must be called from an injection context MatCommonModule_Factory

我目前在使用 webpack 5 模塊聯合的基於微前端的 Angular 12 應用程序中遇到錯誤。 當從同一個遠程主機公開多個微前端時會發生錯誤。 我們遵循多倉庫的結構。 遠程應用程序是 angular cli 項目。 shell 是一個 NX 存儲庫。

這是 shell 應用程序中的 webpack.config.js:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');
const share = mf.share;

const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, '../../tsconfig.base.json'), [
  '@ahs-dev/local-data-store',
  '@ahs-dev/dialog',
  '@ahs-dev/http',
  '@ahs-dev/message-bus',
  '@ahs-dev/ipc',
  '@ahs-dev/info-dialog',
]);

module.exports = {
  output: {
    uniqueName: 'kvaiPro',
    publicPath: 'auto',
  },
  optimization: {
    runtimeChunk: false,
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      shared: share({
        '@angular/core': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/common': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/common/http': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/cdk': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/router': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/material': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/material/core': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ngrx/store': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ngrx/effects': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ahs-dev/restclient-cockpit': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        ...sharedMappings.getDescriptors(),
      }),
    }),
    sharedMappings.getPlugin(),
  ],
};

微前端在引導之前通過 angular 架構師庫 loadRemoteModule-function 預加載:

import { loadRemoteEntry } from '@angular-architects/module-federation';
import { MicroFrontendConfig } from './app/app-config';

fetch(`/assets/config/micro-frontends-config.json`)
  .then((res) => res.json())
  .then((configs: { modules: MicroFrontendConfig[] }) => {
    return configs.modules.map((config) =>
      loadRemoteEntry(config.remoteEntryPath, config.moduleName)
    );
  })
  .catch((err) => console.error('Error loading remote entries', err))
  .then(() => {
    return import('./bootstrap');
  })
  .catch((err) => console.error(err));

雖然它們是在 json 文件中定義的:

{
  "modules": [
    {
      "moduleName": "testDialogNoOne",
      "remoteEntryPath": "http://localhost:5000/remoteEntry.js",
      "urlPath": "test-dialog-no-one",
      "exposedModule": "./module",
      "mainAngularModule": "DialogOneModule",
      "useCase": "anwendungskonfigurationen"
    },
    {
      "moduleName": "mgv",
      "remoteEntryPath": "http://localhost:5100/remoteEntry.js",
      "urlPath": "mgv-zeitbezug",
      "exposedModule": "./zeitbezuege",
      "mainAngularModule": "DialogZeitbezugModule"
    },
    {
      "moduleName": "mgv",
      "remoteEntryPath": "http://localhost:5100/remoteEntry.js",
      "urlPath": "mgv-laufarten",
      "exposedModule": "./laufarten",
      "mainAngularModule": "DialogLaufartenModule"
    }
  ]
}

然后我們使用 json 文件修補 angular 路由器,以在 app.component 中加載微前端:

...
/** @inheritdoc */
  public ngOnInit(): void {
    this.httpClient
      .get<{ modules: MicroFrontendConfig[] }>(
        `/assets/config/micro-frontends-config.json`
      )
      .pipe(
        map((config) => config.modules),
        take(1)
      )
      .subscribe((configs) => {
        const dashboardChildren: Routes = [
          ...configs.map((config) => ({
            path: config.urlPath,
            canActivate: [AuthGuard],
            loadChildren: () => {
              return loadRemoteModule({
                remoteName: config.moduleName,
                exposedModule: config.exposedModule,
              }).then((esm) => {
                return esm[config.mainAngularModule];
              });
            },
          })),
          {
            path: 'neue-aufgabe',
            component: NeueAufgabeDialogOutletComponent,
            canActivate: [AuthGuard],
          },
        ];
        const routes = [
          {
            path: '',
            redirectTo: '/main',
            pathMatch: 'full',
          },
          {
            path: 'main',
            component: DashboardPageComponent,
            children: dashboardChildren,
            pathMatch: 'prefix',
            canActivate: [AuthDashboardGuard],
          },
          {
            // Must be at the end
            path: '**',
            redirectTo: 'main',
          },
        ];

        this.router.resetConfig(routes);
      });
}
...

每個微前端都包含一個對話框,在調用相應的路由時應該打開該對話框。 為此,我們在遠程主機的 repo 中有以下結構:

來自遠程應用程序的 webpack.config.js:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mf = require('@angular-architects/module-federation/webpack');
const path = require('path');
const share = mf.share;

const sharedMappings = new mf.SharedMappings();
sharedMappings.register(path.join(__dirname, 'tsconfig.json'), [
  '@ahs-dev/local-data-store',
  '@ahs-dev/http',
  '@ahs-dev/message-bus',
  '@ahs-dev/ipc',
  '@ahs-dev/info-dialog',
]);

module.exports = {
  output: {
    uniqueName: 'mgv',
    publicPath: 'auto',
  },
  optimization: {
    runtimeChunk: false,
  },
  resolve: {
    alias: {
      ...sharedMappings.getAliases(),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'mgv',
      filename: 'remoteEntry.js',
      exposes: [
        {
          './zeitbezuege':
            './src/app/dialog-zeitbezug/dialog-zeitbezug.module.ts',
        },
        {
          './laufarten':
            './src/app/dialog-laufarten/dialog-laufarten.module.ts',
        },
      ],
      shared: share({
        '@angular/core': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/common': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/common/http': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/cdk': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/router': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/material': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@angular/material/core': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ngrx/store': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ngrx/effects': {
          singleton: false,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ahs-dev/restclient-cockpit': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        '@ahs-dev/dialog': {
          singleton: true,
          // strictVersion: true,
          requiredVersion: 'auto',
        },
        ...sharedMappings.getDescriptors(),
      }),
    }),
    sharedMappings.getPlugin(),
  ],
};

我們公開了兩個模塊,在本例中是dialog-zeitbezug.module.tsdialog-laufarten.module.ts ,未來可能還會更多。 我已經嘗試將公開值寫為{} ,但這沒有幫助。 無論哪種方式都行不通。

dialog-zeitbezug.module.ts包含一個路由模塊,該模塊指向打開出口組件的默認路由:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
// import { DialogOutletComponent } from './dialog-laufart-liste/dialog-outlet.component';
import { DialogOutletComponent } from './dialog-outlet/dialog-outlet.component';

const routes: Routes = [
  {
    path: '',
    component: DialogOutletComponent,
  },
  // This route ALWAYS needs to be the last one:
  {
    path: '**',
    redirectTo: '',
  },
];

@NgModule({
  imports: [CommonModule, RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class DialogZeitbezugRoutingModule {}

然后出口組件只打開相應的對話框組件:

import { DialogService } from '@ahs-dev/dialog';
import { defaultOpenDialogSettings, dialogSize } from '@ahs-dev/utils';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { DialogZeitbezugComponent } from '../dialog-zeitbezug.component';

@Component({
  selector: 'ahs-dialog-outlet',
  template: '',
  styleUrls: ['./dialog-outlet.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: MatDialogRef,
      useValue: {},
    },
  ]
})
export class DialogOutletComponent {
  constructor(private readonly dialogService: DialogService) {
    this.dialogService.open(
      DialogZeitbezugComponent,
      Object.assign({}, defaultOpenDialogSettings(dialogSize.sizeXl), {
        height: '86vh',
      }),
    );
  }
}

我們在材料對話框服務周圍有一個抽象,它允許我們緩存所有 dialogRefs,因為我們需要能夠在按下 shell 中的某個按鈕時關閉所有對話框。 對話服務在 shell 和微前端之間共享。 據我所知,這也像預期的那樣工作。 不要認為問題是由抽象引起的,但誰知道呢。

總結一下:當我只有一個來自遠程主機的微應用程序時,一切都按預期工作。

json 將如下所示:

{
  "modules": [
    {
      "moduleName": "testDialogNoOne",
      "remoteEntryPath": "http://localhost:5000/remoteEntry.js",
      "urlPath": "test-dialog-no-one",
      "exposedModule": "./module",
      "mainAngularModule": "DialogOneModule",
      "useCase": "anwendungskonfigurationen"
    },
    {
      "moduleName": "mgv",
      "remoteEntryPath": "http://localhost:5100/remoteEntry.js",
      "urlPath": "mgv-zeitbezug",
      "exposedModule": "./zeitbezuege",
      "mainAngularModule": "DialogZeitbezugModule"
    }
  ]
}

但是一旦我從同一個模塊和遠程主機添加另一個微應用程序,就像這樣:

{
  "modules": [
    {
      "moduleName": "testDialogNoOne",
      "remoteEntryPath": "http://localhost:5000/remoteEntry.js",
      "urlPath": "test-dialog-no-one",
      "exposedModule": "./module",
      "mainAngularModule": "DialogOneModule",
      "useCase": "anwendungskonfigurationen"
    },
    {
      "moduleName": "mgv",
      "remoteEntryPath": "http://localhost:5100/remoteEntry.js",
      "urlPath": "mgv-zeitbezug",
      "exposedModule": "./zeitbezuege",
      "mainAngularModule": "DialogZeitbezugModule"
    },
    {
      "moduleName": "mgv",
      "remoteEntryPath": "http://localhost:5100/remoteEntry.js",
      "urlPath": "mgv-laufarten",
      "exposedModule": "./laufarten",
      "mainAngularModule": "DialogLaufartenModule"
    }
  ]
}

我收到以下錯誤:

core.js:6479 
        
       ERROR Error: Uncaught (in promise): Error: inject() must be called from an injection context
Error: inject() must be called from an injection context
    at injectInjectorOnly (core.js:4745:1)
    at Module.ɵɵinject (core.js:4755:1)
    at Object.MatCommonModule_Factory [as factory] (core.js:173:111)
    at R3Injector.hydrate (core.js:11438:1)
    at R3Injector.get (core.js:11257:1)
    at core.js:11295:1
    at Set.forEach (<anonymous>)
    at R3Injector._resolveInjectorDefTypes (core.js:11295:1)
    at new NgModuleRef$1 (core.js:25325:1)
    at NgModuleFactory$1.create (core.js:25379:1)
    at resolvePromise (zone.js:1213:1)
    at resolvePromise (zone.js:1167:1)
    at zone.js:1279:1
    at ZoneDelegate.invokeTask (zone.js:406:1)
    at Object.onInvokeTask (core.js:28659:1)
    at ZoneDelegate.invokeTask (zone.js:405:1)
    at Zone.runTask (zone.js:178:1)
    at drainMicroTaskQueue (zone.js:582:1)
    at ZoneTask.invokeTask [as invoke] (zone.js:491:1)
    at invokeTask (zone.js:1600:1)

就我能夠解決它而言,問題在於來自 angular 材料的 HighContrastModeDetector 的注入器。

我已經嘗試在 webpack.configs 中指定固定版本,並將 @angular/cdk 設為 singleton 並共享。 沒有什么似乎解決了這個問題。 我現在嘗試在 shell 和微前端中安裝相同的 angular cli 版本,但我懷疑這是否能解決問題,因為這無法解釋為什么只有一個暴露的模塊它可以工作而兩個它停止工作。

我還嘗試將preserveSymLinks添加到微前端的存儲庫和 shell 的存儲庫中,但這似乎沒有幫助。

對於完成的堆棧,這里是 shell 的 package.json:

{
  "name": "@ahs-dev/cockpit-frontend",
  "version": "0.1.1",
  "license": "",
  "scripts": {
    "ng": "nx",
    "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main",
    "start": "nx serve",
    "start:kvai-pro:standalone": "ng serve kvai-pro",
    "start:kvai-pro:standalone:prod": "ng serve kvai-pro --prod",
    "build:kvai-pro:prod": "nx build kvai-pro --verbose",
    "build:test-dialog-no-one:prod": "nx build test-dialog-no-one --verbose",
    "start:test-dialog-no-one:standalone": "ng serve test-dialog-no-one",
    "start:test-dialog-no-one:standalone:prod": "ng serve test-dialog-no-one --prod",
    "start:test-zeitbezug:standalone": "ng serve test-zeitbezug",
    "start:kvai-pro:dev": "concurrently \"npm run start:kvai-pro:standalone\" \"ng serve test-dialog-no-one\" \"ng serve test-dialog-no-two\"",
    "build-assistent": "nx build assistent",
    "build-utils": "nx build utils",
    "build-translation": "nx build translation",
    "build-inputs": "nx build inputs",
    "build-dialog": "nx build dialog",
    "build-info-dialog": "nx build info-dialog",
    "build-local-data-store": "nx build local-data-store",
    "build-http": "nx build http",
    "build-message-bus": "nx build message-bus",
    "build-table": "nx build table",
    "build-ipc": "nx build ipc",
    "build": "nx build",
    "test": "nx test",
    "storybook:inputs:serve": "nx run inputs:storybook",
    "storybook:inputs:build": "nx run inputs:build-storybook",
    "storybook:info-dialog:serve": "nx run info-dialog:storybook",
    "storybook:info-dialog:build": "nx run info-dialog:build-storybook",
    "affected:apps": "nx affected:apps -uncommited",
    "affected:libs": "nx affected:libs -uncommited",
    "affected:build": "nx affected:build -uncommited",
    "affected:e2e": "nx affected:e2e -uncommited",
    "affected:test": "nx affected:test -uncommited",
    "affected:lint": "nx affected:lint -uncommited",
    "affected:dep-graph": "nx affected:dep-graph -uncommited",
    "affected": "nx affected -uncommited",
    "format:write": "nx format:write",
    "format:check": "nx format:check",
    "update": "ng update @nrwl/workspace",
    "update:check": "ng update",
    "workspace-schematic": "nx workspace-schematic",
    "dep-graph": "nx dep-graph",
    "help": "nx help",
    "json-server": "json-server gen.js",
    "test:test": "jest --coverage",
    "storybook:table": "nx run table:storybook"
  },
  "private": true,
  "dependencies": {
    "@ahs-dev/inputs": "0.0.11",
    "@ahs-dev/restclient-cockpit": "0.1.19",
    "@ahs-dev/restclient-umgebungsservice": "1.2.7",
    "@ahs-dev/theme": "0.0.23",
    "@ahs-dev/utils": "0.0.7",
    "@angular-architects/module-federation": "^12.4.1",
    "@angular/animations": "12.2.3",
    "@angular/cdk": "12.2.3",
    "@angular/common": "12.2.3",
    "@angular/compiler": "12.2.3",
    "@angular/core": "12.2.3",
    "@angular/forms": "12.2.3",
    "@angular/material": "12.2.3",
    "@angular/platform-browser": "12.2.3",
    "@angular/platform-browser-dynamic": "12.2.3",
    "@angular/router": "12.2.3",
    "@ngrx/effects": "12.4.0",
    "@ngrx/store": "12.4.0",
    "@ngrx/store-devtools": "12.4.0",
    "@ngx-translate/core": "13.0.0",
    "@ngx-translate/http-loader": "6.0.0",
    "@nrwl/angular": "12.8.0",
    "angular-oauth2-oidc": "12.1.0",
    "devextreme": "21.1.6",
    "devextreme-angular": "21.1.6",
    "devextreme-schematics": "1.2.21",
    "immer": "9.0.6",
    "jwt-decode": "3.1.2",
    "lodash": "4.17.21",
    "ngrx-immer": "1.0.1",
    "ngx-mat-select-search": "^3.3.0",
    "rxjs": "6.6.0",
    "tslib": "2.3.0",
    "uuid": "8.3.2",
    "zone.js": "0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~12.1.0",
    "@angular-eslint/eslint-plugin": "~12.3.0",
    "@angular-eslint/eslint-plugin-template": "~12.3.0",
    "@angular-eslint/template-parser": "~12.3.0",
    "@angular/cli": "12.2.3",
    "@angular/compiler-cli": "^12.1.0",
    "@angular/language-service": "^12.1.0",
    "@ngrx/schematics": "^12.4.0",
    "@nrwl/cli": "12.8.0",
    "@nrwl/cypress": "12.8.0",
    "@nrwl/eslint-plugin-nx": "12.8.0",
    "@nrwl/jest": "12.8.0",
    "@nrwl/linter": "12.8.0",
    "@nrwl/storybook": "^12.9.0",
    "@nrwl/tao": "12.8.0",
    "@nrwl/workspace": "12.8.0",
    "@storybook/addon-actions": "^6.3.7",
    "@storybook/addon-essentials": "^6.3.7",
    "@storybook/addon-links": "^6.3.7",
    "@storybook/angular": "^6.3.7",
    "@storybook/builder-webpack5": "^6.3.7",
    "@storybook/manager-webpack5": "^6.3.7",
    "@types/jest": "26.0.24",
    "@types/lodash": "^4.14.172",
    "@types/node": "14.14.33",
    "@types/uuid": "^8.3.1",
    "@typescript-eslint/eslint-plugin": "~4.28.3",
    "@typescript-eslint/parser": "~4.28.3",
    "concurrently": "^6.2.1",
    "electron": "13.1.7",
    "eslint": "7.22.0",
    "eslint-config-prettier": "8.1.0",
    "eslint-plugin-cypress": "^2.10.3",
    "faker": "^5.5.3",
    "jest": "27.0.3",
    "jest-preset-angular": "9.0.4",
    "json-server": "^0.16.3",
    "ng-packagr": "^12.1.0",
    "prettier": "^2.3.1",
    "ts-jest": "27.0.3",
    "typescript": "~4.3.5"
  }
}

以及包含微前端的 angular cli 項目的 package.json:

{
  "name": "mgv",
  "version": "0.0.0",
  "scripts": {
    "prepare": "husky install",
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "jest",
    "test:ci": "jest --coverage",
    "test:debug": "jest --detectOpenHandles",
    "lint": "ng lint"
  },
  "private": true,
  "dependencies": {
    "@ahs-dev/assistent": "0.1.2",
    "@ahs-dev/dialog": "0.0.14",
    "@ahs-dev/http": "0.0.3",
    "@ahs-dev/info-dialog": "0.0.9",
    "@ahs-dev/inputs": "0.0.18",
    "@ahs-dev/ipc": "0.0.1",
    "@ahs-dev/local-data-store": "0.0.5",
    "@ahs-dev/message-bus": "0.0.3",
    "@ahs-dev/restclient-cockpit": "0.1.19",
    "@ahs-dev/restclient-kvaipromgvermittlung": "0.0.6",
    "@ahs-dev/theme": "0.0.23",
    "@ahs-dev/utils": "0.0.10",
    "@angular-architects/module-federation": "12.5.0",
    "@angular/animations": "12.2.3",
    "@angular/cdk": "12.2.3",
    "@angular/common": "12.2.3",
    "@angular/compiler": "12.2.3",
    "@angular/core": "12.2.3",
    "@angular/forms": "12.2.3",
    "@angular/material": "12.2.3",
    "@angular/platform-browser": "12.2.3",
    "@angular/platform-browser-dynamic": "12.2.3",
    "@angular/router": "12.2.3",
    "@briebug/jest-schematic": "^3.1.0",
    "@ngrx/effects": "12.4.0",
    "@ngrx/store": "12.4.0",
    "@ngrx/store-devtools": "12.4.0",
    "date-fns": "2.28.0",
    "devextreme": "21.1.6",
    "devextreme-angular": "21.1.6",
    "devextreme-schematics": "1.2.21",
    "immer": "9.0.6",
    "jwt-decode": "3.1.2",
    "lodash": "4.17.21",
    "ngrx-immer": "1.0.1",
    "rxjs": "6.6.0",
    "tslib": "2.3.0",
    "uuid": "8.3.2",
    "zone.js": "0.11.4"
  },
  "devDependencies": {
    "@angular-builders/jest": "^12.1.2",
    "@angular-devkit/build-angular": "12.2.3",
    "@angular-eslint/builder": "12.4.1",
    "@angular-eslint/eslint-plugin": "^12.4.1",
    "@angular-eslint/eslint-plugin-template": "12.4.1",
    "@angular-eslint/schematics": "12.4.1",
    "@angular-eslint/template-parser": "12.4.1",
    "@angular/cli": "12.2.3",
    "@angular/compiler-cli": "~12.2.0",
    "@types/jest": "^27.0.1",
    "@types/lodash": "^4.14.172",
    "@types/node": "^12.11.1",
    "@types/uuid": "^8.3.1",
    "@typescript-eslint/eslint-plugin": "4.28.2",
    "@typescript-eslint/parser": "4.28.2",
    "eslint": "^7.26.0",
    "eslint-plugin-prettier": "^4.0.0",
    "husky": "^7.0.0",
    "jest": "^27.2.1",
    "jest-preset-angular": "^10.0.0",
    "typescript": "~4.3.5"
  }
}

任何想法和幫助將不勝感激。

提前致謝,

塞巴斯蒂安

我發現了這個問題。 這是部分代碼:

import { loadRemoteEntry } from '@angular-architects/module-federation';
import { MicroFrontendConfig } from './app/app-config';

fetch(`/assets/config/micro-frontends-config.json`)
  .then((res) => res.json())
  .then((configs: { modules: MicroFrontendConfig[] }) => {
    return configs.modules.map((config) =>
      loadRemoteEntry(config.remoteEntryPath, config.moduleName)
    );
  })
  .catch((err) => console.error('Error loading remote entries', err))
  .then(() => {
    return import('./bootstrap');
  })
  .catch((err) => console.error(err));

我只是添加了uniqBy(configs.modules, 'moduleName')來加載每個遠程條目一次,這解決了問題。 我之前試圖並行加載 remoteEntry.js 的副本,這導致了所描述的錯誤。

無論如何感謝您的閱讀和最良好的祝願,

塞巴斯蒂安

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM