公司项目过程中遇到的问题(1-10)

一、前言

卑微小前端开始工作了。开始记录一下自己在工作中遇到的问题,有些是粗心出的错,有些是基础不扎实遇到的问题。有些则是真的不知道有这种操作才出现的问题。总之就记录自己的在公司项目中遇到的各式各样的问题。也算是记录一下自己的成长。也方便自己之后遇到相同问题的时候。可以直接来自己博客找。大部分内容都源自别人写的博客,算是个缝合怪。。。。嘛。。起码我这东西还是自己试过可行的。

二、内容

1.event.target与event.srcElement

target 事件属性可返回事件的目标节点(触发该事件的节点),如生成事件的元素、文档或窗口。

在标准浏览器下我们一般使用event.target就能解决,然而低版本IE浏览器总是会出些幺蛾子,这时候就得使用event.srcElement。

这时候就得有一个兼容性的写法了。

1、使用三元运算符

var Target = event.target ? event.target : event.srcElement

2、使用if语句来判断

if(event.target){ 
    // IE10及以下版本浏览器不能识别``  ``
    // do something...``
}else if(event.srcElement){ 
    // IE10及以下版本会执行该代码``  ``
    // do something...``
}

2.js中的getAttribute()

getAttribute()是HTML DOM的一个方法,用以获取HTML元素的属性(如id,name,type以及其他自定义属性)。同理,setAttribute()、removeAttribute()都是对HTML元素的属性进行操作。
style是HTML DOM的一个关于样式的对象,style对象的属性(如background,color,border等等)用以设置元素的样式。
举例说明:

<input type="text" id="btn" style="background-color: red"> 
  1. 获取type属性可以用: document.getElementById(“btn”).getAttribute(“text”); //注意要获取的属性有双引号

  2. 获取元素样式可以用: document.getElementById(“btn”).style.backgroundColor;

  • 注: 如果属性是内联样式,也可直接用getAttribute()来获取其属性,但只能获取style,无法设置style里面具体的background-color里面具体的值。

    利用.getAttribute(“style”)得到的结果是 background-color: red

3.vue 监听键盘回车事件 @keyup.enter || @keyup.enter.native

  • KeyDown:用户摁下摁键时发生
  • KeyPress:用户摁下摁键,并且产生一个字符时发生
  • KeyUp: 用户释放某一个摁键时触发

vue运行为v-on在监听键盘事件时,添加了特殊的键盘修饰符:

<input v-on:keyup.13="submit">

vue还非常贴心地给出了常用按键的别名,这样就不必去记keyCode ~ ~

上面代码,还可以在这样写:

<input v-on:keyup.enter="submit">
<input @keyup.enter="submit">

全部的键盘别名:

.enter

.tab

.delete (捕获 “删除” 和 “退格” 键)

.esc

.space

.up

.down

.left

.right

还有一些组合按键:

.ctrl

.alt

.shift

.meta(window系统下是window键,mac下是command键)

Alt + C :

<input @keyup.alt.67=“doSth”>

Ctrl + Click :

点我

注意!!!如果用了封装组件的话,比如element,这个时候使用按键修饰符需要加上.native

比如:

<el-input v-model="account" placeholder="请输入账号" @keyup.enter.native="search()"></el-input>

4.键盘事件keypress 和 keydown、keyup 的用法与区别

  • KeyDown:用户摁下摁键时发生
  • KeyPress:用户摁下摁键,并且产生一个字符时发生
  • KeyUp: 用户释放某一个摁键时触发

定义和用法

完整的 keypress 过程分为两个部分:1. 按键被按下;2. 按键被松开。
当按钮被按下时,发生 keydown 事件。
keydown() 方法触发 keydown 事件,或规定当发生 keydown 事件时运行的函数。

    <html>  
    <head>  
    <script type="text/javascript" src="/jquery/jquery.js"></script>  
    <script type="text/javascript">  
    $(document).ready(function(){  
      $("input").keydown(function(){  
        $("input").css("background-color","#FFFFCC");  
      });  
      $("input").keyup(function(){  
        $("input").css("background-color","#D6D6FF");  
      });  
    });  
    </script>  
    </head>  
    <body>  
    Enter your name: <input type="text" />  
    <p>当发生 keydown 和 keyup 事件时,输入域会改变颜色。请试着在其中输入内容。</p>  
    </body>  
    </html>  

keyup 是在用户将按键抬起的时候才会触发的,属于整个按键过程中的最后阶段,所以有其特定的用处,就是在左侧输入,右侧同步显示的过程中很有用处。典型的例子就是邮件编辑预览的应用。

 <html xmlns="http://www.w3.org/1999/xhtml">  
<head>  
    <title>无标题页</title>  

    <script src="JS/jquery-1.4.2.js" type="text/javascript"></script>  

    <script type="text/javascript">  
        $(function() {  
            $('#t1').live('keyup', function() {  
                $('#v1').text($(this).val());  
            });  
            $('#t2').live('keydown', function() {  
                $('#v2').text($(this).val());  
            });  
            $('#t3').live('keypress', function() {  
                $('#v3').text($(this).val());  
            });  
        });      
    </script>  

</head>  
<body>  
    <textarea id="t1"></textarea>  
    <div id="v1">  
    </div>  
    <textarea id="t2"></textarea>  
    <div id="v2">  
    </div>  
    <textarea id="t3"></textarea>  
    <div id="v3">  
    </div>  
</body>  
</html>

这里分别用 keydown 、keyup、keypress做了实验,其中只有 keyup可以完整的将输入内容同步获取到,keydown和keypress总是无法获取到最后一个字符,这说明了三者之间的区别:
keydown 总是在摁键摁下的当时触发,不能得到最后的输入结果,keypress也是一样

这是因为keydown操作后,事件触发了,但值还未显示在文本框中,所以这类操作要用 keyup 一个完整的按键动作后,才可以获取文本框的值。

keydown与keypress更适用于通过键盘控制页面类功能的实现。

获取键盘点击的键位:

<html>  
<head>  
<script type="text/javascript" src="/jquery/jquery.js"></script>  
<script type="text/javascript">  
$(document).ready(function(){  
  $("input").keydown(function(event){   
    $("div").html("Key: " + event.which);  
  });  
});  
</script>  
</head>  
<body>  

请随意键入一些字符:<input type="text" />  
<p>当您在上面的框中键入文本时,下面的 div 会显示键位序号。</p>  
<div />  

</body>  
</html>  

windows 窗体通过引发键盘事件来处理键盘入以响应 windows 消息,大多数 windows 窗体应用程序都通过处理键盘事件以独占方式处理键盘输入。

按键的类型

windows 窗体将键盘输入标识为:由按位 keys 枚举表示的虚拟键代码。

使用 keys 枚举,可以综合一系列按键以生成单个值,这些值与 WM_KEYDOWN 和 WM_SYSKEYDOWNWindows 消息所伴随的值相对应。通过处理 keydown 或 keyup 事件可以检测大多数的恶物理按键操作。字符键是 keys 枚举的子集,它们与 WM_KEYDOWN 和 WM_SYSKEYDOWNWindows 消息所伴随的值相对应,如果通过组合按键得到一个字符,则可以处理 keypress 事件来检测该字符。

键盘事件的顺序

  • 用户按 “a”键:则该键将被预处理和调度,而且会发生 keydown 事件
  • 用户按住 “a”键:则该键将被预处理和调度,而且会发生 keypress 事件
  • 用户松开 “a”键:则该键将被预处理和调度,而且会发生 keyup事件

3.键的预处理

像其他消息一样,键盘消息是在窗体或控件的 WindProc 方法中处理的,窗体或控件在处理键盘消息之前 PreProcessMessage 方法会调用一个或多个方法,这些方法可以被重写以处理特殊的字符键和物理按键。
1

keydown 触发后不一定触发 keyup 当 keydown按下后拖动鼠标,那么将不会触发 keyup事件

  • 4 用法区别:

    • 系统由 keydown 返回键盘的代码 后再由 translateMessage 函数翻译成字符,然后在由 keypress 返回键盘的字符值(Ascall 字符)。

      所以,在使用时应根据你的目的,选择性的使用:

      如果你只想读取字符用 keypress;如果想读取各键盘的状态用 keydown

      • keydown: 用户再键盘上按下某按键时触发,一直按着某按键则会不断触发(opera浏览器除外);
      • keypress:用户按下一个按键,并产生一个字符时触发(也就是不管类似 shift、alt、ctrl之类的键只要用户按了一个能在屏幕上输出字符的按键 keypress 事件才会触发)一直按着某键则会不断触发
      • keypress主要用来捕获数字(注意:包括 shift+数字符号)、字母(注意:包括大小写)、小键盘除了F1-12、shift、Alt、Ctrl、insert、home、pgup、delete、end、pgdn、scrolllock、pause、numlock、菜单键、开始键、和方向键外的ANSI字符
      • keydown和keyup通常可以捕获除了 prscrn 所有键(这里不讨论特殊键盘的特殊键)
      • keydown和keyup对于单个字符捕获的 keyvalue 都是一个值,也就是不能判断单个字符的大小写
      • keypress 不区分小键盘和主键盘的数字字符
      • keydown 和 keyup 区分小键盘和主键盘的数字字符
      • 其中 prscrn按键和 keypress 、keydown 和 keyup 都不能捕获
  • 5.系统组合键的判定

    在使用键盘的时候通常会用到 Ctrl+shift+alt 类似的组合键功能,通过 keyup 事件能够处理(说明一下这里不用keydown事件:因为在判定keydown 的时候,ctrl、shift、和Alt属于一直按下状态然后再加另外一个键是不能准确捕获组合键,所以使用keydown 是不能准确判断的,要通过keyup事件来判定)

  • 6.捕获PreScrn按键事件

    通过一种钩子的方式可以判定PrScrn 按键事件,钩子可以获取任何键盘事件

5.process.env.NODE_ENV(判断生产环境或开发环境的依据)

在这里插入图片描述

在调用时需要写为:

<router-link to='/baseData/dictionary'></router-link>

很多人在这里会想到用懒加载来加载组件,代码如下:

{
    path:'/medicalSvc',
    name:'医疗服务',
    component:resolve => require(['../components/medicalData/medicalServe'], resolve),
},

但是如果你构建的是系统类项目,有上百个路由,这样的话 当你用webpack hot进行代码热更新的时候,速度就会超级慢,此时解决方法就是让他在开发环境下不要懒加载,在生产环境下再进行懒加载。我们首先要定义一个变量

const _import = require('./_import_' + process.env.NODE_ENV);

process.env是读取系统环境变量的,比如你在启动服务的时候,设置环境变量为production或者development,那么在程序里面就可以通过process.env.NODE_ENV获取。此时你就需要再写两个文件(注意文件的名称命名)分别导出不同环境下的文件目录如:

*_production.js:  module.exports = file => () => import('@/pages/' + file + '.vue')

*_develope.js:  module.exports = file => import('@/pages/' + file + '.vue')

在这里插入图片描述

然后将组件路径定义为:

{path:'/app/customerSource',component:_import('customerSource')},

那么又是怎么设置环境变量为production或者development值的呢,是在配置文件index.js下

在这里插入图片描述

另外注意:404页面一定要最后加载,如果放在constantRouterMap一同声明了404,它后面的所有页面都会被拦截到404,详细的问题见addRoutes when you’ve got a wildcard route for 404s does not work

const _import = require('./_import_' + process.env.NODE_ENV) //获取组件的方法

//远程运维智能管理

    {
        path: '/operationmanagement/statisticalReport',
    
        name: 'statisticalReport',
    
        component: _import('operationmanagement/statisticalReport'),
    }

process对象是全局变量,它提供当前node.js的有关进程。因为是全局变量,它对于node应用程序是始终可用的,无需require()

env是process的属性,这个属性包含用户环境信息对象,这个属性返回包含用户环境信息的对象

NODE_ENV不是process.env对象上原有的属性,他是我们添加的环境变量,用来确定当前所处的开发阶段。一般生产阶段设为production,开发阶段设为develop,然后在脚本中读取process.env.NODE_ENV。运行脚本时,可以这样改变环境变量,在package.json文件的script里面添加命令。set NODE_ENV=production node build.js

cross-env可以跨平台的设置和使用环境变量

npm install --save-dev cross-env

6.scrollBehavior的用处

在vue项目中,如果前一个页面有滚动条的滚动,当路由跳转后发现滚动条的位置还保持在原来的位置,这个就带来了困扰。
查询了资料,发现可以在路由的导航守卫afterEach里面添加:window.scrollTo(0,0);
这种方法可以在每次路由跳转后手动使滚动条回到头部位置。
如图:

其实在vue官网中介绍了scrollBehavior方法,同样可以实现路由跳转之后滚动条滚到顶部。
https://router.vuejs.org/zh/guide/advanced/scroll-behavior.html#异步滚动

具体代码如下:

scrollBehavior (to, from, savedPosition) {
	if (savedPosition) {
		return savedPosition
	} else {
		return { x: 0, y: 0 }
	}
}
浏览器对用户访问网页的记录
  • 在聊如何管理vue组件滚动行为之前,先简单说说(毕竟深入了我也很模糊o(╯□╰)o)浏览器是如何对用户访问过的页面的保持,浏览器历史记录是对用户所访问的页面按时间顺序进行的记录和保存,以上是MDN对浏览器就如何跟踪用户访问过网页的解释性说明。
  • 通常我们很少会对页面回退或前进进行操作,在浏览器用户界面上提供有前进、回退按钮,页面跳转到离开页面之前的位置,而不是重新刷新页面,这个功能是由浏览器引擎(与渲染引擎、解析引擎概念不同)来完成的。当用户进入一个页面的时候,会往 history 栈中放入当前的记录,对页面级别的操作通过操作内置对象 history 可以满足一些需求。
vue对访问记录的管理
  • 进入正题,vue 路由跳转就是通过对 history.pushState()history.replaceState() 方法的模拟来实现,会往 history 栈中存放一条记录,这也是为什么 vuerouter.push 方法只能在支持 history.pushState() 方法的浏览器中使用,当调用 router.go() 或者 router.back() 方法的时候就和 history.go()history.back() 效果一样,都是对 history 栈中的记录进行访问,上述行为与通过浏览器的回退和前进效果也是一样。
    但是,在不加处理的情况下,组件的滚动行为会跟我们想象的不同。
vue组件滚动行为
  • 设置三个路由 /home/list/about,即对应三个不同的组件,
  <ul class="tab">
      <li>
        <router-link to="/" >首页</router-link>
      </li>
      <li>
        <router-link to="/list" >列表</router-link>
      </li>
      <li>
        <router-link to="/about" >关于</router-link>
      </li>
      <li>
        <a href="#" @click='() => { this.$router.back() }'>点击回退</a>
      </li>
  </ul>
  <router-view></router-view>
  • 每个组件的结构都是 ul>li 的结构
  <!-- 以 home 组件为例 -->
  <ul class="list_content home">
     <li v-for='i in 10'>{{ i }}</li>
  </ul>

长这样:

在这里插入图片描述

请注意,现在开始滚动首页位置至第 5 屏的位置,当切换到列表以及关于页面的时候,会发现这两个页面的滚动行为和首页滚动行为一致。

在这里插入图片描述

既不涉及组件的缓存,也不涉及组件的复用,我们不禁会疑惑为什么首页的滚动会影响到其他两个页面,如果我们有当切换组件的时候,需要让当前组件的内容是从 scrollTop = 0 的时候开始浏览,那这样的结果将会是一个绊脚石。
原因如下,因为基于SPA模式开发,所以页面仅有一个,实现页面切换是利用哈希组件的映射关系,vue-router 是通过哈希来模拟完整的 url,但是对于页面来说仍是一个 url,所以在任何一个组件滚动页面,切换到其他组件的时候,页面仍保持滚动之前的状态,这就是出现上述现象的原因。

如何管理组件的滚动行为
  • 如果你是想简单粗暴的在每次切换组件的时候让页面回到顶部,router.beforeEach() 导航守卫会是一个不错的选择:
    router.beforeEach((to, from, next) => {
        // 让页面回到顶部
        document.documentElement.scrollTop = 0
        // 一定不要忘记调用 next()
        next()
    })

但这不是我们的主题,要借助 vue-router 提供的 scrollBehavior,来管理组件滚动行为。

  • 关于 scrollBehavior,这里贴出官网对概念的介绍 传送门,当然,借助 scrollBehavior,你也能让页面在组件切换的时候回到顶部:
  const scrollBehavior = function (to, from, savedPosition) {
        // savedPosition 会在你使用浏览器前进或后退按钮时候生效
       // 这个跟你使用 router.go() 或 router.back() 效果一致
       // 这也是为什么我在 tab 栏结构中放入了一个 点击回退 的按钮
       if (savedPosition) {
            return savedPosition
          } else {
            // 如果不是通过上述行为切换组件,就会让页面回到顶部
            return {x: 0, y: 0}
        }
    }
  • 上述会定制所有组件的滚动行为,但有时候我们希望,当用户在浏览 home 页面到底部的时候,跳转到 list 页面浏览,当浏览到中间的时候,跳转到 about 页面浏览,当用户每次回退的时候,都希望保持离开之前页面的状态,即:从 about 回退到 list 页面的时候,页面仍是在中间,回退到 home 页面的时候,仍是在底部,这就需要我们个性化定制。
定制不同组件的scrollBehavior

这里用到路由的元信息 meta 更细颗粒度控制滚动行为,这里以 home 组件为例说明:

      const Home = {
          template: `
            <ul class="list_content home">
                <li v-for='i in 10'>{{ i }}</li>
            </ul>
            `,
          data () {
              return {
                  timerId: ''
              }
           },
          mounted () {
                 // 通过 addEventListener 方法注册事件的时候需要格外小心      
                // 如果在 destroyed 钩子函数中没有销毁 scroll 事件
                // 在激活 home 组件的时候会再次绑定 scroll 事件
                // window.addEventListener('scroll', this.justifyPos)
                // 通过 on 方式绑定事件能够有效避免上述情况
                window.onscroll = this.justifyPos
            },
          methods: {
                justifyPos () {
                    // 节流;
                    if (this.timerId) clearTimeout(this.timerId)
                    this.timerId = setTimeout(() => {
                        // 获取页面滚动距离之后设置给当前路由的 元信息
                        this.$route.meta.y = window.pageYOffset
                    }, 300)
                }
            },
          destroyed () {
                // 当组件销毁的时候,移除滚动行为监听, 清空定时器;
                // 该方法是绑定到 window 身上,即使跳转到其他组件,仍然会监听页面的滚动行为
                // window.removeEventListener('scroll', this.justifyPos)
                // clearTimeout(this.timerId)
            }
        }
        const List = {
            template: `
            <ul class="list_content list">
                <li v-for='i in 10'>{{ i }}</li>
            </ul>
            `
        }
        const About = {
            template: `
            <ul class="list_content about">
                <li v-for='i in 10'>{{ i }}</li>
            </ul>
            `
        }
        const routes = [
                // 设置 meta,细颗粒控制组件滚动
                {path: '/', component: Home, meta: {x: 0, y: 0}},
                {path: '/list', component: List, meta: {x: 0, y: 0}},
                {path: '/about', component: About, meta: {x: 0, y: 0}}
            ]
        const scrollBehavior = function (to, from, savedPosition) {
            return to.meta
        }
        const router = new VueRouter({
            routes,
            scrollBehavior,
            linkExactActiveClass: 'current'
        })

上述会在 home 组件滚动停止的时候记录当前组件的滚动位置信息,并且存储到对应 home 组件的路由 meta 这个对象中,当切换到 list 或者 about 页面之后在回到 home 组件,仍会保留着离开之前的位置,而不是简单地让页面回到顶部。
但是,你会发现你只是针对 home 组件的滚动行为进行控制,listabout 组件的滚动行为也能够实现个性化定制,即也会将当前组件的滚动行为记录在对应的路由 meta 中。
这会让人疑惑,因为在 listabout 组件中并没有设置 justifyPos 方法,并且 window.onscroll = this.justifyPosthis 绑定到当前的上下文中。
vue 官网对于组件销毁介绍,会解绑所有的指令以及事件监听,但是对于方法的引用处理没有提到,个人觉得在这里应该抛出警告或者错误的,但是 vue 却没有提示,这也是令我困惑的一点。但是,这却为滚动行为监听提供了更好的处理方法,那就是绑定到 vue 根实例上,而不是某一个单一组件上,因为 this 会自动绑定到当前上下文:

    new Vue({
        router,
        data: {
            timerId: ''
        },
        mounted () {
            window.addEventListener('scroll', this.justifyPos)
        },
        methods: {
            justifyPos () {
                if (this.timerId) clearTimeout(this.timerId)
                this.timerId = setTimeout(() => {
                    this.$route.meta.y = window.pageYOffset
                }, 300)
            }
        }
    }).$mount('#app')
当better-scroll(以下简称bs)遇上vue,如何定制滚动行为
  • 贴上传送门 better-scroll,感兴趣的可以看一下。
  • 之所以会谈到 bs,如果在项目中用到该插件,那么页面滚动行为跟组件滚动行为是完全不一样的,这是因为 bs 特殊的结构要求,父容器需要有个固定的高度,所有的滚动行为是由子元素来产生的,在移动端应用 bs,通常会将父容器的高度设置为屏幕的高度,你的所有应用都应该放到这个父容器内。bs 在移动端性能很出色,但是这却为组件个性化定制 scrollBehavior 带来了一些小麻烦。
  • 原因就是应用 bs 插件的组件,一般会设置高度和屏幕高度一致,这样即使通过 meta 来设置滚动记录,在 vue-routerscrollBehavior 中返回 meta 也没有用处,因为高度是定死了,就不存在滚动,你所看到的滚动式是 bs 插件所处理的。
  • 这时候,就需要用到 bs 提供的一些事件和方法了,仍以 home 组件为例说明,看代码:
    const Home = {
            template: `
            <div class="wrapper" ref="wrapper">
                <ul class="list_content home">
                    <li v-for='i in 10' @click='goList'>{{ i }}</li>
                </ul>
            </div>
            `,
            mounted () {
                this.$nextTick(() => {
                    // 初始化 BS
                    this._initScroll()
                    // 滚动监听
                    this.scroll.on('scrollEnd', (pos) => {
                        // 将滚动信息设置给当前路由元信息
                        this.$route.meta.y = pos.y
                    })
                    // 当前组件激活的时候,滚动到离开前位置
                    // 如果你想要滚动动画效果,可以在 scrollTo 方法中自定义
                    this.scroll.scrollTo(0, this.$route.meta.y, 0)
                })
            },
            methods: {
                _initScroll () {
                    if (!this.$refs.wrapper) return
                    this.scroll = new BScroll(this.$refs.wrapper, {
                        mouseWheel: {
                            speed: 20,
                            invert: false,
                            easeTime: 300
                        },
                        // 派发 click 事件;
                        click: true
                    })
                },
                // 跳转到列表页;
                goList () {
                    this.$router.push({name: 'list'})
                }
            }
        }
        const List = {
            template: `
            <ul class="list_content list">
                <li v-for='i in 10' @click='goHome'>{{ i }}</li>
            </ul>
            `,
            methods: {
                // 回跳到首页
                goHome () {
                    this.$router.push({name: 'home'})
                }
            }
        }
        const routes =  [
                // 设置 meta
                {path: '/', name: 'home', component: Home, meta: {x: 0, y: 0}},
                {path: '/list', name: 'list', component: List, meta: {x: 0, y: 0}},
                {path: '/about', component: About, meta: {x: 0, y: 0}}
            ]
        // scrollBehavior 其实这里已经没有什么作用了,因为当前组件的高度被定死和整个屏幕一样高
        // const scrollBehavior = function (to, from, savedPosition) {
        //    return to.meta
        // }
        // 设置路由
        const router = new VueRouter({
            routes,
            scrollBehavior,
            linkExactActiveClass: 'current'
        })
        // 挂载
        new Vue({router}).$mount('#app')
  • 通过 bs 提供的事件以及方法再结合路由的 meta,也能够实现细颗粒度控制滚动,如果对组件使用了 keep-alive,你应该在每次切换到该组件的时候在 activated 钩子函数中初始化 bsscrollEnd 事件以及 scrollTo 方法;如果你页面有分页的功能,你可能需要在分页边界花费一些心思如何让滚动行为跨越分页,这里建议是使用组件缓存,关于组件如何清除缓存,可以参考另一篇文章 组件去缓存,当然如果你有更好的处理方式,也可以留言。

7.vue后台管理之动态加载路由的方法

在这里我们将会实现一个vue动态路由的案列,当用户登陆成功后,根据用户的角色,拿到他对应的菜单信息,并将它动态的载入到我们的路由中。

我们的通用的后台管理系统中,我们会根据权限的粗细不同,会对每个角色每个权限每个资源进行控制。同样的我们也需要实现一个这样的功能。 这篇文章我将主要讲vue端的实现,关于后台接口我就不会涉及,当我接触的时候我们的后台接口是springcloud实现。

一、思路

在vue-router对象中首先初始化公共路由,比如(404,login)等,然后在用户登陆成功,根据用户的角色信息,获取对应权限菜单信息menuList,并将后台返回的menuList转换成我们需要的router数据结构,然后通过vue-router2.2新添的router.addRouter(routes)方法,同时我们可以将转后的路由信息保存于vuex,这样我们可以在我们的SideBar组件中获取我们的全部路由信息,并且渲染我们的左侧菜单栏,让动态路由实现。

二、实现

1、公共路由定义

import Vue from 'vue'
import Router from 'vue-router'
 
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
 
export const constantRouterMap = [
 { path: '/login', component: () => import('@/views/login/index'), hidden: true },
 { path: '/404', component: () => import('@/views/404'), hidden: true },
 
 {
  path: '/',
  component: Layout,
  redirect: '/dashboard',
  name: 'Dashboard',
  hidden: true,
  children: [{
   path: 'dashboard',
   component: () => import('@/views/dashboard/index')
  }]
 },
]
export default new Router({
 scrollBehavior: () => ({ y: 0 }),
 routes: constantRouterMap
})

2、获取菜单信息

router.beforeEach((to, from, next) => {
 NProgress.start() // start progress bar
 if (getToken()) { // determine if there has token
  /* has token*/
  if (to.path === '/login') {
   next({ path: '/' })
   NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
  } else {
   if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
    store.dispatch('GetInfo').then(res => { // 拉取user_info
     const roles = res.roles
     store.dispatch("GetMenu").then(data => {
      initMenu(router, data);
     });
     next()
    }).catch((err) => {
     store.dispatch('FedLogOut').then(() => {
      Message.error(err || 'Verification failed, please login again')
      next({ path: '/' })
     })
    })
   } else {
    next()
   }
  }
 } else {
  /* has no token*/
  if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
   next()
  } else {
   next('/login') // 否则全部重定向到登录页
   NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
  }
 }
})
 
router.afterEach(() => {
 NProgress.done() // finish progress bar
})

在这里 我们通过在router的beforeEach钩子函数 判断用户是否已经获得角色信息,如果没有,则请求后台获取对应的角色信息,然后通过角色信息再次请求获取对应的菜单列表,获取到菜单列表,然后去格式化菜单列表,使其转换成router数组的结构。

3、动态加载路由

import store from '../store'
 
export const initMenu = (router, menu) => {
 if (menu.length === 0) {
  return
 }
 let menus = formatRoutes(menu);
 // 最后添加
 let unfound = { path: '*', redirect: '/404', hidden: true }
 menus.push(unfound)
 router.addRoutes(menus)
 store.commit('ADD_ROUTERS',menus)
}
 
export const formatRoutes = (aMenu) => {
 const aRouter = []
 aMenu.forEach(oMenu => {
  const {
   path,
   component,
   name,
   icon,
   childrens
  } = oMenu
  if (!validatenull(component)) {
   let filePath;
   const oRouter = {
    path: path,
    component(resolve) {
     let componentPath = ''
     if (component === 'Layout') {
      require(['../views/layout/Layout'], resolve)
      return
     } else {
      componentPath = component
     }
     require([`../${componentPath}.vue`], resolve)
    },
    name: name,
    icon: icon,
    children: validatenull(childrens) ? [] : formatRoutes(childrens)
   }
   aRouter.push(oRouter)
  }
 
 })
 return aRouter
}

在这里我们把menList转换成routerList因为我们后台返回的数据不是规范的router结构,所以这里需要我们处理一下,如果你们后台返回规范的就不需要处理,然后通过router.addRoutes把后台返回的加入到我们的路由中,并且将其保存在我们的vuex中,需要主要的 如果404组件一定要放在动态路由在后载入。

4、渲染菜单

其实这里已经不属于我们的所讲的重点,在这里只需要取出上一步存在vuex中的路由信息,并且将其渲染成我们的左侧菜单栏就可以。在这里我们使用了element-ui。

<template>
 <el-scrollbar wrapClass="scrollbar-wrapper">
  <el-menu
   mode="vertical"
   :show-timeout="200"
   :default-active="$route.path"
   :collapse="isCollapse"
   background-color="#304156"
   text-color="#bfcbd9"
   active-text-color="#409EFF"
  >
   <sidebar-item v-for="route in permission_routers" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
  </el-menu>
 </el-scrollbar>
</template>
 
<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
import { validatenull } from "@/utils/validate";
import { initMenu } from "@/utils/util";
 
export default {
 components: { SidebarItem },
 created() {
 },
 computed: {
  ...mapGetters([
   'permission_routers',
   'sidebar',
   'addRouters'
  ]),
  isCollapse() {
   return !this.sidebar.opened
  }
 }
}
</script>

就这样我们动态加载路由就是实现了,是不是很简单,关键点就是router.addRoute方法。下面我就说一下防踩坑点。

三、防坑

1、关于加载菜单信息的时机

在此之前我将第二步获取菜单信息放在我的SideBar组件的create函数中,当时我发现也没有什么问题。登录跳转到home界面 左侧菜单也成功渲染,点击菜单进入我们动态加载的路由界面,也没问题。但是当我点击刷新的时候问题来。页面空白 控制台也不报错。当时我就蒙蔽了,什么情况,不是好好的嘛?如果大家也遇到这种这时候大家不要着急,冷静的分析整个流程,就会发现问题的所在。

1、登陆成功跳转home界面,home组件是公共路由,存在的没问题。

2、这时候 sidebar组件create钩子触发,成功获取菜单列表

3、菜单列表转成路由数组,并且加载到router实例中和vuex中

4、sidebar从vuex获取到路由数组渲染菜单 进入我们动态加载页面中,显示正常,这一切看起来没什么问题

5、点击浏览器的刷新按钮、或者F5,页面空白。

原因: 第五步中我们我们浏览器刷新,在spa应用整个vue实例会重新加载,也是说我的vue-router会重新初始化,那么我们之前的动态addRoute就不存在了,但是我们此时访问一个不存在的页面,所以我们的sidebar组件也就不会被访问,那么也无法获取菜单信息,就导致页面空白。所以我们需要把加载菜单信息这一步放在router的全局守卫beforeEach中就可以了。

2、关于404组件的位置

大家可以看到

export const initMenu = (router, menu) => 
{
    if (menu.length === 0)
    { return }
    let menus = formatRoutes(menu);// 最后添加
    let unfound = { 
        path: '*', 
        redirect: '/404',
        hidden: true
    }
    menus.push(unfound)
    router.addRoutes(menus)
    store.commit('ADD_ROUTERS',menus)
}

我强调了 404组件一定要放在动态路由组件的最后,不然你刷新动态加载的页面,会跳转到404页面的。

8.Vue.js中this.$nextTick()的使用

this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

假设我们更改了某个dom元素内部的文本,而这时候我们想直接打印出这个被改变后的文本是需要dom更新之后才会实现的,也就好比我们将打印输出的代码放在setTimeout(fn, 0)中;

先来第一个例子看一看

<template>
  <section>
    <div ref="hello">
      <h1>Hello World ~</h1>
    </div>
    <el-button type="danger" @click="get">点击</el-button>
  </section>
</template>
<script>
  export default {
    methods: {
      get() {
      }
    },
    mounted() {
      console.log(333);
      console.log(this.$refs['hello']);
      this.$nextTick(() => {
        console.log(444);
        console.log(this.$refs['hello']);
      });
    },
    created() {
      console.log(111);
      console.log(this.$refs['hello']);
      this.$nextTick(() => {
        console.log(222);
        console.log(this.$refs['hello']);
      });
    }
  }
</script>

img

可以根据打印的顺序看到,在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作并无作用,而在created()里使用this.$nextTick()可以等待dom生成以后再来获取dom对象

然后来看第二个例子

<template>
  <section>
    <h1 ref="hello">{{ value }}</h1>
    <el-button type="danger" @click="get">点击</el-button>
  </section>
</template>
<script>
  export default {
    data() {
      return {
        value: 'Hello World ~'
      };
    },
    methods: {
      get() {
        this.value = '你好啊';
        console.log(this.$refs['hello'].innerText);
        this.$nextTick(() => {
          console.log(this.$refs['hello'].innerText);
        });
      }
    },
    mounted() {
    },
    created() {
    }
  }
</script>

img

根据上面的例子可以看出,在方法里直接打印的话, 由于dom元素还没有更新, 因此打印出来的还是未改变之前的值,而通过this.$nextTick()获取到的值为dom更新之后的值

this.$nextTick()在页面交互,尤其是从后台获取数据后重新生成dom对象之后的操作有很大的优势,这里只是简单的例子,实际应用中更为好用~

9.JS 中如何判断 undefined

JavaScript 中有两个特殊数据类型:undefined 和 null,下节介绍了 null 的判断,下面谈谈 undefined 的判断。

以下是不正确的用法:
var exp = undefined;
if (exp == undefined)
{
    alert("undefined");
}

exp 为 null 时,也会得到与 undefined 相同的结果,虽然 null 和 undefined 不一样。注意:要同时判断 undefined 和 null 时可使用本法。

var exp = undefined;
if (typeof(exp) == undefined)
{
    alert("undefined");
}
以下是正确的用法:
var exp = undefined;
if (typeof(exp) == "undefined")
{
    alert("undefined");
}

10.JS 中如何判断 null

以下是不正确的用法:
var exp = null; 
if (exp == null) 
{ 
alert(“is null); 
}

exp 为 undefined 时,也会得到与 null 相同的结果,虽然 null 和 undefined 不一样。注意:要同时判断 null 和 undefined 时可使用本法。

var exp = null; 
if (!exp) 
{ 
alert(“is null); 
}

如果 exp 为 undefined 或者数字零,也会得到与 null 相同的结果,虽然 null 和二者不一样。注意:要同时判断 null、undefined 和数字零时可使用本法。

var exp = null; 
if (typeof(exp) ==null) 
{ 
alert(“is null); 
}

为了向下兼容,exp 为 null 时,typeof 总返回 object。

var exp = null; 
if (isNull(exp)) 
{ 
alert(“is null); 
}

JavaScript 中没有 isNull 这个函数。
以下是正确的用法:
var exp = null; 
if (!exp && typeof(exp)!=”undefined” && exp!=0) 
{ 
alert(“is null); 
} 

尽管如此,我们在 DOM 应用中,一般只需要用 (!exp) 来判断就可以了,因为 DOM 应用中,可能返回 null,可能返回 undefined,如果具体判断 null 还是 undefined 会使程序过于复杂。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值