Vueはユーザーログイン検証+権限検証+動的ルーティングを実装します(左側のメニューバー)

一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください

9月-07-202107-37-13.gif

1.テクノロジースタックの説明

vue2.6+ vue-router+ vuex+element-ui

2.開始:新しいプロジェクトを作成します

前提条件:パーソナルコンピュータnodejs(私のもの)にインストールした後、組み込みのインストール(私のもの14.15.1を使用しますnodejsnpm包管理器vue@vue/cli 4.5.12

  1. コマンドラインで次のコマンドを使用して、指定したディレクトリに足場をインストールしますvue-cli
npm install -g @vue/cli
复制代码
  1. vueのcreateprojectコマンドvuecreatexxxを使用します(xxxはプロジェクト名を指します)
  • プロジェクトに必要なプラグインを選択します
? Check the features needed for your project: 
 ◉ Choose Vue version			// 选择vue版本
 ◉ Babel				// 支持babel
 ◯ TypeScript				// 支持使用 TypeScript 书写源码
 ◯ Progressive Web App (PWA) Support	// PWA 支持
 ◉ Router				// 支持 vue-router
 ◉ Vuex					// 支持 vuex
 ◯ CSS Pre-processors			// 支持 CSS 预处理器。
 ◉ Linter / Formatter			// 支持代码风格检查和格式化。
 ◯ Unit Testing				// 支持单元测试。
 ◯ E2E Testing				// 支持 E2E 测试。
 
 // 注意:你要集成什么就选就行了(注:空格键是选中与取消,A键是全选)
复制代码
  • vueバージョンを選択します。vue3は約8か月しかリリースされていないため、対象者は広くありません。vue2.xバージョンを選択してください。

image.png

この時点で、プロジェクトの構築が完了しました。cdを実行してプロジェクトを開き、実行できます。

3. element-uiとnprogressおよびnormalize.cssを追加し、vue.config.jsを構成します

  • element-ui最初にインストールしnprogressnormalize.css
npm install element-ui nprogress normalize.css
复制代码

element-uiはsass-loaderを使用しているため、ここにもインストールする必要があります

npm install sass-loader
复制代码

現在のプロジェクトプラグインは次のとおりです。

image.png

  • vue.config.jsを構成します

vue.config.jsは、プロジェクトの(package.jsonと同じレベルの)ルートディレクトリに存在する場合、@ vue/cli-serviceによって自動的にロードされるオプションの構成ファイルです。package.jsonのvueフィールドを使用することもできますが、この記述方法では、JSON形式に厳密に従う必要があることに注意してください。

ルートディレクトリに vue.config.jsを作成します

image.png

公式設定vue.config.jsの詳細な説明

'use strict'
const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
  publicPath: '/',              // 部署应用包时的基本 URL,用法和 webpack 本身的 output.publicPath 一致
  outputDir: 'dist',            // 构建输出目录(打包位置)
  assetsDir: 'static',          // 放置生成的静态资源(js,css,img,fonts)的(相对于outputDir)的目录
  lintOnSave: false,            // 是否校验语法
  productionSourceMap: false,   // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建
  devServer: {
    port: 8888,
    open: true,
  },
  configureWebpack: {           // 绝对路径
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  }
}
复制代码

3.機能の実現

霧の中のコードを見ないように、誰もが一般的な印象を持っているように、最初にアイデアについて話しましょう。

次の手順を実行します。

  1. 前端在本地写好路由表,以及每个路由对应的角色,也就是哪些角色可以看到这个菜单 / 路由。
  2. 登录的时候,向后端请求得到登录用户的角色(管理者,普通用户)
  3. 利用路由守卫者(router.beforeEach),根据取到的用户角色,跟本地的路由表进行对比,过滤出用户对应的路由,并利用路由进行菜单渲染
  1. 我们将储存在将storage中的token作为用户是否登录的标志,如果当前storage中有token,表明当前系统已被登录
  2. 将系统所有页面分为两类,需要登录才能查看的页面,不需要登录的login.vue, register.vue
  3. 前端每次跳转路由时,做以下判断:

image.png

接下来从技术栈的角度补充几点:

  1. vue-routerbeforeEach方法中实现以上逻辑,判断前端跳转去向;
  2. 出于教程考虑,不引入后端,用模拟数据的用户信息作为拦截axios发起的服务请求响应;
  3. 通过window.localStorage.setItemuserInfo的状态管理;

4. 实现

根据上述的步骤,我们进行每一个步骤的实现

1. 写好mock数据,用以模拟后端返回的数据源

dynamicUser里面就是模拟的后端数据,一般的后台数据库里面,就是分为一个user用户表,一个role权限路由表,这里不涉及后端,所以只给出最后后端输出的数据源。

一个完整的后端数据示例如下:

const dynamicUser = [
    {
        name: "管理员",
        avatar: "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/ccb565eca95535ab2caac9f6129b8b7a~300x300.image",
        desc: "管理员 - admin",
        username: "admin",
        password: "654321",
        token: "rtVrM4PhiFK8PNopqWuSjsc1n02oKc3f",
        routes: [
            { id: 1, name: "/", path: "/", component: "Layout", redirect: "/index", hidden: false, children: [
                { name: "index", path: "/index", meta: { title: "index" }, component: "index/index" },
            ]},
            { id: 2, name: "/form", path: "/form", component: "Layout", redirect: "/form/index", hidden: false, children: [
                { name: "/form/index", path: "/form/index", meta: { title: "form" }, component: "form/index" }
            ]},
            { id: 3, name: "/example", path: "/example", component: "Layout", redirect: "/example/tree", meta: { title: "example" }, hidden: false, children: [
                { name: "/tree", path: "/example/tree", meta: { title: "tree" }, component: "tree/index" },
                { name: "/copy", path: "/example/copy", meta: { title: "copy" }, component: "tree/copy" }
            ] },
            { id: 4, name: "/table", path: "/table", component: "Layout", redirect: "/table/index", hidden: false, children: [
                { name: "/table/index", path: "/table/index", meta: { title: "table" }, component: "table/index" }
            ] },
            { id: 5, name: "/admin", path: "/admin", component: "Layout", redirect: "/admin/index", hidden: false, children: [
                { name: "/admin/index", path: "/admin/index", meta: { title: "admin" }, component: "admin/index" }
            ] },
            { id: 6, name: "/people", path: "/people", component: "Layout", redirect: "/people/index", hidden: false, children: [
                { name: "/people/index", path: "/people/index", meta: { title: "people" }, component: "people/index" }
            ] }
        ]
    },
    {
        name: "普通用户",
        avatar: "https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/6364348965908f03e6a2dd188816e927~300x300.image",
        desc: "普通用户 - people",
        username: "people",
        password: "123456",
        token: "4es8eyDwznXrCX3b3439EmTFnIkrBYWh",
        routes: [
            { id: 1, name: "/", path: "/", component: "Layout", redirect: "/index", hidden: false, children: [
                { name: "index", path: "/index", meta: { title: "index" }, component: "index/index" },
            ]},
            { id: 2, name: "/form", path: "/form", component: "Layout", redirect: "/form/index", hidden: false, children: [
                { name: "/form/index", path: "/form/index", meta: { title: "form" }, component: "form/index" }
            ]},
            { id: 3, name: "/example", path: "/example", component: "Layout", redirect: "/example/tree", meta: { title: "example" }, hidden: false, children: [
                { name: "/tree", path: "/example/tree", meta: { title: "tree" }, component: "tree/index" },
                { name: "/copy", path: "/example/copy", meta: { title: "copy" }, component: "tree/copy" }
            ] },
            { id: 4, name: "/table", path: "/table", component: "Layout", redirect: "/table/index", hidden: false, children: [
                { name: "/table/index", path: "/table/index", meta: { title: "table" }, component: "table/index" }
            ] },
            { id: 6, name: "/people", path: "/people", component: "Layout", redirect: "/people/index", hidden: false, children: [
                { name: "/people/index", path: "/people/index", meta: { title: "people" }, component: "people/index" }
            ] }
        ]
    }
]

export default dynamicUser
复制代码

由此可以看出,一般登录后,返回的数据里,包含了一个用户的姓名,头像,简述以及tokenusername和password只是用以模拟登录用到的数据,在正常业务流中,后端不可能带出来的。),routes就是admin管理员people普通用户的差异化动态路由了,admin多了一个admin的页面,而people是没有的。

其实这里是有多种思路的
有些开发者喜欢完整的静态路由都在前端里面,然后根据routermeta属性,写上对应userrole,登录的时候,再根据后端返回的权限,去过滤比对权限,把该用户角色所对应的路由处理好,渲染处理,这也是主流的一种处理方式。这种就等于是把所有的路由和权限业务处理都放在了前端,一旦上线发布后,想要修改就需要重新打包处理,而且不能经由后台动态新增删除

例如:

//代码位置:router/index.js
 {
    path: '',
    component: layout, //整体页面的布局(包含左侧菜单跟主内容区域)
    children: [{
      path: 'main',
      component: main,
      meta: {
        title: '首页', //菜单名称
        roles: ['user', 'admin'], //当前菜单哪些角色可以看到
      }
    }]
  }
复制代码

还有一种解法,就是所有的路由权限等,都交给后端,后端根据前端的账号密码,去获取角色权限,处理路由,丢出就是已经匹配对应角色的路由了。这种写法前端运算量不会太大,而且易于修改和后期维护以及动态的增删改查,本文就是以该种形式实现。

2. 模拟用户登录,获取用户的权限和路由

  1. main.js里面,引入该页面,用于做路由守卫者
import Vue from "vue"
import App from "./App.vue"
import router from "./router"
import store from "./store"
import ElementUI from "element-ui"
import 'element-ui/lib/theme-chalk/index.css'
import "./router/router-config"  // 路由守卫,做动态路由的地方

Vue.config.productionTip = false
Vue.use(ElementUI)

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app")
复制代码
  1. 登录

本来我是写了mock数据,模拟用户登录,请求后端角色的接口,奈何mock挂了,
所以我就直接模拟了:

取到用户角色,存放进localStorage,然后跳转主页

  • 在这里,由于用了element-ui的form表单提交,所以直接this.$refs.userForm.validate

element-ui的form表单提交文档!

  • 这里的dynamicUser是mock的数据流,一般后端是直接直接返回对应的结果,可由于fastmock容易挂掉,所以就直接手写mock了。

定义flag用于做登录校验,如果循环都找不到对应的usernamepassword的话,就告诉用户,该账号密码错误,登录失败..可如果有一次是成功的,那么flag就是为!0的,并且返回对应的用户信息,用户路由等。。最后还会进行路由的跳转初始化页面(首页),并进行动态路由加载和路由跳转。

import dynamicUser from "../../mock"
import { Message } from "element-ui"

login() {
    this.$refs.userForm.validate(( valid ) => {
        if(valid) {
            let flag = !1
            window.localStorage.removeItem("userInfo")
            dynamicUser.forEach(item => {
                if(item["username"] == this.user['username'] && item["password"] == this.user['password']) {
                    flag = !0
                    Message({ type: 'success', message: "登录成功", showClose: true, duration: 3000 })
                    window.localStorage.setItem("userInfo", JSON.stringify(item))
                    // 这里用catch捕获错误,而且不打印,解释在下方
                    this.$router.replace({ path: "/" }).catch(() => {})
                }
            })
            if(!flag) Message({ type: 'warning', message: "账号密码错误,请重试!", showClose: true, duration: 3000 })
        } else return false
    })
}
复制代码

image.png

解释:如果不捕获catch错误,而且不打印的话,就会出现如图所示的错误。
原因:vue-router路由版本更新产生的问题,导致路由跳转失败抛出该错误,但并不影响程序功能

image.png

  • 解决方案1:

在使用编程式导航跳转时,每次使用,后面都跟上.catch方法,捕获错误信息this.$router.push("/xxx").catch(() => {})

  • 解决方法2:

全局解决,替换路由的Push和replace方法,放在src/router/index.js中:

const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location, onResolve, onReject) {
    if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
    return originalPush.call(this, location).catch(err => err)
}
复制代码
  • 解决方案3:

对vue-router的版本降低到3.0.7,手动修改,然后删除node_modules,改完再npm install

image.png

修改为:

image.png

3. (重点)路由守卫者拦截beforeach, 并动态渲染出路由表

1. 在router文件夹下,创建router.config.js文件,用于做路由守卫者的拦截页面

image.png

2. 引入routerLayoutNProgress三个插件
  • router说明

router是引用router/index.js里面导出的router
router/index.js里面, routernew vue-router,相当于vue-router对象

  • layout说明

这是页面的大体框架,具体页面详情如下

image.png

  • NProgress是进度条插件
import router from "./index"
import Layout from "../layout/index"
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
复制代码
  • routerbeforeEach里面分别有三个参数to, formnext,分别对应着去哪儿从哪儿来下一步

  • 接下来,根据去哪儿(to), 就需要判断路由指向是否需要过滤的路由地址数组里,如果在,则直接进入页面,无需判断,例如登录页面, 注册页面, 找回密码等(具体看业务需求)

const filterRoutes = ["/login"]
if (filterRoutes.indexOf(to.path) !== -1) {
    // 如果是无需权限的静态路由,可以直接跳走
    next()
    return false
}
复制代码
  • 然后就是进入动态路由主要部分了,首先判断当前路由栈的数量,如果路由栈的数量等于你在router/index.js里面的静态路由的数量,那么表明当前仍未加载动态路由,需要处理路由了,反之,则可以让它直接进入循环
// 由于我目前的教程里面,只是做了一个login的登录页面,所以静态页面也是仅有一个而已
if (router.options.routes.length == 1) {
      // 此处动态加载路由
} else next()    // 表明路由已加载,可直接进入页面
复制代码
  • 当路由未加载时,就需要获取登录时缓存的token路由栈,由于刷新的时候,vuex的数据无法持久化,所以建议最好routestoken都放在缓存storage里面,当然,cookies里面也是可以的,可是这样的话,浏览器一旦关闭,那么下次打开就需要重新登录了
// 获取token和原始路由数组
// 这里需要做空值合并操作,防止路由存在时,可token已失效,然后JSON.parse转义失败的情况导致的报错
const userInfo = JSON.parse(window.localStorage.getItem("userInfo")) ?? ""

// 当token和原始路由都存在的时候
// 进入路由执行路由过滤和跳转封装函数
// 否则,跳回登录页面
if(userInfo.token && userInfo.routes) onFilterRoutes(to, next, userInfo.routes)
else next({ path: "/login", replace: true })
复制代码

Tips小知识
空值合并操作符??
只有当左侧为null和undefined时,才会返回右侧的数
空值合并操作符(??)是一个逻辑操作符,当左侧的操作数为 null或者 undefined时,返回其右侧操作数,否则返回左侧操作数。

  • 当进入路由过滤和跳转封装的时候
  1. 先执行异步请求,确保路由过滤和路径补全已完成。先把routes传入递归函数(filterASyncRoutes),用于做路径的补全和Layout的判断并赋值,并且当routes存在children(子级路由)的时候,路由需要再次回调递归函数(filterASyncRoutes),最后并把处理好的路由栈,返回给路由过滤函数

  2. 根据异步请求返回的routes,进行路由的排序,毕竟当用户动态处理了路由后,展示出来的顺序跟处理时的顺序不一致,那就不太好了。

  3. 路由都处理完成后,把路由循环,并动态添加进router.options.routes里面,而且路由router里面,要使用addRoute(item),把路由一点点添加进路由表里。

  4. 最后执行路由跳转,跳回当前需要跳转的页面

// 路由拼接
function loadView(view) {
    return () => import(`@/views/${ view }`)
}

// 路由过滤和跳转
async function onFilterRoutes(to, next, e) {
    const routes = await filterASyncRoutes(e)    // 路由过滤
    routes.sort((a, b) => a['id'] - b['id'])
    routes.forEach(item => {
        router.options.routes.push(item)
        router.addRoute(item)
    })
    next({ ...to, replace: true })
}

// 路由过滤   遍历路由 转换为组件对象和路径
function filterASyncRoutes(data) {
    const routes = data.filter(item => {
        if(item["component"] === "Layout") item.component = Layout
        else item["component"] = loadView(item["component"])
        // 路由递归,转换组件对象和路径
        if(item["children"] && item["children"].length > 0) item["children"] = filterASyncRoutes(item.children)
        return true
    })
    return routes
}
复制代码
tips:
  1. 为什么使用router.addroute,而不使用router.addRoutes

新版本router.addRoutes已废弃:使用 router.addRoute() 代替。
官方的解释是 router.addRoute 接受的是一个路由规则,也就是一个对象,或者接受一个字符串和一个对象。

  1. 为什么要使用() => import(@/views/${ view })来做路由拼接

懒加载:又叫延时加载,即在需要的时候进行加载,随用即载
【相关问题】import() webpack4懒加载使用变量报错解决:www.cnblogs.com/chenxi188/p…

import和require的区别

node编程中最重要的思想就是模块化,importrequire都是被模块化所使用。

遵循规范
  • require 是 AMD规范引入方式
  • import是es6的一个语法标准,如果要兼容浏览器的话必须转化成es5的语法
调用时间
  • require是运行时调用,所以require理论上可以运用在代码的任何地方
  • import是编译时调用,所以必须放在文件开头
本质
  • require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
  • import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

项目&源码

源码地址(gitee):gitee.com/lemonote/vu…

项目地址:dynamic.lemonotes.cn/#/login

整体流程走完了,再容易让人蒙的地方

1. 根据路由进行菜单展示\

コードの場所:/src/Layout/sideBar/sidebaritem.vue
最初にelementUIメニューコンポーネントを見て、最初にいくつかの基本的なパラメーターを理解しましょう。
ここでは、メニューレンダリングをコンポーネントとして記述します。再
帰属性は、複数のレベルができることを保証するために使用されます。生成されるメニュー、
私はあなたがそれに精通していないことをお勧めします。あなたはジャンプ機能とアイコン表示を含むメニューをシミュレートして書くためにコンポーネントを使用し、次に私が書いたコンポーネントを見てください。

2.ユーザーはシステム
コードの場所を終了します。/src/layout/headerTemp/index.vue
終了するときは、localStorageに存在するユーザーの役割をクリアしてから、
this。$ router.replace({path:"/login]を使用することを忘れないでください。 "})ジャンプするにはログインページに移動し、

なぜそれを使用するのかlocation.reload()、これはaddRoute前のルートをクリアし、次のユーザーがログインした後に正しいメニューが再レンダリングされることを保証します

// 退出登录
handleLogout(key) {
    if(key == "logout") {
        window.localStorage.removeItem("userInfo")
        Message({ type: 'success', message: "退出登录", showClose: true, duration: 3000 })
        this.$router.replace({ path: "/login" })
        location.reload()
    }
}
复制代码

3. vuexを使用しないのはなぜですか?
元々はルーティング処理にvuexを使用することを目的としていましたが、後でブラウザが手動またはパッシブに更新すると、vuexはデータの永続化を実行できないことがわかりました。つまり、vuexの状態です。の値はクリアされます。そのため、安全のために、ルーティングの問題を処理するためにキャッシュストレージを選択しました。

何か問題があれば訂正してください

最後に

公式アカウント:小何成长、仏典はもっと多くのテキストであり、それらはすべて私が踏んだ穴や私が学んだことです。

興味のある友達は私をフォローすることを歓迎します、私は:何小玍一緒に進歩しましょう

おすすめ

転載: juejin.im/post/7081517906026037284
おすすめ