The overall architecture scheme of the general background management system (Vue)
Project creation, choice of scaffolding (vite or vue-cli)
-
vue-cli
Based onwebpack
package, the ecology is very powerful, the configurability is also very high, and it can almost meet all the requirements of front-end engineering. The disadvantage is that the configuration is complicated, and even some companies have specialwebpack工程师
for configuration. In addition, because webpack needs to be packaged and compiled in the development environment, the development experience is actually not as good asvite
. -
vite
development mode is based onesbuild
, and the package isrollup
. Fast冷启动
and seamlesshmr
get a huge experience improvement in development mode. The disadvantage is that the scaffolding has just started, and it is not as ecologically aswebpack
.
This article mainly explains the use of vite
as a scaffolding development. (You can use vite
as a development server, and use webpack
as a package and compile and put it in the production environment)
Why choose vite instead of vue-cli, whether it is webpack
, parcel
, rollup
and other tools, although they have greatly improved the front-end development experience There is a problem that when the project gets bigger and bigger, the code that needs to be processed js
also grows exponentially, and the packaging process usually takes a long time (even minutes!) to start the development server , the experience will get worse as the project gets bigger.
Since modern browsers already natively support es modules, as long as we use browsers that support esm to develop, does our code need not be packaged? Yes, the principle is that simple. vite will negotiate and cache requests for source modules according to 304 Not Modified
, and dependent modules will be strongly cached through Cache-Control:max-age=31536000,immutable
, so once they are cached, they will not need to be requested again.
Software giant Microsoft said on Wednesday (May 19) that starting June 15, 2022, some versions of the company's Windows software will no longer support the current version of the IE 11 desktop app. 所以利用浏览器的最新特性来开发项目是趋势。
$ npm init @vitejs/app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
Basic settings, code specification support (eslint+prettier+stylelint)
vscode 安装eslint
, prettier
, stylelint
, vetur
(喜欢用vue3 setup语法糖volar
, At this time, disable vetur
)
open vscode eslint
eslint
yarn add --dev eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin
prettier
yarn add --dev prettier eslint-config-prettier eslint-plugin-prettier
stylelint
yarn add --dev stylelint stylelint-config-standard stylelint-order
.prettierrc.js
module.exports = {
printWidth: 180, //一行的字符数,如果超过会进行换行,默认为80
tabWidth: 4, //一个tab代表几个空格数,默认为80
useTabs: true, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号
semi: false, //行位是否使用分号,默认为true
trailingComma: 'none', //是否使用尾逗号,有三个可选值"<none|es5|all>"
bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
jsxSingleQuote: true, // jsx语法中使用单引号
endOfLine: 'auto'
}
.eslintrc.js
//.eslintrc.js
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended'
]
}
.stylelintrc.js
module.exports = {
root: true,
extends: ['stylelint-config-standard'],
plugins: ['stylelint-order'],
rules: {
'indentation': 'tab',
'no-descending-specificity': null,
'no-empty-source': null,
'no-missing-end-of-source-newline': null,
'declaration-block-trailing-semicolon': null,
'function-name-case': null,
'length-zero-no-unit':null,
'rule-empty-line-before':null,
'declaration-empty-line-before': null
}
}
.settings.json (workspace)
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue",
"typescript",
"typescriptreact",
"json"
]
}
Directory Structure Example
├─.vscode // vscode配置文件
├─public // 无需编译的静态资源目录
├─src // 代码源文件目录
│ ├─apis // apis统一管理
│ │ └─modules // api模块
│ ├─assets // 静态资源
│ │ └─images
│ ├─components // 项目组件目录
│ │ ├─Form
│ │ ├─Input
│ │ ├─Message
│ │ ├─Search
│ │ ├─Table
│ ├─directives // 指令目录
│ │ └─print
│ ├─hooks // hooks目录
│ ├─layouts // 布局组件
│ │ ├─dashboard
│ │ │ ├─content
│ │ │ ├─header
│ │ │ └─sider
│ │ └─fullpage
│ ├─mock // mock apu存放地址,和apis对应
│ │ └─modules
│ ├─router // 路由相关
│ │ └─helpers
│ ├─store // 状态管理相关
│ ├─styles // 样式相关(后面降到css架构会涉及具体的目录)
│ ├─types // 类型定义相关
│ ├─utils // 工具类相关
│ └─views // 页面目录地址
│ ├─normal
│ └─system
└─template // 模板相关
├─apis
└─page
CSS Architecture ITCSS
+ BEM
+ ACSS
In real development, we often ignore the architectural design of CSS. The neglect of the style architecture in the early stage, with the increase of the project, led to various problems such as style pollution, coverage, difficulty in tracing, and code duplication. Therefore, CSS architecture design also needs to be taken seriously.
-
ITCSS
ITCSS is a CSS design methodology, it is not a specific CSS constraint, it allows you to better manage and maintain the CSS of your project.
ITCSS divides CSS into the following layers
Layer | effect |
---|---|
Settings | Global variables used by the project |
Tools | mixin, function |
Generic | The most basic settings normalize.css, reset |
Base | type selector |
Objects | Cosmetic-free design patterns |
Components | UI components |
Trumps | The only place helper can use important! |
The above is the given paradigm, we don't have to follow it exactly, we can combine BEM
and ACSS
Currently I give CSS file directory (tentative)
└─styles
├───acss
├───generic
├───theme
├───tools
└───transition
-
BEM
Namely Block, Element, Modifier, is an advanced version ofOOCSS
(object-oriented css), which is a component-based web development method. Blcok can be understood as an independent block. The movement of the block in the page will not affect the internal style (similar to the concept of components, an independent block). The element is the element under the block, which is connected to the block. The modifier is Indicates style size, etc.
Let's take a look at the practice ofelement-ui
The development or packaging of our project components is used uniformly BEM
-
ACSS
Anyone who knowstailwind
should be familiar with this design pattern, ie CSS at the atomic level. Like .fr and .clearfix, they all belong to the design thinking of ACSS. Here we can use this pattern to write some variables etc.
JWT (json web token)
JWT is a cross-domain authentication solution
HTTP requests are stateless, and the server does not recognize the request sent by the front end. For example, when logging in, the server will generate a sessionKey after the login is successful, and the sessionKey will be written into the cookie. The sessionKey will be automatically brought into the next request. Now many users write the user ID into the cookie. This is problematic. For example, to do single sign-on, when a user logs in to server A, the server generates a sessionKey. When logging in to server B, the server does not have a sessionKey, so it does not know who is currently logged in, so sessionKey cannot be used for single sign-on. Click to log in. However, since jwt is a token generated by the server to the client, and there is a client, it can realize single sign-on.
Features
- JWT is cross-language due to the use of json transport
- Easy to transmit, the composition of jwt is very simple, and the byte occupancy is very small, so it is very easy to transmit
- jwt will generate a signature to ensure transmission security
- jwt is time-sensitive
jwt makes more efficient use of clusters for single sign-on
data structure
- Header.Payload.Signature
Data Security
- Sensitive information should not be stored in the payload part of jwt, because this part is decryptable by the client
- Protect the secret private key, which is very important
If possible, use https protocol
manual
How to use
rear end
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) { } } )
front end
// axios请求拦截器,Cookie写入token,请求头添加:Authorization: Bearer `token` service.interceptors.request.use( request => { const token = Cookies.get('token') // 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ' token && (request.headers['Authorization'] = token) return request }, error => { Message.error(error) } )
Backend Validation Validity
const app = new Koa() const router = require('koa-router')() const jwt = require('jsonwebtoken') const koajwt = require('koa-jwt') // 使用koa-jwt中间件不用在接口之前拦截进行校验 app.use(koajwt({ secret:'secret' })) // 验证不通过会将http状态码返回401 app.use(async (ctx, next) => { await next().catch(err => { if(err.status === 401) { ctx.body.msg = 'token认证失败' } }) })
menu design
There are many ways to generate menus. The more traditional one is to maintain a menu tree at the front end and filter according to the menu tree returned by the back end. This method actually registers the route into the instance in advance, which is not the best practice now.
Now the mainstream idea is that the back-end configures the menu through XML
, and generates the menu through the configuration. When the front-end logs in, pull the menu corresponding to the role, and register the corresponding routing address of the menu and the path of the page in the front-end project through the addroute
method. This is more mainstream, but I personally think it is not the most perfect.
Our menu and front-end code are actually strongly coupled, including routing addresses, page paths, icons, redirects, etc. The menu at the beginning of the project may change frequently. Every time you add or modify the menu, you need to notify the back-end to modify XML
, and the back-end XML
actually has no tree. The structure doesn't look very convenient either.
Therefore, I adopt the following design pattern, 前端
maintain a copy of menu.json
, what you write is what you get, and what the json number looks like when you configure it in the menu.
structural design
key | type | description |
---|---|---|
title | string | title of the menu |
name | string | The name of the corresponding route is also the unique identifier of the page or button. Important , see the following precautions |
type | string | MODULE represents a module (subsystem, such as APP and background management system), MENU represents a menu, BUTTON represents a button |
path | string | path, corresponding to the path of the route |
redirect | string | Redirect, the redirect corresponding to the route |
icon | string | menu or button icon |
component | string | When used as the only time, the item loading address of the corresponding menu |
hidden | boolean | Whether to hide in the left menu tree when used as a menu |
noCache | boolean | Whether the menu is cached when used as a menu |
fullscreen | boolean | Whether to display the current menu in full screen when used as a menu |
children | array | As the name suggests, the next level |
注意事项
: If the name of the same level is unique, in actual use, the name of each level is spliced by the name of the previous level-
(will be dynamically imported through the chapter Demonstrate the generation rules of name), which can ensure that each menu or button item has a unique identifier. Whether it is for button permission control or for menu caching, it is related to the name of this splicing. We note that there is no id at this time. Later, we will talk about using md5 to generate id according to the full name of the name.
Process Design
sample code
[
{
"title": "admin",
"name": "admin",
"type": "MODULE",
"children": [
{
"title": "中央控制台",
"path": "/platform",
"name": "platform",
"type": "MENU",
"component": "/platform/index",
"icon": "mdi:monitor-dashboard"
},
{
"title": "系统设置",
"name": "system",
"type": "MENU",
"path": "/system",
"icon": "ri:settings-5-line",
"children": [
{
"title": "用户管理",
"name": "user",
"type": "MENU",
"path": "user",
"component": "/system/user"
},
{
"title": "角色管理",
"name": "role",
"type": "MENU",
"path": "role",
"component": "/system/role"
},
{
"title": "资源管理",
"name": "resource",
"type": "MENU",
"path": "resource",
"component": "/system/resource"
}
]
},
{
"title": "实用功能",
"name": "function",
"type": "MENU",
"path": "/function",
"icon": "ri:settings-5-line",
"children": []
}
]
}
]
Generated menu tree
If you think that the routes of all pages are written in one page is too long and difficult to maintain, you can replacejson
with js and use theimport
mechanism. There are many changes involved here. not mentioned
When using, we have two environments: development
and production
-
development
: In this mode, the menu tree directly reads the menu.json file -
production
: In this mode, the menu tree obtains database data through the interface
How to save to database
OK, as we mentioned before, the menu is maintained by the front-end through menu.json, so how to enter the database? In fact, my design is to read the menu.json
file through node
, and then create a SQL statement, hand it over to the backend and put it in liquibase
, no matter how many In a database environment, as long as the backend gets the SQL statement, menu data can be created in multiple environments. Of course, since json
can communicate across languages, we can directly drop the json
file to the backend, or drop the project json
path to O&M , through the CI/CD
tool to complete the automatic release.
nodejs generate SQL example
// createMenu.js
/**
*
* =================MENU CONFIG======================
*
* this javascript created to genarate SQL for Java
*
* ====================================================
*
*/
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const execSync = require('child_process').execSync //同步子进程
const resolve = (dir) => path.join(__dirname, dir)
const moment = require('moment')
// get the Git user name to trace who exported the SQL
const gitName = execSync('git show -s --format=%cn').toString().trim()
const md5 = require('md5')
// use md5 to generate id
/* =========GLOBAL CONFIG=========== */
// 导入路径
const INPUT_PATH = resolve('src/router/menu.json')
// 导出的文件目录位置
const OUTPUT_PATH = resolve('./menu.sql')
// 表名
const TABLE_NAME = 't_sys_menu'
/* =========GLOBAL CONFIG=========== */
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), // name is unique,so we can use name to generate id
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语句成功,位置:${OUTPUT_PATH}`))
})
})
Note that the above is encrypted by usingmd5
toname
to generate the primary keyid
into the database
We try to execute this js with node
node createMenu.js
Since the production environment will not directly introduce menu.json
, this file will not exist in the online environment that has been packaged and compiled, so there will be no security issues.
How to control down to button level
We know that the carrier of the button (the button here is in a broad sense, for the front end, it may be button, tab, dropdown and other controllable content) must be the carrier of the page, so the button can be directly linked to the menu tree MENU
type resource, there is no page page permission, of course there is no button permission under the page, if there is page permission, we use the v-permission
instruction to control the display of the button sample code
// 生成权限按钮表存到store
const createPermissionBtns = router => {
let btns = []
const c = (router, name = '') => {
router.forEach(v => {
v.type === 'BUTTON' && btns.push((name + '-' + v.name).slice(1))
return v.children && v.children.length ? c(v.children, name + '-' + v.name) : null
})
return btns
}
return c(router)
}
// 权限控制
Vue.directive('permission', {
// 这里是vue3的写法,vue2请使用inserted生命周期
mounted(el, binding, vnode) {
// 获取this
const { context: vm } = vnode
// 获取绑定的值
const name = vm.$options.name + '-' + binding.value
// 获取权限表
const {
state: { permissionBtns }
} = store
// 如果没有权限那就移除
if (permissionBtns.indexOf(name) === -1) {
el.parentNode.removeChild(el)
}
}
})
<el-button type="text" v-permission="'edit'" @click="edit(row.id)">编辑</el-button>
Assuming that the name value of the current page is system-role
and the name value of the button is system-role-edit
, then this command can easily control the permissions of the button
dynamic import
How do we json
or the address of the routing front-end page configured on the interface, register it in vue-router
?
Pay attention to the following name generation rules. Taking the role menu as an example, the form of the name splicing is roughly as follows:
- Level 1 menu: system
- Secondary menu: system-role
- Button under this secondary menu: system-role-edit
vue-cli
vue-cli3 and above can directly use webpack4+ importdynamic import
// 生成可访问的路由表 const generateRoutes = (routes, cname = '') => { return routes.reduce((prev, { type, uri: path, componentPath, name, title, icon, redirectUri: redirect, hidden, fullScreen, noCache, children = [] }) => { // 是菜单项就注册到路由进去 if (type === 'MENU') { prev.push({ path, component: () => import(`@/${componentPath}`), name: (cname + '-' + name).slice(1), props: true, redirect, meta: { title, icon, hidden, type, fullScreen, noCache }, children: children.length ? createRouter(children, cname + '-' + name) : [] }) } return prev }, []) }
vite
You can use glob-import directly after vite2// dynamicImport.ts export default function dynamicImport(component: string) { const dynamicViewsModules = import.meta.glob('../../views/**/*.{vue,tsx}') const keys = Object.keys(dynamicViewsModules) const matchKeys = keys.filter((key) => { const k = key.replace('../../views', '') return k.startsWith(`${component}`) || k.startsWith(`/${component}`) }) if (matchKeys?.length === 1) { const matchKey = matchKeys[0] return dynamicViewsModules[matchKey] } if (matchKeys?.length > 1) { console.warn( 'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure' ) return } return null }
import type { IResource, RouteRecordRaw } from '../types'
import dynamicImport from './dynamicImport'
// 生成可访问的路由表
const generateRoutes = (routes: IResource[], cname = '', level = 1): RouteRecordRaw[] => {
return routes.reduce((prev: RouteRecordRaw[], curr: IResource) => {
// 如果是菜单项则注册进来
const { id, type, path, component, name, title, icon, redirect, hidden, fullscreen, noCache, children } = curr
if (type === 'MENU') {
// 如果是一级菜单没有子菜单,则挂在在app路由下面
if (level === 1 && !(children && children.length)) {
prev.push({
path,
component: dynamicImport(component!),
name,
props: true,
meta: { id, title, icon, type, parentName: 'app', hidden: !!hidden, fullscreen: !!fullscreen, noCache: !!noCache }
})
} else {
prev.push({
path,
component: component ? dynamicImport(component) : () => import('/@/layouts/dashboard'),
name: (cname + '-' + name).slice(1),
props: true,
redirect,
meta: { id, title, icon, type, hidden: !!hidden, fullscreen: !!fullscreen, noCache: !!noCache },
children: children?.length ? generateRoutes(children, cname + '-' + name, level + 1) : []
})
}
}
return prev
}, [])
}
export default generateRoutes
Dynamically register routes
To achieve dynamic addition of routes, that is, only authorized routes will be registered in the Vue instance. Considering that the instance of vue will be lost every time the page is refreshed, and the menu of the role may also be updated, it is the most appropriate time to pull the menu and inject the route every time the page is loaded. So the core is vue-router
addRoute
and navigation guard beforeEach
two methods
To achieve dynamic addition of routes, that is, only authorized routes will be registered in the Vue instance. Considering that the instance of vue will be lost every time the page is refreshed, and the menu of the role may also be updated, it is the most appropriate time to pull the menu and inject the route every time the page is loaded. So the core is vue-router addRoute
and navigation hook beforeEach
two methods
vue-router3x
注
: 3.5.0API has also been updated to addRoute, pay attention to distinguish the version changes
vue-router4x
Personally, I prefer to use the vue-router4x
addRoute
method of ---8eaa286f393e2781a12f4e94bd582832---, which can control the positioning of each route more finely
The general idea is, in beforeEach
in the navigation guard (that is, make a judgment before each route jump), if it has been authorized ( authorized
), go directly to the next method, if not , the routing table is pulled from the backend and registered in the instance. (Introduce the following files or codes directly into the entry file main.js
)
// permission.js
router.beforeEach(async (to, from, next) => {
const token = Cookies.get('token')
if (token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (!store.state.authorized) {
// set authority
await store.dispatch('setAuthority')
// it's a hack func,avoid bug
next({ ...to, replace: true })
} else {
next()
}
}
} else {
if (to.path !== '/login') {
next({ path: '/login' })
} else {
next(true)
}
}
})
Since the route is dynamically registered, the initial route of the project will be very simple. As long as a static basic route that does not require permission is provided, other routes are dynamically registered after returning from the server.
// router.js
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from './types'
// static modules
import Login from '/@/views/sys/Login.vue'
import NotFound from '/@/views/sys/NotFound.vue'
import Homepage from '/@/views/sys/Homepage.vue'
import Layout from '/@/layouts/dashboard'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/homepage'
},
{
path: '/login',
component: Login
},
// for 404 page
{
path: '/:pathMatch(.*)*',
component: NotFound
},
// to place the route who don't have children
{
path: '/app',
component: Layout,
name: 'app',
children: [{ path: '/homepage', component: Homepage, name: 'homepage', meta: { title: '首页' } }]
}
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior() {
// always scroll to top
return { top: 0 }
}
})
export default router
Left menu tree and button generation
In fact, as long as you recursively get the resource whose type is MENU
and register it to the route, filter out the menu of hidden:true
in the tree on the left, and will not repeat it here.
RBAC (Role Based Access Control)
RBAC is a role-based access control (Role-Based Access Control) In RBAC, permissions are associated with roles, and users get the permissions of these roles by becoming a member of the appropriate role. This greatly simplifies the management of permissions. In this way, management is hierarchically interdependent, permissions are assigned to roles, and roles are assigned to users. This kind of permission design is very clear, and it is very convenient to manage.
When logging in like this, just get the user
User select role
Character binding menu
menu
page cache control
Page caching, a function that sounds insignificant, can bring a great improvement to the user experience for customers.
For example, we have a paging list. After entering a query condition, a certain piece of data is filtered out. After clicking on the details, it jumps to a new page. After closing the details, it returns to the page of the paging list. If the status of the previous query does not exist, the user needs to repeat the input. Query conditions, which not only consumes the patience of the user, but also increases unnecessary pressure on the server.
Therefore, cache control is very valuable in the system. We know that vue
there is keep-alive
components can make it easy for us to cache, so are we directly using the root component directly? keep-alive
package it up just fine?
In fact, this is inappropriate. For example, if I have a user list, open the details pages of Xiaoming and Xiaohong and cache them for him. Since the cache is written to the memory, the user will use the system for a long time, and the system will become more and more difficult. More cards. And data similar to the details page should be obtained from the interface every time it is opened to ensure that it is the latest data, and it is inappropriate to cache it. Then on-demand caching is what our system urgently needs to use. Fortunately, keep-alive
provides us with include
this api
Note that this include stores the name of the page, not the name of the route
Therefore, how to define the name of the page is critical
My approach is that the name value of the vue page is connected to the current level of menu.json
name
(actually processed is the full path name when the route is registered), refer to dynamic Introducing the import, this is done for two purposes:
- We know that the cache component of vue
keep-alive
include
option is based on pagename
to cache, we make routename
Thename
keep the same, so that once we change the route, we will save all the routesname
instore
, which is equivalent to saving thename
tostore
, it will be very convenient to do cache control. Of course, if the page does not need to be cached, it can be set totrue
f18cf4c2cd014d3bb5aea7692e4cd12e--- innoCache
menu.json
for this menu, which is also the origin of this field in our menu table structure. - When we develop, we usually install
vue-devtools
for debugging, and the semantic valuename
is convenient for debugging.
such as role management
Corresponding json location
Corresponding vue file
Corresponding vue-devtools
For a better user experience, we use tags in the system to record the status of the pages that the user clicked on before. In fact, this is also a hack
means, nothing more than to solve a pain point of the SPA
project.
renderings
The general idea is to monitor routing changes and store all routing-related information in store
. According to the noCache
field of the route, different small icons are displayed to tell the user whether the route is a route with cache.
Component encapsulation or secondary encapsulation based on UI library
The encapsulation principle of components is nothing more than reuse and extensibility.
When we initially packaged components, we didn’t need to pursue perfection, just to meet the basic business scenarios. In the follow-up, the components will be gradually improved according to the changes in demand.
If it is a large project with a multi-person team, it is recommended to use Jest
to cooperate with unit testing storybook
to generate component documentation.
Regarding the packaging skills of components, there are many detailed tutorials on the Internet. I have limited experience and will not discuss them here.
Create templates with plop
After the basic framework is built and the components are packaged, the rest is the code business function.
For the middle and background management system, most of the business part is inseparable from CRUD
, we see the screenshot above, similar to the user, role and other menus, the components are similar, the front-end part only needs to encapsulate the components (list, form, etc.) , pop-up boxes, etc.), pages can be generated directly through templates. Even now there are many visual configuration tools (low-code), I personally think that it is not suitable for professional front-end at present, because the components of the page in many scenarios are based on business encapsulation, and it is meaningless to simply move the native components of the UI library. Of course, if you have enough time, you can use node to develop low-code tools on your project.
Here we can cooperate with inquirer-directory to select the directory in the console
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) }
In general, after we define the interface of the restful specification with the backend, whenever there is a new business page, we have to do two things, one is to write the interface configuration, the other is to write the page, these two we can pass template to create. We use hbs
to create.
- api.hbs
import request from '../request'
{{#if create}}
// Create
export const create{{ properCase name }} = (data: any) => request.post('{{camelCase name}}/', data)
{{/if}}
{{#if delete}}
// Delete
export const remove{{ properCase name }} = (id: string) => request.delete(`{{camelCase name}}/${id}`)
{{/if}}
{{#if update}}
// Update
export const update{{ properCase name }} = (id: string, data: any) => request.put(`{{camelCase name}}/${id}`, data)
{{/if}}
{{#if get}}
// Retrieve
export const get{{ properCase name }} = (id: string) => request.get(`{{camelCase name}}/${id}`)
{{/if}}
{{#if check}}
// Check Unique
export const check{{ properCase name }} = (data: any) => request.post(`{{camelCase name}}/check`, data)
{{/if}}
{{#if fetchList}}
// List query
export const fetch{{ properCase name }}List = (params: any) => request.get('{{camelCase name}}/list', { params })
{{/if}}
{{#if fetchPage}}
// Page query
export const fetch{{ properCase name }}Page = (params: any) => request.get('{{camelCase name}}/page', { params })
{{/if}}
prompt.js
const { notEmpty } = require('../utils.js') const path = require('path') // 斜杠转驼峰 function toCamel(str) { return str.replace(/(.*)\/(\w)(.*)/g, function (_, $1, $2, $3) { return $1 + $2.toUpperCase() + $3 }) } // 选项框 const choices = ['create', 'update', 'get', 'delete', 'check', 'fetchList', 'fetchPage'].map((type) => ({ name: type, value: type, checked: true })) module.exports = { description: 'generate api template', prompts: [ { type: 'directory', name: 'from', message: 'Please select the file storage address', basePath: path.join(__dirname, '../../src/apis') }, { type: 'input', name: 'name', message: 'api name', validate: notEmpty('name') }, { type: 'checkbox', name: 'types', message: 'api types', choices } ], actions: (data) => { const { from, name, types } = data const actions = [ { type: 'add', path: path.join('src/apis', from, toCamel(name) + '.ts'), templateFile: 'template/apis/index.hbs', data: { name, create: types.includes('create'), update: types.includes('update'), get: types.includes('get'), check: types.includes('check'), delete: types.includes('delete'), fetchList: types.includes('fetchList'), fetchPage: types.includes('fetchPage') } } ] return actions } }
Let's execute plop
Through inquirer-directory
, we can easily select the system directory
Enter the name, which generally corresponds to the controller name of the backend
Use space to select each item, press enter to confirm
final generated file
The way to generate a page is similar to this, and I am just throwing some ideas here, I believe everyone can play it out.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。