项目重构,从零开始搭建一套新的后台管理系统

10,033 阅读11分钟

背景

应公司发展需求,我决定重构公司的后台管理系统,从提出需求建议到现在的实施,期间花了将近半个月的时间,决定把这些都记录下来。

之前的后台管理系统实在是为了实现功能而实现的,没有考虑到后期的扩展性,也没有考虑到后期的维护性,所以我下定决心,向领导提出了重构的建议,经过领导的同意,我开始了重构的工作。

项目前身

之前的后台管理系统是基于ant design pro for vue搭建的,我在开发的过程中,遇到了很多问题,比如:

  1. 项目功能模块没有统一规划。
  2. 项目的代码风格不统一,代码杂乱无章。
  3. 各种第三方库的依赖,有重复的,有无用的。
  4. 项目的目录结构不合理。
  5. 项目API接口没有统一规划。
  6. 项目的权限管理不明确,几乎无用。
  7. 等等等...

当我接手这个项目的时候,我开发了两个大的功能模块,讲道理我这两个功能模块应该是强关联之前的某些功能的;

但是因为这个项目就是为了满足当前的需求,根本就不给你扩展的机会,所以我开发的两个功能模块都是独立的,没有任何关联,造成了相当多的重复工作,但是又不得不重复。

这就导致了后期的维护成本很高,因为我要维护的东西太多了,而且这些东西都是没有关联的,所以我决定重构这个项目。

项目重构

重构是一件很大的事情,当我提出重构的建议时,是确定了我手头上的工作已经完结,后续没有多少工作,所以不要有想法就提,要考虑到后续的工作量以及工作的优先级。

项目规划

当我决定重构这个项目的时候,我就开始了项目的规划,在开始之前我向领导提出了我的想法;

领导肯定不会轻易的同意我的想法,所以我就开始整理项目当中的问题,写文档,画流程图,多次和领导沟通,最终领导同意了我的想法。

我在争取领导的意见做了下面的准备:

  1. 老项目的整体流程我走了一遍,找我我觉得不合理的地方,以及可以优化的地方。
  2. 整理项目的功能模块,以及功能模块之间的关系。
  3. 整理我心中对这个项目的优化建议以及后续的规划。
  4. 画出我心中优化后的项目功能模块的流程图。
  5. 写出我优化项目需要时间的估算。
  6. 制订项目的开发计划,以及开发的功能模块优先级。

上面的准备工作也是修修改改,大会小会开了很多次,最终的结果是领导同意了我的想法,我开始了项目的重构。

技术选型

在领导同意了我的想法之后,我就开始了项目重构前的准备,包括技术选型,项目的目录结构,以及项目的开发计划。

技术选型沿用了之前的技术栈,但是会对一些技术进行升级迭代;

  1. 脚手架由vue-cli修改为vite
  2. 前端框架由vue2修改为vue3
  3. UI框架由ant-design-vue修改为element-plus
  4. 状态管理由vuex修改为pinia
  5. 路由由vue-router修改为vue-router-next

除了上述这些技术的升级迭代之外,还会规划一些代码规范,以及项目的目录结构。

项目目录结构

项目的目录结构以及文件名的命名规范是非常重要的,因为这些都是团队协作的基础,当然我也不会去弄一个很复杂的目录结构或者特立独行的文件名命名规范,我会根据团队或者业界的一些规范来进行规划。

目录结构:

├── public
│   └──  favicon.ico
├── src
│   ├── api                 # 接口请求
│   │   ├── index.js
│   │   └── user.js
│   ├── assets              # 静态资源
│   │   └── logo.png
│   ├── components          # 公共组件
│   │   └── Table           # 表格组件
│   │       └── index.vue
│   ├── layout              # 布局组件
|   |   ├── components
│   │   └── index.vue
│   ├── pages               # 页面
│   │   ├── index
│   │   │   └── index.vue
│   │   └── login
│   │       └── index.vue
│   ├── router              # 路由
│   │   ├── index.js
│   │   └── routes.js
│   ├── store               # 状态管理
│   │   ├── index.js
│   │   └── modules
│   │       └── user.js
│   ├── styles              # 样式
│   │   └── index.scss
│   ├── utils               # 工具函数
│   │   ├── index.ts
│   │   └── request.ts
│   ├── App.vue            # 入口组件
│   └── main.js            # 入口文件
├── .editorconfig          # 编辑器配置
├── .env.development       # 开发环境变量
├── .env.production        # 生产环境变量
├── .eslintrc.json         # eslint配置
├── .gitignore             # git忽略文件
|── index.html             # 入口html文件
├── package.json           # 依赖包
├── README.md              # 项目说明
└── vite.config.js         # vite配置

文件名命名规范:

  1. 文件名全部小写,多个单词用_连接,如:user_info.js
  2. 组件命名大驼峰,如TableComponent
  3. 一个功能模块一个文件夹,文件夹名全部小写,入口文件为index.vue
  4. 公共组件放在components文件夹下,页面组件放在对应的页面文件夹下。
  5. 公共样式放在styles文件夹下,页面样式放在对应的页面文件夹下。
  6. 公共工具函数放在utils文件夹下,页面工具函数放在对应的页面文件夹下。
  7. 所有的请求接口放在api文件夹下,每个模块一个文件,入口文件为index.js

差不多久这么多吧,不需要那么严格,但是项目结构一定要整洁,不然后期维护起来会很麻烦。

项目实战

直接实战环节吧,不多说废话了。

因为我的项目已经都配置好了,后面的讲解可能跨度比较大,所以就不按照流程一步一步的来,这次直接一步到位。

首先附上我的package.json文件,

{
  "name": "my-vue-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --mode development",
    "build": "vite build --mode production",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vueuse/core": "^9.5.0",
    "axios": "^1.1.3",
    "element-plus": "^2.2.22",
    "pinia": "^2.0.24",
    "vue": "^3.2.41",
    "vue-router": "^4.1.6"
  },
  "devDependencies": {
    "@babel/eslint-parser": "^7.19.1",
    "@vitejs/plugin-legacy": "^2.3.1",
    "@vitejs/plugin-vue": "^3.2.0",
    "@vitejs/plugin-vue-jsx": "^2.1.1",
    "consola": "^2.15.3",
    "eslint": "^8.28.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-vue": "^9.7.0",
    "less": "^4.1.3",
    "unplugin-vue-components": "^0.22.9",
    "unplugin-vue-define-options": "^0.12.8",
    "vite": "^3.2.3",
    "vite-plugin-eslint": "^1.8.1",
    "vite-plugin-style-import": "^2.0.0"
  }
}

项目初始化

我们使用vite来初始化项目,根据官网的提示,不同版本的npm安装命令不一样,直接上命令:

# npm 6.x
npm create vite@latest my-vue-app --template vue

# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

安装完成后,进入项目目录,安装依赖,项目前期安装的并不用很多,就是vue全家桶:

cd my-vue-app

npm install vue vue-router@4 pinia axios element-plus -S

安装完成之后,我们需要在main.js中引入对应的依赖:

import {createApp} from "vue"; // 引入vue
import "./styles/index.less"; // 引入全局样式
import App from "./App.vue"; // 引入入口组件
import store from "./store"; // 引入状态管理
import router from "./router"; // 引入路由

import ElementPlus from "element-plus"; // 引入element-plus
import "element-plus/dist/index.css"; // 引入element-plus样式
import * as ElementPlusIconsVue from '@element-plus/icons-vue' // 引入element-plus图标

// 创建vue实例
const app = createApp(App); 

// 注册状态管理
app.use(store); 

// 注册路由
app.use(router); 

// 注册element-plus
app.use(ElementPlus);

// 注册element-plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// 挂载vue实例
app.mount("#app");

main.js中的代码比较多,我们一个一个的实现。

引入全局样式

上面的main.js中,已经是全的了,所以后面讲的东西,都是在main.js中的,就不会讲引入了。

我们在src目录下新建一个styles目录,然后在styles目录下新建一个index.less文件,这个就是全局样式文件,里面写入:

html, body {
  margin: 0;
  padding: 0;
}

引入状态管理

我们在src目录下新建一个store目录,然后在store目录下新建一个index.js文件,这个文件就是状态管理文件,里面写入:

import {createPinia} from "pinia";

const pinia = createPinia();

export default pinia;

引入路由

我们在src目录下新建一个router目录,然后在router目录下新建一个index.js文件,这个文件就是路由文件,里面写入:

import {createRouter, createWebHashHistory} from "vue-router";
import routes from "./routes";

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;

然后我们在router目录下新建一个routes.js文件,这个文件就是路由配置文件,里面写入:

import Layout from "@/layout/index.vue";

const routes = [
  {
    path: "/",
    redirect: "/index",
    component: Layout,
    meta: {
      title: "首页",
    },
    children: [
      {
        path: "/index",
        name: "Index",
        component: () => import("@/pages/index/index.vue"),
        meta: {
          title: "首页",
        },
      }
    ]
  },
  {
    path: "/login",
    name: "Login",
    meta: {
      title: "登录",
      keepAlive: true,
      requireAuth: false
    },
    component: () => import("@/pages/login/index.vue")
  },
];

export default routes;

引入入口组件

这个在创建项目的时候,已经创建好了,所以不用再创建了。

我们在src目录下的App.vue文件,这个文件就是入口组件,里面写入:

<template>
    <router-view/>
</template>

<script>
export default {
  name: "App",
};
</script>

<style lang="less" scoped>
</style>

布局组件

我们在src目录下新建一个layout目录,然后在layout目录下新建一个Layout.vue文件,这个文件就是布局组件,里面写入:

<template>
  <el-container>
    <el-aside width="260px">
      <right-panel/>
    </el-aside>
    <el-container>
      <el-header height="64px">
        <header-panel/>
      </el-header>
      <el-main>
        <router-view/>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import RightPanel from "@/components/RightPanel/index.vue";
import HeaderPanel from "@/components/HeaderPanel/index.vue";
export default {
  name: "Layout",
  components: {
    RightPanel,
    HeaderPanel
  }
};
</script>

<style lang="less" scoped>
</style>

右侧面板组件

我们在src目录下新建一个components目录,然后在components目录下新建一个RightPanel目录,然后在RightPanel目录下新建一个index.vue文件,这个文件就是右侧面板组件,里面写入:

<template>
  <div class="right-panel-container">
    <el-menu
        style="border: none;"
        background-color="#2a5eff"
        class="el-menu-vertical-demo"
        default-active="2"
        text-color="#fff"
        router
    >
      <menu-item v-for="route in routes" :route="route" :key="route.path"/>
    </el-menu>
  </div>
</template>

<script setup>
import { defineComponent, computed } from "vue";
import MenuItem from "@/components/RightPanel/MenuItem.vue";
import routes from "@/router/routes.js";
defineComponent({
  components: {
    MenuItem
  }
});

computed({
  routes: routes
});

</script>

<style lang="less" scoped>

.right-panel-container {
  width: 100%;
  height: 100vh;
  background-color: #2a5eff;
  color: #fff;
  overflow-y: auto;

  :deep(.el-menu .el-sub-menu.is-active .el-sub-menu__title),
  :deep(.el-menu .el-menu-item.is-active) {
    background-color: var(--el-menu-hover-bg-color);
    color: #52cca3;
  }

  :deep(.el-menu .el-menu-item.is-active) {
    border-left: 3px solid #52cca3;
  }

  :deep(.el-sub-menu .el-menu) {
    background-color: darken(#224bcc, 10%);
  }

}

.right-logo-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 64px;

  .right-logo {
    height: 48px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0 8px;

    img {
      height: 48px;
    }

  }
}

</style>

菜单组件

RightPanel目录下新建一个MenuItem.vue文件,这个文件就是菜单组件,里面写入:

<template>
  <el-sub-menu v-if="route.children && route.children.length > 0" :index="route.path">
    <template #title>
      <span>{{ (route.meta || {title: route.path}).title }}</span>
    </template>
    <menu-item
      v-for="item in route.children"
      :route="item"
      :key="item.path"
    />
  </el-sub-menu>
  <el-menu-item :index="route.path" v-else>
    <template #title>
      <span>{{ (route.meta || {title: route.path}).title }}</span>
    </template>
  </el-menu-item>
</template>

<script setup>
import { defineProps } from "vue";

defineProps({
  route: {
    type: Object,
    default: () => ({})
  },
});
</script>

<style scoped>

</style>

头部面板组件

components目录下新建一个HeaderPanel目录,然后在HeaderPanel目录下新建一个index.vue文件,这个文件就是头部面板组件,里面写入:

<template>
<div class="header-panel-container">
  <div class="header-breadcrumb">
    <el-breadcrumb>
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
  </div>

  <div class="header-operate">
    <el-icon><Bell /></el-icon>

    <el-dropdown>
      <span class="user-info">
        <el-avatar size="small" src="avatar.png"></el-avatar>
        <span>admin</span>
        <i class="el-icon-arrow-down el-icon--right"></i>
      </span>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item>个人中心</el-dropdown-item>
          <el-dropdown-item>切换账号</el-dropdown-item>
          <el-dropdown-item>退出登录</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</div>
</template>

<script>
export default {
  name: "HeaderPanel"
};
</script>

<style lang="less" scoped>
.header-panel-container {
  width: 100%;
  height: 100%;
  background-color: #fff;
  color: #333;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  box-sizing: border-box;
  border-bottom: 1px solid #ebeef5;

  .header-operate {
    display: flex;
    align-items: center;
    justify-content: center;

    :deep(.el-icon) {
      font-size: 20px;
      color: #333;
      cursor: pointer;
      padding: 10px;
    }
  }

  .user-info {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-left: 10px;
    cursor: pointer;

    span {
      margin-left: 10px;
    }
  }
}

</style>

到此整个项目的基本框架就搭建完成了,以上是极简主义,没有多余的东西,只是一个最基本的框架,后面会慢慢完善。

vite 配置

上面的准备只是项目结构的搭建,还需要配置一下vite,在项目根目录下新建一个vite.config.js文件,里面写入:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import {
  createStyleImportPlugin,
  ElementPlusResolve,
} from 'vite-plugin-style-import'
import eslintPlugin from 'vite-plugin-eslint'
import vueJsx from '@vitejs/plugin-vue-jsx'
import legacy from '@vitejs/plugin-legacy'
import DefineOptions from 'unplugin-vue-define-options/vite'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    //设置别名
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  build: {
    target: 'es2015', // 默认 "modules"
  },
  plugins: [
    vue(),
    vueJsx({}),
    createStyleImportPlugin({
      resolves:[
        ElementPlusResolve()
      ],
      libs: [
        // 如果没有你需要的resolve,可以在lib内直接写,也可以给我们提供PR
        {
          libraryName: 'element-plus',
          esModule: true,
          resolveStyle: (name) => {
            return `element-plus/lib/theme-chalk/${name}.css`
          },
          ensureStyleFile: true // 忽略文件是否存在, 导入不存在的CSS文件时防止错误。
        },
      ],
    }),
    eslintPlugin({
      include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue']
    }),
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
    DefineOptions(),
  ],
  server: {
    port: 8080, //启动端口
    hmr: {
      host: '127.0.0.1',
      port: 8080
    },
    // 设置 https 代理
    proxy: {
      '/api': {
        target: 'your https address',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    }
  }
});

别名配置

resolve中配置别名,这样在引入文件的时候就可以使用@代替src,比如:

export default defineConfig({
  resolve: {
    //设置别名
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
});

这样在引入文件的时候就可以使用@代替src,比如:

import HeaderPanel from '@/components/HeaderPanel.vue';

vue 插件

plugins中配置vue插件,这样就可以在vue文件中使用jsx语法了,比如:

export default defineConfig({
  plugins: [
    vue(),
    vueJsx({}),
  ]
});

依赖:@vitejs/plugin-vue@vitejs/plugin-vue-jsx

element-plus

plugins中配置element-plus,这样就可以在vue文件中使用element-plus了,比如:

export default defineConfig({
  plugins: [
    createStyleImportPlugin({
      resolves:[
        ElementPlusResolve()
      ],
      libs: [
        // 如果没有你需要的resolve,可以在lib内直接写,也可以给我们提供PR
        {
          libraryName: 'element-plus',
          esModule: true,
          resolveStyle: (name) => {
            return `element-plus/lib/theme-chalk/${name}.css`
          },
          ensureStyleFile: true // 忽略文件是否存在, 导入不存在的CSS文件时防止错误。
        },
      ],
    }),
  ]
});

依赖:element-plusvite-plugin-style-import

eslint

eslint先需要初始化,执行npx eslint -init,然后根据提示选择配置,最后在plugins中配置eslint,这样就可以在vue文件中使用eslint了,比如:

然后在plugins中配置eslint,这样就可以在vue文件中使用eslint了,比如:

export default defineConfig({
  plugins: [
    eslintPlugin({
      include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue']
    }),
  ]
});

eslint还需要配置eslintConfig,在第一步的时候你应该已经自己选择好了,这里就不再赘述了,我的项目中是选择生成.eslintrc.js文件,所以在项目根目录下会生成.eslintrc.json文件。

依赖:vite-plugin-eslint

兼容性

plugins中配置legacy,这样就可以在vue文件中使用es6语法了,比如:

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
  ]
});

依赖:@vitejs/plugin-legacy

环境变量

环境变量不需要配置,直接在根目录下创建.env文件,然后在vue文件中使用process.env就可以了,比如:

VTIE_APP_BASE_URL=http://localhost:8080
console.log(import.meta.env.VTIE_APP_BASE_URL);

注意:VITE_是固定的,后面的APP_BASE_URL是自定义的,可以随意命名,必须得带VITE_前缀,否则会被忽略。

代理配置

server中配置代理,这样在开发环境中就可以使用代理,比如:

export default defineConfig({
 server: {
    port: 8080, //启动端口
    hmr: {
      host: '127.0.0.1',
      port: 8080
    },
    // 设置 https 代理
    proxy: {
      '/api': {
        target: 'your https address',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    }
  }
});

这样在开发环境中就可以使用代理。

打包配置

build中配置打包,这样在打包时就可以使用打包配置,比如:

export default defineConfig({
  build: {
    target: 'es2015', // 默认 "modules"
  },
});

这里指示vite打包时使用es2015语法,而不是es6语法。

自此整个项目的配置就完成了,接下来就可以按照自己的需求进行开发了。

总结

根据实际情况来进行技术选型以及架构的搭建,这样才能更好的满足业务需求,提高开发效率。

我上面并没有上什么高级或者特别的技术,没有上TypeScript,没有上prettier,也没有规范提交的commitizen

如果是自己的项目,我随便玩我会考虑上,但是团队项目终究是要考虑团队的,它不是最好的,但是它是最合适的。

本文正在参加「金石计划 . 瓜分6万现金大奖」