uniapp常见的路由与页面跳转api(uni-app 怎么实现路由拦截)
前言
随着业务的需求 ,项目需要支持H5 、各类小程序以及IOS和Android ,这就需要涉及到跨端技术 ,不然每一端都开发一套 ,人力成本和维护成本太高了 。团队的技术栈主要以Vue为主 ,最终的选型是以uni-app+uview2.0作为跨端技术栈 。以前一直听别人吐槽uni-app怎么怎么不好 ,但是没什么概念 ,这一次需要为团队开发一个项目的基础框架和一些示例页面 ,主要是支持路由拦截 、http请求多实例 、请求数据加密以及登录功能封装 ,发现uni-app的生态不怎么健全 ,比如我们项目很需要的路由拦截 ,http请求拦截 ,这些都没有提供,对于跨端的兼容问题也挺多的 。这篇文章聊聊的路由拦截的调研 ,以及最终的选择和实现 。
实现路由拦截的方式
使用uni-simple-router 重写uni-app跳转方法 对uni-app跳转方法做进一步的封装使用uni-simple-router
uni-simple-router是为uni-app专门提供的路由管理器 ,使用方式跟vue-router的API一致,可以很方便的上手 ,Github 也有了六百多的start ,它可以说是uni-app用来做路由管理很好的选择 ,但是我没有选择使用它 ,个人认为开发h5是可以的 ,但是如果做跨端 ,可能会有一些后患 ,接下来我们聊聊为什么不使用它的原因 。
无法拦截switchTab 、navigateBack
这个其实也不算是一个缺点 ,目前也没找到可以拦截这两个事件的路由插件 ,如果确实需要实现这两种跳转方式的拦截 ,也是可以实现的 ,可以使用下一种方式 ,对这两种方法进行暴力重写 。
没有解决全部的跨端兼容问题
这个其实是我不选择它的主要原因,根据官方文档的说明 ,根据文档去配置和编写 ,基本上能解决所有端上的95%的问题,其他的5%的问题需要去查看编译到端的说明 。代码还是严谨的 ,缺少1%都是不完美的 ,更何况是5% 。这会导致在以后的使用过程中 ,可能因为兼容问题 ,导致自己没办法去解决 ,或者为了解决这个问题 ,需要花费大量的时间和精力 ,有可能得不偿失 。
编译app时 ,不能用’nvue’作为启动页面
nvue 不能直接作为启动页面 。因为在启动时 uni-app 会检测启动页面是否为原生渲染 ,原生渲染时不会执行路由跳转 ,插件无法正确捕捉页面挂载 。这也是一个问题 ,我们可以尽量的去避免 ,但以后有未知的情况,可能我们的启动页必须就是以nvue来实现 。
暴力重写uni-app跳转方法
这种方式虽然有点简单粗暴 ,但是效果挺好的 ,代码也很简短,Vue2.0对于数组的响应式监听也是采用这种方式。虽然实现了 ,但可能有些同学不知道怎么使用 ,直接把这段代码写在main.js就可以了 ,或者也可以在单独的文件里封装一个封装一个函数 ,然后在main.js引入 ,然后执行该方法 。
const routeInterceptor = () => {const methodToPatch = ["navigateTo", "redirectTo", "switchTab", "navigateBack"];methodToPatch.map((type) => {// 通过遍历的方式分别取出 ,uni.navigateTo 、uni.redirectTo 、uni.switchTab 、uni.navigateBack// 并且对相应的方法做重写const original = uni[type];uni[item] = function (options = {}) {if (!token) {// 判断是否存在token ,不存在重定向到登录页uni.navigateTo({url: "/login",});} else {return original.call(this, opt);}};}); } routeInterceptor()这是一个最极简的方式 ,需要添加其他参数和判断逻辑 ,大家可以自行添加 ,这里只是抛砖引玉 ,给大家提供一个思路 。
使用方式 handleDetail() {uni.navigateTo({ url: /detail?id=11111111111}) }对uni-app跳转方法做进一步的封装
这个是 uView提供的一种路由封装方式 ,对于路由传参做了进一步的封装,使用起来更加方便 ,但是不涉及到uni-app跳转方式的重写 ,所以也谈不上改了路由跳转的跨端兼容,所以还是具有uni-app一致的兼容性。但是官方文档没有说明提供了路由拦截 ,但这个还是我们特别需要的功能 ,去查看源码 ,发现还是提供了这个功能 。现在还存在的一个问题是 ,这个功能是跟uView强耦合的 ,可能我们并不想使用uView ,所以我们可以将这个功能独立抽离 。
目录结构
/router/index.js
这个文件主要提供路由拦截函数 ,具体的实现 ,可以大家可以根据自己的需求实现 ,最后向外暴露一个包含install方法的对象 ,使用的时候可以直接用Vue.use进行注册 。
routeConfig这个参数是路由相关的配置 ,resolve 传递一个true或者false表示是否允许跳转 。
routeConfig属性 参数名 类型 默认值 是否必填 说明 type String navigateTo false navigateTo或to对应uni.navigateTo ,redirect或redirectTo对应uni.redirectTo,switchTab或tab对应uni.switchTab ,reLaunch对应uni.reLaunch ,navigateBack或back对应uni.navigateBack url String - false type为navigateTo,redirectTo ,switchTab ,reLaunch时为必填 delta Number 1 false type为navigateBack时用到 ,表示返回的页面数 params Object - false 传递的对象形式的参数 ,如{name: ‘lisa’, age: 18} animationType String pop-in false 只在APP生效 ,详见窗口动画(opens new window) animationDuration Number 300 false 动画持续时间 ,单位ms import route from "./route"; // 配置白名单 const whiteList = ["/pages/login/index"]; const install = function (Vue, options) {uni.$e = { route };Vue.prototype.route = route;uni.$e.routeIntercept = (routeConfig, resolve) => {const path = routeConfig.url.split("?")[0];if (!whiteList.includes(path) && !uni.getStorageSync("token")) {uni.$e.route("/pages/login/index");return;}resolve(true);}; }; export default {install, };/router/route.js
这个文件 ,主要是对于uni-app跳转做了封装 ,主要做的还是传参部分 ,实现跟vue-router一致的传参方式 ,使用起来更加方便优雅 ,同时提供一个uni.$e.routeIntercept路由拦截方法 。
/** * 路由跳转方法 ,该方法相对于直接使用uni.xxx的好处是使用更加简单快捷 * 并且带有路由拦截功能 */ import { queryParams, deepClone, deepMerge, page } from "./utils"; class Router {constructor() {// 原始属性定义this.config = {type: "navigateTo",url: "",delta: 1, // navigateBack页面后退时,回退的层数params: {}, // 传递的参数animationType: "pop-in", // 窗口动画,只在APP有效animationDuration: 300, // 窗口动画持续时间,单位毫秒,只在APP有效intercept: false, // 是否需要拦截};// 因为route方法是需要对外赋值给另外的对象使用,同时route内部有使用this ,会导致route失去上下文// 这里在构造函数中进行this绑定this.route = this.route.bind(this);}// 判断url前面是否有"/" ,如果没有则加上,否则无法跳转addRootPath(url) {return url[0] === "/" ? url : `/${url}`;}// 整合路由参数mixinParam(url, params) {url = url && this.addRootPath(url);// 使用正则匹配 ,主要依据是判断是否有"/","?","="等 ,如“/page/index/index?name=mary"// 如果有url中有get参数 ,转换后无需带上"?"let query = "";if (/.*\/.*\?.*=.*/.test(url)) {// object对象转为get类型的参数query = queryParams(params, false);// 因为已有get参数,所以后面拼接的参数需要带上"&"隔开return (url += `&${query}`);}// 直接拼接参数 ,因为此处url中没有后面的query参数 ,也就没有"?/&"之类的符号query = queryParams(params);return (url += query);}// 对外的方法名称async route(options = {}, params = {}) {// 合并用户的配置和内部的默认配置let mergeConfig = {};if (typeof options === "string") {// 如果options为字符串 ,则为route(url, params)的形式mergeConfig.url = this.mixinParam(options, params);mergeConfig.type = "navigateTo";} else {mergeConfig = deepClone(options, this.config);// 否则正常使用mergeConfig中的url和params进行拼接mergeConfig.url = this.mixinParam(options.url, options.params);}// 如果本次跳转的路径和本页面路径一致 ,不执行跳转 ,防止用户快速点击跳转按钮 ,造成多次跳转同一个页面的问题if (mergeConfig.url === page()) return;if (params.intercept) {this.config.intercept = params.intercept;}// params参数也带给拦截器mergeConfig.params = params;// 合并内外部参数mergeConfig = deepMerge(this.config, mergeConfig);// 判断用户是否定义了拦截器if (typeof uni.$e.routeIntercept === "function") {// 定一个promise ,根据用户执行resolve(true)或者resolve(false)来决定是否进行路由跳转const isNext = await new Promise((resolve, reject) => {uni.$e.routeIntercept(mergeConfig, resolve);});// 如果isNext为true ,则执行路由跳转isNext && this.openPage(mergeConfig);} else {this.openPage(mergeConfig);}}// 执行路由跳转openPage(config) {// 解构参数const { url, type, delta, animationType, animationDuration } = config;if (config.type == "navigateTo" || config.type == "to") {uni.navigateTo({url,animationType,animationDuration,});}if (config.type == "redirectTo" || config.type == "redirect") {uni.redirectTo({url,});}if (config.type == "switchTab" || config.type == "tab") {uni.switchTab({url,});}if (config.type == "reLaunch" || config.type == "launch") {uni.reLaunch({url,});}if (config.type == "navigateBack" || config.type == "back") {uni.navigateBack({delta,});}} } export default new Router().route;/router/uitls.js
这个文件主要是为路由封装提供一些工具函数
/** * @description 对象转url参数 * @param {object} data,对象 * @param {Boolean} isPrefix,是否自动加上"?" * @param {string} arrayFormat 规则 indices|brackets|repeat|comma */ export const queryParams = ( data = {},isPrefix = true,arrayFormat = "brackets" ) => {const prefix = isPrefix ? "?" : "";const _result = [];if (["indices", "brackets", "repeat", "comma"].indexOf(arrayFormat) == -1)arrayFormat = "brackets";for (const key in data) {const value = data[key];// 去掉为空的参数if (["", undefined, null].indexOf(value) >= 0) {continue;}// 如果值为数组 ,另行处理if (value.constructor === Array) {// e.g. {ids: [1, 2, 3]}switch (arrayFormat) {case "indices":// 结果: ids[0]=1&ids[1]=2&ids[2]=3for (let i = 0; i < value.length; i++) {_result.push(`${key}[${i}]=${value[i]}`);}break;case "brackets":// 结果: ids[]=1&ids[]=2&ids[]=3value.forEach((_value) => {_result.push(`${key}[]=${_value}`);});break;case "repeat":// 结果: ids=1&ids=2&ids=3value.forEach((_value) => {_result.push(`${key}=${_value}`);});break;case "comma":// 结果: ids=1,2,3let commaStr = "";value.forEach((_value) => {commaStr += (commaStr ? "," : "") + _value;});_result.push(`${key}=${commaStr}`);break;default:value.forEach((_value) => {_result.push(`${key}[]=${_value}`);});}} else {_result.push(`${key}=${value}`);}}return _result.length ? prefix + _result.join("&") : ""; }; /** * 是否数组 */ function isArray(value) {if (typeof Array.isArray === "function") {return Array.isArray(value);}return Object.prototype.toString.call(value) === "[object Array]"; } /** * @description 深度克隆 * @param {object} obj 需要深度克隆的对象 * @returns {*} 克隆后的对象或者原值(不是对象) */ export const deepClone = (obj) => {// 对常见的“非 ”值,直接返回原来值if ([null, undefined, NaN, false].includes(obj)) return obj;if (typeof obj !== "object" && typeof obj !== "function") {// 原始类型直接返回return obj;}const o = isArray(obj) ? [] : {};for (const i in obj) {if (obj.hasOwnProperty(i)) {o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];}}return o; }; /** * @description JS对象深度合并 * @param {object} target 需要拷贝的对象 * @param {object} source 拷贝的来源对象 * @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象) */ export const deepMerge = (target = {}, source = {}) => {target = deepClone(target);if (typeof target !== "object" || typeof source !== "object") return false;for (const prop in source) {if (!source.hasOwnProperty(prop)) continue;if (prop in target) {if (typeof target[prop] !== "object") {target[prop] = source[prop];} else if (typeof source[prop] !== "object") {target[prop] = source[prop];} else if (target[prop].concat && source[prop].concat) {target[prop] = target[prop].concat(source[prop]);} else {target[prop] = deepMerge(target[prop], source[prop]);}} else {target[prop] = source[prop];}}return target; }; /** * @description 获取当前页面路径 */ export const page = () => {const pages = getCurrentPages();// 某些特殊情况下(比如页面进行redirectTo时的一些时机) ,pages可能为空数组return `/${pages[pages.length - 1]?.route ?? ""}`; };路由配置
在main.js引入
import router from "./router"; Vue.use(router);使用方式
更全的使用方式可以查看 uView路由跳转文档
全局使用 uni.$e.route(/pages/info/index); vue文件中使用 this.route(/pages/info/index);拦截switchTab 、navigateBack
现在的方式还是没办法支持拦截switchTab 、navigateBack ,所以需要借助第二种方式,重写这两种方法 ,具体实现 ,完善 /router/index.js
// /router/index.js import route from "./route"; // 配置白名单 const whiteList = ["/pages/login/index"]; const handleOverwirteRoute = () => {// 重写switchTab 、navigateBackconst methodToPatch = ["switchTab", "navigateBack"];methodToPatch.map((type) => {// 通过遍历的方式分别取出 ,uni.switchTab 、uni.navigateBack// 并且对相应的方法做重写const original = uni[type];uni[type] = function (options = {}) {const { url: path } = options;if (!whiteList.includes(path) && !uni.getStorageSync("token")) {// 判断是否存在token ,不存在重定向到登录页uni.$e.route("/pages/login/index");} else {return original.call(this, options);}};}); }; const install = function (Vue, options) {uni.$e = { route };Vue.prototype.route = route;// 重写uni方法handleOverwirteRoute();// 路由拦截器uni.$e.routeIntercept = (routeConfig, resolve) => {const path = routeConfig.url.split("?")[0];if (!whiteList.includes(path) && !uni.getStorageSync("token")) {uni.$e.route("/pages/login/index");return;}resolve(true);}; }; export default {install, };补充
在系统第一进入的时候 ,是不会触发拦截事件的 ,需要在App.js的onLanch去做进一步的实现 。
onLaunch: function () {if (!uni.getStorageSync("token")) {uni.navigateTo({ url: "/pages/login/index" });} },小结
关于uni-app实现路由拦截的探索到这里就告一段落了 ,三种方式都是可以的 ,uni-simple-router使用人数也挺多的 ,不考虑跨很多端 ,暴露出来的问题可能就比较少 ,也是一种选择 。第二种方式 ,重写uni的跳转方式,比较简单 ,效果也是很明显 ,不过使用稍微没那么友好,需要做进一步封装 ,第三种方式算是做了封装 ,同时结合第二种方式 ,算是有个比较好的效果 。
最后
为大家准备了一个前端资料包 。包含54本 ,2.57G的前端相关电子书 ,《前端面试宝典(附答案和解析)》 ,难点 、重点知识视频教程(全套) 。
有需要的小伙伴 ,可以点击下方卡片领取 ,无偿分享创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!