补充上一文《使用uni-app从0.5到1搭建App记录-1》。要记录的还有很多,但是等到写的时候,发现以前碰到的问题和解决思路都快忘光了。
因为一直赶项目没有很多时间去梳理,但是梳理脉络和总结记录又是很重要的事,所以尽管再忙,也要努力把它记录完。文中如果有错误或者不严谨的地方,或者你有更好的解决思路,也请您提出宝贵的建议!
那我们进入正题吧!上一文已经把框架选型、项目搭建已经梳理完。那接下来我们将会围绕着项目完善、项目落地展开。
1.项目完善
1.1 路由配置和路由防卫
(1)我们首先从插件市场下载uni-simple-router
插件。
注意:
插件市场安装的用户 你需要在你项目目录下执行 npm init -y && npm install query-string -D
。uni-simple-router
是专为 uni-app 打造的路由器。它与 uni-app 核心深度集成,使用 uni-app 轻松构建单页应用程序变得轻而易举。
(2)在这里只介绍重要的知识点,基本使用可以参考官网,我们接下来将从这几点开始我们的学习
配置白名单、全局前置路由守卫、全局后置路由守卫,主页退出后登录页禁止返回,路由跳转和传参
首先可以看到我们在全局引入了Router,这样我们便可以在全局很方便的使用它,还有一点的是我们需要在main.js挂载我们的全局监听,这样我们就可以在路由守卫中监听到页面的变化,而白名单是为了不需要判断token是否存在或过期你也可以跳转到该界面,就比如登录页。这也是官网中快速上手能知道的,这里不过多赘述。如下写法:涉及了白名单、路由守卫的使用。
import modules from './modules'
import Vue from 'vue'
import Router from '@/plugin/uni-simple-router/index.js'
import {ACCESS_TOKEN} from '@/common/util/constants.js'
Vue.use(Router) // 全局注册路由
//初始化
const router = new Router({
encodeURI: true,
routes: [...modules]//路由表
});
const whiteList = ['/pages/login/login'];
//全局路由前置守卫
router.beforeEach((to, from, next) => {
let token = uni.getStorageSync(ACCESS_TOKEN);
if (token) {
next()
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next({path: '/pages/login/login'})
}
}
})
// 全局路由后置守卫
router.afterEach((to, from) => {
console.log(to, from)
})
export default router;
但是上面写法我碰到了一个很严重的问题,在手机App中,我登录退出后跳转到登录页,然后关闭该应用后,我再次打开,我点返回页,它会返回到主页。这是我不想看到的,所以我尝试着在登录页这样写,但是效果不理想。
onBackPress(options) { //返回时会调到的
this.backButtonPress++; //别忘了定义一下
if (this.backButtonPress > 1) {
plus.runtime.quit();
return false; //允许返回
} else {
plus.nativeUI.toast('再按一次退出应用');
return true; //阻止返回
}
setTimeout(function() {
this.backButtonPress = 0;
}, 1000);
},
所以我最终还是在路由守卫中解决了这个问题。如下,我在没有登录的前提下(也就是没有token的情况下),用repalceAll来关闭所有历史记录,如果点击返回跳转的不是登录页,那么我就替换到登录页。如下代码:
import modules from './modules'
import Vue from 'vue'
import Router from '@/plugin/uni-simple-router/index.js'
import {ACCESS_TOKEN} from '@/common/util/constants.js'
Vue.use(Router) // 全局注册路由
//初始化
const router = new Router({
encodeURI: true,
routes: [...modules]//路由表
});
//全局路由前置守卫
router.beforeEach((to, from, next) => {
let token = uni.getStorageSync(ACCESS_TOKEN);
if (!token&&to.path !== '/pages/login/login') {
return next({
path:'/pages/login/login',
NAVTYPE:'replaceAll'
});
}else{
next()
}
})
// 全局路由后置守卫
router.afterEach((to, from) => {
console.log(to, from)
})
export default router;
接下来我们在简单介绍几种路由跳转的方式,分别在以下场景:页面跳转,页面返回,页面登出,页面传参取值我们都需要使用哪些api:
- 页面跳转:
this.$Router.push({name:'我们的name'})
// 其中name对应routes中的路由表
const router = new Router({
encodeURI: true,
routes: [...modules]//路由表
});
// 路由表示例:
{
path: "/pages/login/login",
name: 'login',
meta: {
title: '登录',
},
},
2.页面返回
// 上一个,等同 uni.navigateBack
this.$Router.back();
// 后退 2 步
this.$Router.back(2)
//
3.页面登出
this.$Router.replaceAll({name: 'index'})
4.路由传参和取值
// 传参
this.$Router.push({name: 'index',params:{
a:1
}})
// 取值
let params=this.$Route.query
console.log(params.a)
1.2 request请求封装和api地址动态配置
1.首先我们还是从uniapp插件库中引用luch-request
,我使用的是V2.0.1
luch-request
是一个基于Promise 开发的uni-app跨平台、项目级别的请求库,它有更小的体积,易用的api,方便简单的自定义能力。
2.新建公共service目录和api目录。
service目录用来存储公共request封装,api目录用来写api请求的。如下:
3.使用luch-request
- config.service.js文件代码
通过config.service.js配置不同环境的地址,或者地址公共后缀。
let BASE_URL = ''
if (process.env.NODE_ENV == 'development') {
BASE_URL = 'http://127.0.0.1:8080/tunnel'; // 开发环境
} else {
BASE_URL = 'http://127.0.0.1:8080/tunnel'; // 生产环境
}
// let routeUrl = BASE_URL;
let routeUrl = BASE_URL + '/route';
const configService = {
apiUrl: routeUrl,
loginUrl: BASE_URL,
};
export default configService
- service.js文件代码
service.js涉及了token校验判断是否登录过期、时间超时配置、请求拦截器中动态修改ip端口、响应拦截器中判断状态码退出到登录页
import Request from '@/common/luch-request/index.js'
import {ACCESS_TOKEN} from '@/common/util/constants.js'
import configService from './config.service.js'
import tip from '@/common/util/tip.js';
// let preUrl = configService.preUrl;
let apiUrl = configService.apiUrl;
const getTokenStorage = () => {
let token = ''
try {
token = uni.getStorageSync(ACCESS_TOKEN)
} catch (e) {
//TODO handle the exception
console.log("getTokenStorage", token)
}
return token
}
const http = new Request()
http.setConfig((config) => { /* 设置全局配置 */
config.baseUrl = apiUrl /* 根域名不同 */
config.header = {
...config.header,
}
config.timeout=15000
return config
})
/**
* 自定义验证器,如果返回true 则进入响应拦截器的响应成功函数(resolve),否则进入响应拦截器的响应错误函数(reject)
* @param { Number } statusCode - 请求响应体statusCode(只读)
* @return { Boolean } 如果为true,则 resolve, 否则 reject
*/
// 有默认,非必写
http.validateStatus = (statusCode) => {
return statusCode === 200
}
http.interceptor.request((config, cancel) => { /* 请求之前拦截器 */
config.header = {
...config.header,
'X-Access-Token': `sessionId=${getTokenStorage()}`,
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
}
// 用来动态修改ip和端口的
let ip = uni.getStorageSync('ip');
let port = uni.getStorageSync('port');
if (!!ip && !!port) {
config.baseUrl = 'https://' + ip + ':' + port;
}
/*
if (!token) { // 如果token不存在,调用cancel 会取消本次请求,但是该函数的catch() 仍会执行
cancel('token 不存在') // 接收一个参数,会传给catch((err) => {}) err.errMsg === 'token 不存在'
}
*/
return config
})
// 必须使用异步函数,注意
http.interceptor.response(async (response) => { /* 请求之后拦截器 */
if (response.statusCode !== 200) { // 服务端返回的状态码不等于200,则reject()
this.$tip.info(response?.data.msg || response.data.message || '未知异常,请联系管理员!')
return Promise.reject(response)
}else{
let data = response.data;
if (!data.success) {
// 失败校验
let timeout = setTimeout(
uni.showToast({
title: data.msg || data.message || '未知异常,请联系管理员!',
icon: 'none'
})
, 1000);
clearTimeout(timeout);
}
}
return response
}, (response) => {
// 请求错误做点什么
console.log("请求错误做点什么", response);
if (response) {
let {data,statusCode} = response;
const token = uni.getStorageSync(ACCESS_TOKEN);
if (statusCode === 401) {
let timeout = setTimeout(tip.alert('登录已过期'), 1000);
uni.removeStorageSync(ACCESS_TOKEN);
uni.redirectTo({
url: '/pages/login/login'
});
clearTimeout(timeout);
return;
}
}
return response
})
export {
http
}
- api目录service示例
import { http } from '@/common/service/service.js'
import configService from '@/common/service/config.service.js';
const apiUrl = configService.loginUrl;
const apiService = {
/**
* 退出
*/
logout(params) {
return http.get(apiUrl+'/loggingout',params);
},
/**
* 修改密码
*/
updatePwd(params) {
return http.post('/sysUser/updatePwd',params);
},
};
export default apiService;
- 界面请求api修改密码示例
confirmPwd() {
this.showPwdModal = true;
const {userPwd, newPwd, pwd_check} = this.passwordModel;
const pwdParams = {
userPwd: md5(userPwd),
newPwd: md5(newPwd),
pwd_check: md5(pwd_check),
}
if (this.passwordModel.newPwd != this.passwordModel.pwd_check) {
this.$tip.toast('新密码和确认密码不一致');
return;
}
this.$refs.uForm.validate().then(res => {
api.updatePwd({...pwdParams}).then(res => {
if (res?.data?.success) {
this.showPwdModal = false;
this.$tip.success('修改成功!');
uni.removeStorageSync(ACCESS_TOKEN);
uni.removeStorageSync(USER_NAME);
uni.removeStorageSync(USER_INFO);
this.$Router.replaceAll({name: 'login'})
} else {
console.log(res)
this.$tip.toast(res?.data?.data?.msg||'修改失败!');
}
})
}).catch(errors => {
this.$tip.error('校验失败')
})
},
- 方法技巧:如果你觉得每个界面都引入api目录很麻烦,你可以这样做:
你每个模块一个服务模块文件,然后通过index.js文件在main.js进行挂载就可以了。
// index.js文件
import electromechanicalApi from "./electromechanicalApi";
import inspectionTasks from "./inspectionApi";
import warningApi from "./warningApi";
import tunnelApi from "./tunnelApi";
import realTimeSensorApi from "./realTimeSensorApi";
import api from "./api";
export {electromechanicalApi,api,inspectionTasks,warningApi,tunnelApi,realTimeSensorApi};
// main.js
import apiRequest from './api/index';
Vue.prototype.$apiRequest=apiRequest;
然后你就可以在任意界面访问模块服务了,比如我们上面的修改密码可以写成这样
this.$apiRequest.api.updatePwd({...pwdParams}).then(res => {
...
})
同理很多公用组件和公用方法都可以这样做!
2.阶段疑难汇总
2.1app横屏锁定
- 配置manifest.json文件:
"app-plus" : {
"screenOrientation" : [
//可选,字符串数组类型,应用支持的横竖屏
"portrait-primary" //可选,字符串类型,支持竖屏
]
}
- 在App.vue文件onLaunch生命周期函数新增
// #ifdef APP-PLUS
plus.screen.lockOrientation("portrait-primary") // 竖屏锁定
// #endif
- 注意
这个属性已经废弃,一开始我就是使用这个没生效搞了半天!!!在这里给大家避个坑
应该使用这个:
- 配置链接
2.2横屏竖屏切换,样式错乱问题
这个官方好像没有具体的解决办法,所以只好自己尝试。我这里先说我的解决方案以及场景,毕竟每个人的像素使用方案不一致。
-
场景:除了横屏都是使用upx写的界面,然后横屏使用的方案是vw,vh,px。
-
问题描述:因为项目属于内部项目,所以只能画图描述自己的问题,如下图
3.解决方案:
(1)在横屏B界面加入如下代码。
onLoad() {
// #ifdef APP-PLUS
uni.showLoading({
title: "加载中..."
})
this.timer1=setTimeout(() => {
let orientation = plus.navigator.getOrientation();
if (orientation == 0) {
plus.screen.unlockOrientation(); //解锁横屏竖屏方向
plus.screen.lockOrientation('landscape-primary'); // 锁定横屏
plus.navigator.setFullscreen(true); // 设置全屏
// plus.navigator.hideSystemNavigation(); // 隐藏系统导航栏
}
uni.hideLoading();
}, 800)
// #endif
},
onUnload() {
// #ifdef APP-PLUS
plus.screen.unlockOrientation();
plus.screen.lockOrientation('portrait-primary');
plus.navigator.setFullscreen(false);
// plus.navigator.showSystemNavigation();
// #endif
if(this.timer1){
clearInterval(this.timer1);
this.timer1=null
}
},
到这里我当时以为是解决了,后面发现是白高兴一场,只是部分手机可行,而一些配置较低的手机还是会出现同样的问题,所以我加了下面的步骤。
(2)在page.json文件加入:
"globalStyle": {
"rpxCalcMaxDeviceWidth":0
}
到这里完美解决了,但是我发现我要隐藏掉系统导航栏,旧手机进入这个界面会出现高度变得非常小的问题,暂时还没解决方案,后续有解决办法继续补充!!
4.官方讨论
后续请看下文…