Vue2 + ElementUI 登录页保姆级教程:从接口封装到路由跳转的完整流程
Vue2 ElementUI 登录页实战从接口封装到状态管理的企业级解决方案登录功能作为Web应用的入口其稳定性和安全性直接影响用户体验。本文将带您从零构建一个生产级别的Vue2登录系统涵盖API封装、Token管理、路由守卫等核心环节而非简单的界面绘制。1. 项目初始化与基础配置在开始登录功能开发前我们需要搭建一个规范的Vue2项目结构。与简单演示不同这里采用模块化思想组织代码为后续维护和扩展打下基础。首先创建Vue项目并安装必要依赖vue create vue2-login-demo cd vue2-login-demo yarn add vue-router axios element-ui less less-loader项目目录结构建议如下src/ ├── api/ # 接口封装层 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 └── App.vue在main.js中全局引入ElementUI时推荐按需加载以优化打包体积import Vue from vue import { Button, Form, FormItem, Input, Message } from element-ui Vue.use(Button) Vue.use(Form) Vue.use(FormItem) Vue.use(Input) Vue.prototype.$message Message2. 接口服务层封装企业级项目中API请求应当统一管理。我们创建src/utils/request.js作为axios实例的配置文件import axios from axios import { Message } from element-ui // 创建axios实例 const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(access_token) if (token) { config.headers[Authorization] Bearer ${token} } return config }, error { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || Error) return Promise.reject(new Error(res.message || Error)) } return res }, error { Message.error(error.message) return Promise.reject(error) } ) export default service接着在src/api/login.js中封装登录接口import request from /utils/request export function login(data) { return request({ url: /auth/login, method: post, data }) } export function getInfo() { return request({ url: /auth/info, method: get }) }这种分层设计带来三大优势统一错误处理机制自动携带Token便于接口复用和维护3. 登录页面与表单验证使用ElementUI构建登录表单时我们需要关注三个核心点UI布局、表单验证和交互逻辑。首先创建src/views/login/index.vuetemplate div classlogin-container el-form refloginForm :modelloginForm :rulesloginRules classlogin-form auto-completeon div classtitle-container h3 classtitle系统登录/h3 /div el-form-item propusername el-input refusername v-modelloginForm.username placeholder用户名 nameusername typetext tabindex1 auto-completeon / /el-form-item el-form-item proppassword el-input :keypasswordType refpassword v-modelloginForm.password :typepasswordType placeholder密码 namepassword tabindex2 auto-completeon keyup.enter.nativehandleLogin / /el-form-item el-button :loadingloading typeprimary stylewidth:100%;margin-bottom:30px; click.native.preventhandleLogin 登录 /el-button /el-form /div /template script import { validUsername } from /utils/validate import { login } from /api/auth export default { name: Login, data() { const validateUsername (rule, value, callback) { if (!validUsername(value)) { callback(new Error(请输入正确的用户名)) } else { callback() } } const validatePassword (rule, value, callback) { if (value.length 6) { callback(new Error(密码不能少于6位)) } else { callback() } } return { loginForm: { username: admin, password: 123456 }, loginRules: { username: [{ required: true, trigger: blur, validator: validateUsername }], password: [{ required: true, trigger: blur, validator: validatePassword }] }, loading: false, passwordType: password, redirect: undefined } }, watch: { $route: { handler: function(route) { this.redirect route.query route.query.redirect }, immediate: true } }, methods: { handleLogin() { this.$refs.loginForm.validate(valid { if (valid) { this.loading true login(this.loginForm) .then(response { this.$store.dispatch(user/login, response.data) this.$router.push({ path: this.redirect || / }) }) .catch(() { this.loading false }) } }) } } } /script表单验证规则单独提取到src/utils/validate.js中export function validUsername(str) { const valid_map [admin, editor] return valid_map.indexOf(str.trim()) 0 }4. 状态管理与路由守卫使用Vuex集中管理登录状态能确保应用各组件间的数据一致性。创建src/store/modules/user.jsimport { login, getInfo } from /api/auth import { getToken, setToken, removeToken } from /utils/auth const state { token: getToken(), name: , avatar: , roles: [] } const mutations { SET_TOKEN: (state, token) { state.token token }, SET_NAME: (state, name) { state.name name }, SET_AVATAR: (state, avatar) { state.avatar avatar }, SET_ROLES: (state, roles) { state.roles roles } } const actions { login({ commit }, userInfo) { const { username, password } userInfo return new Promise((resolve, reject) { login({ username: username.trim(), password: password }) .then(response { const { data } response commit(SET_TOKEN, data.token) setToken(data.token) resolve() }) .catch(error { reject(error) }) }) }, getInfo({ commit, state }) { return new Promise((resolve, reject) { getInfo(state.token) .then(response { const { data } response commit(SET_ROLES, data.roles) commit(SET_NAME, data.name) commit(SET_AVATAR, data.avatar) resolve(data) }) .catch(error { reject(error) }) }) }, logout({ commit }) { return new Promise(resolve { commit(SET_TOKEN, ) removeToken() resolve() }) } } export default { namespaced: true, state, mutations, actions }在路由配置中添加全局前置守卫实现权限控制import router from ./router import store from ./store import { Message } from element-ui import NProgress from nprogress NProgress.configure({ showSpinner: false }) const whiteList [/login] router.beforeEach(async (to, from, next) { NProgress.start() const hasToken store.getters.token if (hasToken) { if (to.path /login) { next({ path: / }) NProgress.done() } else { const hasRoles store.getters.roles store.getters.roles.length 0 if (hasRoles) { next() } else { try { const { roles } await store.dispatch(user/getInfo) const accessRoutes await store.dispatch(permission/generateRoutes, roles) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } catch (error) { await store.dispatch(user/logout) Message.error(error || Has Error) next(/login?redirect${to.path}) NProgress.done() } } } } else { if (whiteList.indexOf(to.path) ! -1) { next() } else { next(/login?redirect${to.path}) NProgress.done() } } }) router.afterEach(() { NProgress.done() })5. 安全加固与性能优化生产环境中的登录功能需要考虑更多安全因素Token存储方案对比存储方式安全性持久性XSS风险CSRF风险实现复杂度localStorage中高高低低sessionStorage中低高低低Cookie HttpOnly高可配置低高中内存存储高低低低高推荐组合方案短期Token存内存长期Refresh Token存HttpOnly Cookie敏感操作需二次验证接口防刷策略// 在request.js中添加防抖逻辑 let pendingRequests new Map() const addPending (config) { const key ${config.url}${config.method} if (pendingRequests.has(key)) { pendingRequests.get(key).abort() } const controller new AbortController() config.signal controller.signal pendingRequests.set(key, controller) } const removePending (config) { const key ${config.url}${config.method} if (pendingRequests.has(key)) { pendingRequests.delete(key) } } // 在请求拦截器中 service.interceptors.request.use(config { removePending(config) addPending(config) // ...其他逻辑 }) // 在响应拦截器中 service.interceptors.response.use(response { removePending(response.config) // ...其他逻辑 })性能优化建议使用路由懒加载const Login () import(/views/login/index)表单组件按需注册接口请求节流处理关键资源预加载6. 扩展功能实现现代登录系统通常需要支持多种登录方式下面展示第三方登录的集成方法微信登录流程前端生成state参数防止CSRF攻击跳转到微信授权页面后端处理回调获取用户信息返回系统自定义Token// 在登录组件中添加 methods: { wechatLogin() { const state Math.random().toString(36).substring(7) localStorage.setItem(wx_state, state) const appId YOUR_APP_ID const redirectUri encodeURIComponent(${window.location.origin}/auth/wechat/callback) const url https://open.weixin.qq.com/connect/qrconnect?appid${appId}redirect_uri${redirectUri}response_typecodescopesnsapi_loginstate${state}#wechat_redirect window.location.href url } }短信验证码登录实现要点使用图形验证码防止机器请求服务端限制发送频率验证码有效期控制失败次数限制template el-form-item propmobile el-input v-modelloginForm.mobile placeholder手机号 / /el-form-item el-form-item propcaptcha div classcaptcha-container el-input v-modelloginForm.captcha placeholder验证码 / el-button :disabledcountdown 0 clicksendSms {{ countdown ? ${countdown}s后重试 : 获取验证码 }} /el-button /div /el-form-item /template script export default { data() { return { countdown: 0 } }, methods: { sendSms() { if (!this.validateMobile()) return this.countdown 60 const timer setInterval(() { this.countdown-- if (this.countdown 0) { clearInterval(timer) } }, 1000) // 调用发送短信接口 } } } /script7. 错误处理与监控完善的错误处理机制能显著提升用户体验常见登录错误分类错误类型处理方式用户提示网络错误自动重试3次网络异常请检查连接验证失败清除密码输入框账号或密码错误账号锁定禁用登录按钮30分钟尝试次数过多请稍候服务端异常记录日志并通知运维系统繁忙请稍后重试前端监控实现// 在响应拦截器中添加 service.interceptors.response.use( response { // ...原有逻辑 }, error { if (error.response) { const { status, data } error.response logError({ type: API_ERROR, url: error.config.url, status, message: data.message, time: new Date() }) } return Promise.reject(error) } ) function logError(errorInfo) { if (process.env.NODE_ENV production) { // 发送到监控系统 navigator.sendBeacon(/log/error, JSON.stringify(errorInfo)) } else { console.error(Error:, errorInfo) } }性能监控指标登录接口响应时间页面加载时间资源加载异常用户操作路径// 在登录方法中添加性能标记 methods: { handleLogin() { const startTime performance.now() login(this.loginForm).then(() { const loadTime performance.now() - startTime trackPerformance(login, loadTime) }) } }