這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 隨著業務的需求,項目需要支持H5、各類小程式以及IOS和Android,這就需要涉及到跨端技術,不然每一端都開發一套,人力成本和維護成本太高了。團隊的技術棧主要以Vue為主,最終的選型是以uni-app+uview2.0作為跨端技術 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
隨著業務的需求,項目需要支持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]=3 for (let i = 0; i < value.length; i++) { _result.push(`${key}[${i}]=${value[i]}`); } break; case "brackets": // 結果: ids[]=1&ids[]=2&ids[]=3 value.forEach((_value) => { _result.push(`${key}[]=${_value}`); }); break; case "repeat": // 結果: ids=1&ids=2&ids=3 value.forEach((_value) => { _result.push(`${key}=${_value}`); }); break; case "comma": // 結果: ids=1,2,3 let 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、navigateBack const 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" }); } },