项目介绍
为卓信ID、推必安业务开发的SAAS平台,包含开发者管理、服务商管理、SDK版本管理、权限管理、多重配置管理、设备命中策略等功能。
卓信SAAS平台-从微前端到BFF层的全方位技术架构升级
项目概述
卓信业务SaaS平台是一个集成了卓信ID反欺诈和推必安推送内容审核两大核心业务的综合性平台。该平台主要为广告商、服务商等B端客户提供客制化的数据业务服务,需要具备高度的可扩展性、安全性和性能表现。
核心功能模块
- 开发者管理-开发者账户管理、权限分配
- 服务商管理-第三方服务商接入与管理
- SDK版本管理-多版本SDK发布与维护
- 权限管理-细粒度权限控制系统
- 多重配置管理-灵活的配置管理机制
- 设备命中策略-智能设备识别与策略配置
项目背景与挑战
在项目初期,我们面临着单体应用架构带来的一系列挑战-
核心痛点
| 问题类型 | 具体表现 | 影响程度 |
|---|---|---|
| 性能问题 | 首屏加载时间长达7秒 | 严重影响用户体验 |
| 架构问题 | 不同业务模块耦合度高 | 迭代效率低下 |
| 集成问题 | 第三方服务商接入复杂 | API调用成本高 |
| 安全问题 | 缺乏统一的权限控制 | 租户隔离机制不完善 |
解决方案
为了解决这些问题,我们决定进行全面的技术架构升级,采用微前端、BFF层等现代化技术方案。
技术栈选型
在项目重构过程中,我们选择了以下技术栈-
前端技术栈
| 技术类型 | 选择方案 | 选择理由 |
|---|---|---|
| 前端框架 | Nuxt.js 3 + Vue 3 + TypeScript | SSR支持,开发体验优秀 |
| UI组件库 | ArcoDesign | 企业级组件库,定制性强 |
| 微前端框架 | Wujie (无界) | 性能优秀,Vue生态友好 |
| 构建工具 | Vite + Rollup | 快速构建,插件生态丰富 |
| 图表库 | ECharts | 功能强大,可视化效果好 |
| 测试工具 | Vitest | 与Vite集成度高 |
后端技术栈
- 后端框架-NestJS - 企业级Node.js框架
- 数据分析-Python - 强大的数据处理能力
一、微前端架构设计与实践
1.1 微前端选型分析
在微前端框架选型过程中,我们对比了主流的几种方案-
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Single-spa | 轻量级,灵活性高 | 配置复杂,需要手动处理样式隔离 | 简单场景 |
| Qiankun | 功能完善,开箱即用 | 体积较大,对老项目兼容性一般,对Vue3的支持不友好 | 中大型应用 |
| Wujie | 性能优秀,API简洁 | 生态相对较新 | 现代前端项目 |
最终选择Wujie的主要原因-
- 性能优势-基于Web Component和Shadow DOM,沙箱隔离更彻底
- API简洁-接入成本低,学习曲线平缓
- Nuxt.js友好-对Vue 3和Nuxt.js支持良好
- 体积小巧-核心包体积小,对主应用性能影响小
1.2 微前端架构设计
我们采用了**“基座管理 + 子应用独立运行 + 共享模块按需加载”**的架构方案-
// 主应用微前端配置 (main.ts)
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { setupApp } from 'wujie'
import App from './App.vue'
import routes from './routes'
// 初始化无界微前端
setupApp({
preloadApps: ['user', 'payment', 'data'],
lifeCycles: {
beforeLoad: (app) => {
console.log('before load:', app.name)
},
afterMount: (app) => {
console.log('after mount:', app.name)
}
}
})
const router = createRouter({
history: createWebHistory(),
routes
})
createApp(App)
.use(router)
.mount('#app')1.3 子应用注册与配置
// 子应用注册配置 (apps.ts)
export const microApps = [
{
name: 'user',
entry: import.meta.env.VITE_USER_APP_URL,
container: '#micro-user',
activeRule: '/user',
props: {
baseRoute: '/user'
}
},
{
name: 'payment',
entry: import.meta.env.VITE_PAYMENT_APP_URL,
container: '#micro-payment',
activeRule: '/payment',
props: {
baseRoute: '/payment'
}
},
{
name: 'data',
entry: import.meta.env.VITE_DATA_APP_URL,
container: '#micro-data',
activeRule: '/data',
props: {
baseRoute: '/data'
}
}
]1.4 沙箱通信机制实现
为了保障数据安全和通信效率,我们设计了一套完整的沙箱通信机制-
// 主应用通信工具 (event-bus.ts)
import { EventEmitter } from 'events'
class MicroAppEventBus extends EventEmitter {
constructor() {
super()
this.setMaxListeners(100)
}
// 主应用发送消息到子应用
sendToApp(appName: string, eventName: string, data: any) {
this.emit(`${appName}:${eventName}`, data)
}
// 子应用发送消息到主应用
sendToMain(eventName: string, data: any) {
this.emit(`main:${eventName}`, data)
}
// 主应用监听子应用消息
onMainEvent(eventName: string, callback: (...args: any[]) => void) {
this.on(`main:${eventName}`, callback)
}
// 子应用监听主应用消息
onAppEvent(appName: string, eventName: string, callback: (...args: any[]) => void) {
this.on(`${appName}:${eventName}`, callback)
}
}
export const eventBus = new MicroAppEventBus()1.5 共享模块设计
为了避免代码重复和资源浪费,我们设计了共享模块机制-
// 共享模块配置 (shared.ts)
export const sharedModules = {
'vue': () => import('vue'),
'vue-router': () => import('vue-router'),
'pinia': () => import('pinia'),
'@arco-design/web-vue': () => import('@arco-design/web-vue'),
'axios': () => import('axios')
}
// 主应用共享模块注册
setupApp({
share: {
deps: sharedModules,
scope: 'global'
}
})1.5.1 共享API接口设计
为了统一管理各个子应用的API调用,我们设计了共享API接口机制-
// 共享API服务 (shared-api.ts)
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import { eventBus } from './event-bus'
class SharedApiService {
private axiosInstance: AxiosInstance
private baseURL: string
private tenantId: string
constructor() {
this.baseURL = import.meta.env.VITE_API_BASE_URL
this.axiosInstance = axios.create({
baseURL: this.baseURL,
timeout: 10000
})
this.setupInterceptors()
}
// 设置租户ID
setTenantId(tenantId: string) {
this.tenantId = tenantId
}
// 请求拦截器
private setupInterceptors() {
this.axiosInstance.interceptors.request.use(
(config) => {
// 添加租户头信息
if (this.tenantId) {
config.headers['X-Tenant-Id'] = this.tenantId
}
// 添加认证token
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 通知所有子应用token过期
eventBus.emit('auth:token-expired')
}
return Promise.reject(error)
}
)
}
// 通用API方法
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.get(url, config)
return response.data
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.post(url, data, config)
return response.data
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.put(url, data, config)
return response.data
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.delete(url, config)
return response.data
}
}
// 导出单例实例
export const sharedApi = new SharedApiService()1.5.2 业务API模块封装
// 用户相关API (shared-user-api.ts)
import { sharedApi } from './shared-api'
export interface User {
id: string
name: string
email: string
role: string
tenantId: string
}
export interface UserListParams {
page?: number
pageSize?: number
keyword?: string
role?: string
}
export class UserApiService {
// 获取用户列表
static async getUserList(params: UserListParams = {}) {
return sharedApi.get<{
data: User[]
total: number
page: number
pageSize: number
}>('/api/users', { params })
}
// 获取用户详情
static async getUserDetail(userId: string) {
return sharedApi.get<User>(`/api/users/${userId}`)
}
// 创建用户
static async createUser(userData: Omit<User, 'id'>) {
return sharedApi.post<User>('/api/users', userData)
}
// 更新用户
static async updateUser(userId: string, userData: Partial<User>) {
return sharedApi.put<User>(`/api/users/${userId}`, userData)
}
// 删除用户
static async deleteUser(userId: string) {
return sharedApi.delete(`/api/users/${userId}`)
}
}1.5.3 权限管理API
// 权限相关API (shared-permission-api.ts)
import { sharedApi } from './shared-api'
export interface Permission {
id: string
name: string
code: string
resource: string
action: string
}
export interface Role {
id: string
name: string
code: string
permissions: Permission[]
}
export class PermissionApiService {
// 获取当前用户权限
static async getCurrentUserPermissions() {
return sharedApi.get<Permission[]>('/api/auth/permissions')
}
// 获取角色列表
static async getRoleList() {
return sharedApi.get<Role[]>('/api/roles')
}
// 检查权限
static async checkPermission(resource: string, action: string) {
return sharedApi.post<{ hasPermission: boolean }>('/api/auth/check-permission', {
resource,
action
})
}
// 获取用户角色
static async getUserRoles(userId: string) {
return sharedApi.get<Role[]>(`/api/users/${userId}/roles`)
}
}1.5.4 子应用API使用示例
// 子应用中使用共享API (user-management-app.vue)
<template>
<div class="user-management">
<a-table
:columns="columns"
:data="userList"
:loading="loading"
@page-change="handlePageChange"
>
<template #action="{ record }">
<a-button @click="editUser(record)">编辑</a-button>
<a-button status="danger" @click="deleteUser(record.id)">删除</a-button>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { UserApiService, type User } from '@shared/user-api'
import { PermissionApiService } from '@shared/permission-api'
const userList = ref<User[]>([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
try {
const response = await UserApiService.getUserList({
page: currentPage.value,
pageSize: pageSize.value
})
userList.value = response.data
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 编辑用户
const editUser = async (user: User) => {
// 检查编辑权限
const permissionCheck = await PermissionApiService.checkPermission('user', 'update')
if (!permissionCheck.hasPermission) {
console.warn('没有编辑用户的权限')
return
}
// 执行编辑逻辑
console.log('编辑用户:', user)
}
// 删除用户
const deleteUser = async (userId: string) => {
// 检查删除权限
const permissionCheck = await PermissionApiService.checkPermission('user', 'delete')
if (!permissionCheck.hasPermission) {
console.warn('没有删除用户的权限')
return
}
try {
await UserApiService.deleteUser(userId)
await fetchUserList() // 重新获取列表
} catch (error) {
console.error('删除用户失败:', error)
}
}
// 分页处理
const handlePageChange = (page: number) => {
currentPage.value = page
fetchUserList()
}
onMounted(() => {
fetchUserList()
})
</script>二、Vite定制构建流程优化
2.1 Vite插件开发
为了满足项目的特殊需求,我们开发了多个Vite插件-
// 自定义Vite插件 (vite-plugin-custom.ts)
import { Plugin } from 'vite'
export function customVitePlugin(options: any = {}): Plugin {
return {
name: 'custom-vite-plugin',
configResolved(config) {
// 配置解析完成后的处理
console.log('Vite config resolved')
},
buildStart(opts) {
// 构建开始时的处理
console.log('Build started')
},
transform(code, id) {
// 代码转换
if (id.endsWith('.special')) {
return {
code: transformSpecialFile(code),
map: null
}
}
},
generateBundle(options, bundle) {
// 生成bundle后的处理
console.log('Bundle generated')
}
}
}
function transformSpecialFile(code: string): string {
// 自定义文件转换逻辑
return code.replace(/{{(\w+)}}/g, (_, key) => {
return process.env[key] || ''
})
}2.2 资源加载策略优化
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { customVitePlugin } from './plugins/custom-vite-plugin'
export default defineConfig({
plugins: [
vue(),
customVitePlugin()
],
build: {
rollupOptions: {
output: {
manualChunks: {
// 精细化代码分割
vendor: ['vue', 'vue-router', 'pinia'],
arco: ['@arco-design/web-vue'],
utils: ['lodash-es', 'dayjs'],
charts: ['echarts']
},
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
if (facadeModuleId) {
if (facadeModuleId.includes('node_modules')) {
return 'vendor/[name].[hash].js'
}
return 'js/[name].[hash].js'
}
return 'js/[name].[hash].js'
}
}
}
},
optimizeDeps: {
// 预构建优化
include: [
'vue',
'vue-router',
'pinia',
'@arco-design/web-vue',
'axios'
],
exclude: ['@my-lib/shared']
}
})2.3 预加载策略配置
// 预加载插件 (vite-plugin-preload.ts)
import { Plugin } from 'vite'
export function preloadPlugin(): Plugin {
return {
name: 'preload-plugin',
generateBundle(options, bundle) {
const htmlFiles = Object.entries(bundle).filter(
([fileName]) => fileName.endsWith('.html')
)
htmlFiles.forEach(([fileName, chunk]) => {
if (chunk.type === 'asset' && typeof chunk.source === 'string') {
// 添加预加载链接
const preloadLinks = `
<link rel="preload" href="/vendor/vendor.[hash].js" as="script">
<link rel="preload" href="/vendor/arco.[hash].js" as="script">
<link rel="preload" href="/css/main.[hash].css" as="style">
`
chunk.source = chunk.source.replace(
'</head>',
`${preloadLinks}</head>`
)
}
})
}
}
}2.4 Tree-shaking优化
package.json配置-
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}vite.config.ts中启用生产环境优化-
export default defineConfig({
build: {
minify: 'esbuild',
target: 'es2015',
rollupOptions: {
output: {
compact: true
}
}
}
})三、NestJS BFF层架构设计
3.1 BFF层架构概述
BFF(Backend For Frontend)层作为前端和后端服务之间的中间层,主要承担以下职责-
核心职责
| 职责类型 | 具体功能 | 业务价值 |
|---|---|---|
| 请求转发 | 将前端请求转发到对应的微服务 | 简化前端调用复杂度 |
| 数据聚合 | 聚合多个微服务的数据,减少前端请求次数 | 提升性能,减少网络开销 |
| 权限控制 | 统一的身份认证和权限校验 | 增强安全性 |
| 租户隔离 | 实现多租户数据隔离 | 保障数据安全 |
| 缓存管理 | 提供数据缓存能力 | 提升响应速度 |
3.2 租户架构设计
3.2.1 租户识别中间件
// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { TenantService } from './tenant.service';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(private readonly tenantService: TenantService) {}
async use(req: Request, res: Response, next: NextFunction) {
try {
// 从请求头或域名中获取租户标识
const tenantId = req.headers['x-tenant-id'] || this.extractTenantFromHost(req.hostname);
if (!tenantId) {
return res.status(400).json({
statusCode: 400,
message: 'Tenant ID is required'
});
}
// 验证租户合法性
const tenant = await this.tenantService.validateTenant(tenantId as string);
if (!tenant) {
return res.status(403).json({
statusCode: 403,
message: 'Invalid tenant'
});
}
// 将租户信息添加到请求对象
req.tenant = tenant;
next();
} catch (error) {
next(error);
}
}
private extractTenantFromHost(hostname: string): string | null {
// 从域名中提取租户信息,如 tenant1.example.com
const parts = hostname.split('.');
if (parts.length >= 3) {
return parts[0];
}
return null;
}
}3.2.2 请求作用域租户上下文
// tenant.context.ts
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
private tenantId: string;
private tenantConfig: any;
setTenant(tenantId: string, config: any) {
this.tenantId = tenantId;
this.tenantConfig = config;
}
getTenantId(): string {
return this.tenantId;
}
getTenantConfig(): any {
return this.tenantConfig;
}
// 获取租户特定的数据库连接
getDatabaseConnection() {
return `tenant_${this.tenantId}`;
}
}3.2.3 JWT鉴权与租户权限控制
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
return false;
}
try {
const payload = await this.jwtService.verifyAsync(token);
// 验证租户权限
const requiredTenantRoles = this.reflector.get<string[]>(
'tenant_roles',
context.getHandler()
);
if (requiredTenantRoles && !requiredTenantRoles.includes(payload.role)) {
return false;
}
// 将用户信息添加到请求对象
request.user = payload;
return true;
} catch {
return false;
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}3.3 BFF层数据聚合实现
// user.bff.service.ts
import { Injectable, HttpService } from '@nestjs/common';
import { firstValueFrom } from 'rxjs';
import { TenantContext } from '../tenant/tenant.context';
@Injectable()
export class UserBffService {
constructor(
private readonly httpService: HttpService,
private readonly tenantContext: TenantContext
) {}
async getUserWithDetails(userId: string) {
// 并行调用多个微服务接口
const [user, orders, profile] = await Promise.all([
this.getUserBaseInfo(userId),
this.getUserOrders(userId),
this.getUserProfile(userId)
]);
// 数据聚合
return {
...user,
orders: orders.data,
profile: profile.data,
tenantSpecificData: this.getTenantSpecificData(user)
};
}
private async getUserBaseInfo(userId: string) {
const response = await firstValueFrom(
this.httpService.get(`${process.env.USER_SERVICE_URL}/users/${userId}`, {
headers: {
'X-Tenant-Id': this.tenantContext.getTenantId()
}
})
);
return response.data;
}
private async getUserOrders(userId: string) {
return firstValueFrom(
this.httpService.get(`${process.env.ORDER_SERVICE_URL}/users/${userId}/orders`)
);
}
private async getUserProfile(userId: string) {
return firstValueFrom(
this.httpService.get(`${process.env.PROFILE_SERVICE_URL}/users/${userId}/profile`)
);
}
private getTenantSpecificData(user: any) {
const tenantConfig = this.tenantContext.getTenantConfig();
// 根据租户配置返回不同的数据结构
return {
customFields: tenantConfig.customFields?.map(field => ({
name: field,
value: user[field] || null
})) || []
};
}
}3.4 缓存策略实现
// cache.service.ts
import { Injectable, CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { TenantContext } from '../tenant/tenant.context';
@Injectable()
export class CacheService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly tenantContext: TenantContext
) {}
private getTenantCacheKey(key: string): string {
return `${this.tenantContext.getTenantId()}:${key}`;
}
async get<T>(key: string): Promise<T | undefined> {
return this.cacheManager.get<T>(this.getTenantCacheKey(key));
}
async set<T>(key: string, value: T, ttl = 3600): Promise<void> {
await this.cacheManager.set(
this.getTenantCacheKey(key),
value,
ttl * 1000
);
}
async del(key: string): Promise<void> {
await this.cacheManager.del(this.getTenantCacheKey(key));
}
async wrap<T>(key: string, fn: () => Promise<T>, ttl = 3600): Promise<T> {
const cacheKey = this.getTenantCacheKey(key);
const cachedValue = await this.cacheManager.get<T>(cacheKey);
if (cachedValue) {
return cachedValue;
}
const result = await fn();
await this.cacheManager.set(cacheKey, result, ttl * 1000);
return result;
}
}四、性能优化成果
4.1 前端性能优化结果
通过微前端架构和Vite构建优化,我们取得了显著的性能提升-
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 首屏加载时间 | 7s | 1s | 85.7% |
| LCP (最大内容绘制) | 6.8s | 0.7s | 90% |
| TTI (交互时间) | 8.2s | 1.5s | 81.7% |
| 包体积 | 2.8MB | 1.2MB | 57.1% |
4.2 后端性能优化结果
BFF层架构的实施也带来了后端性能的显著提升-
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 请求延迟 | 500ms+ | <200ms | 60% |
| 并发处理能力 | 5k QPS | 20k QPS | 300% |
| 部署时间 | 36min | 14min | 61.1% |
| API调用复杂度 | 高 | 低 | 30%+ 减少 |
五、项目经验总结
5.1 技术选型经验
- 适合的才是最好的-技术选型要结合项目实际需求,而不是盲目追求新技术
- 渐进式迁移-对于大型项目,采用渐进式微前端迁移策略,降低风险
- 性能优先-在架构设计中始终将性能作为重要考量因素
5.2 开发实践经验
- 标准化开发-建立统一的开发规范和代码质量标准
- 自动化测试-完善的单元测试和E2E测试保障代码质量
- CI/CD流水线-自动化部署流程提高开发效率
5.3 架构设计经验
- 模块化设计-合理的模块划分提高代码复用性和维护性
- 缓存策略-多级缓存机制显著提升系统性能
- 监控告警-完善的监控体系保障系统稳定运行
5.4 遇到的挑战与解决方案
挑战1-微前端样式隔离问题
- 解决方案-使用Shadow DOM和CSS Modules双重隔离机制
挑战2-多租户数据安全
- 解决方案-请求作用域的租户上下文和严格的权限控制
挑战3-构建性能优化
- 解决方案-Vite插件开发和精细化的代码分割策略
结语
卓信业务SaaS平台的技术架构升级是一个系统性工程,涉及微前端、BFF层、构建优化等多个技术领域。通过这次架构升级,我们不仅解决了现有系统的性能和扩展性问题,也为未来的业务发展奠定了坚实的技术基础。
在这个过程中,深刻体会到-
- 架构设计要面向未来-技术选型和架构设计需要考虑业务的长期发展
- 性能优化是持续过程-性能优化不是一次性工作,需要持续监控和改进
- 团队协作是关键-复杂项目的成功离不开团队成员的密切配合