vue-element-admin原始碼分析
這兩天看花褲衩大大的手摸手系列,使用vue+element+vuex+axios實現了一個後臺模板(專案地址),在閱讀原始碼的過程中收益匪淺,以下做一些筆記。(由於是學習大大專案的思想,所以略去了很多大大的程式碼)。
這裡只是做一個登陸頁面,然後能提交資料給後臺並能接收資料,暫時沒有做路由守衛同跳轉。
首先配置並安裝好好所需要的main.js
import Vue from 'vue'
import App from './App'
import router from './router' //路由
import './assets/styles/reset.css'//初始化css樣式
Vue.config.productionTip = false
import Element from 'element-ui' //引入element-ui
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(Element)
import store from '@/store/index' //vuex
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
配置路由router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
component: () => import('@/views/login/index'),
hidden: true
},
]
})
登陸頁面:views/login/index.vue
1、這裡自定義校驗規則,在el-form裡定義:rules
並傳入約定的驗證規則,並將 Form-Item 的 prop 屬性設定為需校驗的欄位名,然後在data裡宣告規則,這裡定義表單都是必須填寫,當滑鼠失去焦點,即滑鼠點了其他地方會觸發,校驗器為自定義行數,自定義函式最後呼叫callback()。如:
data(){
const validateUsername = (rule,value,callback)=>{..some code... callback();}
const validatePassword= (rule,value,callback)=>{..some code... callback();}//自定義校驗規則,傳入3個引數value表示要校驗的資料。
return {
loginRules:{
username: [{required:true,trigger:'blur',validator:validateUsername}],//這裡表示必填表單,失去焦點時觸發,檢驗器為:validateUsername
password: [{required:true,trigger:'blur',validator:validatePassword}]
},
}
}
2、當點選按鈕是做兩個工作,一是判斷表單是否已經完成校驗,二是傳送請求。這裡為了方便儲存並呼叫使用者資訊,使用vuex管理使用者狀態,使用this.$store.dispatch(這裡現在main.js配置好store)方法傳給vuex的actions。
在methods裡通過 this.$refs.loginForm.validate(valid=>{})
校驗表單是否驗證正確,若 驗證正確valid=true,若驗證失敗valid=false
程式碼如下:
<template>
<div class="login-container">
<el-form :model="loginForm" :rules="loginRules" ref="loginForm" class="login-form" auto-complate="on">
<div class="title-container">
<h3 class="title">後臺登入</h3>
</div>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="使用者名稱"
name="username"
type="text"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="密碼"
name="password"
type="text"
auto-complete="on"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登入</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
const validateUsername = (rule,value,callback) => {
const usernamemap = ['admin','editor'];
if(!usernamemap.indexOf(value.trim()) >= 0){
callback(new Error ('please input the correct username'))
}else{
callback()
}
}
const validatePassword = (rule,value,callback) => {
if(value.length<6){
callback(new Error ('The password can not be less then 6 digits'))
}else{
callback()
}
}
return {
loginForm:{
username:'admin',
password:'111111'
},
loginRules:{
username: [{required:true,trigger:'blur',validator:validateUsername}],
password: [{required:true,trigger:'blur',validator:validatePassword}]
},
loading:false,
}
},
methods:{
handleLogin(){
this.$refs.loginForm.validate(valid => {
if(valid){//檢驗通過
this.loading=true;
//this.$store.dispatch('LoginByUsername',this.loginForm)
this.$store.dispatch('LoginByUsername',this.loginForm).then(()=>{this.loading=false;console.log('success')}).catch(()=>{console.log('err')})
}
})
}
},
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;
.login-container{
position:fixed;
width:100%;
height:100%;
background-color:$bg;
.login-form{
position:absolute;
left:0;
right:0;
width:520px;
max-width:100%;
margin:120px auto;
}
}
.title-container{
postion:relative;
.title{
font-size:26px;
color:$light_gray;
margin: 0px auto 40px auto;
text-align:center;
font-weight:bold;
}
}
</style>
傳輸資料與儲存資料
請求攔截器的封裝
這裡使用axios傳送與接受請求,為了做許可權認證這裡在每次傳送請求header都會攜帶上一個X-Token,所以封裝了一個axios的攔截器。設定傳送請求的字首為http://localhost/vue/(這是我自己定義的)
utils/request.js
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: 'http://localhost/vue/', // api 的 base_url
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// Do something before request is sent
if (store.getters.token) {
// 讓每個請求攜帶token-- ['X-Token']為自定義key 請根據實際情況自行修改
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => response,
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
傳送請求 api/login.js
使用php的同學需要用qs.stringify對傳輸的資料轉換一下格式。這裡的loginByUsername返回的是一個axios例項,方便後邊vuex進行then或catch操作
import request from '@/utils/request' //引入攔截器
import axios from 'axios'
import qs from 'qs'
export function loginByUsername(username, password) {
const data = {
username,
password
}
return request({
url: 'login.php',
method: 'post',
data:qs.stringify(data) //這裡對資料進行了轉換(原文是沒有轉換的,但我用的是php)
})
}
login.php
該檔案與先專案存在同源策略問題,故需要配置一下header,讓非同源請求可以請求並受到相應的資料,注意需要設定可以X-Token。為了做出一個效果,這裡隨便簡單地寫一個邏輯。
<?php
<?php
header('Access-Control-Allow-Origin:*'); // 響應型別
header('Access-Control-Allow-Methods:*'); // 響應頭設定
header('Access-Control-Allow-Headers:x-requested-with,content-type,X-Token'); //設定可以接受X-Token頭
if(isset($_POST['username'])&&isset($_POST['password'])){
if($_POST['username']=='admin'&&$_POST['password']=='111111'){
$res['login'] = md5(true);
}else{
$res['login'] = md5(false);
}
//$res['login'] = true;
echo json_encode($res);
}
?>
Vuex
這裡的actions.LoginByUsername返回一個Promise物件,方便login頁面作進一步then或catch操作
import Vue from 'vue'
import Vuex from 'vuex'
import { loginByUsername } from '@/api/login'
import { setToken,getToken } from '@/utils/auth'
Vue.use(Vuex);
const store = new Vuex.Store({
state:{
token:getToken() || ''
},
mutations:{
LoginByUsername(state,data){
state.token = setToken(data.login)
}
},
actions:{
LoginByUsername(ctx,userInfo){
return new Promise((resolve,reject)=>{
loginByUsername(userInfo.username, userInfo.password).then(response=>{
ctx.commit('LoginByUsername',response.data);
resolve();
}).catch(error => {reject(error)})
})
}
}
})
export default store
執行流程:
login頁面進行表單驗證,然後驗證成功點選按鈕,將資料傳送到vuex,有actions的方法傳送請求LoginByUsername,傳送請求時會進行一個請求攔截,會在請求頭header里加入X-Token,php返回token以及其他資料如許可權等並儲存在vuex和cookie,LoginByUsername會返回一個Promise物件,方便login頁面呼叫then或catch操作。
通過前端驗證登入資訊
這裡主要用到vue-router的導航守衛,原始碼裡這部分內容放在src/permission.js裡
科普一下導航前置守衛
router.beforeEach註冊了一個全域性前置守衛,每當通過vue-router跳轉都會先執行這裡,相當於過濾器(當你學過servlet,可以想到filter).
beforeEach接收3個引數
to 表示即將要進入的目標路由物件,
from 表示當前導航正要離開的路由,
next() 一定要呼叫該方法來 resolve 這個鉤子。執行效果依賴 next 方法的呼叫引數。
next(): 進行管道中的下一個鉤子。如果全部鉤子執行完了,則導航的狀態就是 confirmed (確認的)。
next(false): 中斷當前的導航。如果瀏覽器的 URL 改變了 (可能是使用者手動或者瀏覽器後退按鈕),那麼 URL 地址會重置到 from 路由對應的地址。
next(’/’) 或者 next({ path: ‘/’ }): 跳轉到一個不同的地址。當前的導航被中斷,然後進行一個新的導航。你可以向 next 傳遞任意位置物件,且允許設定諸如 replace: true、name: ‘home’ 之類的選項以及任何用在 router-link 的 to prop 或 router.push 中的選項。
next(error): (2.4.0+) 如果傳入 next 的引數是一個 Error 例項,則導航會被終止且該錯誤會被傳遞給 router.onError() 註冊過的回撥。
確保要呼叫 next 方法,否則鉤子就不會被 resolved。
這裡為了方便整合到src/router/index.js裡
index.js
import Vue from 'vue'
import Router from 'vue-router'
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import store from '@/store/index'
Vue.use(Router)
function hasPermission(roles,permissionRoles){
if(roles.indexOf('admin') >= 0) return true //admin permission passed directly
if(!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role)>=0)
}
const whiteList = ['/login', '/auth-redirect']// no redirect whitelist
export const constantRouterMap = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path*',
component: () => import('@/views/redirect/index')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/auth-redirect',
component: () => import('@/views/login/authredirect'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/errorPage/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/errorPage/401'),
hidden: true
},
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard', noCache: true }
}
]
},
{
path: '/documentation',
component: Layout,
redirect: '/documentation/index',
children: [
{
path: 'index',
component: () => import('@/views/documentation/index'),
name: 'Documentation',
meta: { title: 'documentation', icon: 'documentation', noCache: true }
}
]
},
{
path: '/guide',
component: Layout,
redirect: '/guide/index',
children: [
{
path: 'index',
component: () => import('@/views/guide/index'),
name: 'Guide',
meta: { title: 'guide', icon: 'guide', noCache: true }
}
]
}
]
var router = new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
export const asyncRouterMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'PagePermission',
meta: {
title: 'pagePermission',
roles: ['admin'] // or you can only set roles in sub nav
}
},
{
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'DirectivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}
]
},
//......此處省略很多
{ path: '*', redirect: '/404', hidden: true }
]
router.beforeEach((to,from,next)=>{
NProgress.start() // 頂部bar
if(getToken()){//判斷是否有token
if(to.path==='/login'){
next({path:'/'})//跳轉到/
}else{
next();
if(store.getters.roles.length === 0){//判斷當前使用者是否已經拉取到user_info資訊
store.dispatch('GetUserInfo').then(res=>{ //執行vuex中的action
const roles = res.data.roles //roles must be a array,示例['admin']
store.dispatch('GenerateRoutes',{ roles }).then(()=>{// 根據roles許可權生成可訪問的路由表
router.addRoutes(store.getters.addRouters)// 動態新增可訪問路由表
next({...to,replace:true})//hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err)=>{
store.dispatch('LogOut').then(()=>{//LogOut
Message.error(err||'Verification failed,please login again')
next({path:'/'})//重新定義
})
})
} else{
//沒有動態改變許可權的需求可直接next()刪除下方的許可權判斷
if(hasPermission(store.getters.roles,to.meta.roles)){
next()
}else{
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
}
}
}else{
if(whiteList.indexOf(to.path) !== -1){//在免登入白名單,直接進入
next()
}else{
next(`/login?redirect=${to.path}`) 否則全部重定向到登入頁
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
});
router.afterEach(() => {
NProgress.done() // finish progress bar
})
export default router
附上部分vuex中用到的方法
import { asyncRouterMap, constantRouterMap } from '@/router' //從router/index.js中引入這兩個陣列
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
let accessedRouters
if (roles.includes('admin')) {
accessedRouters = asyncRouterMap
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
}
}
前端驗證登入資訊的過程。
當訪問一個url時,經過以下一系列的過程
1、如果帶有token
1.1:如果要跳到的url是login,則通過next方法跳轉到跟目錄
1.2:否則,判斷使用者是否已經拉取到角色列表,
1.2.1:如果沒有拉取到角色列表,執行vuex(store/index)中的action.GetUserInfo方法
獲取到角色列表,根據角色許可權組合可訪問的路由表,並儲存起來(store/getters.js中的permission_routers中),router.addRoutes動態新增可訪問路由表。
1.2.2:沒有動態改變許可權的需求可直接next()
2、沒有帶token
2.1、如果是在免登入白名單中,直接進入
2.2、否則全部重定向到登入頁面
注意上述程式碼並沒有經過驗證,只是為了方便講解拼湊在一起,有意者看原始碼
佈局與側欄導航
通過router/index.js,我們可以看到每個路由有主父元件Layout以及其他子元件組成。
我們到views/layout/Layout.vue看一看
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container"/>
<div class="main-container">
<navbar/>
<tags-view/>
<app-main/>
</div>
</div>
</template>
可見一個頁面由幾個元件來組成(sidebar,navbar,tags-view,app-main)
sidebar側欄
看到這裡解決了我一直以來的困惑,為什麼路由陣列要單獨給一個變數,而不直接寫在new Router裡邊,這樣做不僅僅可以方便根據許可權組合路由,還方便展示路由的某些屬性,比如title,path等。
sidebar還是個遞迴元件,在元件內宣告name,可以實現遞迴元件。
navbar
這個暫時不講
tags-view
這裡快取被訪問過的頁面路徑
app-main
這裡是展示子元件。
keep-alive,能在元件切換過程中將狀態保留在記憶體中,防止重複渲染DOM。include : 字串或正規表示式。只有匹配的元件會被快取。
router-view展示子元件。key是用來切換時重新整理元件
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key"/>
</keep-alive>
</transition>
</section>
</template>
相關文章
- Retrofit原始碼分析三 原始碼分析原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 【Android原始碼】Fragment 原始碼分析Android原始碼Fragment
- 【Android原始碼】Intent 原始碼分析Android原始碼Intent
- k8s client-go原始碼分析 informer原始碼分析(6)-Indexer原始碼分析K8SclientGo原始碼ORMIndex
- k8s client-go原始碼分析 informer原始碼分析(4)-DeltaFIFO原始碼分析K8SclientGo原始碼ORM
- 以太坊原始碼分析(20)core-bloombits原始碼分析原始碼OOM
- 以太坊原始碼分析(24)core-state原始碼分析原始碼
- 以太坊原始碼分析(29)core-vm原始碼分析原始碼
- 【MyBatis原始碼分析】select原始碼分析及小結MyBatis原始碼
- redis原始碼分析(二)、redis原始碼分析之sds字串Redis原始碼字串
- ArrayList 原始碼分析原始碼
- kubeproxy原始碼分析原始碼
- [原始碼分析]ArrayList原始碼
- redux原始碼分析Redux原始碼
- preact原始碼分析React原始碼
- Snackbar原始碼分析原始碼
- React原始碼分析React原始碼
- CAS原始碼分析原始碼
- Redux 原始碼分析Redux原始碼
- SDWebImage 原始碼分析Web原始碼
- Aspects原始碼分析原始碼
- httprouter 原始碼分析HTTP原始碼
- PowerManagerService原始碼分析原始碼
- HashSet原始碼分析原始碼
- 原始碼分析——HashMap原始碼HashMap