Vue + Spring Boot 项目实战(七):前端路由与登录拦截器
文章目錄
- 前言
- 一、前端路由
- 二、使用 History 模式
- 三、后端登錄攔截器
- 3.1. LoginController
- 3.2. LoginInterceptor
- 3.3. WebConfigurer
- 3.4. 效果檢驗
- 四、Vuex 與前端登錄攔截器
- 4.1. 引入 Vuex
- 4.2. 修改路由配置
- 4.3. 使用鉤子函數判斷是否攔截
- 4.4. 修改 Login.vue
- 4.5. 效果檢驗
前言
這一篇主要講前端路由與登錄攔截器的實現。放在一起講是因為我在開發登錄攔截器時因為這個路由的問題遇到了很多坑,花費了很長時間,網上的解決方案都不怎么靠譜,綜合了好幾種辦法才最終成功,其實關于這個部分能寫兩三篇文章,名字起好了還能多很多訪問量,不過為了保證文章的質量我沒有這么做,畢竟我不是大神,如果再不用點心,寫出來的文章自己都不想再看第二遍,更別提能對大家有所幫助了。
一、前端路由
大家如果留心觀察就會發現,之前我們做的頁面的 URL 里有一個 # 號,這個 # 號有什么含義呢?
假設在 html 中有這么一段代碼:<div id="test">This is a test</div>,如果我們想讓頁面定位到這個 div 所在的位置,可以加一個超鏈接 <a herf="#test">Jump to test</a>,這里的 # 被稱為“錨點”,點擊超鏈接,可以發現網頁的 URL 發生了變化,但頁面并不會跳轉。
在互聯網流量如此龐大的今天,我們需要想辦法后端服務器的壓力,利用 AJAX,我們可以不重載頁面就刷新數據,如果再加上 # 號的特性(即改變 URL 卻不請求后端),我們就可以在前端實現頁面的整體變化,而不用每次都去請求后端。
為了實現前端路由,我們可以監聽 # 號后面內容的變化(hashChange),從而動態改變頁面內容。URL 的 # 號后面的地址被稱為 hash ,估計是哪個大神拍腦袋想的,不用深究。這種實現方式我們稱之為 Hash 模式,是非常典型的前端路由方式。
另一種常用的方式被稱為 History 模式,這種方式使用了 History API,History API 顧名思義就是針對歷史記錄的 API ,這種模式的原理是先把頁面的狀態保存到一個對象(state)里,當頁面的 URL 變化時找到對應的對象,從而還原這個頁面。其實原本人家這個功能是為了方便瀏覽器前進后退的,不得不說程序員們的腦洞真大。使用了這種模式,就可以擺脫 # 號實現前端路由。
Vue 已經為我們提供了兩種模式的前端路由,無需我們自己去實現。
二、使用 History 模式
首先我們把 Vue 中配置的路由從默認的 hash 模式切換為 histroy 模式。打開我們的前端項目 wj-vue,修改 router\index.js,加入 mode: 'history 這句話。整體代碼如下:
import Vue from 'vue' import Router from 'vue-router' // 導入剛才編寫的組件 import AppIndex from '@/components/home/AppIndex' import Login from '@/components/Login'Vue.use(Router)export default new Router({mode: 'history',routes: [// 下面是固定寫法{path: '/login',name: 'Login',component: Login},{path: '/index',name: 'AppIndex',component: AppIndex}] })運行項目,訪問不加 # 號的 http://localhost:8080/login ,成功加載頁面。
運行項目:npm run dev
瀏覽器訪問:http://localhost:8080/login
接下來,我們把前端打包后部署在后端。這不是前后端分離項目推薦的做法,之前我們講過其實應該把前后端分別部署在不同的服務器中,但實際上仍有不少項目會選擇把前后端整合在一起,只使用一個服務器,所以這里我們也提及一下這種方式,但在之后的開發中不會這樣部署。
先在項目目錄執行 npm run build,控制臺輸出如下內容表明執行完畢:
這時在項目的 dist 文件夾下生成了 static 文件夾和 index.html 文件,把這兩個文件,拷貝到我們后端項目的 wj\src\main\resources\static 文件夾下,一定要注意這個位置,這時后端配置的靜態文件的 path,雖然看起來很詭異,但一般都不作修改。
接下來,打開后端項目并運行,訪問 http://localhost:8443/index.html ,(注意輸入后綴 .html)發現頁面是空白的,但確實取到了這個頁面。
再訪問 http://localhost:8443/login ,發現跳轉到了錯誤頁面(White Error Page)
這里我們回顧一下單頁面應用的概念,在我們這個項目中,其實只有 index.html 這一個頁面,所有的其它內容都是在這個頁面里動態渲染的。當我們直接在后端訪問 /login 路徑時,服務器會后端并沒有這個路徑對應的內容,所以返回了 Error Page。
為了獲取到我們需要的內容,我們要想辦法觸發前端路由,即在后端添加處理內容,把 通過這個 URL 渲染出的 index.html 返回到瀏覽器。
在后端項目中新建一個 package 名為 error,新建實現 ErrorPageRegistrar 接口的類 ErrorConfig,把默認的錯誤頁面設置為 /index.html,代碼如下
package com.gblfy.wj.error;import org.springframework.boot.web.server.ErrorPageRegistrar; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.ErrorPageRegistry; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component;@Component public class ErrorConfig implements ErrorPageRegistrar {@Overridepublic void registerErrorPages(ErrorPageRegistry registry) {ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");registry.addErrorPages(error404Page);}}重新啟動項目,訪問 http://localhost:8443/login ,成功進入登錄頁面。
三、后端登錄攔截器
為了完善登錄功能,我們需要限制未登錄狀態下對核心功能頁面的訪問。登錄攔截可以由多種方式來實現,我們首先講解后端攔截器的開發。(注意如果沒有把前后端項目整合起來,就沒有辦法使用這種方式)
一個簡單攔截器的邏輯如下:
1.用戶訪問 URL,檢測是否為登錄頁面,如果是登錄頁面則不攔截
2.如果用戶訪問的不是登錄頁面,檢測用戶是否已登錄,如果未登錄則跳轉到登錄頁面
3.1. LoginController
首先我們修改 LoginController 的內容。之前我們實現了通過查詢數據庫驗證用戶名是否正確,但僅此而已。
為了保存登錄狀態,我們可以把用戶信息存在 Session 對象中(當用戶在應用程序的 Web 頁之間跳轉時,存儲在 Session 對象中的變量不會丟失),這樣在訪問別的頁面時,可以通過判斷是否存在用戶變量來判斷用戶是否登錄。這是一種比較簡單的方式,感興趣的同學可以嘗試別的方法。
修改后的代碼內容如下:
package com.gblfy.wj.controller;import com.gblfy.wj.pojo.User; import com.gblfy.wj.result.Result; import com.gblfy.wj.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.HtmlUtils;import javax.servlet.http.HttpSession;@Controller public class LoginController {@AutowiredUserService userService;@CrossOrigin@PostMapping(value = "/api/login")@ResponseBodypublic Result login(@RequestBody User requestUser, HttpSession session) {String username = requestUser.getUsername();username = HtmlUtils.htmlEscape(username);User user = userService.get(username, requestUser.getPassword());if (null == user) {return new Result(400);} else {session.setAttribute("user", user);return new Result(200);}} }其實只是添加了一條語句 session.setAttribute(“user”, user);
3.2. LoginInterceptor
新建 package 名為 interceptor,新建類 LoginInterceptor。
Interceptor 即攔截器,在 Springboot 我們可以直接繼承攔截器的接口,然后實現 preHandle 方法。preHandle 方法里的代碼會在訪問需要攔截的頁面時執行。
代碼如下:
看起來似乎比較長,其實就是判斷 session 中是否存在 user 屬性,如果存在就放行,如果不存在就跳轉到 login 頁面。這里使用了一個路徑列表(requireAuthPages),可以在里面寫下需要攔截的路徑。當然我們也可以攔截所有路徑,那樣就不用寫這么多了,但會有邏輯上的問題,就是你訪問了 \login 頁面,仍然會需要跳轉,這樣就會引發多次重定向問題。
3.3. WebConfigurer
我們寫完了攔截器,但是它卻并不會生效,因為我們還沒有把它配置到項目中。
新建 package 名為 config,新建類 MyWebConfigurer,代碼如下:
package com.gblfy.wj.config;import com.gblfy.wj.interceptor.LoginInterceptor; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.*;@SpringBootConfiguration public class MyWebConfigurer implements WebMvcConfigurer {@Beanpublic LoginInterceptor getLoginIntercepter() {return new LoginInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(getLoginIntercepter()).addPathPatterns("/**").excludePathPatterns("/index.html");} }通過這個配置類,我們添加了之前寫好的攔截器。這里有一句非常重要的語句,即
registry.addInterceptor(getLoginIntercepter()).addPathPatterns("/**").excludePathPatterns("/index.html");這條語句的作用是對所有路徑應用攔截器,除了 /index.html。
之前我們在攔截器 LoginInterceptor 中配置的路徑,即 index,觸發的時機是在攔截器生效之后。也就是說,我們訪問一個 URL,會首先通過 Configurer 判斷是否需要攔截,如果需要,才會觸發攔截器 LoginInterceptor,根據我們自定義的規則進行再次判斷。
/index 與 /index.html 是不同的,也就是說 /index 會觸發攔截器而 /index.html 不會,但根據攔截器 LoginInterceptor 中我們定義的判斷條件,以 /index 開頭的路徑都會被轉發,包括 index.html。
因為我們做的是單頁面應用,之前通過配置 ErrorPage,實際上訪問所有路徑都會重定向到 /index.html 。我們直接在瀏覽器地址欄輸入 /index 會觸發攔截器,經過攔截器重定向到 /login,然后 /login 再經過 Configurer 的判斷,再次觸發攔截器,由于不在需要攔截的路徑中,所以被放行,頁面則重新定向到了 /index.html,如果沒有再 Configurer 中取消對 /index.html 的攔截,則會再次觸發攔截器,再次重定向到 /login,引發如下錯誤。
上述過程比較繞,這是我開發時的失誤,但我覺得恰好可以幫助大家理解攔截器與單頁面應用,所以保留了下來。
3.4. 效果檢驗
運行后端項目,訪問http://localhost:8443/index,發現頁面自動跳轉到了 http://localhost:8443/login,輸入用戶名和密碼登錄,跳轉到 http://localhost:8443/index , 這時可以把瀏覽器標簽關掉,再在一個新標簽頁輸入 http://localhost:8443/index ,發現不會被攔截。
四、Vuex 與前端登錄攔截器
前面我們使用了后端攔截器,但這種攔截器只有在將前后端項目整合在一起時才能生效,而前后端分離的項目實際上不推薦這么做,接下來我們嘗試用前端實現相似的功能。
實現前端登錄器,需要在前端判斷用戶的登錄狀態。我們可以像之前那樣在組件的 data 中設置一個狀態標志,但登錄狀態應該被視為一個全局屬性,而不應該只寫在某一組件中。所以我們需要引入一個新的工具——Vuex,它是專門為 Vue 開發的狀態管理方案,我們可以把需要在各個組件中傳遞使用的變量、方法定義在這里。之前我一直沒有使用它,所以在不同組件傳值的問題上十分頭疼,要寫很多多余的代碼來調用不同組件的值,所以推薦大家從一開始就去熟悉這種管理方式。
4.1. 引入 Vuex
在我們的項目文件夾中,運行 npm install vuex --save,之后,在 src 目錄下新建一個文件夾 store,并在該目錄下新建 index.js 文件,在該文件中引入 vue 和 vuex,代碼如下:
安裝前:
安裝后:
之后,我們在 index.js 里設置我們需要的狀態變量和方法。為了實現登錄攔截器,我們需要一個記錄用戶信息的變量。為了方便日后的擴展(權限認證等),我們使用一個用戶對象而不是僅僅使用一個布爾變量。同時,設置一個方法,觸發這個方法時可以為我們的用戶對象賦值。完整的代碼如下:
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {user: {username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username}},mutations: {login (state, user) {state.user = userwindow.localStorage.setItem('user', JSON.stringify(user))}} })這里我們還用到了 localStorage,即本地存儲,在項目打開的時候會判斷本地存儲中是否有 user 這個對象存在,如果存在就取出來并獲得 username 的值,否則則把 username 設置為空。這樣我們只要不清除緩存,登錄的狀態就會一直保存。
4.2. 修改路由配置
為了區分頁面是否需要攔截,我們需要修改一下 src\router\index.js,在需要攔截的路由中加一條元數據,設置一個 requireAuth 字段如下:
{path: '/index',name: 'AppIndex',component: AppIndex,meta: {requireAuth: true}}完整的 index.js 代碼如下:
import Vue from 'vue' import Router from 'vue-router' import AppIndex from '@/components/home/AppIndex' import Login from '@/components/Login'Vue.use(Router)export default new Router({mode: 'history',routes: [{path: '/login',name: 'Login',component: Login},{path: '/index',name: 'AppIndex',component: AppIndex,meta: {requireAuth: true}}]`在這里插入代碼片` })4.3. 使用鉤子函數判斷是否攔截
鉤子函數及在某些時機會被調用的函數。這里我們使用 router.beforeEach(),意思是在訪問每一個路由前調用。
打開 src\main.js ,首先添加對 store 的引用
import store from './store'并修改 Vue 對象里的內容
new Vue({el: '#app',render: h => h(App),router,// 注意這里store,components: { App },template: '<App/>' })接著寫 beforeEach() 函數
router.beforeEach((to, from, next) => {if (to.meta.requireAuth) {if (store.state.user.username) {next()} else {next({path: 'login',query: {redirect: to.fullPath}})}} else {next()}} )這個的邏輯很簡單,首先判斷訪問的路徑是否需要登錄,如果需要,判斷 store 里有沒有存儲 user 的信息,如果存在,則放行,否則跳轉到登錄頁面,并存儲訪問的頁面路徑(以便在登錄后跳轉到訪問頁)。
完整的 main.js 代碼如下:
import Vue from 'vue' import App from './App' import router from './router' import store from './store'// 引入ElementUI import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css'var axios = require('axios') // 設置反向代理,前端請求默認發送到 http://localhost:8443/api axios.defaults.baseURL = 'http://localhost:8443/api' // 全局注冊,之后可在其它組件中通過 this.$axios 發送數據 Vue.prototype.$axios = axios Vue.config.productionTip = false // Vue使用ElementUI Vue.use(ElementUI)router.beforeEach((to, from, next) => {if (to.meta.requireAuth) {if (store.state.user.username) {next()} else {next({path: 'login',query: {redirect: to.fullPath}})}} else {next()} } )/* eslint-disable no-new */ new Vue({el: '#app',render: h => h(App),router,// 注意這里store,components: { App },template: '<App/>' })4.4. 修改 Login.vue
之前的登錄組件中,我們只是判斷后端返回的狀態碼,如果是 200,就重定向到首頁。在經過前面的配置后,我們需要修改一下登錄邏輯,以最終實現登錄攔截。
修改后的邏輯如下:
1.點擊登錄按鈕,向后端發送數據
2.受到后端返回的成功代碼時,觸發 store 中的 login() 方法,把 loginForm 對象傳遞給 store 中的 user 對象
(*這里只是簡單的實現,在后端我們可以通過用戶名和密碼查詢數據庫,獲得 user 表的完整信息,比如用戶昵稱、用戶級別等,返回前端,并傳遞給 user 對象,以實現更復雜的功能)
3.獲取登錄前頁面的路徑并跳轉,如果該路徑不存在,則跳轉到首頁
修改后的 login() 方法如下:
login () {var _this = thisconsole.log(this.$store.state)this.$axios.post('/login', {username: this.loginForm.username,password: this.loginForm.password}).then(successResponse => {if (successResponse.data.code === 200) {// var data = this.loginForm_this.$store.commit('login', _this.loginForm)var path = this.$route.query.redirectthis.$router.replace({path: path === '/' || path === undefined ? '/index' : path})}}).catch(failResponse => {}) }完整的 Login.vue 代碼如下
<template><body id="poster"><el-form class="login-container" label-position="left"label-width="0px"><h3 class="login_title">系統登錄</h3><el-form-item><el-input type="text" v-model="loginForm.username"auto-complete="off" placeholder="賬號"></el-input></el-form-item><el-form-item><el-input type="password" v-model="loginForm.password"auto-complete="off" placeholder="密碼"></el-input></el-form-item><el-form-item style="width: 100%"><el-button type="primary" style="width: 100%;background: #505458;border: none" v-on:click="login">登錄</el-button></el-form-item></el-form></body> </template><script>export default {name: 'Login',data () {return {loginForm: {username: 'admin',password: '123'},responseResult: []}},methods: {login () {var _this = thisconsole.log(this.$store.state)this.$axios.post('/login', {username: this.loginForm.username,password: this.loginForm.password}).then(successResponse => {if (successResponse.data.code === 200) {// var data = this.loginForm_this.$store.commit('login', _this.loginForm)var path = this.$route.query.redirectthis.$router.replace({path: path === '/' || path === undefined ? '/index' : path})}}).catch(failResponse => {})}}} </script><style>#poster {background:url("../assets/eva.jpg") no-repeat;background-position: center;height: 100%;width: 100%;background-size: cover;position: fixed;}body{margin: 0px;}.login-container {border-radius: 15px;background-clip: padding-box;margin: 90px auto;width: 350px;padding: 35px 35px 15px 35px;background: #fff;border: 1px solid #eaeaea;box-shadow: 0 0 25px #cac6c6;}.login_title {margin: 0px auto 40px auto;text-align: center;color: #505458;}</style>4.5. 效果檢驗
同時運行前后端項目,訪問 http://localhost:8080/index ,發現頁面直接跳轉到了 http://localhost:8080/login?redirect=%2Findex
輸入賬號密碼后登錄,成功跳轉到 http://localhost:8080/index ,之后再次訪問則無需登錄(除非清除緩存)。
讀者大人們請原諒我沒有及時更新,作為一個不知名博主,十分感謝大家的支持。我會堅持下去的,希望大家都能堅持做自己想做的事,一起變得更加牛批!
總結
以上是生活随笔為你收集整理的Vue + Spring Boot 项目实战(七):前端路由与登录拦截器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql遇见Expression #1
- 下一篇: 'vue-cli-service' 不是