likes
comments
collection
share

后台管理项目酱紫做?

作者站长头像
站长
· 阅读数 14

原作者在这,感谢这位大佬提供非常优秀的项目,这篇文章主要内容是我在做这个项目时遇到的坑和很多的收获,这里是我的github

思路梳理

目录结构:

|— dist
|— public
    |— favicon.ico
    |— index.html

|— src
    |— assets
    |— icons
    |— layout
    |— mock
    |— pages
    |— router
    |— store
    |— styles
    |— utils
    |— vendor
    |— .eslintrc.js
    |— App.vue
    |— main.js
    |— permission.js

|— package.json
|— vue.config.js
|— README.md

布局

大多数页面都是基于这个layout的,除了个别页面,比如: 404、login。

页面呈现是基于嵌套路由实现的,所以一般情况下,你增加或者修改页面只会影响app-main这个主体区域,而侧边栏和导航栏不会变化。

嵌套路由具体实现(以document为例):

{
  path:'/document',
  component: Layout,
  children: [
    {
      path: '/document',
      component: () => import('../pages/Document'),
      meta: {
        text: 'Document',
        className: 'iconfont icon-document_fill'
      }
    }
  ]
}

Layout.vuehtml结构如下:

<template>
  <div class="common-layout">
    <el-container>
      <el-aside>
        <span class="siderbarBox">
          <Sidebar/>
        </span>
      </el-aside>
      <el-container>
        <el-header style="backgroundColor:coral" height="50px">
          <Header/>
        </el-header>
        <el-main>
          <AppMain/>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

其中AppMain组件的html结构:

<template>
  <div class='container'>
    <!-- 指定路由的呈现位置 -->
    <router-view></router-view>
  </div>
</template>

侧边栏和路由

侧边栏和路由是绑定在一起的。

侧边栏

登录的时候获取到role,基于role动态生成路由表。再基于这个路由表,把需要展示到侧边栏的路由筛选出来,展示到侧边栏。

怎么筛选需要展示到侧边栏的路由?

那些不需要展示的路由我都配置了一个hidden属性,值为true。在utils/filyerRoutes.js文件里定义了一个筛选函数,这个函数会判断有没有hidden属性,如果没有hidden属性就将这个路由压入到新的路由数组里。 Sidebar组件拿到这个新生成的路由数组,遍历呈现到侧边栏。

路由

路由分为两种:constantRoutesasyncRoutes

  • constantRoutes:不需要判断权限的路由,如:登录页、404等。
  • asyncRoutes:需要判断权限,再通过addRoute动态添加的路由。

登录和权限验证(重要)

对于路由的管理和权限的验证都是基于vuex实现的。

登录

用户填完账号密码之后向服务器验证是否正确并获得身份(admin / editor),服务器基于role返回token。拿到token之后(将token贮存到cookie中,保证刷新页面后记住用户状态),前端会根据token向user_info接口抓取用户信息(role / avatar / introduction / ...)

权限验证

上面说的获取到用户的role,根据role算出其对应的路由权限,通过router.addRoute动态加载路由。

permission.js部分代码:

/**
 * 基于roles生成路由表 
 * accessRoutes 异步路由中有权限的部分路由
 */
const accessRoutes = await store.dispatch('generateRoutes', roles)

// 遍历accessRoutes动态添加路由
accessRoutes.forEach(route => {
  router.addRoute(route)
})

下面是我实操时遇到的那些“小惊喜”

零、小坑小点

echarts大小自适应

第一版:

// 图像大小自适应
window.addEventListener('resize', ()=>{
  myChart1.resize()
  myChart2.resize()
  myChart3.resize()
  myChart4.resize()
})

存在的问题:只有窗口大小改变的时候能自适应,container(app-main)容器大小变化的时候不能自适应。解决:使用element-resize-detector插件blog.csdn.net/Ag_wenbi/ar…

// 图像大小自适应
/**
 * 防抖
 * @param {*} event 
 * @param {*} wait 
 */
function antiShake(event, wait){
  let timer
  return function(){
    if(timer){
      console.log('清空定时器');
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      event()
    }, wait);
  }
}
// 监听元素大小变化
const elementResizeDetectorMaker = require("element-resize-detector");
const erd = elementResizeDetectorMaker()
erd.listenTo(container.value, antiShake(myChart1.resize, 300) )
erd.listenTo(container.value, antiShake(myChart2.resize, 300) )
erd.listenTo(container.value, antiShake(myChart3.resize, 300) )
erd.listenTo(container.value, antiShake(myChart4.resize, 300) )

echarts警告

后台管理项目酱紫做?

没解决,不知道啥情况

切换路由组件的时候echarts不再显示的问题

(echarts常用api:blog.csdn.net/Uncle_long/…)在组件销毁的时候释放echarts:myChart.clear():清空绘画内容,清空后实例可用,因为并非释放示例的资源,释放资源我们需要dispose()myChart.dispose():释放图表实例,释放后实例不再可用。

onBeforeUnmount(()=>{
  console.log('我要走啦');
  myChart1.clear()
  myChart1.dispose()
  myChart2.clear()
  myChart2.dispose()
  myChart3.clear()
  myChart3.dispose()
  myChart4.clear()
  myChart4.dispose()
})

vue3获取节点元素

  1. 在节点元素上做标记:<div class='container' ref="container">
  2. 在setup中做如下声明:**const container = ref(null)** // 拿到container节点
  3. **onMounted**钩子中就可以获取到该节点。
  4. return出去!

mock的小坑

在mock文件夹创建一个文件之后,要记得在main.js中引入!

import"./mock/table"  // 引入mockjs文件

全局引入scss变量

  • CSDN 具体步骤:
  1. 安装包:npm install sass-resources-loader --save-dev
  2. 配置vue.config.js:
// 全局scss配置
const globalSass = config => {
  const oneOfsMap = config.module.rule('scss').oneOfs.store
  oneOfsMap.forEach((item) => {
      item
          .use('sass-resources-loader')
          .loader('sass-resources-loader')
          .options({
              resources: './src/styles/common.scss'  //相对路径
          })
          .end()
  })
}

module.exports = defineConfig({
  chainWebpack(config) {
    //全局scss的配置
    globalSass(config),
    ...
  }
})

一、登录篇

设置默认路由

routes:[
  {
    path:'/',
    redirect: 'dashboard'
  },
  {...}
]

由dev-tool引起的一个bug

blog.csdn.net/Rich4st/art…

正则表达式判断字符串是否符合标准

regexp.test()。返回 true / false 。

cookie的使用

Lazy Loading Routes

来自官网:In general, it's a good idea to always use dynamic imports for all your routes. router.vuejs.org/guide/advan…

{
  path: '/dashboard',
  component: () => import('../pages/Dashboard'),
  meta: {
    text: 'Dashboard',
    className: 'iconfont icon-dashboard'
  }
}

报错:Cannot use 'in' operator to search for 'path' in undefined

路由里面:history: createWebHashHistory()引起的问题 blog.csdn.net/m0_67402026…

whiteList解决重定向死循环问题

permission.js

const whiteList = ['/login']
...
if(whiteList.indexOf(to.path)){// 如果要去白名单里的地址,直接放过
  next()
} else {
  next('/login')
}

奇奇怪怪的bug

我的代码:

<template>
    <router-view></router-view>
</template>
  
<script>
  import Login from './pages/Login/index.vue'

  export default {
    name:'App',
    components:{ Login }
  }
</script>

然后就会报错,如下:后台管理项目酱紫做?但是像下面这样写就没问题:

<template>
    <!-- <Login/> -->
    <router-view></router-view>
</template>
  
<script>
  export default {
    name:'App'
  }
</script>

所以为什么不能import这个路由组件捏?

vuex和路由跳转的顺序

在登录的时候,点击登录按钮之后要做两件事:1、通知vuex更新数据并埋下token 2、路由跳转。我想要的是先获取token再路由跳转,但是实际效果是先跳转再获取token。我模仿的项目是用vue2写的,我仔细看了他的逻辑,并没有刻意去解决这个问题,就好像他没有遇到这个问题。我用的是vue3,所以我在想会不会是vue版本的问题。

我的代码:(先跳转后去获取token)

function login(){
// console.log('点击了登录');
// 用户名和密码不能为空
if(!unameCheckValue.value && uname.value.trim() !== '' && !pwdCheckValue.value && pwd.value.trim() !== ''){
  // 将用户名和密码打包成一个对象
  const userinfo = {
    username: uname.value,
    password: pwd.value
  }
  // 把数据交给vuex操作
  store.dispatch('loginByUsername', userinfo)
    .then(()=>{ // 成功之后定向到 /layout 
      console.log('登录成功!!!');
      router.replace({path: '/'})
    })
    .catch(err => {
      console.log(err) // 登录失败提示错误信息
    })
  }
}

我的暂时解决方案:设置定时器

// 设定时器是为了vuex的操作先执行完(获取token之后)再路由跳转
setTimeout(() => {
  console.log('登录成功');
  router.replace({path: '/'})
}, 100);

router.addRoute()踩坑

之前的写法:

// accessRoutes是要动态添加的路由
const accessRoutes = await store.dispatch('generateRoutes', roles)
// 动态添加路由
router.addRoute(accessRoutes)

我之前没了解过router.addRoute()方法,所以看别人(vue2版本)写的router.addRoutes()里面可以跟一个数组,我也就跟了一个数组。但是这个方法在vue3里变动了,只能一个个添加。来看正确写法:

// accessRoutes是要动态添加的路由
const accessRoutes = await store.dispatch('generateRoutes', roles)
// 遍历accessRoutes动态添加路由
accessRoutes.forEach(route => {
  router.addRoute(route)
})

匹配路由的404

vue3中对404的捕捉细节

路由的展示位置

这样写直接展示在App中了:

{
  path:'/permission',
  component:() => import('../pages/Permission'),
  meta: {
    roles: ['admin', 'editor'],
    text: 'Permission',
    className: 'iconfont icon-permission'
  }
},
{
  path:'/todo',
  component:() => import('../pages/Todo'),
  meta: {
    roles: ['admin'],
    text: 'Todo',
    className: 'iconfont icon-todo'
  }
}

这样写才是展示在layout中:

{
  path:'/permission',
  component: Layout,
  children: [
    {
      path:'/permission',
      component:() => import('../pages/Permission'),
      meta: {
        roles: ['admin', 'editor'],
        text: 'Permission',
        className: 'iconfont icon-permission'
      }
    }
  ]
},
{
  path:'/todo',
  component: Layout,
  children: [
    {
      path:'/todo',
      component:() => import('../pages/Todo'),
      meta: {
        roles: ['admin'],
        text: 'Todo',
        className: 'iconfont icon-todo'
      }
    }
  ]
},
  ...

addRoute 页面刷新后路由失效

next() => next({ ...to, replace:true })CSDN链接作者的原话:

这里还有一个小hack的地方,就是router.addRoutes之后的next()可能会失效,因为可能next()的时候路由并没有完全add完成,好在查阅文档发现: next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started. 这样我们就可以简单的通过next(to)巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach这个钩子,这时候再通过next()来释放钩子,就能确保所有的路由都已经挂载完成了。

登出 Logout

  1. 更新store中的token和role为空,删除浏览器的token
logout({ commit }){
  commit('SET_ROLES', [])
  commit('SET_TOKEN', '')
  removeToken()
}
  1. 刷新页面,自动跳转到 /login

这里不能用路由跳转,使用路由跳转的话先登editor是正常的,但是再登admin的时候生成的路由和editor一样,不会改变。

二、实战篇

Table 拖拽排序

SortableJS

使用 **SortableJS**帮助我们实现拖拽效果。github.com/SortableJS/…一个小坑:sortable元素的样式单独写在<style></style>才有用:

<style>
  .sortable-ghost{
    background-color: cadetblue !important;
    color: #fff;
  }
</style>

写在<style lang="scss"></style>中没用。

排序逻辑

// 实现表格拖拽效果
onMounted(()=>{
  let el = document.querySelector('tbody')
  let sortable =  new Sortable(el, {
    ghostClass: "sortable-ghost", 
    onEnd: (e) => {
      // 将旧位置的值挪到新位置
      const oldValue = newList.splice(e.oldIndex, 1)[0]
      newList.splice(e.newIndex, 0, oldValue)
    }
  })
})

Table 内联编辑

待完成

tinyMCE 富文本

注意

路由缓存

404

Markdown

一开始使用simplemde,但是它好像只能适配vue2.0,所以我又去找markdown编辑器,并找到了v-md-editorv-md-editor的使用

Markdown to HTML

使用:

<div class="output" v-html="compileMarkDown(text)"></div>
...
let text = ref('# Hello markdown!')
let showdown  = require('showdown'),
    converter = new showdown.Converter();
function compileMarkDown(md){
  return converter.makeHtml(md)
}

注意

路由缓存

Excel

...

三、项目优化

分析包大小

  • 如何生成打包分析文件

package.json的scripts里配置: "build-report": "vue-cli-service build --report",然后运行npm run build-report

路由懒加载

{
  path: '/login',
  component: () => import('../pages/Login'),
  hidden: true
}

添加防抖节流

  • resize添加防抖

静态资源使用CDN

<!-- echarts -->
<script src="https://unpkg.com/echarts@5.3.2/dist/echarts.js"></script>
<!-- showdown -->
<script src="https://unpkg.com/showdown/dist/showdown.min.js"></script>
<!-- tinyMCE -->
<script 
  src="https://cdn.tiny.cloud/1/jafg0w4pf4yrzk9db68y63tgysskrcsvhzz5a05v7swqktad/tinymce/6/tinymce.min.js" 
  referrerpolicy="origin">
</script>
<!-- sortablejs -->
<script src="https://unpkg.com/sortablejs@1.15.0/Sortable.min.js"></script>
...

将CSS 放在文件头部,JS 放在底部

  • CSS 执行会阻塞渲染,阻止 JS 执行
  • JS 加载和执行会阻塞 HTML 解析,阻止 CSSOM 构建

如果这些 CSS、JS 标签放在 HEAD 标签里,并且需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部(不阻止 DOM 解析,但会阻塞渲染),等 HTML 解析完了再加载 JS 文件,尽早向用户呈现页面的内容。那为什么 CSS 文件还要放在头部呢?

因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。

gzip压缩

好像是需要前端和服务器端配合使用

查看是否开启gzip:www.jianshu.com/p/b24e90619…

  1. 下载插件
npm install compression-webpack-plugin --save-dev
npm install compression
  1. webpack配置
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}
  1. node配置
const compression = require('compression')
// 在其他中间件前使用
app.use(compression())

第三方库按需加载

  • echarts
  • element-plus

尝试下element-plus自动导入

我的报错了。。。

webpack排除打包

排除了element-plus

打包去除console.log

加载动画

将来会持续更新。。。