一、为什么前端架构越做越重?
在接手广告投放平台这个项目之前,我问自己一个问题:到底什么样的前端架构才算"合适"?是文件夹分得够清楚?是页面够快?是组件够优雅?还是 CI/CD、权限系统、状态管理一个不少?
答案其实很简单:能顶住业务复杂度,并且团队能跑得快。
这篇文章,我结合自己从0搭建一个中大型广告平台前端的经历,讲讲我在架构设计上的深度思考和实战经验。
二、项目背景:广告投放平台的典型特征
- 多模块:广告管理、投放策略、创意素材、财务对账、数据报表
- 多端适配:PC/H5/小程序,未来还可能接入 uniapp
- 多角色权限:广告主、运营、审核员、管理员等
- 多状态联动:广告生命周期驱动状态流转
- 多服务接口:需要对接广告服务、素材服务、用户体系、计费系统等
这类系统非常容易"越写越乱"。所以,第一步,我必须从架构层面建立边界感。
三、从目录结构入手:模块驱动不是说说而已
我的策略是模块驱动(module-based):
/src
/modules
/adPlan
index.vue
useAdPlan.ts
api.ts
store.ts
/material
/report
/user
/components
/composables
/router
/stores
/utils
/assets
✦ 每个模块:独立 page + API + composable + store
例如 /modules/adPlan/api.ts:
import request from '@/utils/request'
export const getAdPlans = (params) => {
return request.get('/api/ad/plans', { params })
}
export const createAdPlan = (data) => {
return request.post('/api/ad/plan', data)
}
四、权限系统:动态路由 + 组件粒度权限封装
登录后获取用户权限:
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
permissions: []
}),
actions: {
async fetchPermissions() {
const res = await getUserPermissions()
this.permissions = res.data || []
}
}
})
菜单与按钮控制:
// 指令 v-permission
app.directive('permission', {
mounted(el, binding) {
const userStore = useUserStore()
if (!userStore.permissions.includes(binding.value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
路由权限控制:
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (!userStore.permissions.includes(to.meta.permission)) {
return next('/403')
}
next()
})
五、状态管理:Pinia 分模块 + 响应式设计
// stores/adPlan.ts
export const useAdPlanStore = defineStore('adPlan', {
state: () => ({
adList: [],
loading: false
}),
actions: {
async fetchAdList(params) {
this.loading = true
try {
const res = await getAdPlans(params)
this.adList = res.data.list
} finally {
this.loading = false
}
}
}
})
在页面中使用:
<script setup>
const adPlanStore = useAdPlanStore()
onMounted(() => {
adPlanStore.fetchAdList()
})
</script>
六、组件系统:核心抽象三大件
DataTable.vue(支持分页、搜索、loading)
<template>
<n-card>
<n-form :model="searchParams">
<n-form-item label="计划名称">
<n-input v-model:value="searchParams.name" />
</n-form-item>
</n-form>
<n-button @click="loadData">查询</n-button>
<n-data-table :columns="columns" :data="data" :loading="loading" />
<n-pagination :page="page" :page-size="pageSize" @update:page="onPageChange" />
</n-card>
</template>
// 逻辑提取
const loadData = async () => {
loading.value = true
const res = await getAdPlans({ name: searchParams.name })
data.value = res.data.list
loading.value = false
}
七、CI/CD 流程配置
GitLab CI 配置文件 .gitlab-ci.yml
stages:
- build
- deploy
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist
deploy:
stage: deploy
script:
- scp -r dist/* user@server:/var/www/html/ad-platform
Sentry 异常上报封装:
import * as Sentry from '@sentry/vue'
Sentry.init({
app,
dsn: 'https://xxx@sentry.io/xxx',
integrations: [...]
})
八、总结
架构的本质,是为业务服务。
大量组件抽象、状态拆分、权限中台、工程体系,不是为了“炫技”,而是确保业务在复杂变化中仍然稳定、高效、可维护。
这一套体系,不仅支撑了广告投放平台快速交付,也让我具备了在网易这类大厂环境中胜任资深工程师的能力。