如何构建一个能扛业务复杂度的前端架构体系?——从0到1搭建广告投放平台的实践

284 阅读2分钟

一、为什么前端架构越做越重?

在接手广告投放平台这个项目之前,我问自己一个问题:到底什么样的前端架构才算"合适"?是文件夹分得够清楚?是页面够快?是组件够优雅?还是 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: [...]
})

八、总结

架构的本质,是为业务服务。

大量组件抽象、状态拆分、权限中台、工程体系,不是为了“炫技”,而是确保业务在复杂变化中仍然稳定、高效、可维护。

这一套体系,不仅支撑了广告投放平台快速交付,也让我具备了在网易这类大厂环境中胜任资深工程师的能力。