29
头图

The overall architecture scheme of the general background management system (Vue)

Project creation, choice of scaffolding (vite or vue-cli)

  • vue-cli Based on webpack 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 special webpack工程师 for configuration. In addition, because webpack needs to be packaged and compiled in the development environment, the development experience is actually not as good as vite .
  • vite development mode is based on esbuild , and the package is rollup . Fast 冷启动 and seamless hmr get a huge experience improvement in development mode. The disadvantage is that the scaffolding has just started, and it is not as ecologically as webpack .

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
image.png

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.

image.png

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 of OOCSS (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 of element-ui

image.png


image.png

The development or packaging of our project components is used uniformly BEM

  • ACSS
    Anyone who knows tailwind 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

image.png

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

    image.png

    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
get_resource.png

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
image.png

If you think that the routes of all pages are written in one page is too long and difficult to maintain, you can replace json with js and use the import 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 using md5 to name to generate the primary key id into the database

We try to execute this js with node

 node createMenu.js

image.png

image.png

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+ import dynamic 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
image.png

: 3.5.0API has also been updated to addRoute, pay attention to distinguish the version changes

vue-router4x
image.png

Personally, I prefer to use the vue-router4x addRoute method of ---8eaa286f393e2781a12f4e94bd582832---, which can control the positioning of each route more finely
image.png

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
image.png

Character binding menu
image.png

menu
image.png

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

image.png

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 page name to cache, we make route name The name keep the same, so that once we change the route, we will save all the routes name in store , which is equivalent to saving the name to store , it will be very convenient to do cache control. Of course, if the page does not need to be cached, it can be set to true f18cf4c2cd014d3bb5aea7692e4cd12e--- in noCache 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 value name is convenient for debugging.

such as role management

Corresponding json location
image.png

Corresponding vue file
image.png

Corresponding vue-devtools
image.png

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
image.png

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
image.png

Enter the name, which generally corresponds to the controller name of the backend
image.png

Use space to select each item, press enter to confirm
image.png

final generated file
image.png

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.
project address

levi-vue-admin

Personal WX: sky124380729

sky124380729
322 声望36 粉丝

前端小白