Vue 动态路由接口数据结构化为符合VueRouter的声明结构及菜单导航结构、动态路由懒加载方法

Vue 动态路由接口数据结构化为符合VueRouter的声明结构及菜单导航结构、动态路由懒加载方法

实现目标

  • 项目打包代码实现按需分割
  • 路由懒加载按需打包,排除引入子组件的冗余打包(仅处理打包冗余现象,不影响生产部署)
  • 解决路由懒加载 import 方法内引入变量报错问题

可能碰到的问题

1.ESLint: Cannot read properties of null (reading 'range') Occurred while linting
2.eslint 语法分析报错:Syntax Error: TypeError: Cannot read property 'value' of null.
// import 方法内不可直接使用模板字符串 
return () => import(`@/views/${view}`).catch(() => import('@/views/error/notfound'))
3.动态路由按需加载-Cannot find module
4.不同系统环境代码分包路径匹配问题(路径分隔符不兼容)

个人最终解决方法

1.开发环境(本人实测)
  • 系统环境:Windows 11、Linux、MacOS
  • node 版本:v14.21.2
  • npm 版本:6.14.17
  • vue:@vue/cli 5.0.8
  • webpack:6.14.17
  • 项目依赖 -
{
  "name": "v1",
  "version": "1.0.0",
  "description": "xxx",
  "author": "xx <xx@gmail.com>",
  "scripts": {
    "dev": "vue-cli-service serve",
    "build:prod": "vue-cli-service build",
    "build:stage": "vue-cli-service build --mode staging",
    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
    "lint": "eslint --ext .js,.vue src"
  },
  "dependencies": {
    "@riophae/vue-treeselect": "^0.4.0",
    "axios": "^0.21.1",
    "core-js": "^3.27.2",
    "echarts": "^5.4.0",
    "echarts-wordcloud": "^2.1.0",
    "element-ui": "^2.15.10",
    "js-cookie": "2.2.0",
    "lodash.merge": "^4.6.2",
    "monotone-chain-convex-hull": "^1.1.0",
    "normalize.css": "7.0.0",
    "nprogress": "0.2.0",
    "ol": "^6.14.1",
    "ol-ext": "^4.0.4",
    "path-to-regexp": "2.4.0",
    "screenfull": "^5.2.0",
    "swiper": "^5.4.5",
    "vue": "^2.7.13",
    "vue-awesome-swiper": "^4.1.1",
    "vue-cropper": "^0.5.8",
    "vue-router": "^3.6.5",
    "vuex": "^3.6.2"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "4.4.6",
    "@vue/cli-plugin-eslint": "4.4.6",
    "@vue/cli-service": "4.4.6",
    "babel-eslint": "10.1.0",
    "chalk": "4.1.0",
    "eslint": "7.15.0",
    "eslint-plugin-vue": "7.2.0",
    "sass": "1.32.13",
    "sass-loader": "10.1.1",
    "script-ext-html-webpack-plugin": "2.1.5",
    "svg-sprite-loader": "5.1.1",
    "vue-template-compiler": "2.6.12",
    "autoprefixer": "9.5.1",
    "sass-resources-loader": "^2.1.1",
    "serve-static": "1.13.2",
    "svgo": "1.2.2",
    "worker-loader": "^3.0.8"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ],
  "engines": {
    "node": ">=8.9",
    "npm": ">= 3.0.0"
  },
  "license": "MIT"
}

  • Babel 完整配置
module.exports = {
  presets: [
    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
    '@vue/cli-plugin-babel/preset'
    // https://blog.csdn.net/jayccx/article/details/128200440
    // ['@vue/cli-plugin-babel/preset', { 'exclude': ['proposal-dynamic-import'] }]
  ]
  // 'env': {
  // 'development': {
  //   // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
  //   // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
  //   'plugins': ['dynamic-import-node']
  // }
  // 'production': {
  //   // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
  //   // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
  //   'plugins': ['dynamic-import-node']
  // }
  // }
}

  • ESLint完整配置.eslintrc.js
module.exports = {
  root: true,
  parserOptions: {
    parser: 'babel-eslint',
    sourceType: 'module'
  },
  env: {
    browser: true,
    node: true,
    es6: true
  },
  extends: ['plugin:vue/recommended', 'eslint:recommended'],

  // add your custom rules here
  // it is base on https://github.com/vuejs/eslint-config-vue
  rules: {
    'vue/html-closing-bracket-newline': 'off',
    'vue/require-default-prop': 'off',
    'vue/html-indent': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    'vue/component-definition-name-casing': ['error', 'PascalCase'],
    'vue/no-v-html': 'off',
    'accessor-pairs': 2,
    'arrow-spacing': [2, {
      'before': true,
      'after': true
    }],
    'block-spacing': [2, 'always'],
    'brace-style': [2, '1tbs', {
      'allowSingleLine': true
    }],
    'camelcase': [0, {
      'properties': 'always'
    }],
    'comma-dangle': [2, 'never'],
    'comma-spacing': [2, {
      'before': false,
      'after': true
    }],
    'comma-style': [2, 'last'],
    'constructor-super': 2,
    'curly': [2, 'multi-line'],
    'dot-location': [2, 'property'],
    'eol-last': 2,
    'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
    'generator-star-spacing': [2, {
      'before': true,
      'after': true
    }],
    'space-before-function-paren': ['error', {
      'anonymous': 'always',
      'named': 'ignore',
      'asyncArrow': 'always'
    }],
    'handle-callback-err': 'off',
    'jsx-quotes': [2, 'prefer-single'],
    'key-spacing': [2, {
      'beforeColon': false,
      'afterColon': true
    }],
    'keyword-spacing': [2, {
      'before': true,
      'after': true
    }],
    'new-cap': [2, {
      'newIsCap': true,
      'capIsNew': false
    }],
    'new-parens': 2,
    'no-array-constructor': 2,
    'no-caller': 2,
    'no-console': 'off',
    'no-class-assign': 2,
    'no-cond-assign': 2,
    'no-const-assign': 2,
    'no-control-regex': 0,
    'no-delete-var': 2,
    'no-dupe-args': 2,
    'no-dupe-class-members': 2,
    'no-dupe-keys': 2,
    'no-duplicate-case': 2,
    'no-empty-character-class': 2,
    'no-empty-pattern': 2,
    'no-eval': 2,
    'no-ex-assign': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-extra-boolean-cast': 2,
    'no-extra-parens': [2, 'functions'],
    'no-fallthrough': 2,
    'no-floating-decimal': 2,
    'no-func-assign': 2,
    'no-implied-eval': 2,
    'no-inner-declarations': [2, 'functions'],
    'no-invalid-regexp': 2,
    'no-irregular-whitespace': 2,
    'no-iterator': 2,
    'no-label-var': 2,
    'no-labels': [2, {
      'allowLoop': false,
      'allowSwitch': false
    }],
    'no-lone-blocks': 2,
    'no-mixed-spaces-and-tabs': 2,
    'no-multi-spaces': 2,
    'no-multi-str': 2,
    'no-multiple-empty-lines': [2, {
      'max': 1
    }],
    'no-native-reassign': 2,
    'no-negated-in-lhs': 2,
    'no-new-object': 2,
    'no-new-require': 2,
    'no-new-symbol': 2,
    'no-new-wrappers': 2,
    'no-obj-calls': 2,
    'no-octal': 2,
    'no-octal-escape': 2,
    'no-path-concat': 2,
    'no-proto': 2,
    'no-redeclare': 2,
    'no-regex-spaces': 2,
    'no-return-assign': [2, 'except-parens'],
    'no-self-assign': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-spaced-func': 2,
    'no-sparse-arrays': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef': 2,
    'no-undef-init': 2,
    'no-unexpected-multiline': 2,
    'no-unmodified-loop-condition': 2,
    'no-unneeded-ternary': [2, {
      'defaultAssignment': false
    }],
    'no-unreachable': 2,
    'no-unsafe-finally': 2,
    'no-unused-vars': [2, {
      'vars': 'all',
      'args': 'none'
    }],
    'no-useless-call': 2,
    'no-useless-computed-key': 2,
    'no-useless-constructor': 2,
    'no-useless-escape': 0,
    'no-whitespace-before-property': 2,
    'no-with': 2,
    'one-var': [2, {
      'initialized': 'never'
    }],
    'operator-linebreak': [2, 'after', {
      'overrides': {
        '?': 'before',
        ':': 'before'
      }
    }],
    'padded-blocks': [2, 'never'],
    'quotes': [2, 'single', {
      'avoidEscape': true,
      'allowTemplateLiterals': true
    }],
    'semi': [2, 'never'],
    'semi-spacing': [2, {
      'before': false,
      'after': true
    }],
    'space-before-blocks': [2, 'always'],
    'space-in-parens': [2, 'never'],
    'space-infix-ops': 2,
    'space-unary-ops': [2, {
      'words': true,
      'nonwords': false
    }],
    'spaced-comment': [2, 'always', {
      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
    }],
    'template-curly-spacing': [2, 'never'],
    'use-isnan': 2,
    'valid-typeof': 2,
    'wrap-iife': [2, 'any'],
    'yield-star-spacing': [2, 'both'],
    'yoda': [2, 'never'],
    'prefer-const': 2,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    'object-curly-spacing': [2, 'always', {
      objectsInObjects: true
    }],
    'array-bracket-spacing': [2, 'never']
  }
}

2.项目中接口数据生成路由结构及菜单结构(重点关注以下代码中的loadView函数)
import { constantRoutes } from '@/router'
import { getRouters } from '@/api/system/menu'
import pathToRegexp from 'path-to-regexp'
import Layout from '@/layout/index'
const route = {
  state: {
    routes: [],
    addRoutes: [],
    allSidebarRouters: [],
    sidebarRouters: []
  },
  mutations: {
    SET_ROUTES: (state, routes) => {
      state.addRoutes = routes
      state.routes = constantRoutes.concat(routes)
    },
    SET_ALL_SIDEBAR_ROUTERS: (state, routers) => {
      state.allSidebarRouters = routers
    },
    SET_SIDEBAR_ROUTERS: (state, routers) => {
      state.sidebarRouters = routers
    }
  },
  actions: {
    // 生成路由
    GenerateRoutes({ commit }) {
      return new Promise(resolve => {
        // 向后端请求路由数据
        getRouters().then(res => {
          const sdata = JSON.parse(JSON.stringify(res.data))
          const rdata = JSON.parse(JSON.stringify(res.data))
          /* 符合菜单的数据结构 */
          const allSidebarRoutes = filterAsyncRouter(sdata)
          /* 符合路由的数据结构 */
          const rewriteRoutes = filterAsyncRouter(rdata, true)
          /* 路由通配符 */
          rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
          commit('SET_ROUTES', rewriteRoutes)
          commit('SET_ALL_SIDEBAR_ROUTERS', allSidebarRoutes)
          // commit('SET_SIDEBAR_ROUTERS', allSidebarRoutes[0].children)
          resolve(rewriteRoutes)
        })
      })
    },
    /* 切换菜单 */
    SwitchSiderBar({ commit }, routes) {
      commit('SET_SIDEBAR_ROUTERS', routes)
    }
  }
}

/* 匹配参数 */
const regParams = /\;[^\/]*/g
/* 匹配值 /user/:id;1 */
const regValue = /(?:\:)[^;]*(?:;)/g
/* 遍历后台传来的路由字符串,转换为组件对象(一级目录及一级菜单后端数据则自动添加根/路径) */
function filterAsyncRouter(asyncRouterMap, isRewrite = false/* 是否生成为路由标准 */, parentRoute) {
  return asyncRouterMap.filter(route => {
    if (parentRoute) {
      route.path = parentRoute.path + route.path
    }
    if (isRewrite) {
      route.path = route.path.replace(regParams, '')
    } else {
      route.path = route.path.replace(regValue, '')
      route._regex = pathToRegexp(route.path, pathToRegexp.parse(route.path), {
        sensitive: true,
        strict: true
      })
    }
    if (isRewrite && route.children) {
      route.children = filterChildren(route.children)
    }
    if (route.component && route.component !== 'ParentView') {
      if (route.component === 'Layout') {
        route.component = Layout
      } else {
        /* 记录源代码位置 */
        route.meta && (route.meta.src = route.component)
        route.component = loadView(route.component)
      }
    }
    if (route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children, isRewrite, route)
    }
    return true
  })
}
/* 递归扁平化路由结构 */
function filterChildren(childrenMap, parentRoute) {
  var children = []
  var hasRoute = {}
  childrenMap.forEach((el, index) => {
    el.path = el.path.replace(regParams, '')
    if (parentRoute) {
      el.path = parentRoute.path
    }
    /* 当存在子路由时,将子路由添加到定义 */
    if (el.children && el.children.length) {
      /* ParentView 的处理使系统多级菜单的展现出现在Layout组件下成为可能 */
      let childs = []
      el.children.forEach(c => {
        c.path = el.path + c.path.replace(regParams, '')
        if (c.children && c.children.length) {
          childs = childs.concat(filterChildren(c.children, c))
          return
        }
        if (hasRoute[c.path]) return
        hasRoute[c.path] = true
        childs.push(c)
      })
      /* 父级路由明确为目录时(ParentView),不再将父路由加入到路由定义中 */
      if (el.component === 'ParentView') {
        children = children.concat(childs)
      } else {
        /* 否则将父路由作为嵌套路由加入到路由定义中 */
        el.children = childs
        children = children.concat(el)
      }
      return
      /* 父级路由明确为目录时(ParentView),不存在子路由,则直接将路由组件设置为notfound组件 */
    } else if (el.component === 'ParentView') {
      el.component = 'error/notfound'
    }
    if (hasRoute[el.path]) return
    hasRoute[el.path] = true
    children = children.concat(el)
  })
  return children
}

/* 路由懒加载失败时重置为notfound页面 */
export const loadView = (view) => {
  if (process.env.NODE_ENV === 'development') {
    return (resolve) => {
      require([`@/views/${view}`], resolve, err => {
        require([`@/views/error/notfound`], resolve)
        console.log(err)
      })
    }
  } else {
    /**
     * 使用 import 实现生产环境的路由懒加载
     * !注意:import 方法内不可直接使用模板字符串 ,eslint 语法分析报错:Syntax Error: TypeError: Cannot read property 'value' of null。
     * !注意:正则匹配时应注意不同系统的路径分隔符区别,例如,Linux、MacOS 系统的路径分隔符为 "/",Windows 系统的路径分隔符为 "\"
     *        因此正则中路径分隔符的表达式,应匹配以上两种情况 [\/\\]。
     */
    return () => import(/* webpackChunkName: "[request]",webpackInclude: /.+[\/\\][a-z0-9\-]+.vue$/ */'@/views/' + view).catch(() => import('@/views/error/notfound'))
  }
}

export default route

参考文档

VueRouter 路由懒加载
Webpack import
Ruoyi-Vue issue
Ruoyi-Vue 路由逻辑

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值