10分钟精通开源字节/source-vue前端架构:从源码到实战的架构解密

10分钟精通开源字节/source-vue前端架构:从源码到实战的架构解密

【免费下载链接】source-vue 🔥 一直想做一款追求极致用户体验的快速开发平台,看了很多优秀的开源项目但是发现没有合适的。于是利用空闲休息时间对若依框架进行扩展写了一套快速开发系统。如此有了开源字节快速开发平台。该平台基于 Spring Boot + MyBatis + Vue & Element ,包含微信小程序 & Uniapp, Web 报表、可视化大屏、三方登录、支付、短信、邮件、OSS... 【免费下载链接】source-vue 项目地址: https://gitcode.com/open-source-byte/source-vue

引言:为什么选择开源字节前端架构?

你是否还在为快速开发平台的前端架构设计而烦恼?是否在寻找一个既灵活又高效的企业级前端解决方案?开源字节(source-vue)基于Vue & Element UI构建的前端架构或许正是你需要的答案。本文将带你深入了解这一架构的设计理念、核心组件和实战应用,帮助你在10分钟内从入门到精通。

读完本文,你将能够:

  • 理解开源字节前端架构的整体设计与核心组件
  • 掌握权限控制、状态管理、路由设计的实现方式
  • 学会如何快速上手并扩展这一架构
  • 了解性能优化和最佳实践

1. 开源字节前端架构概览

1.1 架构总览

开源字节(source-vue)是一个基于Spring Boot + MyBatis + Vue & Element的快速开发平台。其前端架构采用了现代化的前后端分离设计,主要特点包括:

  • 基于Vue 2.x和Element UI构建
  • 支持响应式设计,适配PC和移动端
  • 内置完善的权限控制体系
  • 丰富的UI组件和工具类
  • 支持多种数据可视化方案
  • 提供代码生成器,一键生成前后端代码

1.2 技术栈选择

技术版本说明
Vue2.x核心前端框架
Element UI2.xUI组件库
Vue Router3.x路由管理
Vuex3.x状态管理
Axios0.21.xHTTP客户端
ECharts5.x数据可视化
UCharts2.x移动端图表
DataV2.x可视化大屏
Luckysheet2.x在线Excel
Vxe-table3.x高级表格

1.3 架构分层

mermaid

2. 核心功能模块解析

2.1 权限控制模块

开源字节的权限控制模块是其核心功能之一,实现了细粒度的权限管理。主要包括:

  • 菜单权限:控制用户可访问的菜单
  • 按钮权限:控制页面按钮的显示与操作权限
  • 数据权限:控制用户可查看的数据范围
2.1.1 权限控制实现

权限控制主要通过以下几个部分实现:

  1. 后端权限数据接口
  2. 前端权限路由动态生成
  3. 权限指令和组件
  4. 权限工具类

权限控制流程:

mermaid

2.1.2 权限工具类解析

source-common/src/main/java/cn/source/common/utils目录下,我们可以找到权限相关的工具类:

public class SecurityUtils {
    // 获取当前用户ID
    public static Long getUserId() {
        // 实现代码
    }
    
    // 获取当前部门ID
    public static Long getDeptId() {
        // 实现代码
    }
    
    // 获取当前用户名
    public static String getUsername() {
        // 实现代码
    }
    
    // 获取当前登录用户信息
    public static LoginUser getLoginUser() {
        // 实现代码
    }
    
    // 加密密码
    public static String encryptPassword(String password) {
        // 实现代码
    }
}

2.2 路由管理

路由管理是前端架构的重要组成部分,负责页面之间的跳转和导航。开源字节的路由设计具有以下特点:

  • 支持动态路由生成
  • 实现路由守卫,控制页面访问权限
  • 支持路由懒加载,优化性能
  • 提供面包屑导航支持
2.2.1 路由配置示例
// 静态路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  
  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },
  
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard', affix: true }
    }]
  }
]

// 动态路由
export const asyncRoutes = [
  // 系统管理模块
  {
    path: '/system',
    component: Layout,
    redirect: '/system/user',
    name: 'System',
    meta: { title: '系统管理', icon: 'system' },
    children: [
      {
        path: 'user',
        name: 'User',
        component: () => import('@/views/system/user/index'),
        meta: { title: '用户管理', icon: 'user', perm: 'system:user:list' }
      },
      // 更多子路由...
    ]
  },
  // 更多路由模块...
]

2.3 数据请求与处理

数据请求模块负责与后端API交互,是前后端通信的桥梁。开源字节使用Axios作为HTTP客户端,并对其进行了封装,提供了统一的请求处理方案。

2.3.1 请求封装
// request.js
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // API基础路径
  timeout: 5000 // 请求超时时间
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加token
    if (store.getters.token) {
      config.headers['Authorization'] = 'Bearer ' + getToken()
    }
    // 设置请求头
    config.headers['Content-Type'] = 'application/json;charset=utf-8'
    return config
  },
  error => {
    // 请求错误处理
    console.log('request error:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    
    // 状态码不是20000的情况
    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      
      // token过期或未登录
      if (res.code === 401) {
        MessageBox.confirm('您已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('response error:', error)
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
2.3.2 API调用示例
// api/article.js
import request from '@/utils/request'

export function getArticleList(params) {
  return request({
    url: '/system/article/list',
    method: 'get',
    params
  })
}

export function getArticle(id) {
  return request({
    url: `/system/article/${id}`,
    method: 'get'
  })
}

export function addArticle(data) {
  return request({
    url: '/system/article',
    method: 'post',
    data
  })
}

export function updateArticle(data) {
  return request({
    url: '/system/article',
    method: 'put',
    data
  })
}

export function deleteArticle(ids) {
  return request({
    url: `/system/article/${ids}`,
    method: 'delete'
  })
}

2.4 状态管理

状态管理模块采用Vuex实现,用于管理应用的全局状态。开源字节的状态管理按照功能模块进行了划分,使代码结构更加清晰。

mermaid

2.4.1 用户状态管理示例
// store/modules/user.js
import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'

const state = {
  token: getToken(),
  name: '',
  avatar: '',
  introduction: '',
  roles: []
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_INTRODUCTION: (state, introduction) => {
    state.introduction = introduction
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  }
}

const actions = {
  // 登录
  login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 获取用户信息
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        const { data } = response

        if (!data) {
          return reject('Verification failed, please Login again.')
        }

        const { roles, name, avatar, introduction } = data

        // 角色必须是非空数组
        if (!roles || roles.length <= 0) {
          reject('getInfo: roles must be a non-null array!')
        }

        commit('SET_ROLES', roles)
        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        commit('SET_INTRODUCTION', introduction)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 登出
  logout({ commit, state, dispatch }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        commit('SET_ROLES', [])
        removeToken()
        resetRouter()

        // 重置已访问的视图和缓存的视图
        dispatch('tagsView/delAllViews', null, { root: true })

        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 重置token
  resetToken({ commit }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      commit('SET_ROLES', [])
      removeToken()
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

3. 关键技术点深入

3.1 动态权限路由实现

动态权限路由是开源字节的核心功能之一,它根据用户的角色动态生成可访问的路由表。

// src/store/modules/permission.js
import { asyncRoutes, constantRoutes } from '@/router'

/**
 * 使用meta.role确定当前用户是否有权限
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 * 递归过滤异步路由表
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

3.2 按钮级权限控制

除了菜单权限外,开源字节还实现了按钮级别的权限控制。

// src/directive/permission/index.js
import permission from './permission'

const install = function(Vue) {
  Vue.directive('permission', permission)
}

if (window.Vue) {
  window['permission'] = permission
  Vue.use(install); // eslint-disable-line
}

permission.install = install
export default permission

// src/directive/permission/permission.js
import store from '@/store'

export default {
  inserted(el, binding, vnode) {
    const { value } = binding
    const roles = store.getters && store.getters.roles
    const permissions = store.getters && store.getters.permissions

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value

      const hasPermission = permissions.some(permission => {
        return permissionFlag.includes(permission)
      })

      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error(`need permissions! Like v-permission="['system:user:add','system:user:edit']"`)
    }
  }
}

使用示例:

<el-button 
  v-permission="['system:user:add']" 
  type="primary" 
  @click="handleAdd"
>
  添加用户
</el-button>

3.3 工具类解析

开源字节提供了丰富的工具类,简化了日常开发工作。以下是几个核心工具类的解析:

3.3.1 日期工具类
// src/utils/dateUtils.js
import moment from 'moment'

export function formatDate(date, fmt) {
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
  }
  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  }
  for (let k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      let str = o[k] + ''
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str))
    }
  }
  return fmt
}

function padLeftZero(str) {
  return ('00' + str).substr(str.length)
}

export function formatTime(time, option) {
  time = +time * 1000
  const d = new Date(time)
  const now = Date.now()

  const diff = (now - d) / 1000

  if (diff < 30) {
    return '刚刚'
  } else if (diff < 3600) { // less 1 hour
    return Math.ceil(diff / 60) + '分钟前'
  } else if (diff < 3600 * 24) {
    return Math.ceil(diff / 3600) + '小时前'
  } else if (diff < 3600 * 24 * 2) {
    return '1天前'
  }
  if (option) {
    return moment(d).format(option)
  } else {
    return moment(d).format('yyyy-MM-dd')
  }
}
3.3.2 字符串工具类
// src/utils/stringUtils.js
export function trim(str) {
  return str.replace(/^\s+|\s+$/g, '')
}

export function isEmpty(str) {
  return str === null || str === undefined || trim(str) === ''
}

export function isNotEmpty(str) {
  return !isEmpty(str)
}

export function format(template, ...params) {
  if (params.length === 0) return template
  let result = template
  for (let i = 0; i < params.length; i++) {
    const reg = new RegExp(`\\{${i}\\}`, 'g')
    result = result.replace(reg, params[i])
  }
  return result
}

export function toUnderScoreCase(str) {
  if (str) {
    return str.replace(/([A-Z])/g, '_$1').toLowerCase()
  }
  return str
}

export function toCamelCase(str) {
  if (str) {
    return str.replace(/_(\w)/g, (_, letter) => letter.toUpperCase())
  }
  return str
}

4. 实战应用:快速开发一个文章管理模块

4.1 使用代码生成器

开源字节提供了强大的代码生成器,可以根据数据库表结构一键生成前后端代码。以下是使用步骤:

  1. 登录系统,进入代码生成器模块
  2. 选择数据库表(如cms_article)
  3. 配置生成信息(包名、模块名、作者等)
  4. 点击生成代码,下载压缩包
  5. 解压并将代码复制到项目中

4.2 手动实现文章列表页

如果需要手动实现一个文章列表页,可以按照以下步骤进行:

4.2.1 创建API文件
// src/api/article.js
import request from '@/utils/request'

export function getArticleList(params) {
  return request({
    url: '/system/article/list',
    method: 'get',
    params
  })
}

export function getArticle(id) {
  return request({
    url: `/system/article/${id}`,
    method: 'get'
  })
}

export function addArticle(data) {
  return request({
    url: '/system/article',
    method: 'post',
    data
  })
}

export function updateArticle(data) {
  return request({
    url: '/system/article',
    method: 'put',
    data
  })
}

export function deleteArticle(ids) {
  return request({
    url: `/system/article/${ids}`,
    method: 'delete'
  })
}

export function exportArticle(params) {
  return request({
    url: '/system/article/export',
    method: 'post',
    params,
    responseType: 'blob'
  })
}
4.2.2 创建页面组件
<!-- src/views/article/index.vue -->
<template>
  <div class="app-container">
    <div class="filter-container">
      <el-input 
        v-model="queryParams.title" 
        placeholder="文章标题" 
        style="width: 200px;" 
        class="filter-item"
      />
      <el-select 
        v-model="queryParams.status" 
        placeholder="状态" 
        style="width: 150px;" 
        class="filter-item"
      >
        <el-option label="全部" value=""></el-option>
        <el-option label="发布" value="1"></el-option>
        <el-option label="草稿" value="0"></el-option>
      </el-select>
      <el-date-picker
        v-model="dateRange"
        type="daterange"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        class="filter-item"
      ></el-date-picker>
      <el-button 
        type="primary" 
        icon="el-icon-search" 
        @click="handleQuery"
      >
        搜索
      </el-button>
      <el-button 
        icon="el-icon-refresh" 
        @click="resetQuery"
      >
        重置
      </el-button>
      <el-button 
        type="primary" 
        icon="el-icon-plus" 
        @click="handleAdd"
        v-permission="['system:article:add']"
      >
        新增
      </el-button>
      <el-button 
        type="success" 
        icon="el-icon-download" 
        @click="handleExport"
        v-permission="['system:article:export']"
      >
        导出
      </el-button>
    </div>

    <el-table
      v-loading="loading"
      :data="articleList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="ID" prop="id" width="80" align="center" />
      <el-table-column label="文章标题" prop="title" :show-overflow-tooltip="true" />
      <el-table-column label="分类" prop="categoryName" width="120" align="center" />
      <el-table-column label="作者" prop="author" width="100" align="center" />
      <el-table-column label="状态" prop="status" width="100" align="center">
        <template slot-scope="scope">
          <el-tag :type="scope.row.status === '1' ? 'success' : 'info'">
            {{ scope.row.status === '1' ? '发布' : '草稿' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="阅读量" prop="viewCount" width="100" align="center" />
      <el-table-column label="创建时间" prop="createTime" width="180" align="center" />
      <el-table-column label="操作" width="230" align="center">
        <template slot-scope="scope">
          <el-button 
            size="mini" 
            type="text" 
            icon="el-icon-view" 
            @click="handleView(scope.row)"
          >
            查看
          </el-button>
          <el-button 
            size="mini" 
            type="text" 
            icon="el-icon-edit" 
            @click="handleEdit(scope.row)"
            v-permission="['system:article:edit']"
          >
            编辑
          </el-button>
          <el-button 
            size="mini" 
            type="text" 
            icon="el-icon-delete" 
            @click="handleDelete(scope.row)"
            v-permission="['system:article:remove']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加/编辑文章对话框 -->
    <article-modal
      :visible.sync="open"
      :article="article"
      @submit="handleSubmit"
    />
  </div>
</template>

<script>
import { getArticleList, deleteArticle, exportArticle } from '@/api/article'
import ArticleModal from './components/ArticleModal'

export default {
  name: 'Article',
  components: { ArticleModal },
  data() {
    return {
      // 遮罩层
      loading: true,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 文章表格数据
      articleList: [],
      // 弹出层标题
      title: '',
      // 是否显示弹出层
      open: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        title: '',
        status: '',
        beginTime: '',
        endTime: ''
      },
      // 日期范围
      dateRange: [],
      // 文章对象
      article: {}
    }
  },
  created() {
    this.getList()
  },
  methods: {
    /** 查询文章列表 */
    getList() {
      this.loading = true
      if (this.dateRange && this.dateRange.length > 0) {
        this.queryParams.beginTime = this.dateRange[0]
        this.queryParams.endTime = this.dateRange[1]
      } else {
        this.queryParams.beginTime = ''
        this.queryParams.endTime = ''
      }
      getArticleList(this.queryParams).then(response => {
        this.articleList = response.rows
        this.total = response.total
        this.loading = false
      })
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1
      this.getList()
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.dateRange = []
      this.resetForm('queryForm')
      this.handleQuery()
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.article = {}
      this.open = true
      this.title = '添加文章'
    },
    /** 修改按钮操作 */
    handleEdit(row) {
      this.article = { ...row }
      this.open = true
      this.title = '修改文章'
    },
    /** 查看按钮操作 */
    handleView(row) {
      this.article = { ...row }
      this.open = true
      this.title = '查看文章'
    },
    /** 提交按钮 */
    handleSubmit() {
      this.$refs.articleModal.submitForm().then(() => {
        this.open = false
        this.getList()
      })
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      this.$confirm('是否确认删除文章 "' + row.title + '"?', '警告', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        return deleteArticle(row.id)
      }).then(() => {
        this.getList()
        this.msgSuccess('删除成功')
      }).catch(() => {})
    },
    /** 批量删除按钮操作 */
    handleDeleteAll() {
      const ids = this.ids.map(row => row.id)
      this.$confirm('是否确认删除选中的 ' + ids.length + ' 条文章?', '警告', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        return deleteArticle(ids.join(','))
      }).then(() => {
        this.getList()
        this.ids = []
        this.msgSuccess('删除成功')
      }).catch(() => {})
    },
    /** 导出按钮操作 */
    handleExport() {
      const queryParams = { ...this.queryParams }
      if (this.dateRange && this.dateRange.length > 0) {
        queryParams.beginTime = this.dateRange[0]
        queryParams.endTime = this.dateRange[1]
      }
      this.downloadLoading = true
      exportArticle(queryParams).then(response => {
        this.downloadLoading = false
        this.download(response)
      }).catch(() => {
        this.downloadLoading = false
      })
    },
    /** 表格选中切换 */
    handleSelectionChange(selection) {
      this.ids = selection
      this.single = selection.length !== 1
      this.multiple = !selection.length
    }
  }
}
</script>
4.2.3 创建表单组件
<!-- src/views/article/components/ArticleModal.vue -->
<template>
  <el-dialog
    :title="title"
    :visible.sync="visible"
    :width="dialogWidth"
    :close-on-click-modal="false"
  >
    <el-form
      ref="form"
      :model="form"
      :rules="rules"
      label-width="100px"
      :disabled="isView"
    >
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item label="文章标题" prop="title">
            <el-input v-model="form.title" placeholder="请输入文章标题" maxlength="100" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="所属分类" prop="categoryId">
            <el-select v-model="form.categoryId" placeholder="请选择分类">
              <el-option
                v-for="category in categoryList"
                :key="category.id"
                :label="category.name"
                :value="category.id"
              ></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="状态" prop="status">
            <el-radio-group v-model="form.status">
              <el-radio label="1">发布</el-radio>
              <el-radio label="0">草稿</el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="文章摘要" prop="summary">
            <el-input
              v-model="form.summary"
              type="textarea"
              rows="3"
              placeholder="请输入文章摘要"
              maxlength="500"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="文章内容" prop="content">
            <tinymce v-model="form.content" :height="400" />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="封面图片">
            <image-upload v-model="form.coverImage" :limit="1" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="cancel">取消</el-button>
      <el-button v-if="!isView" type="primary" @click="submitForm">确定</el-button>
    </div>
  </el-dialog>
</template>

<script>
import { getArticle, addArticle, updateArticle } from '@/api/article'
import { getCategoryList } from '@/api/category'
import Tinymce from '@/components/Tinymce'
import ImageUpload from '@/components/ImageUpload'

export default {
  components: { Tinymce, ImageUpload },
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    article: {
      type: Object,
      default: () => ({})
    },
    title: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      form: {
        id: null,
        title: '',
        categoryId: null,
        status: '1',
        summary: '',
        content: '',
        coverImage: ''
      },
      rules: {
        title: [
          { required: true, message: '请输入文章标题', trigger: 'blur' }
        ],
        categoryId: [
          { required: true, message: '请选择文章分类', trigger: 'change' }
        ],
        content: [
          { required: true, message: '请输入文章内容', trigger: 'blur' }
        ]
      },
      categoryList: [],
      dialogWidth: '80%'
    }
  },
  computed: {
    isView() {
      return this.title.includes('查看')
    }
  },
  watch: {
    visible(val) {
      if (val) {
        this.init()
      } else {
        this.reset()
      }
    }
  },
  methods: {
    init() {
      this.getCategoryList()
      if (this.article.id) {
        // 编辑或查看
        this.form = { ...this.article }
      } else {
        // 新增
        this.form = {
          id: null,
          title: '',
          categoryId: null,
          status: '1',
          summary: '',
          content: '',
          coverImage: ''
        }
      }
    },
    reset() {
      this.$refs.form.resetFields()
    },
    cancel() {
      this.$emit('update:visible', false)
      this.reset()
    },
    getCategoryList() {
      getCategoryList().then(response => {
        this.categoryList = response.data
      })
    },
    submitForm() {
      return new Promise((resolve, reject) => {
        this.$refs.form.validate(valid => {
          if (valid) {
            if (this.form.id) {
              updateArticle(this.form).then(response => {
                this.$message.success('修改成功')
                resolve()
              }).catch(error => {
                reject(error)
              })
            } else {
              addArticle(this.form).then(response => {
                this.$message.success('新增成功')
                resolve()
              }).catch(error => {
                reject(error)
              })
            }
          } else {
            reject(new Error('表单验证失败'))
          }
        })
      })
    }
  }
}
</script>

5. 性能优化与最佳实践

5.1 性能优化策略

  1. 路由懒加载

    // 将
    import ArticleList from '@/views/article/list'
    
    // 改为
    const ArticleList = () => import('@/views/article/list')
    
  2. 组件缓存

    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
    
  3. 图片懒加载

    <img v-lazy="imageUrl" alt="图片">
    
  4. 数据缓存

    // 使用localStorage缓存不常变动的数据
    export function getCategoryList() {
      const cached = localStorage.getItem('categoryList')
      if (cached) {
        return Promise.resolve(JSON.parse(cached))
      }
    
      return request({
        url: '/system/category/list',
        method: 'get'
      }).then(response => {
        localStorage.setItem('categoryList', JSON.stringify(response.data))
        return response
      })
    }
    
  5. 合理使用v-if和v-show

    • v-if: 适合不常切换的场景
    • v-show: 适合频繁切换的场景

5.2 开发最佳实践

  1. 代码规范

    • 使用ESLint和Prettier保持代码风格一致
    • 遵循Vue官方风格指南
  2. 组件设计

    • 抽取通用逻辑为mixins或Composition API
    • 复杂组件拆分为多个小组件
    • 使用作用域插槽提高组件复用性
  3. API请求

    • 统一的请求封装和错误处理
    • 请求结果的统一格式化
  4. 状态管理

    • 合理划分模块,避免单一状态树过大
    • 本地状态和全局状态区分管理
  5. 错误处理

    • 全局错误捕获
    • 友好的错误提示

6. 总结与展望

开源字节(source-vue)前端架构基于Vue & Element UI构建,提供了一套完整的企业级前端解决方案。其核心优势包括:

  1. 完善的权限控制:支持菜单权限、按钮权限和数据权限
  2. 丰富的组件库:内置多种UI组件和业务组件
  3. 高效的开发工具:提供代码生成器,一键生成前后端代码
  4. 灵活的扩展性:模块化设计,便于功能扩展
  5. 良好的性能优化:路由懒加载、组件缓存等多种优化手段

未来,开源字节前端架构可以在以下方面进一步优化:

  1. 升级到Vue 3和Element Plus:提升性能和开发体验
  2. 引入TypeScript:提高代码质量和可维护性
  3. 微前端架构:支持更大规模的应用开发
  4. 组件库完善:增加更多业务组件
  5. 低代码平台:进一步提高开发效率

7. 快速上手

7.1 环境准备

  • Node.js 12.x+
  • npm 6.x+
  • Git

7.2 安装步骤

# 克隆仓库
git clone https://gitcode.com/open-source-byte/source-vue.git

# 进入项目目录
cd source-vue

# 安装依赖
npm install

# 启动开发服务器
npm run dev

# 构建生产版本
npm run build:prod

7.3 目录结构

src/
├── api/           # API请求
├── assets/        # 静态资源
├── components/    # 公共组件
├── directive/     # 自定义指令
├── layout/        # 布局组件
├── router/        # 路由配置
├── store/         # 状态管理
├── styles/        # 全局样式
├── utils/         # 工具类
├── views/         # 页面组件
├── App.vue        # 应用入口
└── main.js        # 入口文件

结语

开源字节(source-vue)前端架构为企业级应用开发提供了一套完整的解决方案,既可以满足快速开发的需求,又具备良好的可扩展性和性能。通过本文的介绍,相信你已经对这一架构有了深入的了解。如果你正在寻找一个高效、灵活的前端架构,不妨尝试使用开源字节,它可能会成为你项目开发的得力助手。

如果你觉得本文对你有帮助,请点赞、收藏并关注作者,以便获取更多技术分享。下期我们将介绍开源字节的后端架构设计,敬请期待!

【免费下载链接】source-vue 🔥 一直想做一款追求极致用户体验的快速开发平台,看了很多优秀的开源项目但是发现没有合适的。于是利用空闲休息时间对若依框架进行扩展写了一套快速开发系统。如此有了开源字节快速开发平台。该平台基于 Spring Boot + MyBatis + Vue & Element ,包含微信小程序 & Uniapp, Web 报表、可视化大屏、三方登录、支付、短信、邮件、OSS... 【免费下载链接】source-vue 项目地址: https://gitcode.com/open-source-byte/source-vue

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值