文章目錄
一、前言
卑微小前端開始工作了。開始記錄一下自己在工作中遇到的問題,有些是粗心出的錯,有些是基礎不紮實遇到的問題。有些則是真的不知道有這種操作纔出現的問題。總之就記錄自己的在公司項目中遇到的各式各樣的問題。也算是記錄一下自己的成長。也方便自己之後遇到相同問題的時候。可以直接來自己博客找。大部分內容都源自別人寫的博客,算是個縫合怪。。。。嘛。。起碼我這東西還是自己試過可行的。
二、內容
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">
-
獲取type屬性可以用: document.getElementById(“btn”).getAttribute(“text”); //注意要獲取的屬性有雙引號
-
獲取元素樣式可以用: 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
棧中存放一條記錄,這也是爲什麼vue
的router.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
組件的滾動行爲進行控制,list
和 about
組件的滾動行爲也能夠實現個性化定製,即也會將當前組件的滾動行爲記錄在對應的路由 meta
中。
這會讓人疑惑,因爲在 list
和 about
組件中並沒有設置 justifyPos
方法,並且 window.onscroll = this.justifyPos
將 this
綁定到當前的上下文中。
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-router
的scrollBehavior
中返回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
鉤子函數中初始化bs
、scrollEnd
事件以及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>
可以根據打印的順序看到,在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>
根據上面的例子可以看出,在方法裏直接打印的話, 由於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 會使程序過於複雜。