You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular Universal多子域名/跨域名部署及差异化配置技术问询

Angular Universal 多子域名差异化实现方案

针对你需要用Angular Universal实现单一代码库多子域名差异化运行的需求,核心是在SSR环境下引导前获取请求域名,再通过全局标记控制路由、主题和功能,以下是具体实现步骤:

一、SSR环境中获取请求域名(无需window)

Angular Universal通过REQUEST令牌传递服务端请求上下文,我们可以在服务端渲染时提取请求域名,并注入到应用中:

  1. 修改server.ts,在渲染引擎中提供请求域名令牌:

    // server.ts
    import { ngExpressEngine } from '@nguniversal/express-engine';
    import { AppServerModule } from './src/main.server';
    import { REQUEST } from '@nguniversal/express-engine/tokens';
    
    app.engine('html', ngExpressEngine({
      bootstrap: AppServerModule,
      providers: [
        {
          provide: 'REQUEST_DOMAIN',
          useFactory: (req: Request) => req.hostname,
          deps: [REQUEST]
        }
      ]
    }));
    
  2. 在应用根模块定义令牌,并处理客户端兼容:

    // app.module.ts
    import { InjectionToken, PLATFORM_ID } from '@angular/core';
    import { isPlatformServer } from '@angular/common';
    
    export const REQUEST_DOMAIN = new InjectionToken<string>('REQUEST_DOMAIN');
    
    // 客户端环境下,从location获取域名,避免注入错误
    export const requestDomainProvider = {
      provide: REQUEST_DOMAIN,
      useFactory: (platformId: object) => 
        isPlatformServer(platformId) ? '' : window.location.hostname,
      deps: [PLATFORM_ID]
    };
    

    同时在app.server.module.ts中确保服务端注入生效:

    // app.server.module.ts
    import { NgModule } from '@angular/core';
    import { ServerModule } from '@angular/platform-server';
    import { AppModule } from './app.module';
    import { AppComponent } from './app.component';
    import { REQUEST_DOMAIN } from './app.module';
    import { REQUEST } from '@nguniversal/express-engine/tokens';
    
    @NgModule({
      imports: [AppModule, ServerModule],
      bootstrap: [AppComponent],
      providers: [
        {
          provide: REQUEST_DOMAIN,
          useFactory: (req: any) => req.hostname,
          deps: [REQUEST]
        }
      ]
    })
    export class AppServerModule {}
    

二、引导前设置全局条件标记

使用APP_INITIALIZER在应用引导前解析域名,设置全局标记,确保所有模块初始化时能读取到差异化配置:

// app.module.ts
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { REQUEST_DOMAIN, requestDomainProvider } from './app.module';
import { PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';

// 全局配置服务,存储域名相关标记
@Injectable({ providedIn: 'root' })
export class DomainConfigService {
  domainFlags = {
    isMain: false,
    isMarketing: false,
    isTenant: false,
    tenantId: ''
  };

  parseDomain(hostname: string) {
    switch(hostname) {
      case 'www.app.com':
        this.domainFlags.isMain = true;
        break;
      case 'markeeting.app.com':
        this.domainFlags.isMarketing = true;
        break;
      default:
        if (hostname.endsWith('.app.com')) {
          this.domainFlags.isTenant = true;
          this.domainFlags.tenantId = hostname.split('.')[0];
        }
    }
  }
}

// 初始化函数,引导前执行域名解析
export function initDomainConfig(
  config: DomainConfigService, 
  domain: string, 
  platformId: object
) {
  return () => {
    const hostname = isPlatformServer(platformId) ? domain : window.location.hostname;
    config.parseDomain(hostname);
    return Promise.resolve();
  };
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule.withServerTransition({ appId: 'serverApp' })],
  providers: [
    requestDomainProvider,
    {
      provide: APP_INITIALIZER,
      useFactory: initDomainConfig,
      deps: [DomainConfigService, REQUEST_DOMAIN, PLATFORM_ID],
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

三、差异化路由配置(营销子域名)

在路由模块中,根据全局标记动态替换路由配置:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DomainConfigService } from './app.module';

// 主域名路由
const mainRoutes: Routes = [
  { path: '', loadChildren: () => import('./main/main.module').then(m => m.MainModule) },
  // 其他主域名路由
];

// 营销域名路由
const marketingRoutes: Routes = [
  { path: '', loadChildren: () => import('./marketing/marketing.module').then(m => m.MarketingModule) },
  // 其他营销域名专属路由
];

@NgModule({
  imports: [RouterModule.forRoot([])],
  exports: [RouterModule]
})
export class AppRoutingModule {
  constructor(private router: Router, private domainConfig: DomainConfigService) {
    // 引导后重置路由配置
    this.router.resetConfig(
      this.domainConfig.domainFlags.isMarketing ? marketingRoutes : mainRoutes
    );
  }
}

四、租户子域名的主题与功能控制

在组件或服务中注入DomainConfigService,根据标记动态调整UI和功能:

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { DomainConfigService } from './app.module';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(private domainConfig: DomainConfigService) {}

  ngOnInit() {
    // 动态设置主题类
    const body = document.body;
    body.classList.remove('main-theme', 'marketing-theme');
    
    if (this.domainConfig.domainFlags.isTenant) {
      body.classList.add(`tenant-${this.domainConfig.domainFlags.tenantId}`);
    } else if (this.domainConfig.domainFlags.isMarketing) {
      body.classList.add('marketing-theme');
    } else {
      body.classList.add('main-theme');
    }
  }

  // 控制功能显示
  canShowFeature(featureKey: string): boolean {
    // 租户域名隐藏admin面板
    if (this.domainConfig.domainFlags.isTenant && featureKey === 'admin-panel') {
      return false;
    }
    return true;
  }
}

在模板中使用控制逻辑:

<!-- app.component.html -->
<nav>
  <a routerLink="/">首页</a>
  <a *ngIf="canShowFeature('admin-panel')" routerLink="/admin">管理面板</a>
</nav>
<router-outlet></router-outlet>

内容的提问来源于stack exchange,提问作者oneofthedevs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 14:19:53