实现uniapp路由守卫(前置加后置)

2,093 阅读4分钟

前言

因为uniapp没有自带路由守卫这个功能,然后每次做一些项目用到鉴权的地方就只能在app.vue文件里面写,在网上也找了很多资料,本文主要用到:

  1. 重写路由方法
  2. 为页面添加混入(mixin)
  3. 利用onShow,onHide生命钩子函数解决首屏加载和切换自带的tabbar的时候调用路由守卫

router.d.ts文件

type RouterOption = {
  url: string;
  path: string;
  query: any;
};
type RouterEvent = {
  to: RouterOption;
  from: RouterOption;
};
type RouterBeforeEach = ((e: RouterEvent) => RouterPage | boolean) | null;
type RouterAfterEach = ((e: RouterEvent) => void) | null;
type RouterPage = {
  router:"navigateTo"| "redirectTo" | "switchTab" | "reLaunch" ;
}&UniNamespace.NavigateToOptions
type RouterMixin = {
  beforeEachFunc: RouterBeforeEach;
  afterEachFunc: RouterAfterEach;
  beforeEach: (callback: RouterBeforeEach) => void;
  afterEach: (callback:RouterAfterEach) => void;
  onShow: () => void;
  onHide: () => void;
};

代码

import { tabbar } from "@/utils/page";  // pages.json中的tabbar.list的tabbar数组

/**
 复制一份原生的路由方法
*/
const nativeRoutersFunc = {
  navigateTo: uni.navigateTo,
  redirectTo: uni.redirectTo,
  switchTab: uni.switchTab,
  reLaunch: uni.reLaunch,
  navigateBack: uni.navigateBack,
};


let that; // 存this
const tabbarMap = {};  // 创建tabbar是否调用钩子
let parentRoute = { path: "/", $page: { fullPath: "/" }, options: {} }; // 上级路由
const replaceRouters = ["navigateTo", "redirectTo", "switchTab", "reLaunch"]; // 需要重写的原生方法


/**
 自定义方法:解决路由传参拆割路径和对象
*/
function getQueryParams(url: string): { path: string; query: Object } {
  const query = {};
  if (url.indexOf("?") === -1) {
    return { path: url, query };
  }
  const routerPathArr = url.split("?");
  const path = routerPathArr[0];
  const queryString = routerPathArr[1];
  const keyValuePairs = queryString.split("&");
  keyValuePairs.forEach((keyValuePair) => {
    const [key, value] = keyValuePair.split("=");
    query[key] = value;
  });
  return { path, query };
}


/*
 获取当前页面信息或者上级路由信息
 (因为多处地方要用到所以抽出来写)
*/
function getCurrentPage(index?: number=1): UniApp.Page {
  const pages = getCurrentPages();
  const currentPage = pages[pages.length - index];
  return currentPage
}


/**
  处理路由需要的options和调用后置钩子
  在跳转前先获取当前页面信息处理成form
  将要跳转的options处理成to,
  先存储一份原有options的success函数,
  在自己写一个success跳转成功之后先调用钩子在调用原先的success
  
  需要判断to的路由是否属于tabbar,如果是的话需要将阈值设置成true让页面onShow的生命周期里不会执行路由守卫
*/
function handleOptions(options: UniNamespace.NavigateToOptions, afterEach: RouterAfterEach): RouterEvent {
  const currentPage = getCurrentPage()
  const pagesOption = currentPage.options;
  const pagesUrl = currentPage.route;
  const from = {
    path: "/" + pagesUrl,
    query: pagesOption,
    url: currentPage.$page.fullPath,
  };
  let to = { ...options };
  let success = to.success || (() => {});
  const { path, query } = getQueryParams(options.url);
  to.query = query;
  to.path = path;
  if (tabbar.find((item) => "/" + item.pagePath === to.path)) {
    tabbarMap[to.path] = true;
  }
  to.success = () => {
    afterEach({ to, from });
    success();
  };
  return { to, from };
}


/*
 处理Before返回来是不是对象
*/
function handleBeforeValue(value: RouterPage | boolean) {
  if ((typeof value != "object" && typeof value != "boolean") || (value==null || Array.isArray(value))) {
    throw new Error("beforeEach 返回值必须是一个布尔值或页面路由信息");
  }
  if (typeof value === "object") {
    nativeRoutersFunc[value.router](value);
  }
 }
 

/**
  重写路由方法
  因为uni.navigateBack比较特殊单独拿出来写不放在replaceRouters,
  其他方法调用的格式都类似所以直接用循环写了
  每次拿到options的时候处理一下在调用前置钩子判断前置钩子返回的是不是布尔类型或者对象类型(分别对应处理)
  如果是对象类型的话要带上router属性代表你是那种跳转方式,如果是布尔类型的话为true就是放行,为false就是不动
  
  重写uni.navigateBack的思路:还没返回前的路由就是form,
  返回不传options默认就是1,通过路由表取对应的路由信息拼接成钩子需要用到的to和form在调用前置钩子和后置钩子
  然后需要判断一下是否是属于tabbar页面如果是的话需要将对应的tabbar页面是否调用onShow生命周期中的钩子
  设置成true是不调用,如果是false的话就会调用.
  (因为钩子的调用已经在这里处理了所以就用不到onShow生命周期里的调用钩子了)
*/
function replaceUniRouterHooks(beforeEach: RouterBeforeEach, afterEach: RouterAfterEach) {
  replaceRouters.forEach((item) => {
    uni[item] = (options:UniNamespace.NavigateToOptions) => {
      options = handleOptions(options, afterEach);
      const value:RouterPage|boolean = beforeEach(options);
      handleBeforeValue(value)
      if (typeof value === "boolean") {
        nativeRoutersFunc[item](options.to);
      }
    };
  });
  uni.navigateBack = (options?: UniNamespace.NavigateBackOptions = {}) => {
    let newOptions:UniNamespace.NavigateBackOptions = {delta:1,...options}
    let oddSuccess =  options.success || (() => {});
    const currentPage = getCurrentPage();
    const parentPage = getCurrentPage(newOptions.delta+1);
    const hooksBody:RouterEvent = {
      to: {},
      from: {}
    }
    hooksBody.to = {
      path: "/" + parentPage?.route,
      query: parentPage?.options,
      url: parentPage?.$page?.fullPath,
    };
    hooksBody.from = {
      path: "/" + currentPage?.route,
      query: currentPage?.options,
      url: currentPage?.$page?.fullPath,
    };
    if (tabbar.find(item=>item.pagePath===parentPage?.route)) {
      tabbarMap["/" + parentPage?.route] = true;
    }
    
    if (beforeEach(hooksBody)) {
      nativeRoutersFunc.navigateBack({
        ...newOptions,
        success() {
          afterEach(hooksBody);
          oddSuccess();
        }
      });
    }
  };
}


//**
 创建混入用户到时候app.mixin()使用
 因为首次加载和你在切换自带的tabbar的时候你是触发不了上面的路由方法
 onShow和onHide主要用于处理tabbar页面的,onShow和onHide的特性每次进入和离开都会调用
 
 先说onHide主要用于存上级路由,在离开页面的时候把当前的tabbar页面的路由存到上级路由中,并将自己的阈值设置成false(因为上面调用switchTab方法的话会设置成true)
 
 onShow生命钩子:
 先判断当前页面是否属于tabbar页面以及tabbarMap中当前路由阈值是否为false (都满足才会调用)
 
 

*/
const createRouter = ():RouterMixin => {
  const router:RouterMixin = {
    // 定义 beforeEach 钩子
    beforeEachFunc: null, // 将 beforeEach 设为一个默认的空方法
    afterEachFunc: null, // 将 afterEach 设为一个默认的空方法
    // 用于挂载用户自定义的路由前置钩子
    beforeEach(callback: RouterBeforeEach) {
      if (typeof callback === "function") {
        this.beforeEachFunc = callback;
        that = this;
      } else {
       throw new Error("beforeEach 必须是一个函数");
      }
    },
     // 用于挂载用户自定义的路由后置钩子
    afterEach(callback: RouterAfterEach) {
      if (typeof callback === "function") {
        this.afterEachFunc = callback;
      } else {
        throw new Error("afterEach 必须是一个函数");
      }
    },
    onShow() {
      const currentPage = getCurrentPage();
      if (
        tabbar.find((item) => item.pagePath === currentPage?.route) &&
        !tabbarMap["/" + currentPage?.route]
      ) {
        const hooksBody:RouterEvent = {
          to: {},
          form: {},
        };
        hooksBody.to = {
          path: "/" + currentPage?.route,
          query: currentPage?.options,
          url: currentPage?.$page?.fullPath,
        };
        hooksBody.form =
          parentRoute.path == "/"
            ? { path: "/", query: {}, url: "/" }
            : {
                path: "/" + parentRoute?.route,
                query: parentRoute?.options,
                url: parentRoute?.$page?.fullPath,
              };
        const value = that.beforeEachFunc(hooksBody);
        handleBeforeValue(value);
        if (value) {
          that.afterEachFunc(hooksBody);
        }
      }
    },
    onHide() {
      const currentPage = getCurrentPage();
      if (tabbar.find((item) => item.pagePath === currentPage.route)) {
        tabbarMap["/" + currentPage.route] = false;
        parentRoute = currentPage;
      }
    },
  };

  // 调用重写路由方法
  replaceUniRouterHooks(
    (url) => {
      if (typeof router.beforeEachFunc === "function") {
        return router.beforeEachFunc(url);
      }
      return true; // 如果 beforeEach 不是函数,默认返回 true
    },
    (url) => {
      if (typeof router.afterEachFunc === "function") {
        return router.afterEachFunc(url);
      }
    }
  );
  return router;
};

使用

const router = createRouter();

// 设置 beforeEach 钩子函数
router.beforeEach((e: RouterEvent) => {
  console.log(e);
  if (e.to.path === "/pages/about/index") {
    return {router: "switchTab",url:"/pages/index/index"}
  }
  return true; // 允许导航
});
router.afterEach((e: RouterEvent) => {
  console.log("afterEach钩子",e);
});

export default router;



// main.ts
app.mixin(router)