Browse Source

feat: 引入路由拦截

菲鸽 2 năm trước cách đây
mục cha
commit
d6d1a7a7be
7 tập tin đã thay đổi với 248 bổ sung50 xóa
  1. 2 0
      src/interceptors/index.ts
  2. 53 0
      src/interceptors/request.ts
  3. 54 0
      src/interceptors/route.ts
  4. 3 0
      src/main.ts
  5. 4 1
      src/store/user.ts
  6. 39 49
      src/utils/http.ts
  7. 93 0
      src/utils/index.ts

+ 2 - 0
src/interceptors/index.ts

@@ -0,0 +1,2 @@
+export { routeInterceptor } from './route'
+export { requestInterceptor } from './request'

+ 53 - 0
src/interceptors/request.ts

@@ -0,0 +1,53 @@
+/* eslint-disable no-param-reassign */
+import qs from 'qs'
+import { useUserStore } from '@/store'
+
+export type CustomRequestOptions = UniApp.RequestOptions & {
+  query?: Record<string, any>
+} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
+
+// 请求基地址
+const baseURL = import.meta.env.VITE_SERVER_BASEURL
+
+// 拦截器配置
+const httpInterceptor = {
+  // 拦截前触发
+  invoke(options: CustomRequestOptions) {
+    // 接口请求支持通过 query 参数配置 queryString
+    if (options.query) {
+      const queryStr = qs.stringify(options.query)
+      if (options.url.includes('?')) {
+        options.url += `&${queryStr}`
+      } else {
+        options.url += `?${queryStr}`
+      }
+    }
+
+    // 1. 非 http 开头需拼接地址
+    if (!options.url.startsWith('http')) {
+      options.url = baseURL + options.url
+    }
+    // 2. 请求超时
+    options.timeout = 10000 // 10s
+    // 3. 添加小程序端请求头标识
+    options.header = {
+      platform: 'mp-weixin', // 可选值与 uniapp 定义的平台一致,告诉后台来源
+      ...options.header,
+    }
+    // 4. 添加 token 请求头标识
+    const userStore = useUserStore()
+    const { token } = userStore.userInfo as unknown as IUserInfo
+    if (token) {
+      options.header.Authorization = `Bearer ${token}`
+    }
+  },
+}
+
+export const requestInterceptor = {
+  install() {
+    // 拦截 request 请求
+    uni.addInterceptor('request', httpInterceptor)
+    // 拦截 uploadFile 文件上传
+    uni.addInterceptor('uploadFile', httpInterceptor)
+  },
+}

+ 54 - 0
src/interceptors/route.ts

@@ -0,0 +1,54 @@
+/**
+ * by 菲鸽 on 2024-03-06
+ * 路由拦截,通常也是登录拦截
+ * 可以设置路由白名单,或者黑名单,看业务需要选哪一个
+ * 我这里应为大部分都可以随便进入,所以使用黑名单
+ */
+import { useUserStore } from '@/store'
+import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils'
+
+// TODO Check
+const loginRoute = '/pages/login/index'
+
+const isLogined = () => {
+  const userStore = useUserStore()
+  return userStore.isLogined
+}
+
+const isDev = import.meta.env.DEV
+
+// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
+const navigateToInterceptor = {
+  // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
+  invoke({ url }: { url: string }) {
+    console.log(url) // /pages/route-interceptor/index?name=feige&age=30
+    const path = url.split('?')[0]
+    let needLoginPages: string[] = []
+    // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
+    if (isDev) {
+      needLoginPages = getNeedLoginPages()
+    } else {
+      needLoginPages = _needLoginPages
+    }
+    console.log(needLoginPages.includes(path))
+
+    if (needLoginPages.includes(path)) {
+      const isLogin = isLogined()
+      if (isLogin) {
+        return true
+      }
+      const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
+      uni.navigateTo({ url: redirectRoute })
+      return false
+    }
+    return true
+  },
+}
+
+export const routeInterceptor = {
+  install() {
+    uni.addInterceptor('navigateTo', navigateToInterceptor)
+    uni.addInterceptor('reLaunch', navigateToInterceptor)
+    uni.addInterceptor('redirectTo', navigateToInterceptor)
+  },
+}

+ 3 - 0
src/main.ts

@@ -1,6 +1,7 @@
 import { createSSRApp } from 'vue'
 import App from './App.vue'
 import store from './store'
+import { routeInterceptor, requestInterceptor } from './interceptors'
 import 'virtual:svg-icons-register'
 import 'virtual:uno.css'
 import '@/style/index.scss'
@@ -8,6 +9,8 @@ import '@/style/index.scss'
 export function createApp() {
   const app = createSSRApp(App)
   app.use(store)
+  app.use(routeInterceptor)
+  app.use(requestInterceptor)
   return {
     app,
   }

+ 4 - 1
src/store/user.ts

@@ -13,16 +13,19 @@ export const useUserStore = defineStore(
     }
 
     const clearUserInfo = () => {
-      userInfo.value = undefined
+      userInfo.value = { ...initState }
     }
+    // 一般没有reset需求,不需要的可以删除
     const reset = () => {
       userInfo.value = { ...initState }
     }
+    const isLogined = computed(() => !!userInfo.value.token)
 
     return {
       userInfo,
       setUserInfo,
       clearUserInfo,
+      isLogined,
       reset,
     }
   },

+ 39 - 49
src/utils/http.ts

@@ -1,51 +1,4 @@
-/* eslint-disable no-param-reassign */
-import qs from 'qs'
-import { useUserStore } from '@/store'
-
-type CustomRequestOptions = UniApp.RequestOptions & { query?: Record<string, any> }
-
-// 请求基地址
-const baseURL = import.meta.env.VITE_SERVER_BASEURL
-// console.log(import.meta.env)
-
-// 拦截器配置
-const httpInterceptor = {
-  // 拦截前触发
-  invoke(options: CustomRequestOptions) {
-    // 接口请求支持通过 query 参数配置 queryString
-    if (options.query) {
-      const queryStr = qs.stringify(options.query)
-      if (options.url.includes('?')) {
-        options.url += `&${queryStr}`
-      } else {
-        options.url += `?${queryStr}`
-      }
-    }
-
-    // 1. 非 http 开头需拼接地址
-    if (!options.url.startsWith('http')) {
-      options.url = baseURL + options.url
-    }
-    // 2. 请求超时
-    options.timeout = 10000 // 10s
-    // 3. 添加小程序端请求头标识
-    options.header = {
-      platform: 'mp-weixin', // 可选值与 uniapp 定义的平台一致,告诉后台来源
-      ...options.header,
-    }
-    // 4. 添加 token 请求头标识
-    const userStore = useUserStore()
-    const { token } = userStore.userInfo as unknown as IUserInfo
-    if (token) {
-      options.header.Authorization = `Bearer ${token}`
-    }
-  },
-}
-
-// 拦截 request 请求
-uni.addInterceptor('request', httpInterceptor)
-// 拦截 uploadFile 文件上传
-uni.addInterceptor('uploadFile', httpInterceptor)
+import { CustomRequestOptions } from '@/interceptors/request'
 
 export const http = <T>(options: CustomRequestOptions) => {
   // 1. 返回 Promise 对象
@@ -88,4 +41,41 @@ export const http = <T>(options: CustomRequestOptions) => {
   })
 }
 
-export default http
+// uni.uploadFile封装
+export const uniFileUpload = <T>(options: CustomRequestOptions) => {
+  // 1. 返回 Promise 对象
+  return new Promise<IResData<T>>((resolve, reject) => {
+    uni.uploadFile({
+      ...options,
+      // 响应成功
+      success(res) {
+        // 状态码 2xx,参考 axios 的设计
+        if (res.statusCode >= 200 && res.statusCode < 300) {
+          // 文件上传接口的rea.data的类型为string,这里转一下
+          const resData = JSON.parse(res.data) as IResData<T>
+          resolve(resData)
+        } else if (res.statusCode === 401) {
+          // 401错误  -> 清理用户信息,跳转到登录页
+          // userStore.clearUserInfo()
+          // uni.navigateTo({ url: '/pages/login/login' })
+          reject(res)
+        } else {
+          // 其他错误 -> 根据后端错误信息轻提示
+          uni.showToast({
+            icon: 'none',
+            title: '文件上传错误',
+          })
+          reject(res)
+        }
+      },
+      // 响应失败
+      fail(err) {
+        uni.showToast({
+          icon: 'none',
+          title: '网络错误,换个网络试试',
+        })
+        reject(err)
+      },
+    })
+  })
+}

+ 93 - 0
src/utils/index.ts

@@ -13,6 +13,99 @@ export const getIsTabbar = () => {
   return !!pagesJson.tabBar.list.find((e) => e.pagePath === currPath)
 }
 
+/**
+ * 获取当前页面路由的 path 路劲和 redirectPath 路径
+ * path 如 ‘/pages/login/index’
+ * redirectPath 如 ‘/pages/demo/base/route-interceptor’
+ */
+export const currRoute = () => {
+  const pages = getCurrentPages()
+  console.log('pages:', pages)
+
+  const lastPage = getArrElementByIdx(pages, -1)
+  const currRoute = (lastPage as any).$page
+  // console.log('lastPage.$page:', currRoute)
+  // console.log('lastPage.$page.fullpath:', currRoute.fullPath)
+  // console.log('lastPage.$page.options:', currRoute.options)
+  // console.log('lastPage.options:', (lastPage as any).options)
+  // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
+  const { fullPath } = currRoute as { fullPath: string }
+  console.log(fullPath)
+  // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
+  // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
+  return getUrlObj(fullPath)
+}
+
+const ensureDecodeURIComponent = (url: string) => {
+  if (url.startsWith('%')) {
+    return ensureDecodeURIComponent(decodeURIComponent(url))
+  }
+  return url
+}
+/**
+ * 解析 url 得到 path 和 query
+ * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
+ * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
+ */
+export const getUrlObj = (url: string) => {
+  const [path, queryStr] = url.split('?')
+  console.log(path, queryStr)
+
+  const query: Record<string, string> = {}
+  queryStr.split('&').forEach((item) => {
+    const [key, value] = item.split('=')
+    console.log(key, value)
+    query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y
+  })
+  return { path, query }
+}
+/**
+ * 得到所有的需要登录的pages,包括主包和分包的
+ * 这里设计得通用一点,可以传递key作为判断依据,默认是 needLogin, 与 route-block 配对使用
+ * 如果没有传 key,则表示所有的pages,如果传递了 key, 则表示通过 key 过滤
+ */
+export const getAllPages = (key = 'needLogin') => {
+  // 这里处理主包
+  const pages = [
+    ...pagesJson.pages
+      .filter((page) => !key || page[key])
+      .map((page) => ({
+        ...page,
+        path: `/${page.path}`,
+      })),
+  ]
+  // 这里处理分包
+  const subPages: any[] = []
+  pagesJson.subPackages.forEach((subPageObj) => {
+    // console.log(subPageObj)
+    const { root } = subPageObj
+
+    subPageObj.pages
+      .filter((page) => !key || page[key])
+      .forEach((page: { path: string } & Record<string, any>) => {
+        subPages.push({
+          ...page,
+          path: `/${root}/${page.path}`,
+        })
+      })
+  })
+  const result = [...pages, ...subPages]
+  console.log(`getAllPages by ${key} result: `, result)
+  return result
+}
+
+/**
+ * 得到所有的需要登录的pages,包括主包和分包的
+ * 只得到 path 数组
+ */
+export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map((page) => page.path)
+
+/**
+ * 得到所有的需要登录的pages,包括主包和分包的
+ * 只得到 path 数组
+ */
+export const needLoginPages: string[] = getAllPages('needLogin').map((page) => page.path)
+
 export const getArrElementByIdx = (arr: any[], index: number) => {
   if (index < 0) return arr[arr.length + index]
   if (index >= arr.length) return undefined