Comprehensive Architecture Guide for a Generic Backend Management System Using Vue and Vite
This article presents a detailed architecture solution for building a generic backend management system with Vue, covering project scaffolding choices, Vite configuration, ESLint/Prettier setup, CSS architecture (ITCSS, BEM, ACSS), JWT authentication, dynamic menu design, RBAC, route registration, component caching, and template generation using Plop.
General Backend Management System Architecture (Vue)
Project Creation and Scaffold Selection (vite or vue-cli)
vue-cli is based on webpack , offering a powerful ecosystem and high configurability but with complex configuration and slower development experience compared to vite .
vite uses esbuild for dev mode and rollup for bundling, providing fast cold starts and seamless HMR; however its ecosystem is still catching up with webpack .
This article mainly explains how to use vite as the scaffold for development (developers can also use webpack for production builds).
Choosing vite over vue-cli avoids the exponential growth of JavaScript bundle size and long build times as projects scale.
Modern browsers support native ES modules, allowing Vite to serve source modules directly with 304 caching and long‑term Cache‑Control headers.
Microsoft announced that IE11 will be unsupported after June 15, 2022; leveraging the latest browser features is the trend.
$ npm init @vitejs/app
$ cd
$ npm install
$ npm run devBasic Setup and Code Style Support (eslint + prettier)
Install VSCode extensions: eslint , prettier , vetur (or volar for Vue 3 setup syntax).
eslint
yarn add --dev eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-pluginprettier
yarn add --dev prettier eslint-config-prettier eslint-plugin-prettier.prettierrc.js
module.exports = {
printWidth: 180, // line length
tabWidth: 4,
useTabs: false,
singleQuote: true,
semi: false,
trailingComma: 'none',
bracketSpacing: true,
jsxSingleQuote: true,
endOfLine: 'auto'
}.eslintrc.js
//.eslintrc.js
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
ecmaVersion: 2020, // Allows parsing modern ECMAScript features
sourceType: 'module', // Allows use of imports
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended'
]
}.settings.json (workspace)
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue",
"typescript",
"typescriptreact",
"json"
]
}Directory Structure Example
├─.vscode // VSCode config files
├─public // Static assets (no compilation)
├─src // Source code
│ ├─apis // Unified API management
│ │ └─modules // API modules
│ ├─assets // Static resources
│ │ └─images
│ ├─components // Project components (Form, Input, Message, Search, Table, etc.)
│ ├─directives // Directives (e.g., print)
│ ├─hooks // Custom hooks
│ ├─layouts // Layout components (dashboard, fullpage)
│ ├─mock // Mock API data
│ ├─router // Routing helpers
│ ├─store // State management
│ ├─styles // Styles (CSS architecture later)
│ ├─types // Type definitions
│ ├─utils // Utility functions
│ └─views // Page components (normal, system)
└─template // Template filesCSS Architecture: ITCSS + BEM + ACSS
Neglecting CSS architecture leads to style pollution, overrides, and maintenance difficulties as projects grow.
ITCSS provides a layered methodology for managing CSS.
ITCSS layers:
Layer
Purpose
Settings
Global variables
Tools
Mixins, functions
Generic
Normalize, reset
Base
Type selectors
Objects
Cosmetic‑free design patterns
Components
UI components
Trumps
Only place for
!importantCombine ITCSS with BEM (Block‑Element‑Modifier) and ACSS (Atomic CSS, similar to Tailwind) for a robust style system.
JWT (JSON Web Token)
JWT provides a cross‑domain authentication solution, enabling single sign‑on without server‑side session storage.
Features
Cross‑language via JSON
Small payload, easy transmission
Signed for integrity
Has expiration
Efficient for clustered SSO
Data Structure
Header.Payload.Signature
Security Considerations
Do not store sensitive data in the payload (client can decode).
Protect the secret key.
Prefer HTTPS.
Implementation
Backend (Koa) example:
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
router.post('/login', async (ctx) => {
try {
const { userName, userPwd } = ctx.request.body;
const res = await User.findOne({ userName, userPwd });
const data = res._doc;
const token = jwt.sign({ data }, 'secret', { expiresIn: '1h' });
if (res) {
data.token = token;
ctx.body = data;
}
} catch (e) {}
});Frontend Axios interceptor:
// axios request interceptor, add token to Authorization header
service.interceptors.request.use(request => {
const token = Cookies.get('token');
token && (request.headers['Authorization'] = token);
return request;
}, error => {
Message.error(error);
});Koa JWT validation:
const app = new Koa();
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const koajwt = require('koa-jwt');
app.use(koajwt({ secret: 'secret' }));
app.use(async (ctx, next) => {
await next().catch(err => {
if (err.status === 401) {
ctx.body.msg = 'token authentication failed';
}
});
});Menu Design
Traditional approach stores a static menu tree on the frontend; modern practice fetches menu data from the backend (XML configuration) and registers routes dynamically.
Proposed design: maintain a menu.json on the frontend (what‑you‑see‑is‑what‑you‑get), then generate SQL for the backend via Node scripts.
Structure Design
key
type
description
title
string
Menu title
name
string
Unique identifier for routing and buttons (must be unique per level)
type
string
MODULE, MENU, or BUTTON
path
string
Route path
redirect
string
Redirect path
icon
string
Icon for menu/button
component
string
Component path for menu items
hidden
boolean
Whether the menu is hidden in the sidebar
noCache
boolean
Whether the page should be cached
fullscreen
boolean
Whether the page displays fullscreen
children
array
Sub‑menus or buttons
Note: sibling name values must be unique; they are concatenated with hyphens to form a full identifier.
Example menu.json snippet:
[
{
"title": "admin",
"name": "admin",
"type": "MODULE",
"children": [
{
"title": "Central Console",
"path": "/platform",
"name": "platform",
"type": "MENU",
"component": "/platform/index",
"icon": "mdi:monitor-dashboard"
},
{
"title": "System Settings",
"name": "system",
"type": "MENU",
"path": "/system",
"icon": "ri:settings-5-line",
"children": [
{
"title": "User Management",
"name": "user",
"type": "MENU",
"path": "user",
"component": "/system/user"
},
{
"title": "Role Management",
"name": "role",
"type": "MENU",
"path": "role",
"component": "/system/role"
},
{
"title": "Resource Management",
"name": "resource",
"type": "MENU",
"path": "resource",
"component": "/system/resource"
}
]
},
{
"title": "Utility Functions",
"name": "function",
"type": "MENU",
"path": "/function",
"icon": "ri:settings-5-line",
"children": []
}
]
}
]The generated SQL can be created by a Node script ( createMenu.js ) that reads menu.json , computes MD5 IDs, and writes a Liquibase‑compatible SQL file.
Node Script Example (createMenu.js)
// createMenu.js
/**
* =================MENU CONFIG======================
* This JavaScript creates SQL for Java
*/
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const execSync = require('child_process').execSync; // sync child process
const resolve = dir => path.join(__dirname, dir);
const moment = require('moment');
const gitName = execSync('git show -s --format=%cn').toString().trim();
const md5 = require('md5');
// GLOBAL CONFIG
const INPUT_PATH = resolve('src/router/menu.json');
const OUTPUT_PATH = resolve('./menu.sql');
const TABLE_NAME = 't_sys_menu';
function createSQL(data, name = '', pid, arr = []) {
data.forEach(function(v, d) {
if (v.children && v.children.length) {
createSQL(v.children, name + '-' + v.name, v.id, arr);
}
arr.push({
id: v.id || md5(v.name),
created_at: moment().format('YYYY-MM-DD HH:mm:ss'),
modified_at: moment().format('YYYY-MM-DD HH:mm:ss'),
created_by: gitName,
modified_by: gitName,
version: 1,
is_delete: false,
code: (name + '-' + v.name).slice(1),
name: v.name,
title: v.title,
icon: v.icon,
path: v.path,
sort: d + 1,
parent_id: pid,
type: v.type,
component: v.component,
redirect: v.redirect,
full_screen: v.fullScreen || false,
hidden: v.hidden || false,
no_cache: v.noCache || false
});
});
return arr;
}
fs.readFile(INPUT_PATH, 'utf-8', (err, data) => {
if (err) chalk.red(err);
const menuList = createSQL(JSON.parse(data));
const sql = menuList
.map(sql => {
let value = '';
for (const v of Object.values(sql)) {
value += ',';
if (v === true) {
value += 1;
} else if (v === false) {
value += 0;
} else {
value += v ? `'<${v}>'` : null;
}
}
return 'INSERT INTO `' + TABLE_NAME + '` VALUES (' + value.slice(1) + ')\n';
})
.join(';');
const mySQL =
'DROP TABLE IF EXISTS `' +
TABLE_NAME +
'`;\n' +
'CREATE TABLE `' +
TABLE_NAME +
'`(\n' +
'`id` varchar(64) NOT NULL,\n' +
"`created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间',\n" +
"`modified_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',\n" +
"`created_by` varchar(64) DEFAULT NULL COMMENT '创建人',\n" +
"`modified_by` varchar(64) DEFAULT NULL COMMENT '更新人',\n" +
"`version` int(11) DEFAULT NULL COMMENT '版本(乐观锁)',\n" +
"`is_delete` int(11) DEFAULT NULL COMMENT '逻辑删除',\n" +
"`code` varchar(150) NOT NULL COMMENT '编码',\n" +
"`name` varchar(50) DEFAULT NULL COMMENT '名称',\n" +
"`title` varchar(50) DEFAULT NULL COMMENT '标题',\n" +
"`icon` varchar(50) DEFAULT NULL COMMENT '图标',\n" +
"`path` varchar(250) DEFAULT NULL COMMENT '路径',\n" +
"`sort` int(11) DEFAULT NULL COMMENT '排序',\n" +
"`parent_id` varchar(64) DEFAULT NULL COMMENT '父id',\n" +
"`type` char(10) DEFAULT NULL COMMENT '类型',\n" +
"`component` varchar(250) DEFAULT NULL COMMENT '组件路径',\n" +
"`redirect` varchar(250) DEFAULT NULL COMMENT '重定向路径',\n" +
"`full_screen` int(11) DEFAULT NULL COMMENT '全屏',\n" +
"`hidden` int(11) DEFAULT NULL COMMENT '隐藏',\n" +
"`no_cache` int(11) DEFAULT NULL COMMENT '缓存',\n" +
'PRIMARY KEY (`id`),\n' +
'UNIQUE KEY `code` (`code`) USING BTREE\n' +
') ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=\'资源\';\n' +
sql;
fs.writeFile(OUTPUT_PATH, mySQL, err => {
if (err) return chalk.red(err);
console.log(chalk.cyanBright(`SQL file created at ${OUTPUT_PATH}`));
});
});RBAC (Role‑Based Access Control)
RBAC links permissions to roles; users inherit permissions by belonging to roles, simplifying permission management.
Page Cache Control
Using Vue's keep-alive with the include option allows selective caching of pages based on their component name , improving user experience while preventing memory bloat.
Note: the include list stores component names, not route names.
Component Packaging and UI Library Wrappers
Encapsulate reusable components with Jest unit tests and Storybook documentation for large teams.
Template Generation with Plop
Use Plop and Inquirer‑directory to generate API and page templates, reducing repetitive boilerplate.
// plopfile.js
const promptDirectory = require('inquirer-directory');
const pageGenerator = require('./template/page/prompt');
const apisGenerator = require('./template/apis/prompt');
module.exports = function (plop) {
plop.setPrompt('directory', promptDirectory);
plop.setGenerator('page', pageGenerator);
plop.setGenerator('apis', apisGenerator);
};Running Plop guides the user through selecting a target directory, entering an API name, and choosing CRUD operations to generate a ready‑to‑use TypeScript file.
Project Repository
GitHub: https://github.com/sky124380729/levi-vue-admin
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.