浅学前端:Vue篇(五)

2023-11-15 15:00:13 浏览数 (1)

这里选择了 vue-element-admin 这个项目骨架,它采用的技术与我们之前学过的较为契合

  • vue 2
  • element-ui 2
  • vue-router 3
  • vuex 3
  • axios

1. 安装

代码语言:javascript复制
 git clone https://gitee.com/panjiachen/vue-element-admin.git study03_vue2_client_action
 ​
 cd study03_vue2_client_action
 ​
 # 列出所有分支
 git branch -a
 ​
 # 我们当前在的master分支是只支持英文的,需要切换分支
 # git checkout -b 创建并切换分支
 git checkout -b i18n remotes/origin/i18n
 ​
 # 将git的地址凡是以git://打头的,都替换为https://打头
 # 因为npm的过程需要访问以为git仓库,如果是git:// 打头,下载的时候可能会出现问题
 git config --global url."https://".insteadOf git://
 ​
 npm install
 ​
 npm run dev
  • 需要切换分支到 i18n,否则不支持国际化(中文)功能
  • npm install 要多试几次,因为中间会连接 gitbub 下载一些依赖,网络不稳定会导致失败
  • npm run dev 运行后回自动打开浏览器,使用的端口是 9527

2. 后端路径

此时系统已经运行起来了 ,会有同学有疑问,它没有后端服务器的支撑,是怎么完成整个登录的流程的呢,整个登录的流程是如何走通的呢?

实际上点击登录按钮之后,是会发一个真正的请求,只不过这个请求不是发给后台的,是发给9527自己的,9527里有一段自己的代码来处理请求,只不过他返回了一个mock的响应(假的响应),这个加的响应就包含了登录需要的一些模拟数据。

具体的跟着视频

开发环境下执行下面命令

代码语言:javascript复制
 npm run dev
  • 会同时启动 mock-server

根据刚才登录发起的请求,通过后缀user/login可以找到两个文件:

我们想要让他不把请求发到自己mock的服务端,而是发给我们自己的后端,需要修改这个baseURL,根据刚才请求的前缀可以找到开发环境的baseURL在文件 .env.development 中:

在开发环境下,后端访问路径起始路径配置在文件 .env.development

代码语言:javascript复制
 # base api
 # VUE_APP_BASE_API = '/dev-api'
 VUE_APP_BASE_API = 'http://localhost:8080/api'
  • 默认向后台的请求都发给 http://localhost:9527/dev-api 的 mock-server 获得的都是模拟数据
  • 需要跟真实后台联调时,可以改动以上地址为 VUE_APP_BASE_API = 'http://localhost:8080/api'

修改baseURL之后需要重启服务器

发送请求的 axios 工具被封装在 src/utils/request.js 中

代码语言:javascript复制
 import axios from 'axios'
 import { MessageBox, Message } from 'element-ui'
 import store from '@/store'
 import { getToken } from '@/utils/auth'
 ​
 // create an axios instance
 const service = axios.create({
   baseURL: process.env.VUE_APP_BASE_API, // url = base url   request url
   // withCredentials: true, // send cookies when cross-domain requests
   timeout: 5000 // request timeout
 })
 ​
 // ...

原有代码的 URI 路径都是这样的:

代码语言:javascript复制
 /vue-element-admin/user/login
 /vue-element-admin/user/info
 /vue-element-admin/user/logout
 ...

如果觉得不爽,可以来一个全局替换:

代码语言:javascript复制
 /user/login
 /user/info
 /user/logout
 ...

token 的请求头修改一下,在 src/utils/request.js 中

代码语言:javascript复制
 ...
 service.interceptors.request.use(
   config => {
     // do something before request is sent
 ​
     if (store.getters.token) {
       // let each request carry token
       // ['X-Token'] is a custom headers key
       // please modify it according to the actual situation
       config.headers['Authorization'] = getToken()
     }
     return config
   },
   error => {
     // do something with request error
     console.log(error) // for debug
     return Promise.reject(error)
   }
 )
 ...

登录流程

1. src/views/login/index.vue
代码语言:javascript复制
 <script>
 import { validUsername } from '@/utils/validate'
 import LangSelect from '@/components/LangSelect'
 import SocialSign from './components/SocialSignin'
 ​
 export default {
   // ...
   methods: {    
     handleLogin() {
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true
           this.$store.dispatch('user/login', this.loginForm)
             .then(() => {
               this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
               this.loading = false
             })
             .catch(() => {
               this.loading = false
             })
         } else {
           console.log('error submit!!')
           return false
         }
       })
     }
     // ...
   }
 }
 </script>

这里调用了 store 的 actions,user/login

  • 因为是异步调用,因此只能用 actions
  • 登录成功会优先跳转至 this.redirect 路径、否则跳转至 /
  • / 查看 src/router/index.js 的路由表可知,会重定向至 /dashboard
2. src/store/modules/user.js
代码语言:javascript复制
 import { login, logout, getInfo } from '@/api/user'
 // ...
 const actions = {
   // user login
   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)
       })
     })
   }
   // ...
 }
  • 发请求用了 src/api/user.js,请求成功使用 commit 将 token 存入 mutations,同时往 cookie 存储了一份
  • 这里的 response 其实是真正的 response.data,见后面的说明
  • 评价
    • 向 cookie 或 sessionStorage 存储 token 即可,token 无需做成响应式,不必放入 store
    • 作者使用了 Promise API,其实可以改变为 await 方式,提高可读性
3. src/api/user.js
代码语言:javascript复制
 import request from '@/utils/request'
 ​
 export function login(data) {
   return request({
     url: '/user/login',
     method: 'post',
     data
   })
 }
 ​
 // ...
  • 其中 request 相当于我们之前封装的 myaxios
4. src/utils/request.js
代码语言:javascript复制
 import axios from 'axios'
 import { MessageBox, Message } from 'element-ui'
 import store from '@/store'
 import { getToken } from '@/utils/auth'
 ​
 // create an axios instance
 const service = axios.create({
   baseURL: process.env.VUE_APP_BASE_API, // url = base url   request url
   // withCredentials: true, // send cookies when cross-domain requests
   timeout: 5000 // request timeout
 })
 ​
 // ... 
 ​
 service.interceptors.response.use(
   // ...
   response => {
     const res = response.data
     if (res.code !== 20000) {
       // ...
     } else {
       return res
     }
   },
   error => {
     // ...
   }
 )
 ​
 export default service
  • 其中响应拦截器发现响应正确,返回 resp.data 这样,其它处代码解构时少了一层 data
5. src/permission.js

登录成功后,只是获得了 token,还未获取用户信息,获取用户信息是在路由跳转的 beforeEach 里做的

关键代码

代码语言:javascript复制
 import router from './router'
 ​
 // ...
 ​
 router.beforeEach(async(to, from, next) => {
   // ...
   const hasToken = getToken()
 ​
   if (hasToken) {
     if (to.path === '/login') {
       // ...
     } else {
       // ...
       const { roles } = await store.dispatch('user/getInfo')
       // ...
     }
   } else {
     // ...
   }
 })
  • 登录后跳转至 / 之前进入这里的 beforeEach 方法,方法内主要做两件事
    • 一是调用 actions 方法获取用户角色,见 6
    • 二是根据用户角色,动态生成路由,见 7
6. src/store/modules/user.js

这里用其中 getInfo 方法获取用户信息,其中角色返回给 beforeEach

代码语言:javascript复制
 import { login, logout, getInfo } from '@/api/user'
 // ...
 const actions = {
   getInfo({ commit, state }) {
     return new Promise((resolve, reject) => {
       getInfo(state.token).then(response => {
         const { data } = response
 ​
         if (!data) {
           reject('Verification failed, please Login again.')
         }
 ​
         const { roles, name, avatar, introduction } = data
 ​
         if (!roles || roles.length <= 0) {
           reject('getInfo: roles must be a non-null array!')
         }
 ​
         commit('SET_ROLES', roles)
         commit('SET_NAME', name)
         commit('SET_AVATAR', avatar)
         commit('SET_INTRODUCTION', introduction)
         resolve(data)
       }).catch(error => {
         reject(error)
       })
     })
   }
 }
7. src/router/index.js

路由表中路由分成两部分,静态路由与动态路由

代码语言:javascript复制
 export const constantRoutes = [
   // ...
   {
     path: '/login',
     component: () => import('@/views/login/index'),
     hidden: true
   },
   {
     path: '/',
     component: Layout,
     redirect: '/dashboard',
     children: [
       {
         path: 'dashboard',
         component: () => import('@/views/dashboard/index'),
         name: 'Dashboard',
         meta: { title: 'dashboard', icon: 'dashboard', affix: true }
       }
     ]
   }
   // ...
 ]
  • 其中 hidden: true 的路由只做路由跳转,不会在左侧导航菜单展示

动态路由

代码语言:javascript复制
 export const asyncRoutes = [
   {
     path: '/permission',
     component: Layout,
     redirect: '/permission/page',
     alwaysShow: true, // will always show the root menu
     name: 'Permission',
     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: 'role',
         component: () => import('@/views/permission/role'),
         name: 'RolePermission',
         meta: {
           title: 'rolePermission',
           roles: ['admin']
         }
       }
     ]
   },
 ​
   {
     path: '/icon',
     component: Layout,
     children: [
       {
         path: 'index',
         component: () => import('@/views/icons/index'),
         name: 'Icons',
         meta: { title: 'icons', icon: 'icon', noCache: true, roles: ['admin'] }
       }
     ]
   }
   // ...
 }
  • 动态路由中关联了角色信息,根据用户的角色决定那些路由可用,但这样做的缺点是把角色和路由绑定死了
8. src/layout/index.vue

它对应的是我们之前介绍的 Container.vue 完成主页布局的,路由路径是 /

其中又由多部分组成,其中固定不变的是

  • 侧边栏
  • 导航栏
  • 标签栏
  • 设置

变化的是中间的 dashboard 部分(AppMain),它由 router-view 配合子路由切换显示

  • 进入 / 后,就会 redirect 重定向到 /dashboard 子路由
  • 进入首页后,会有一个 /api/transaction/list 的后台请求报 404,作为练习,把它补充完整

第三方登录

  1. 9527 打开新窗口,请求 https://gitee.com/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code
  2. gitee 认证通过,重定向至 8080,并携带 code
  3. 8080 发送请求 https://gitee.com/oauth/token 携带 client_id、client_secret、code,gitee 返回 access_token 给 8080
    • 这时走的是 https 协议,并且不经过浏览器,能够保证数据传输的安全性
    • 重定向到 8080 时,如果被有心人拿到了 code,也没事,因为接下来会把 client_secret 发给 gitee 验证(client_secret 应当只存在 8080),只要 client_secret 不泄露,就可以保证安全
    • 如果改成前端拿 code 换 access_token,那就意味着 access_token 得保存在前端,所有保存在前端的都有风险
  4. 8080 可以访问 gitee 的 api 了,拿到用户信息,存入数据库,返回 8080 的 token
  5. 8080 可以通过 window.opener.postMessage 把 token 给 9527 的老窗口
    • 这里又会涉及到跨域,不过 9527 与 8080 直接存在信任关系,设置一下就好
  6. 9527 再走之前的逻辑就可以了,在 router 的 beforeEach 方法里,用 8080 token 换用户信息

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞