HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

标梵微信插件开发教程5:建立全局css风格

插件开发 css 微信app

设置全局css样式。目的在于统一所有page页面page,text,view,navigator,swiper,swiper-item,image等组件的padding、margin和border-size样式。标梵微信插件开发教程5:建立全局css风格
1 设置全局边距
我们需要在app.wxss中设置page,text,view,navigator,swiper,swiper-item,image等组件的全局padding为0px,margin为0px,border-sizing为border-box 。
page,text,view,navigator,swiper,swiper-item,image{
/设置组件全局边距/
margin: 0px;
padding: 0px;
/*

  • 指定元素的宽或高包含padding和border,不包含mmargin。
  • 如果不设置默认为content-box,元素的宽或高不包含padding和border
    /
    box-sizing: border-box;
    }
    因为border-sizing全局样式设置为border-box,所以全局pages默认情况下:
    (1)组件总宽度 = 边框border + 内边距pardding + 内容宽度
    (2)组件总高度 = 边框border + 内边距pardding + 内容高度
    下图为网上找的截图,可以看到border-sizing属性两个值(content-box和border-box)的区别!
    2 设置text标签全局字体大小
    为了统一整个app小程序页面的字体大小,我们需要在app.wxss中设置page和text标签内容的全局字体大小为:30rpx。如下:
    /
    全局page和text内容字体大小/
    page,text{
    font-size: 30rpx;
    }
    3 app.wxss全局样式全部代码
    /app.wxss/
    page,text,view,navigator,swiper,swiper-item,image{
    /
    设置组件全局边距/
    margin: 0px;
    padding: 0px;
    /
  • 指定元素的宽或高包含padding和border,不包含mmargin。
  • 如果不设置默认为content-box,元素的宽或高不包含padding和border
    /
    box-sizing: border-box;
    }
    /
    全局page和text内容字体大小*/
    page,text{
    font-size: 30rpx;
    }
    本文来源:标梵互动(https://www.biaofun.com/)
继续阅读 »

设置全局css样式。目的在于统一所有page页面page,text,view,navigator,swiper,swiper-item,image等组件的padding、margin和border-size样式。标梵微信插件开发教程5:建立全局css风格
1 设置全局边距
我们需要在app.wxss中设置page,text,view,navigator,swiper,swiper-item,image等组件的全局padding为0px,margin为0px,border-sizing为border-box 。
page,text,view,navigator,swiper,swiper-item,image{
/设置组件全局边距/
margin: 0px;
padding: 0px;
/*

  • 指定元素的宽或高包含padding和border,不包含mmargin。
  • 如果不设置默认为content-box,元素的宽或高不包含padding和border
    /
    box-sizing: border-box;
    }
    因为border-sizing全局样式设置为border-box,所以全局pages默认情况下:
    (1)组件总宽度 = 边框border + 内边距pardding + 内容宽度
    (2)组件总高度 = 边框border + 内边距pardding + 内容高度
    下图为网上找的截图,可以看到border-sizing属性两个值(content-box和border-box)的区别!
    2 设置text标签全局字体大小
    为了统一整个app小程序页面的字体大小,我们需要在app.wxss中设置page和text标签内容的全局字体大小为:30rpx。如下:
    /
    全局page和text内容字体大小/
    page,text{
    font-size: 30rpx;
    }
    3 app.wxss全局样式全部代码
    /app.wxss/
    page,text,view,navigator,swiper,swiper-item,image{
    /
    设置组件全局边距/
    margin: 0px;
    padding: 0px;
    /
  • 指定元素的宽或高包含padding和border,不包含mmargin。
  • 如果不设置默认为content-box,元素的宽或高不包含padding和border
    /
    box-sizing: border-box;
    }
    /
    全局page和text内容字体大小*/
    page,text{
    font-size: 30rpx;
    }
    本文来源:标梵互动(https://www.biaofun.com/)
收起阅读 »

大白话web开发和uniapp开发有什么区别?什么是打包?什么时候需要重新打包自定义基座?

打包

网页依赖于浏览器,本身仅能提供多媒体的展示,没有“直接”与硬件交互的能力。
比如微信有摇一摇功能因为安卓/苹果语言可以和手机的陀螺仪通讯,附近的人的功能是与手机GPS硬件的通讯等等。
想必这时候你要说微信公众号是网页语言开发,但是也能具备以上功能吧。答案是:微信公众号之所以可以实现摇一摇地理位置的监测属于“间接”而非直接,是由js将命令发给微信(此时微信就是该网页的浏览器),由浏览器完成后将数据调回给网页。典型的例子还有dcloud的h5+plus详情:http://www.html5plus.org/doc/h5p.html
和超级强大的uni-app,详情:https://uniapp.dcloud.io/

什么是打包?什么时候需要重新打包

一款app有自己的包名,签名,证书,应用图标等这一系列内容设定的过程就是app打包的过程。
典型的:微信支付功能开发,需要设置app的包名和签名等,所以参数一旦不正确就需要重新打包。

同理什么时候需要【重新编译】或者【重启】后生效,比如写了原生子窗体subnvue或者原生导航等原生相关内容。

继续阅读 »

网页依赖于浏览器,本身仅能提供多媒体的展示,没有“直接”与硬件交互的能力。
比如微信有摇一摇功能因为安卓/苹果语言可以和手机的陀螺仪通讯,附近的人的功能是与手机GPS硬件的通讯等等。
想必这时候你要说微信公众号是网页语言开发,但是也能具备以上功能吧。答案是:微信公众号之所以可以实现摇一摇地理位置的监测属于“间接”而非直接,是由js将命令发给微信(此时微信就是该网页的浏览器),由浏览器完成后将数据调回给网页。典型的例子还有dcloud的h5+plus详情:http://www.html5plus.org/doc/h5p.html
和超级强大的uni-app,详情:https://uniapp.dcloud.io/

什么是打包?什么时候需要重新打包

一款app有自己的包名,签名,证书,应用图标等这一系列内容设定的过程就是app打包的过程。
典型的:微信支付功能开发,需要设置app的包名和签名等,所以参数一旦不正确就需要重新打包。

同理什么时候需要【重新编译】或者【重启】后生效,比如写了原生子窗体subnvue或者原生导航等原生相关内容。

收起阅读 »

uniapp 使用vant weapp 做app时 又发现一个bug

uniapp 导入vant weapp 后 调用 van-dropdown-menu
菜单头能显示,点击时报:
TypeError: Object [object HTMLUnknownElement] has no method 'matches' at view.umd.min.js:1

继续阅读 »

uniapp 导入vant weapp 后 调用 van-dropdown-menu
菜单头能显示,点击时报:
TypeError: Object [object HTMLUnknownElement] has no method 'matches' at view.umd.min.js:1

收起阅读 »

uni.chooseImage 相机相册选择图片弹框国际化

国际化

自己封装的一个实现选择图片弹框国际化的方法,返回的格式与 uni.chooseImage 相同,因此可以把 uni.chooseImage 替换掉,在main.js挂到了Vue原型上:

/** 封装国际化选择图片  
 * @param {Object} self 组件中的 this  
 * @param {Object} params 配置参数  
 */  
Vue.prototype.selectImage = (self, params = {}) => {  
    let count = params.count || 9;                                  // 数量  
    let sizeType = params.sizeType || ['original', 'compressed'];   // 原图和压缩图  
    let sourceType = params.sourceType || ['camera', 'album'];      // 相册和相机  
    let success = params.success || function success(){};           // 成功回调  
    let fail = params.fail || function fail(){};                    // 失败回调  
    let option = {  
        cancel: self.$t('prompt.btn2'),          // i18n取消文字  
        buttons: [{  
                title: self.$t('prompt.imgBtn1') // i18n相机文字    
            },  
            {  
                title: self.$t('prompt.imgBtn2') // i18n相册文字    
            }  
        ]  
    };  
    if(!sourceType.includes('camera')) delete option.buttons[0];  
    if(!sourceType.includes('album')) delete option.buttons[1];  
    plus.nativeUI.actionSheet(option,  
        async (tap) => {  
            if(sourceType.length === 2) {  
                if( tap.index === 1) {  
                    // 拍照  
                    const cmr = plus.camera.getCamera();  
                    cmr.captureImage((path) => {  
                        plus.io.getFileInfo({  
                            filePath: path,  
                            success: res => {  
                                let result = {  
                                    errMsg: "chooseImage:ok",  
                                    tempFilePaths: [path],  
                                    tempFiles: [{  
                                        path: path,  
                                        size: res.size  
                                    }]  
                                };  
                                success(result);  
                            },  
                            fail: err => fail(err)  
                        });  
                        }, (err) => {fail(err)});  
                } else if (tap.index === 2) {  
                    // 从相册选择  
                    plus.gallery.pick(  
                    (res) => {  
                        let filesList = res.files;  
                        let tempFiles = [];  
                        for (let path of filesList) {  
                            plus.io.getFileInfo({  
                                filePath: path,  
                                success: r => {  
                                    tempFiles.push({  
                                        path: path,  
                                        size: r.size  
                                    });  
                                    if(tempFiles.length === filesList.length) {  
                                        success({  
                                            errMsg: "chooseImage:ok",  
                                            tempFilePaths: filesList,  
                                            tempFiles  
                                        })  
                                    }  
                                },  
                                fail: err => fail(err)  
                            });  
                        }  
                    }, (err) => {fail(err)},{  
                        filter: "image",  
                        multiple: true,  
                        maximum: count,  
                        system: false,  
                    });  
                }  
            } else if (sourceType.length === 1) {  
                if(sourceType.includes('camera')) {  
                    // 拍照  
                    const cmr = plus.camera.getCamera();  
                    cmr.captureImage((path) => {  
                        plus.io.getFileInfo({  
                            filePath: path,  
                            success: res => {  
                                let result = {  
                                    errMsg: "chooseImage:ok",  
                                    tempFilePaths: [path],  
                                    tempFiles: [{  
                                        path: path,  
                                        size: res.size  
                                    }]  
                                };  
                                success(result);  
                            },  
                            fail: err => fail(err)  
                        });  
                        }, (err) => {fail(err)});  
                } else {  
                    // 从相册选择  
                    plus.gallery.pick(  
                    (res) => {  
                        let filesList = res.files;  
                        let tempFiles = [];  
                        for (let path of filesList) {  
                            plus.io.getFileInfo({  
                                filePath: path,  
                                success: r => {  
                                    tempFiles.push({  
                                        path: path,  
                                        size: r.size  
                                    });  
                                    if(tempFiles.length === filesList.length) {  
                                        success({  
                                            errMsg: "chooseImage:ok",  
                                            tempFilePaths: filesList,  
                                            tempFiles  
                                        })  
                                    }  
                                },  
                                fail: err => fail(err)  
                            });  
                        }  
                    }, (err) => {fail(err)},{  
                        filter: "image",  
                        multiple: true,  
                        maximum: count,  
                        system: false,  
                    });  
                }  
            }  
        });  
}
继续阅读 »

自己封装的一个实现选择图片弹框国际化的方法,返回的格式与 uni.chooseImage 相同,因此可以把 uni.chooseImage 替换掉,在main.js挂到了Vue原型上:

/** 封装国际化选择图片  
 * @param {Object} self 组件中的 this  
 * @param {Object} params 配置参数  
 */  
Vue.prototype.selectImage = (self, params = {}) => {  
    let count = params.count || 9;                                  // 数量  
    let sizeType = params.sizeType || ['original', 'compressed'];   // 原图和压缩图  
    let sourceType = params.sourceType || ['camera', 'album'];      // 相册和相机  
    let success = params.success || function success(){};           // 成功回调  
    let fail = params.fail || function fail(){};                    // 失败回调  
    let option = {  
        cancel: self.$t('prompt.btn2'),          // i18n取消文字  
        buttons: [{  
                title: self.$t('prompt.imgBtn1') // i18n相机文字    
            },  
            {  
                title: self.$t('prompt.imgBtn2') // i18n相册文字    
            }  
        ]  
    };  
    if(!sourceType.includes('camera')) delete option.buttons[0];  
    if(!sourceType.includes('album')) delete option.buttons[1];  
    plus.nativeUI.actionSheet(option,  
        async (tap) => {  
            if(sourceType.length === 2) {  
                if( tap.index === 1) {  
                    // 拍照  
                    const cmr = plus.camera.getCamera();  
                    cmr.captureImage((path) => {  
                        plus.io.getFileInfo({  
                            filePath: path,  
                            success: res => {  
                                let result = {  
                                    errMsg: "chooseImage:ok",  
                                    tempFilePaths: [path],  
                                    tempFiles: [{  
                                        path: path,  
                                        size: res.size  
                                    }]  
                                };  
                                success(result);  
                            },  
                            fail: err => fail(err)  
                        });  
                        }, (err) => {fail(err)});  
                } else if (tap.index === 2) {  
                    // 从相册选择  
                    plus.gallery.pick(  
                    (res) => {  
                        let filesList = res.files;  
                        let tempFiles = [];  
                        for (let path of filesList) {  
                            plus.io.getFileInfo({  
                                filePath: path,  
                                success: r => {  
                                    tempFiles.push({  
                                        path: path,  
                                        size: r.size  
                                    });  
                                    if(tempFiles.length === filesList.length) {  
                                        success({  
                                            errMsg: "chooseImage:ok",  
                                            tempFilePaths: filesList,  
                                            tempFiles  
                                        })  
                                    }  
                                },  
                                fail: err => fail(err)  
                            });  
                        }  
                    }, (err) => {fail(err)},{  
                        filter: "image",  
                        multiple: true,  
                        maximum: count,  
                        system: false,  
                    });  
                }  
            } else if (sourceType.length === 1) {  
                if(sourceType.includes('camera')) {  
                    // 拍照  
                    const cmr = plus.camera.getCamera();  
                    cmr.captureImage((path) => {  
                        plus.io.getFileInfo({  
                            filePath: path,  
                            success: res => {  
                                let result = {  
                                    errMsg: "chooseImage:ok",  
                                    tempFilePaths: [path],  
                                    tempFiles: [{  
                                        path: path,  
                                        size: res.size  
                                    }]  
                                };  
                                success(result);  
                            },  
                            fail: err => fail(err)  
                        });  
                        }, (err) => {fail(err)});  
                } else {  
                    // 从相册选择  
                    plus.gallery.pick(  
                    (res) => {  
                        let filesList = res.files;  
                        let tempFiles = [];  
                        for (let path of filesList) {  
                            plus.io.getFileInfo({  
                                filePath: path,  
                                success: r => {  
                                    tempFiles.push({  
                                        path: path,  
                                        size: r.size  
                                    });  
                                    if(tempFiles.length === filesList.length) {  
                                        success({  
                                            errMsg: "chooseImage:ok",  
                                            tempFilePaths: filesList,  
                                            tempFiles  
                                        })  
                                    }  
                                },  
                                fail: err => fail(err)  
                            });  
                        }  
                    }, (err) => {fail(err)},{  
                        filter: "image",  
                        multiple: true,  
                        maximum: count,  
                        system: false,  
                    });  
                }  
            }  
        });  
}
收起阅读 »

uniapp动态路由、动态tabbar实战方案

动态加载 tabbar 路由 uniapp

uniapp用来开发app还是比较方便快捷的,官网教程挺细致,几乎所有问题都能找到答案。网上也有不少入门教程,腾讯课堂、b站上有好几个视频教程,感觉还可以(其实我没怎么看),比较偏实战,可以动手试一下。

本文假设阅读者已经基本掌握了uniapp的入门开发技能,如果还没有掌握uniapp,但你准备进行相关开发,赶紧从官方文档入手,再结合实战视频操作就好了,前面推荐的都不错。好了,进入本文主题——如何基于uniapp框架实现动态路由、动态tabbar

现状分析

vue项目上有路由插件Vue Router,所有路由都是统一管理,可以统一拦截,控制下一步动作,但是在uniapp上,情况就不一样了。uniapp上没有路由插件,然后页面分为tabbar和非tabbar两种,页面跳转也有自己的一套api,最重要的路由拦截功能并不支持,所有的页面都是在page.json文件里提前配置好。

需求分析

那假设我们要在app上做一套对应着用户权限的东西,就是不同用户,根据权限配置,决定了他进来后能用哪些页面哪些按钮,甚至是能控制到tabbar这块。这个需求,就需要根据权限控制路由内容及导向,但在目前条件下是实现不了的。不过在插件市场有不少人写了路由插件,思路其实都是向web端的vue Router看齐的,能完整满足需求的只有这个路由插件

还有很重要的一个需求就是动态tabbar,这是一个很重要内容,就是在app登录后主页下面的一栏按钮,这些tabbar基本上包括了app的主要内容,如何实现动态的tabbar,像上面提到的根据权限控制。比如说tabbar最多只能有五个,那我如何控制甲能看到5个,乙只能看到3个?这个需求在原生的uniapp上无法实现,原生的只能配置好pages,tabBar是pages的一个子项配置。不过插件市场上有人实现了tabbar的组件化,我们可以尝试下定制开发。

总结起来说需求其实就两个:

1、实现uniapp路由守卫;

2、实现动态tabbar;

第一个需求,前面提到过一些方案;第二个需求,动态tabbar,需要结合uview组件库tabbar组件来实现。我们再分析下目前的框架、插件能力,糅合上述需求,就有这张图:

erete

这张图里有些内容前面没有提到,比如状态管理、本地缓存,这是后面会用到的一个关键部分,下面的设计部分会提到。

方案设计

我们来捋一捋业务流程设计:

1、app输入用户名密码登录;

2、在路由守卫进行拦截,判断用户登录时的本地数据是否存在路由信息;如果没有则走第3步,如果有就走第4步;

3、调用接口获取到路由并从服务端获取路由数据;将路由数据存入本地(vuex、uniapp缓存);

4、将tabbar这一层级数据单独提出来存储,结合uview的tabbar组件实现动态tabbar;

5、在本地存一个全量的app按钮集合(除了tabbar,其他页面都是通过按钮跳转),与服务端获取的数据进行比对,得到一个按钮展示与否的配置数据集;

6、路由信息初始化后,进入用户定制化的初始页或者首页;

ok,分析得差不多了,下面介绍下具体实操步骤。

解决方案

一、实现路由守卫

路由插件推荐uni-simple-router,具体教程可以参考它的官方文档,写得比较详细了。以下是我的简单实战教程。

安装uni-simple-router插件
npm install uni-simple-router

接下来进行模块化配置,创建文件夹router,文件夹内容如下:

image-20201008215619273

home文件里是所有页面的路由配置,就像这样:

const home = [  
    {  
      path: '/pages/login/login',  
      aliasPath:'/app/login',  //对于h5端适用  
      name: 'login',  
        meta: {  
            title: '登录',  
        }  
    },  
    {  
      path: '/pages/index/index',  
      aliasPath:'/app/index',  //对于h5端适用  
      name: 'index',  
        meta: {  
            title: '首页',  
        }  
    }]  
export default home

这里必须要多说一句,由于这个插件没有动态写入功能,所以我们要实现权限管理,必须在本地配置完整的路由;而且pages.json中的内容也要完整配置,不过tabbar配置有些不同,后面会说到;

modules文件夹下的index只是一个模块读取的代码;

const files = require.context('.', false, /\.js$/)  
const modules = []  

files.keys().forEach(key => {  
  if (key === './index.js') return  
  const item = files(key).default  
  modules.push(...item)  
})  

export default modules

router根级目录下的index内容主要就是路由守卫:

import modules from './modules/index.js'  
import Vue from 'vue'  
import Router from 'uni-simple-router'  
import store from '@/store/store.js'  

Vue.use(Router)  
//初始化  
const router = new Router({  
    APP: {  
        holdTabbar: false //默认true  
    },  
    h5: {  
        vueRouterDev: true, //完全使用vue-router开发 默认 false    
    },  
    routes: [...modules] //路由表  
});  

//全局路由前置守卫  
router.beforeEach((to, from, next) => {  
    // 首先判断是否存在路由信息  
    //不存在就先调用接口得到数据  
   //具体内容可以参照上文的方案设计内容  
})  
// 全局路由后置守卫  
router.afterEach((to, from) => {})  
export default router;  

最后在app的main.js里需要这么引用:

import router from './router/index.js'  
import { RouterMount } from 'uni-simple-router'  
...  

//v1.3.5起 H5端 你应该去除原有的app.$mount();使用路由自带的渲染方式  
// #ifdef H5  
    RouterMount(app,'#app');  
// #endif  

//为了兼容小程序及app端必须这样写才有效果  
// #ifndef H5  
    app.$mount();   
// #endif

至于后面的路由用法,直接看官方文档,比较清楚,用法跟vue-router差不多。这里要特别说明的是上面代码初始化的那一块的几个配置:holdTabbar: false,vueRouterDev: true,前者表示在app端取代原生的tabbar拦截,在这个插件里拦截;后者表示在h5中完全用vue-router的api,废弃了包括此插件及uniapp的原生api,后者请慎用。

二、结合状态管理、uniapp数据缓存管理app缓存开销

这里其实没什么难度,也不给代码了,很简单。

从服务端获取的数据我可以存在状态管理里,但是vuex的数据存在内存中,容易丢失。比如说,app用一会儿,我拉到了后台,立马再点开是没问题可以用的;但如果时间太长了,不小心清了缓存,或者超过了服务端的超时限制,此时再次点开app,可能就缺失了部分数据。这个问题就要在路由守卫进行处理了,在路由跳转时就判断相关的状态数据是否存在,不存在就再进行处理。

再介绍一下uniapp的数据缓存api,更稳定,特别在app上,它不是缓存的概念,是持久化存在的,除非你调用它的清除方法。你可以利用这二者,分别存一些数据,搭配做一些控制,具体实现就自己琢磨下。比如你从后台拉回,状态管理的数据没了,但这个时间还在服务端超时范围内,那这个未超时的token数据就要存在uniapp的数据缓存中,我根据这个token再查到相关权限路由信息,给vuex赋值,再进行后续操作;如果拉回来,超时了,那就直接回到登录页。这一块,我只是提供思路,而且是成功的思路,亲测有效的。

三、实现动态tabbar

好了,能走到这一步,万里长征就走了一半了。别看下面的内容就那么一点,我把相关的资料拼起来实际上花了比上面那些更多的时间。

由于tabbar是单独配置的,原生的uniapp没有动态配置的方法。找了一圈,发现uview的tabbar组件可以实现。这块还是把步骤列一下:一步步来,是可以实现的!

1、修改pages.json配置
"tabBar": {  
        "list": [{  
            "pagePath": "pages/index/index"  
        },{  
            "pagePath": "pages/about/about"  
        }]  
    },

像这样,只留这些内容,跟原生配置比少了许多内容。

2、将tabbar信息单独存储到一个全局对象上

这一步可以存在vuex里,因为读取方便,uniapp的数据缓存读取稍微麻烦点。放在vuex里,取的时候很方便,像这样:

this.$store.state.userInfo.tabbarlist
3、在每一个tabbar页面配置tabbar组件
<template>  
    <view>  
        <view class="content">  
            ...  
        </view>  
        <u-tabbar :list="$store.state.userInfo.tabbarlist" @change="changeTb" :inactive-color="inactiveColor" :active-color="activeColor"></u-tabbar>  
    </view>  
</template>

模板部分格式要像上面这样,tabbar与正文内容并列;跳转代码如下,两种方式都是可以的,详细见文档

changeTb(index) {  
                // uni.switchTab({  
                //  url: this.$store.state.userInfo.tabbarlist[index].pagePath  
                // });  
                this.$Router.pushTab(this.$store.state.userInfo.tabbarlist[index].pagePath)  
            }

需要说明的是,这种方法是把uniapp的原生tabbar给隐藏了的;上面用到的状态管理中的tabbarlist数据是一个结构参数完整的tabbar配置,但是tabbar页面可能不是完整的,由用户权限决定。

好了,本文到此已经把动态路由、动态tabbar的完整方案讲清楚了,细节部分自己顺着我的思路,根据你自己的需要,逐步完善即可,希望对你会有启发和帮助。
这是我的原文链接

继续阅读 »

uniapp用来开发app还是比较方便快捷的,官网教程挺细致,几乎所有问题都能找到答案。网上也有不少入门教程,腾讯课堂、b站上有好几个视频教程,感觉还可以(其实我没怎么看),比较偏实战,可以动手试一下。

本文假设阅读者已经基本掌握了uniapp的入门开发技能,如果还没有掌握uniapp,但你准备进行相关开发,赶紧从官方文档入手,再结合实战视频操作就好了,前面推荐的都不错。好了,进入本文主题——如何基于uniapp框架实现动态路由、动态tabbar

现状分析

vue项目上有路由插件Vue Router,所有路由都是统一管理,可以统一拦截,控制下一步动作,但是在uniapp上,情况就不一样了。uniapp上没有路由插件,然后页面分为tabbar和非tabbar两种,页面跳转也有自己的一套api,最重要的路由拦截功能并不支持,所有的页面都是在page.json文件里提前配置好。

需求分析

那假设我们要在app上做一套对应着用户权限的东西,就是不同用户,根据权限配置,决定了他进来后能用哪些页面哪些按钮,甚至是能控制到tabbar这块。这个需求,就需要根据权限控制路由内容及导向,但在目前条件下是实现不了的。不过在插件市场有不少人写了路由插件,思路其实都是向web端的vue Router看齐的,能完整满足需求的只有这个路由插件

还有很重要的一个需求就是动态tabbar,这是一个很重要内容,就是在app登录后主页下面的一栏按钮,这些tabbar基本上包括了app的主要内容,如何实现动态的tabbar,像上面提到的根据权限控制。比如说tabbar最多只能有五个,那我如何控制甲能看到5个,乙只能看到3个?这个需求在原生的uniapp上无法实现,原生的只能配置好pages,tabBar是pages的一个子项配置。不过插件市场上有人实现了tabbar的组件化,我们可以尝试下定制开发。

总结起来说需求其实就两个:

1、实现uniapp路由守卫;

2、实现动态tabbar;

第一个需求,前面提到过一些方案;第二个需求,动态tabbar,需要结合uview组件库tabbar组件来实现。我们再分析下目前的框架、插件能力,糅合上述需求,就有这张图:

erete

这张图里有些内容前面没有提到,比如状态管理、本地缓存,这是后面会用到的一个关键部分,下面的设计部分会提到。

方案设计

我们来捋一捋业务流程设计:

1、app输入用户名密码登录;

2、在路由守卫进行拦截,判断用户登录时的本地数据是否存在路由信息;如果没有则走第3步,如果有就走第4步;

3、调用接口获取到路由并从服务端获取路由数据;将路由数据存入本地(vuex、uniapp缓存);

4、将tabbar这一层级数据单独提出来存储,结合uview的tabbar组件实现动态tabbar;

5、在本地存一个全量的app按钮集合(除了tabbar,其他页面都是通过按钮跳转),与服务端获取的数据进行比对,得到一个按钮展示与否的配置数据集;

6、路由信息初始化后,进入用户定制化的初始页或者首页;

ok,分析得差不多了,下面介绍下具体实操步骤。

解决方案

一、实现路由守卫

路由插件推荐uni-simple-router,具体教程可以参考它的官方文档,写得比较详细了。以下是我的简单实战教程。

安装uni-simple-router插件
npm install uni-simple-router

接下来进行模块化配置,创建文件夹router,文件夹内容如下:

image-20201008215619273

home文件里是所有页面的路由配置,就像这样:

const home = [  
    {  
      path: '/pages/login/login',  
      aliasPath:'/app/login',  //对于h5端适用  
      name: 'login',  
        meta: {  
            title: '登录',  
        }  
    },  
    {  
      path: '/pages/index/index',  
      aliasPath:'/app/index',  //对于h5端适用  
      name: 'index',  
        meta: {  
            title: '首页',  
        }  
    }]  
export default home

这里必须要多说一句,由于这个插件没有动态写入功能,所以我们要实现权限管理,必须在本地配置完整的路由;而且pages.json中的内容也要完整配置,不过tabbar配置有些不同,后面会说到;

modules文件夹下的index只是一个模块读取的代码;

const files = require.context('.', false, /\.js$/)  
const modules = []  

files.keys().forEach(key => {  
  if (key === './index.js') return  
  const item = files(key).default  
  modules.push(...item)  
})  

export default modules

router根级目录下的index内容主要就是路由守卫:

import modules from './modules/index.js'  
import Vue from 'vue'  
import Router from 'uni-simple-router'  
import store from '@/store/store.js'  

Vue.use(Router)  
//初始化  
const router = new Router({  
    APP: {  
        holdTabbar: false //默认true  
    },  
    h5: {  
        vueRouterDev: true, //完全使用vue-router开发 默认 false    
    },  
    routes: [...modules] //路由表  
});  

//全局路由前置守卫  
router.beforeEach((to, from, next) => {  
    // 首先判断是否存在路由信息  
    //不存在就先调用接口得到数据  
   //具体内容可以参照上文的方案设计内容  
})  
// 全局路由后置守卫  
router.afterEach((to, from) => {})  
export default router;  

最后在app的main.js里需要这么引用:

import router from './router/index.js'  
import { RouterMount } from 'uni-simple-router'  
...  

//v1.3.5起 H5端 你应该去除原有的app.$mount();使用路由自带的渲染方式  
// #ifdef H5  
    RouterMount(app,'#app');  
// #endif  

//为了兼容小程序及app端必须这样写才有效果  
// #ifndef H5  
    app.$mount();   
// #endif

至于后面的路由用法,直接看官方文档,比较清楚,用法跟vue-router差不多。这里要特别说明的是上面代码初始化的那一块的几个配置:holdTabbar: false,vueRouterDev: true,前者表示在app端取代原生的tabbar拦截,在这个插件里拦截;后者表示在h5中完全用vue-router的api,废弃了包括此插件及uniapp的原生api,后者请慎用。

二、结合状态管理、uniapp数据缓存管理app缓存开销

这里其实没什么难度,也不给代码了,很简单。

从服务端获取的数据我可以存在状态管理里,但是vuex的数据存在内存中,容易丢失。比如说,app用一会儿,我拉到了后台,立马再点开是没问题可以用的;但如果时间太长了,不小心清了缓存,或者超过了服务端的超时限制,此时再次点开app,可能就缺失了部分数据。这个问题就要在路由守卫进行处理了,在路由跳转时就判断相关的状态数据是否存在,不存在就再进行处理。

再介绍一下uniapp的数据缓存api,更稳定,特别在app上,它不是缓存的概念,是持久化存在的,除非你调用它的清除方法。你可以利用这二者,分别存一些数据,搭配做一些控制,具体实现就自己琢磨下。比如你从后台拉回,状态管理的数据没了,但这个时间还在服务端超时范围内,那这个未超时的token数据就要存在uniapp的数据缓存中,我根据这个token再查到相关权限路由信息,给vuex赋值,再进行后续操作;如果拉回来,超时了,那就直接回到登录页。这一块,我只是提供思路,而且是成功的思路,亲测有效的。

三、实现动态tabbar

好了,能走到这一步,万里长征就走了一半了。别看下面的内容就那么一点,我把相关的资料拼起来实际上花了比上面那些更多的时间。

由于tabbar是单独配置的,原生的uniapp没有动态配置的方法。找了一圈,发现uview的tabbar组件可以实现。这块还是把步骤列一下:一步步来,是可以实现的!

1、修改pages.json配置
"tabBar": {  
        "list": [{  
            "pagePath": "pages/index/index"  
        },{  
            "pagePath": "pages/about/about"  
        }]  
    },

像这样,只留这些内容,跟原生配置比少了许多内容。

2、将tabbar信息单独存储到一个全局对象上

这一步可以存在vuex里,因为读取方便,uniapp的数据缓存读取稍微麻烦点。放在vuex里,取的时候很方便,像这样:

this.$store.state.userInfo.tabbarlist
3、在每一个tabbar页面配置tabbar组件
<template>  
    <view>  
        <view class="content">  
            ...  
        </view>  
        <u-tabbar :list="$store.state.userInfo.tabbarlist" @change="changeTb" :inactive-color="inactiveColor" :active-color="activeColor"></u-tabbar>  
    </view>  
</template>

模板部分格式要像上面这样,tabbar与正文内容并列;跳转代码如下,两种方式都是可以的,详细见文档

changeTb(index) {  
                // uni.switchTab({  
                //  url: this.$store.state.userInfo.tabbarlist[index].pagePath  
                // });  
                this.$Router.pushTab(this.$store.state.userInfo.tabbarlist[index].pagePath)  
            }

需要说明的是,这种方法是把uniapp的原生tabbar给隐藏了的;上面用到的状态管理中的tabbarlist数据是一个结构参数完整的tabbar配置,但是tabbar页面可能不是完整的,由用户权限决定。

好了,本文到此已经把动态路由、动态tabbar的完整方案讲清楚了,细节部分自己顺着我的思路,根据你自己的需要,逐步完善即可,希望对你会有启发和帮助。
这是我的原文链接

收起阅读 »

HBuilderX 3.0.0 uniCloud目录结构调整说明

HBuilderX

HBuilderX, 3.0.0版本,调整了uniCloud目录结构。

项目根目录下为uniCloud目录,其下有二级目录cloudfunctionsdatabase。database目录存放数据表schema扩展验证函数

为什么调整?

现有的cloudfunctions-[aliyun|tcb]目录结构在小项目下可以满足使用的要求,随着项目的越来越大,模块之间的关系越来越复杂,开发人员可能会将大部分的精力放在维护模块关系和处理代码的问题上。

那么如何让开发人员花跟多的精力在真正应该关心的核心业务上?

简单的调用和依赖关系已经不能满足复杂的业务场景,为了更好的管理模块之间的关系,使调试和维护更简单,模块解耦合的先进理念也应该深入到云函数的设计中,为此,我们推出下一阶段的uniCloud目录结构,调整了原来的cloudfunctions-[aliyun|tcb]目录结构

1. uniCloud目录结构列表详情

2. 目录迁移

如何将cloudfunctions-tcb|aliyun迁移到uniCloud-tcb|aliyun?
HBuilderX启动后,如果您的项目管理器,项目下存在cloudfunctions-tcb|aliyun目录,则会自动弹窗迁移窗口。

当然,您也可以在项目上手动操作。选择cloudfunctions-tcb|aliyun目录,右键菜单点击【迁移cloudfunctions目录】

3. 初始化云函数cloudfunctions_init.json文件调整

cloudfunctions_init.json文件内容,迁移至云函数下package.json文件
如下图所示,package.json文件,cloudfunction-config字段,即为cloudfunctions_init.json文件内容。

选中云函数,右键菜单,点击【上传部署】,会一并更新云函数初始化配置

继续阅读 »

HBuilderX, 3.0.0版本,调整了uniCloud目录结构。

项目根目录下为uniCloud目录,其下有二级目录cloudfunctionsdatabase。database目录存放数据表schema扩展验证函数

为什么调整?

现有的cloudfunctions-[aliyun|tcb]目录结构在小项目下可以满足使用的要求,随着项目的越来越大,模块之间的关系越来越复杂,开发人员可能会将大部分的精力放在维护模块关系和处理代码的问题上。

那么如何让开发人员花跟多的精力在真正应该关心的核心业务上?

简单的调用和依赖关系已经不能满足复杂的业务场景,为了更好的管理模块之间的关系,使调试和维护更简单,模块解耦合的先进理念也应该深入到云函数的设计中,为此,我们推出下一阶段的uniCloud目录结构,调整了原来的cloudfunctions-[aliyun|tcb]目录结构

1. uniCloud目录结构列表详情

2. 目录迁移

如何将cloudfunctions-tcb|aliyun迁移到uniCloud-tcb|aliyun?
HBuilderX启动后,如果您的项目管理器,项目下存在cloudfunctions-tcb|aliyun目录,则会自动弹窗迁移窗口。

当然,您也可以在项目上手动操作。选择cloudfunctions-tcb|aliyun目录,右键菜单点击【迁移cloudfunctions目录】

3. 初始化云函数cloudfunctions_init.json文件调整

cloudfunctions_init.json文件内容,迁移至云函数下package.json文件
如下图所示,package.json文件,cloudfunction-config字段,即为cloudfunctions_init.json文件内容。

选中云函数,右键菜单,点击【上传部署】,会一并更新云函数初始化配置

收起阅读 »

【报BUG】阿里云云数据库DB Schema权限配置无效,请官方重视

bug已修复

云数据库中配置DB Schema如下

{  
  "bsonType": "object",  
  "required": [  
    "title"  
  ],  
  "permission": {  
    "read": true,  
    "create": true,  
    "update": "doc.uid == auth.uid",  
    "delete": "doc.uid == auth.uid"  
  },  
  "properties": {  
    "_id": {  
      "description": "ID,系统自动生成"  
    },  
    "uid": {  
      "forceDefaultValue": {  
        "$env": "uid"  
      },  
      "foreignKey": "uni-id-users._id",  
      "permission": {  
        "read": true,  
        "create": false,  
        "update": false,  
        "delete": false  
      },  
      "description": "用户ID"  
    },  
    "updated_at": {  
      "forceDefaultValue": {  
        "$env": "now"  
      },  
      "description": "数据更新时间"  
    },  
    "ip_address": {  
      "permission": {  
        "read": false,  
        "write": false  
      },  
      "forceDefaultValue": {  
        "$env": "clientIP"  
      },  
      "description": "数据更新时间"  
    },  
    "created_at": {  
      "bsonType": "int",  
      "permission": {  
        "read": true,  
        "create": false,  
        "update": false,  
        "delete": false  
      },  
      "forceDefaultValue": {  
        "$env": "now"  
      },  
      "description": "数据发布时间"  
    },  
    "title": {  
      "bsonType": "string",  
      "description": "标题"  
    }  
  }  
}

在前端使用clientDB查询数据,其中ip_address字段的值正常返回,并没有因为在权限中设置了read:false而不返回
这是严重的安全问题,望官方尽快修解决



顶起

继续阅读 »

云数据库中配置DB Schema如下

{  
  "bsonType": "object",  
  "required": [  
    "title"  
  ],  
  "permission": {  
    "read": true,  
    "create": true,  
    "update": "doc.uid == auth.uid",  
    "delete": "doc.uid == auth.uid"  
  },  
  "properties": {  
    "_id": {  
      "description": "ID,系统自动生成"  
    },  
    "uid": {  
      "forceDefaultValue": {  
        "$env": "uid"  
      },  
      "foreignKey": "uni-id-users._id",  
      "permission": {  
        "read": true,  
        "create": false,  
        "update": false,  
        "delete": false  
      },  
      "description": "用户ID"  
    },  
    "updated_at": {  
      "forceDefaultValue": {  
        "$env": "now"  
      },  
      "description": "数据更新时间"  
    },  
    "ip_address": {  
      "permission": {  
        "read": false,  
        "write": false  
      },  
      "forceDefaultValue": {  
        "$env": "clientIP"  
      },  
      "description": "数据更新时间"  
    },  
    "created_at": {  
      "bsonType": "int",  
      "permission": {  
        "read": true,  
        "create": false,  
        "update": false,  
        "delete": false  
      },  
      "forceDefaultValue": {  
        "$env": "now"  
      },  
      "description": "数据发布时间"  
    },  
    "title": {  
      "bsonType": "string",  
      "description": "标题"  
    }  
  }  
}

在前端使用clientDB查询数据,其中ip_address字段的值正常返回,并没有因为在权限中设置了read:false而不返回
这是严重的安全问题,望官方尽快修解决



顶起

收起阅读 »

建议编辑器把html及js的语义默认为html(ES6+)及javascript(ES6+),现在每次都去切换,有些麻烦

建议编辑器把html及js的语义默认为html(ES6+)及javascript(ES6+),现在每次都去切换,有些麻烦

建议编辑器把html及js的语义默认为html(ES6+)及javascript(ES6+),现在每次都去切换,有些麻烦

文件io操作中的moveTo和copyTo的坑

moveTo和copyTo的第二个参数,文档中说明的是可选,我自己实际操作,如果不给这个参数,永远都不成功,这两个参数给了就成功了。分享一下

moveTo和copyTo的第二个参数,文档中说明的是可选,我自己实际操作,如果不给这个参数,永远都不成功,这两个参数给了就成功了。分享一下

uniCloud.deleteFile相关

删除文件 uniCloud

试了一下uniCloud.deleteFile删除的方法,发现第一种未删除成功,第二种就删除成功了。有人知道啥原因嘛

试了一下uniCloud.deleteFile删除的方法,发现第一种未删除成功,第二种就删除成功了。有人知道啥原因嘛