HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

3.98 uni.getLocation 失效了嘛?

uni.getlocation

win 10
HX 3.98
调用uni.getLocation success 没结果,fail 也没结果 也不报错

win 10
HX 3.98
调用uni.getLocation success 没结果,fail 也没结果 也不报错

身份证阅读器社保卡读卡器Uniapp安卓原生项目实战SDK二次开发包

DONSEE系列身份证多功能读写器Android Uniapp API接口规范V1.0.0

本项目Uniapp调用了身份证读卡器的库文件:DonseeDeviceLib-debug.aar,该库放到nativeplugins\donsee-card\android,然后会自动加载。SDK会自动检查是否拥有USB设备权限,如没有权限,会自动进行申请,如果您的安卓设备是定制设备,遇到找不到设备时,请询问设备定制方是否开放了USB接口的系统层权限。

<!--
//========================================================================
// Uniapp Android 端,调用身份证阅读器.aar原生态读卡库
// 版权所有 广东东信智能科技有限公司
// 未经授权不允许对程序代码以任何形式任何目的的再发布
// 网站:http://www.eastcoms.com/
// 前端修改日期:2023.12.07
// =======================================================================
-->

<template>
<view class="content">
<text class="title">{{ title }}</text>
<view>

             <image  
                  v-bind:src="imageUlr"  
                 style="width:204rpx; height: 256rpx;"   
                  >  
                </image>  
        <view  
        class="resultInfor"  
        style="white-space: pre-wrap;"  
        >{{ result }}</view>  

        <button type="primary" @click="open">打开设备</button>  
        <button type="primary" @click="beep">蜂鸣器</button>  
        <button type="primary" @click="readIDCard">读取身份</button>  
        <button type="primary" @click="readSSCard">读取社保卡</button>  
        <button type="primary" @click="getBankCardNo">读取银行</button>  
        <button type="primary" @click="readICUid">IC卡卡号</button>  
        <button type="primary" @click="close">关闭设备</button>  
        <!-- <view class="">  
            {{result1}}  
        </view> -->  

    </view>  
</view>  

</template>

<script>
const DonseeDevice = uni.requireNativePlugin('Card-Module');

export default {
data() {
return {
title: '欢迎使用广东东信智能科技有限公司SDK',
result: '请先打开读卡设备',
result1:0,
imageUlr: '',
};

},  
onLoad() {},  
methods: {  

    open(){  

        DonseeDevice.Donsee_Open(data => {  
                if(data.result == 0){  
                    this.result = "打开设备成功"  
                }else if(data.result == -2){  
                    this.result = "打开设备失败:未发现读卡器";  
                }else if(data.result == -4){  
                    this.result = "打开设备失败:未获取usb权限";  
                }else {  
                    this.result = "打开设备失败:"+data.result;  
                }  

            });  

    },  
    beep(){  
        let ret =  DonseeDevice.Donsee_Beep();  
        if(ret == 0){  
            this.result ="蜂鸣成功";  
        }else{  
            this.result ="蜂鸣失败 "+ret;  
        }  

    },  
    close(){  

    let ret =  DonseeDevice.Donsee_Close();  
    if(ret == 0){  
        this.result ="关闭设备成功";  
    }else{  
        this.result ="关闭设备失败 "+ret;  
    }  

    },  
    readIDCard(){  

        let idInfo = DonseeDevice.Donsee_ReadIDCard(1);  //1:文字+照片  2:文字+照片+指纹  

        if(idInfo.code == 0){  
                this.result = "中文姓名:"+ idInfo.name+"\n"  
                +"英文姓名:"+ idInfo.enFullName+"\n"//如果是Y,则需要和英文姓名备用reserveName组合才是完整姓名  
                +"性    别:"+ idInfo.sex+"\n"  
                +"民    族:"+ idInfo.nation+"\n"  
                +"出身日期:"+ idInfo.birthDate+"\n"  
                +"家庭住址:"+ idInfo.address+"\n"  
                +"身份号:"+ idInfo.idNO+"\n"  
                +"签发单位:"+ idInfo.organs+"\n"  
                +"开始有效期限:"+ idInfo.issueDate+"\n"  
                +"结束有效期限:"+ idInfo.expireDate+"\n"  
                +"证件类别:"+ idInfo.certType+"\n"  //0 I J Y四种证件类型  
                +"证件版本:"+ idInfo.certVersion+"\n"  
                +"英文姓名备用:"+ idInfo.reserveName+"\n"    
                +"既往版本号码:"+ idInfo.previousVersionNO+"\n"  
                +"通行证号:"+ idInfo.passNu+"\n"  
                +"签发数次:"+ idInfo.signCount+"\n"  
                //+"指纹数据:"+ idInfo.figData+"\n"  
                this.imageUlr = "data:image/png;base64,"+idInfo.headStr;  //照片base64数据  
        }else{  
            this.result = idInfo.code;    
        }  

    },  

    readSSCard(){  

        let ssCardInfor = DonseeDevice.Donsee_ReadSSCard();  

        if(ssCardInfor.code == 0){  

                this.result = "姓    名:"+ ssCardInfor.name+"\n"  
                +"性    别:"+ ssCardInfor.sex+"\n"  
                +"民    族:"+ ssCardInfor.nation+"\n"  
                +"出身日期:"+ ssCardInfor.birthDate+"\n"  
                +"城市代码:"+ ssCardInfor.city+"\n"  
                +"身份号:"+ ssCardInfor.idNO+"\n"  
                +"社保卡号:"+ ssCardInfor.cardNO+"\n"  
                +"开始有效期限:"+ ssCardInfor.issueDate+"\n"  
                +"结束有效期限:"+ ssCardInfor.expireDate+"\n"  
                +"社保版本:"+ ssCardInfor.fullVersion+"\n"  

        }else{  
            this.result = ssCardInfor.code;   
        }  

    },  

    getBankCardNo(){  

        let cardInfor = DonseeDevice.Donsee_GetBankCardNo();  

        if(cardInfor.code == 0){  
                this.result = "卡号:"+ cardInfor.cardNumber  

        }else{  
            this.result = cardInfor.code;     
        }  

    },  

    readICUid(){  
        let cardInfor = DonseeDevice.Donsee_ReadICUid();  

        if(cardInfor.code == 0){  
                this.result = "卡号:"+ cardInfor.cardNumber  

        }else{  
            this.result = cardInfor.code;     
        }  

    }  
}  

};
</script>

<style>
.content {
text-align: left;
height: 400upx;
}
.resultInfor {
text-align: left;
}

.title {
font-size: 36upx;
color: #8f8f94;
}

button {
margin-top: 20upx;
margin-bottom: 20upx;
}

.button-sp-area {
margin: 0 auto;
width: 60%;
}

.content {
text-align: center;
height: 400upx;
}

.wrapper {
flex-direction: column;
justify-content: center;
}

.button {
width: 200px;
margin-top: 20px;
margin-left: 20px;
padding-top: 20px;
padding-bottom: 20px;
border-width: 2px;
border-style: solid;
border-color: #458b00;
background-color: #458b00;
}

.text {
font-size: 30px;
color: #666666;
text-align: center;
}
</style>

继续阅读 »

DONSEE系列身份证多功能读写器Android Uniapp API接口规范V1.0.0

本项目Uniapp调用了身份证读卡器的库文件:DonseeDeviceLib-debug.aar,该库放到nativeplugins\donsee-card\android,然后会自动加载。SDK会自动检查是否拥有USB设备权限,如没有权限,会自动进行申请,如果您的安卓设备是定制设备,遇到找不到设备时,请询问设备定制方是否开放了USB接口的系统层权限。

<!--
//========================================================================
// Uniapp Android 端,调用身份证阅读器.aar原生态读卡库
// 版权所有 广东东信智能科技有限公司
// 未经授权不允许对程序代码以任何形式任何目的的再发布
// 网站:http://www.eastcoms.com/
// 前端修改日期:2023.12.07
// =======================================================================
-->

<template>
<view class="content">
<text class="title">{{ title }}</text>
<view>

             <image  
                  v-bind:src="imageUlr"  
                 style="width:204rpx; height: 256rpx;"   
                  >  
                </image>  
        <view  
        class="resultInfor"  
        style="white-space: pre-wrap;"  
        >{{ result }}</view>  

        <button type="primary" @click="open">打开设备</button>  
        <button type="primary" @click="beep">蜂鸣器</button>  
        <button type="primary" @click="readIDCard">读取身份</button>  
        <button type="primary" @click="readSSCard">读取社保卡</button>  
        <button type="primary" @click="getBankCardNo">读取银行</button>  
        <button type="primary" @click="readICUid">IC卡卡号</button>  
        <button type="primary" @click="close">关闭设备</button>  
        <!-- <view class="">  
            {{result1}}  
        </view> -->  

    </view>  
</view>  

</template>

<script>
const DonseeDevice = uni.requireNativePlugin('Card-Module');

export default {
data() {
return {
title: '欢迎使用广东东信智能科技有限公司SDK',
result: '请先打开读卡设备',
result1:0,
imageUlr: '',
};

},  
onLoad() {},  
methods: {  

    open(){  

        DonseeDevice.Donsee_Open(data => {  
                if(data.result == 0){  
                    this.result = "打开设备成功"  
                }else if(data.result == -2){  
                    this.result = "打开设备失败:未发现读卡器";  
                }else if(data.result == -4){  
                    this.result = "打开设备失败:未获取usb权限";  
                }else {  
                    this.result = "打开设备失败:"+data.result;  
                }  

            });  

    },  
    beep(){  
        let ret =  DonseeDevice.Donsee_Beep();  
        if(ret == 0){  
            this.result ="蜂鸣成功";  
        }else{  
            this.result ="蜂鸣失败 "+ret;  
        }  

    },  
    close(){  

    let ret =  DonseeDevice.Donsee_Close();  
    if(ret == 0){  
        this.result ="关闭设备成功";  
    }else{  
        this.result ="关闭设备失败 "+ret;  
    }  

    },  
    readIDCard(){  

        let idInfo = DonseeDevice.Donsee_ReadIDCard(1);  //1:文字+照片  2:文字+照片+指纹  

        if(idInfo.code == 0){  
                this.result = "中文姓名:"+ idInfo.name+"\n"  
                +"英文姓名:"+ idInfo.enFullName+"\n"//如果是Y,则需要和英文姓名备用reserveName组合才是完整姓名  
                +"性    别:"+ idInfo.sex+"\n"  
                +"民    族:"+ idInfo.nation+"\n"  
                +"出身日期:"+ idInfo.birthDate+"\n"  
                +"家庭住址:"+ idInfo.address+"\n"  
                +"身份号:"+ idInfo.idNO+"\n"  
                +"签发单位:"+ idInfo.organs+"\n"  
                +"开始有效期限:"+ idInfo.issueDate+"\n"  
                +"结束有效期限:"+ idInfo.expireDate+"\n"  
                +"证件类别:"+ idInfo.certType+"\n"  //0 I J Y四种证件类型  
                +"证件版本:"+ idInfo.certVersion+"\n"  
                +"英文姓名备用:"+ idInfo.reserveName+"\n"    
                +"既往版本号码:"+ idInfo.previousVersionNO+"\n"  
                +"通行证号:"+ idInfo.passNu+"\n"  
                +"签发数次:"+ idInfo.signCount+"\n"  
                //+"指纹数据:"+ idInfo.figData+"\n"  
                this.imageUlr = "data:image/png;base64,"+idInfo.headStr;  //照片base64数据  
        }else{  
            this.result = idInfo.code;    
        }  

    },  

    readSSCard(){  

        let ssCardInfor = DonseeDevice.Donsee_ReadSSCard();  

        if(ssCardInfor.code == 0){  

                this.result = "姓    名:"+ ssCardInfor.name+"\n"  
                +"性    别:"+ ssCardInfor.sex+"\n"  
                +"民    族:"+ ssCardInfor.nation+"\n"  
                +"出身日期:"+ ssCardInfor.birthDate+"\n"  
                +"城市代码:"+ ssCardInfor.city+"\n"  
                +"身份号:"+ ssCardInfor.idNO+"\n"  
                +"社保卡号:"+ ssCardInfor.cardNO+"\n"  
                +"开始有效期限:"+ ssCardInfor.issueDate+"\n"  
                +"结束有效期限:"+ ssCardInfor.expireDate+"\n"  
                +"社保版本:"+ ssCardInfor.fullVersion+"\n"  

        }else{  
            this.result = ssCardInfor.code;   
        }  

    },  

    getBankCardNo(){  

        let cardInfor = DonseeDevice.Donsee_GetBankCardNo();  

        if(cardInfor.code == 0){  
                this.result = "卡号:"+ cardInfor.cardNumber  

        }else{  
            this.result = cardInfor.code;     
        }  

    },  

    readICUid(){  
        let cardInfor = DonseeDevice.Donsee_ReadICUid();  

        if(cardInfor.code == 0){  
                this.result = "卡号:"+ cardInfor.cardNumber  

        }else{  
            this.result = cardInfor.code;     
        }  

    }  
}  

};
</script>

<style>
.content {
text-align: left;
height: 400upx;
}
.resultInfor {
text-align: left;
}

.title {
font-size: 36upx;
color: #8f8f94;
}

button {
margin-top: 20upx;
margin-bottom: 20upx;
}

.button-sp-area {
margin: 0 auto;
width: 60%;
}

.content {
text-align: center;
height: 400upx;
}

.wrapper {
flex-direction: column;
justify-content: center;
}

.button {
width: 200px;
margin-top: 20px;
margin-left: 20px;
padding-top: 20px;
padding-bottom: 20px;
border-width: 2px;
border-style: solid;
border-color: #458b00;
background-color: #458b00;
}

.text {
font-size: 30px;
color: #666666;
text-align: center;
}
</style>

收起阅读 »

【解决】IOS,苹果上传提示:Error: Unexpected CFBundleExecutable Key.

App 苹果 iOS打包

来源:来源地址

找到第三方插件,所有的info.plist文件内找出 key 是 CFBundleExecutable(或 Executable file)的配置行,删除即可
window系统可以用vscode下载扩展插件Binary Plist就可以编辑

↓↓↓ 各位大佬点点赞

继续阅读 »

来源:来源地址

找到第三方插件,所有的info.plist文件内找出 key 是 CFBundleExecutable(或 Executable file)的配置行,删除即可
window系统可以用vscode下载扩展插件Binary Plist就可以编辑

↓↓↓ 各位大佬点点赞

收起阅读 »

HBuilderX与Vue3:开启Uniapp开发的新路程(教程)

HBuilderX入门教程 入门教程 uniapp 教程 uniapp

HBuilderX与Vue3:开启Uniapp开发的新纪元

随着技术的不断发展,使用最新版本的Vue3语法在HBuilderX中进行Uniapp开发已经成为了一种趋势。在这篇文章中,我们将为您详细介绍如何使用HBuilderX和Vue3来提高您的Uniapp开发效率。
一、HBuilderX与Vue3的完美结合
HBuilderX是一款强大的集成开发环境(IDE),它为Vue3开发者提供了一站式的解决方案。通过使用HBuilderX,您可以轻松地创建Vue3项目、编写代码、调试应用程序,以及预览和发布您的项目。此外,HBuilderX还支持多种浏览器和移动设备模拟器,让您可以在开发过程中实时查看应用程序的效果。

二、使用Vue3语法开发Uniapp的优势
使用Vue3语法开发Uniapp具有以下优势:

  1. 更好的性能:Vue3相比Vue2在性能上有了显著的提升,而Uniapp则可以在多个平台上运行,因此使用Vue3可以大大提高Uniapp的性能。
  2. 更简单的语法:Vue3的语法更加简洁、易读,使得开发者可以更快速地编写代码,同时减少了出错的可能性。
  3. 更好的组件化:Vue3引入了Composition API,使得组件的开发更加灵活和可维护。在Uniapp中,您可以利用Vue3的组件化特性,提高代码的可重用性和可维护性。

三、课程优势
1.一站式学习:本课程将为您详细介绍如何使用HBuilderX和Vue3进行Uniapp开发的全过程,让您从零开始,逐步掌握核心技能。

  1. 实战案例:我们为您准备了多个实战案例,让您在实际操作中深入理解知识点的运用,达到融会贯通的效果。
  2. 高效学习:课程采用线上回答+录播的形式,让您随时随地学习,高效利用时间。同时,我们还将为您提供丰富的课后资料,助您巩固知识,拓展视野。
  3. 课程目录:大于180课时从基础组件开始,每日更新。
    课程地址(点击跳转)
    5.社群交流:加入我们的学习社群,与志同道合的朋友一起交流心得,互相学习,共同进步。同时,我们还将定期进行更多实战项目和源码线上分享,让您收获更多干货。
    V:ANKR6699

四、课程特色
1.学员们可以免费使用讲师开发的Uniapp插件市场内的全部插件。
点击跳转
2.后期讲师开发的项目模板全套免费使用。
3.学员实际项目开发中,答疑遇到的问题。
4.后期录制的其他方面的课程可享受折扣。
8.HBuilderX支持实时预览功能,可以实时查看应用在真机和模拟器上的运行效果,方便开发者进行调试和优化。
9.代码提示和自动补全:HBuilderX提供了智能代码提示和自动补全功能,可以帮助开发者快速编写代码,减少出错率。
10.集成调试器:HBuilderX集成了调试器,可以帮助开发者进行代码调试,快速定位和解决问题。

五、课程收获

  1. 掌握HBuilderX的使用技巧和Vue3的最新语法,提升您的开发效率。
  2. 学会如何利用Vue3的组件化特性来提高代码的可重用性和可维护性。
  3. 掌握如何调试和优化Uniapp应用程序的性能,使其在多个平台上达到最佳运行效果。
  4. 通过实战案例的练习,让您具备独立开发高质量Uniapp应用程序的能力。
  5. 学会前后端全端开发,一技在手全栈拿手。
    还在等什么?快来加入我们的课程吧!让我们一起开启HBuilderX与Vue3的Uniapp开发之旅,共创美好未来!

六、如何参与学习
想要参与学习,您可以按照以下步骤操作:
1.访问我们的课程网站。
课程地址(点击跳转)
2.在课程页面中,您会看到课程介绍、课程大纲、课程费用等相关信息。请仔细阅读,了解课程的基本情况。
(每日更新)

  1. 在课程期间,我们将为您提供必要的学习资料和指导,帮助您更好地掌握知识和技能。请积极参与课堂讨论和实操练习,与同学们互相学习、共同进步。

七、结语
通过我们的HBuilderX与Vue3教学课程,您将掌握最新的Uniapp开发技能,为未来的职业发展打下坚实基础。我们期待您的加入,与我们一起开启全新的技术之旅!

八、附加价值
除了核心的知识和技能,我们的课程还为您提供了以下附加价值:

  1. 案例分享:我们会定期邀请成功的开发者进行案例分享,让您了解最新的开发趋势和实践经验,拓宽您的视野。
  2. 职业规划建议:我们提供个性化的职业规划建议,帮助您更好地规划未来的职业发展路径,提升职业竞争力。

九、总结
通过我们的HBuilderX与Vue3教学课程,您将获得最新的Uniapp开发技能、实战经验和附加价值。我们的导师团队将为您提供全方位的学习支持,帮助您在短时间内快速提升开发能力。加入我们的课程,开启全新的技术之旅,为未来的职业发展打下坚实基础!
现在就来报名参加我们的课程吧!让我们一起学习、成长和进步!

继续阅读 »

HBuilderX与Vue3:开启Uniapp开发的新纪元

随着技术的不断发展,使用最新版本的Vue3语法在HBuilderX中进行Uniapp开发已经成为了一种趋势。在这篇文章中,我们将为您详细介绍如何使用HBuilderX和Vue3来提高您的Uniapp开发效率。
一、HBuilderX与Vue3的完美结合
HBuilderX是一款强大的集成开发环境(IDE),它为Vue3开发者提供了一站式的解决方案。通过使用HBuilderX,您可以轻松地创建Vue3项目、编写代码、调试应用程序,以及预览和发布您的项目。此外,HBuilderX还支持多种浏览器和移动设备模拟器,让您可以在开发过程中实时查看应用程序的效果。

二、使用Vue3语法开发Uniapp的优势
使用Vue3语法开发Uniapp具有以下优势:

  1. 更好的性能:Vue3相比Vue2在性能上有了显著的提升,而Uniapp则可以在多个平台上运行,因此使用Vue3可以大大提高Uniapp的性能。
  2. 更简单的语法:Vue3的语法更加简洁、易读,使得开发者可以更快速地编写代码,同时减少了出错的可能性。
  3. 更好的组件化:Vue3引入了Composition API,使得组件的开发更加灵活和可维护。在Uniapp中,您可以利用Vue3的组件化特性,提高代码的可重用性和可维护性。

三、课程优势
1.一站式学习:本课程将为您详细介绍如何使用HBuilderX和Vue3进行Uniapp开发的全过程,让您从零开始,逐步掌握核心技能。

  1. 实战案例:我们为您准备了多个实战案例,让您在实际操作中深入理解知识点的运用,达到融会贯通的效果。
  2. 高效学习:课程采用线上回答+录播的形式,让您随时随地学习,高效利用时间。同时,我们还将为您提供丰富的课后资料,助您巩固知识,拓展视野。
  3. 课程目录:大于180课时从基础组件开始,每日更新。
    课程地址(点击跳转)
    5.社群交流:加入我们的学习社群,与志同道合的朋友一起交流心得,互相学习,共同进步。同时,我们还将定期进行更多实战项目和源码线上分享,让您收获更多干货。
    V:ANKR6699

四、课程特色
1.学员们可以免费使用讲师开发的Uniapp插件市场内的全部插件。
点击跳转
2.后期讲师开发的项目模板全套免费使用。
3.学员实际项目开发中,答疑遇到的问题。
4.后期录制的其他方面的课程可享受折扣。
8.HBuilderX支持实时预览功能,可以实时查看应用在真机和模拟器上的运行效果,方便开发者进行调试和优化。
9.代码提示和自动补全:HBuilderX提供了智能代码提示和自动补全功能,可以帮助开发者快速编写代码,减少出错率。
10.集成调试器:HBuilderX集成了调试器,可以帮助开发者进行代码调试,快速定位和解决问题。

五、课程收获

  1. 掌握HBuilderX的使用技巧和Vue3的最新语法,提升您的开发效率。
  2. 学会如何利用Vue3的组件化特性来提高代码的可重用性和可维护性。
  3. 掌握如何调试和优化Uniapp应用程序的性能,使其在多个平台上达到最佳运行效果。
  4. 通过实战案例的练习,让您具备独立开发高质量Uniapp应用程序的能力。
  5. 学会前后端全端开发,一技在手全栈拿手。
    还在等什么?快来加入我们的课程吧!让我们一起开启HBuilderX与Vue3的Uniapp开发之旅,共创美好未来!

六、如何参与学习
想要参与学习,您可以按照以下步骤操作:
1.访问我们的课程网站。
课程地址(点击跳转)
2.在课程页面中,您会看到课程介绍、课程大纲、课程费用等相关信息。请仔细阅读,了解课程的基本情况。
(每日更新)

  1. 在课程期间,我们将为您提供必要的学习资料和指导,帮助您更好地掌握知识和技能。请积极参与课堂讨论和实操练习,与同学们互相学习、共同进步。

七、结语
通过我们的HBuilderX与Vue3教学课程,您将掌握最新的Uniapp开发技能,为未来的职业发展打下坚实基础。我们期待您的加入,与我们一起开启全新的技术之旅!

八、附加价值
除了核心的知识和技能,我们的课程还为您提供了以下附加价值:

  1. 案例分享:我们会定期邀请成功的开发者进行案例分享,让您了解最新的开发趋势和实践经验,拓宽您的视野。
  2. 职业规划建议:我们提供个性化的职业规划建议,帮助您更好地规划未来的职业发展路径,提升职业竞争力。

九、总结
通过我们的HBuilderX与Vue3教学课程,您将获得最新的Uniapp开发技能、实战经验和附加价值。我们的导师团队将为您提供全方位的学习支持,帮助您在短时间内快速提升开发能力。加入我们的课程,开启全新的技术之旅,为未来的职业发展打下坚实基础!
现在就来报名参加我们的课程吧!让我们一起学习、成长和进步!

收起阅读 »

web-view组件显示异常问题,可能为本地开发环境导致

uni_app

分享一次踩坑。
我习惯用本地环境调试,通过web-view url访问本地开发环境的一个前端页面,结果无论web浏览器还是app浏览器都可以正常显示,进入app就出问题。
最后,只要把代码打包部署到服务器上,url改为服务器上的页面,就可以正常显示了。因为本地开发环境的一些代码兼容性较差,打包之后就没有这个问题了

继续阅读 »

分享一次踩坑。
我习惯用本地环境调试,通过web-view url访问本地开发环境的一个前端页面,结果无论web浏览器还是app浏览器都可以正常显示,进入app就出问题。
最后,只要把代码打包部署到服务器上,url改为服务器上的页面,就可以正常显示了。因为本地开发环境的一些代码兼容性较差,打包之后就没有这个问题了

收起阅读 »

应用闪退分析与 uniapp 安卓原生插件开发

uniapp离线打包 uniapp uniapp原生插件

前言

公司使用 uniapp 开发的 App 端项目在红米 Note11T Pro 中出现了拍照后闪退的问题,也是折腾了挺久才研究出原因和解决方案,在这里记录和分享。

调试分析

首先可以肯定的是并非代码的问题,在调用拍摄 Api(uni.chooseImage)前、成功回调、失败回调中都打了断点,除了拍摄前的断点进入了,成功和失败回调是没有进入的,而且闪退只在少部分机型上出现,不是必现的行为。

在网上查了一些 uniapp 拍照闪退的相关资料,可以排除的原因有

  • 没有配置权限
  • 拍照像素过大,GPU 渲染崩溃
    • 按下拍摄键还没确认的时候应用其实就已经闪退了,也就不存在 GPU 渲染问题

既然前端代码没有问题,那我们就得往低一层去调试分析了,使用离线打包配置,在 as 中运行项目到真机并开启 logcat:

20231203154344

目前的日志有点多,包含了整个系统的日志输出,所以我们需要做个过滤:

20231203155600

过滤包名为 com.android.simple 且等级为 warnerror 的日志输出。完整的过滤语法看官网文档 使用 Logcat 查看日志

接下来我们需要在设备(真机、模拟器)上进行操作来复现闪退的行为,然后查找可疑的 crash 日志。

a51f4013-6f9a-4a80-a69e-10a88a34f338

这样操作了几次,虽然会复现闪退行为,但并没有找到相关的 crash 日志,得到的信息只有:按下拍摄键后应用进程被结束。

20231203173635

到这里信息基本算是断了,但是没有 crash 日志,闪退的可能性降低了,那是不是系统回收了应用资源呢?我们搜索对应的机型 + 关键词:

20231203174504

通过上面的两篇文章,我们可以发现一些共同点:

  • 调用系统相机进行拍摄
  • 拍摄时应用进入后台,此时可能会被系统回收资源

这两篇文章的问题与我们目前经历的很像,都是调用系统相机拍摄后应用被结束了进程,那么很大的可能是应用进入后台后被系统回收了资源。

如果是这样的话那我们需要降低应用进入后台后被系统回收资源的几率,也就是保活,为此找到了一篇 Android 保活相关的文章:

从这篇文章里知道了一个关键的信息:应用后台优先级 — oom_adj 值。

这个值是反映进程的优先级的,在系统内存不足时,会根据这个值去决定将哪些进程回收(kill),值越低表示优先级越高,越不可能被回收资源。常见的值有:

  • 0:前台进程,应用目前在前台运行
  • 1:可感知进程,比如播放音乐,通知栏有可交互的控件
  • 负数:属于系统级别的进程

这个值是根据你的应用状态实时变化的,可以通过以下命令查看你的应用 oom_adj 值:

- adb shell "ps|grep your package name"  

- adb shell  
- cat /proc/pid/oom_adj

前台

20231203181714

后台

20231203182001

可以发现什么也没做的话(如果开通了厂商提供的通知服务,如 MiPush,会开启一个专属的通知进程,可以提升应用的后台优先级,但是测试发现红米 Note11T Pro Android13 里这个通知进程也很容易被 kill)应用进入后台时的优先级是很低的,调用拍摄又是个消耗大量内存的行为,也就不奇怪会出现应用被回收资源的问题了。

uniapp Android 原生插件开发

既然知道了可能的原因,我们就需要有针对的去解决,这里我采用 Android实现进程保活方案解析 中的前台服务方案,开发一个 Android 原生插件,尝试提升应用的优先级。参考 uniapp 文档:

关于环境的配置就不过多介绍,需要的可以看下面这篇文章:

module 配置

我们先新建一个 module:

20231203184854

20231203185003

20231203185030

可能会出现报错,我们直接 cv uniapp Android SDK 中 richalert.gradle 配置并点击 Try Again

20231203185332

如果出现错误 package name not found,我们直接点击进入对应的文件:

20231203185630

添加自己的包名并再次尝试:

20231203185801

删除这两个文件:

20231203190230

新建一个 uniapp module 类供其调用:

20231203190442

20231203190548

根据文档,这个需要和 uniapp 打交道的类需要继承 UniModule:

20231203190821

我们可以定义在前端代码中调用的方法,这个方法需要加上 @UniJSMethod(uiThread = boolean) 注解,uiThread 标识是否运行在 ui 线程:

20231203191508

然后需要在 uniapp 的配置中配置这个类,并在主模块的 .gradle 配置中添加依赖,然后编译:

20231203191950

20231203192121

现在可以试试是否可以调用:

20231203192749

20231203193312

保活功能实现

新建一个类并继承自 Service:

20231203194231

在这个类中创建一个前台服务:

20231203202148

AndroidManifest.json 中注册服务并添加前台服务权限:

20231203202427

修改之前的 startForeground 方法,开启一个服务:

20231203202324

得到的效果如下:

edd3fa92-afe3-4c75-af14-39240c39e1d0

查看应用后台时的 oom_adj 值确实变小了,也没有再次测到拍照闪退的问题,现在可以肯定是系统回收资源导致的了:

20231203204159

可以使用手机的开发者选项,开启后台进程限制,选择最多不超过两个后台进程,然后开启你的应用和两个额外的应用,在这三个应用间切换,你会发现被杀死的总是另外两个应用,你的应用是一直存活的。

后话

本来到这里以为已经结束了,谁知道同事说以前做过保活功能,但是被应用商店给打回了,目前国内对后台运行、自启动、关联启动基本是 0 容忍。

也尝试过双进程守护等保活实现,但是现在的系统对于后台服务在后台运行超过一定时间后会直接杀死,可以说目前国内想实现保活基本是不可能了。

目前还没有更新发版,不确定这种轻量级的保活能不能审核通过;有大佬提供过一种思路:不调用系统相机,自定义拍照页面来完成拍照功能。这样应用还是在前台的,也就不会被系统杀死了。

最后附上另外一位热心大佬和我的沟通讨论:应用保活讨论

2023 年 12 月 4 日更新

尝试使用了插件市场中的 自定义相机 自动裁剪 自定义拍照 支持配置大小【更多自定义相机请联系作者】 插件,实测红米 Note11T Pro 未出现应用被系统资源回收的行为,且查看调用自定义相机时的应用 oom_adj 值为 0,即当前应用进程仍处于前台:

oom_adj

继续阅读 »

前言

公司使用 uniapp 开发的 App 端项目在红米 Note11T Pro 中出现了拍照后闪退的问题,也是折腾了挺久才研究出原因和解决方案,在这里记录和分享。

调试分析

首先可以肯定的是并非代码的问题,在调用拍摄 Api(uni.chooseImage)前、成功回调、失败回调中都打了断点,除了拍摄前的断点进入了,成功和失败回调是没有进入的,而且闪退只在少部分机型上出现,不是必现的行为。

在网上查了一些 uniapp 拍照闪退的相关资料,可以排除的原因有

  • 没有配置权限
  • 拍照像素过大,GPU 渲染崩溃
    • 按下拍摄键还没确认的时候应用其实就已经闪退了,也就不存在 GPU 渲染问题

既然前端代码没有问题,那我们就得往低一层去调试分析了,使用离线打包配置,在 as 中运行项目到真机并开启 logcat:

20231203154344

目前的日志有点多,包含了整个系统的日志输出,所以我们需要做个过滤:

20231203155600

过滤包名为 com.android.simple 且等级为 warnerror 的日志输出。完整的过滤语法看官网文档 使用 Logcat 查看日志

接下来我们需要在设备(真机、模拟器)上进行操作来复现闪退的行为,然后查找可疑的 crash 日志。

a51f4013-6f9a-4a80-a69e-10a88a34f338

这样操作了几次,虽然会复现闪退行为,但并没有找到相关的 crash 日志,得到的信息只有:按下拍摄键后应用进程被结束。

20231203173635

到这里信息基本算是断了,但是没有 crash 日志,闪退的可能性降低了,那是不是系统回收了应用资源呢?我们搜索对应的机型 + 关键词:

20231203174504

通过上面的两篇文章,我们可以发现一些共同点:

  • 调用系统相机进行拍摄
  • 拍摄时应用进入后台,此时可能会被系统回收资源

这两篇文章的问题与我们目前经历的很像,都是调用系统相机拍摄后应用被结束了进程,那么很大的可能是应用进入后台后被系统回收了资源。

如果是这样的话那我们需要降低应用进入后台后被系统回收资源的几率,也就是保活,为此找到了一篇 Android 保活相关的文章:

从这篇文章里知道了一个关键的信息:应用后台优先级 — oom_adj 值。

这个值是反映进程的优先级的,在系统内存不足时,会根据这个值去决定将哪些进程回收(kill),值越低表示优先级越高,越不可能被回收资源。常见的值有:

  • 0:前台进程,应用目前在前台运行
  • 1:可感知进程,比如播放音乐,通知栏有可交互的控件
  • 负数:属于系统级别的进程

这个值是根据你的应用状态实时变化的,可以通过以下命令查看你的应用 oom_adj 值:

- adb shell "ps|grep your package name"  

- adb shell  
- cat /proc/pid/oom_adj

前台

20231203181714

后台

20231203182001

可以发现什么也没做的话(如果开通了厂商提供的通知服务,如 MiPush,会开启一个专属的通知进程,可以提升应用的后台优先级,但是测试发现红米 Note11T Pro Android13 里这个通知进程也很容易被 kill)应用进入后台时的优先级是很低的,调用拍摄又是个消耗大量内存的行为,也就不奇怪会出现应用被回收资源的问题了。

uniapp Android 原生插件开发

既然知道了可能的原因,我们就需要有针对的去解决,这里我采用 Android实现进程保活方案解析 中的前台服务方案,开发一个 Android 原生插件,尝试提升应用的优先级。参考 uniapp 文档:

关于环境的配置就不过多介绍,需要的可以看下面这篇文章:

module 配置

我们先新建一个 module:

20231203184854

20231203185003

20231203185030

可能会出现报错,我们直接 cv uniapp Android SDK 中 richalert.gradle 配置并点击 Try Again

20231203185332

如果出现错误 package name not found,我们直接点击进入对应的文件:

20231203185630

添加自己的包名并再次尝试:

20231203185801

删除这两个文件:

20231203190230

新建一个 uniapp module 类供其调用:

20231203190442

20231203190548

根据文档,这个需要和 uniapp 打交道的类需要继承 UniModule:

20231203190821

我们可以定义在前端代码中调用的方法,这个方法需要加上 @UniJSMethod(uiThread = boolean) 注解,uiThread 标识是否运行在 ui 线程:

20231203191508

然后需要在 uniapp 的配置中配置这个类,并在主模块的 .gradle 配置中添加依赖,然后编译:

20231203191950

20231203192121

现在可以试试是否可以调用:

20231203192749

20231203193312

保活功能实现

新建一个类并继承自 Service:

20231203194231

在这个类中创建一个前台服务:

20231203202148

AndroidManifest.json 中注册服务并添加前台服务权限:

20231203202427

修改之前的 startForeground 方法,开启一个服务:

20231203202324

得到的效果如下:

edd3fa92-afe3-4c75-af14-39240c39e1d0

查看应用后台时的 oom_adj 值确实变小了,也没有再次测到拍照闪退的问题,现在可以肯定是系统回收资源导致的了:

20231203204159

可以使用手机的开发者选项,开启后台进程限制,选择最多不超过两个后台进程,然后开启你的应用和两个额外的应用,在这三个应用间切换,你会发现被杀死的总是另外两个应用,你的应用是一直存活的。

后话

本来到这里以为已经结束了,谁知道同事说以前做过保活功能,但是被应用商店给打回了,目前国内对后台运行、自启动、关联启动基本是 0 容忍。

也尝试过双进程守护等保活实现,但是现在的系统对于后台服务在后台运行超过一定时间后会直接杀死,可以说目前国内想实现保活基本是不可能了。

目前还没有更新发版,不确定这种轻量级的保活能不能审核通过;有大佬提供过一种思路:不调用系统相机,自定义拍照页面来完成拍照功能。这样应用还是在前台的,也就不会被系统杀死了。

最后附上另外一位热心大佬和我的沟通讨论:应用保活讨论

2023 年 12 月 4 日更新

尝试使用了插件市场中的 自定义相机 自动裁剪 自定义拍照 支持配置大小【更多自定义相机请联系作者】 插件,实测红米 Note11T Pro 未出现应用被系统资源回收的行为,且查看调用自定义相机时的应用 oom_adj 值为 0,即当前应用进程仍处于前台:

oom_adj

收起阅读 »

全套Uniapp组件、API,Vue3的API等等进行详细教学包含视频及课件示例源码

vue3 uniapp 教程

全套Uniapp组件、API,Vue3的API等等进行详细教学包含视频及课件示例源码

链接:https://edu.csdn.net/course/detail/39132

全套Uniapp组件、API,Vue3的API等等进行详细教学包含视频及课件示例源码

链接:https://edu.csdn.net/course/detail/39132

继续阅读 »

全套Uniapp组件、API,Vue3的API等等进行详细教学包含视频及课件示例源码

链接:https://edu.csdn.net/course/detail/39132

全套Uniapp组件、API,Vue3的API等等进行详细教学包含视频及课件示例源码

链接:https://edu.csdn.net/course/detail/39132

收起阅读 »

关于华为应用市场上架,申请权限未告知目的被驳回问题的简单处理方式

华为应用市场

近期关于华为应用市场上架过程中出现的【您的应用在运行时,未同步告知权限申请的使用目的,向用户索取(存储、拍照)等权限,不符合华为应用市场审核标准。】

请参考《审核指南》第7.21相关审核要求:https://developer.huawei.com/consumer/cn/doc/app/50104-07#h3-1683701612940-2
(应用申请的权限,都必须有明确、合理的使用场景和功能说明,禁止诱导或误导用户授权,应用使用的权限必须与申请所述的一致。在申请敏感权限时,需同步告知用户申请该权限的目的。未经用户同意,不得更改用户权限授权状态。)

针对以上问题,个人结合现有问题贴处理方式如下,仅供参考,各位可拿去按照自己需求进行细化调整,代码如下:

使用方式:
1.引入:

import permision from "@/js_sdk/permission.js"

2.判断:

var result = await permision.premissionCheck("EXTERNAL_STORAGE")  
if(result == 1) {  
    // 此处处理各自业务 如:uni.chooseImage  
}

3.permission.js源码参考如下

var isIos  
// #ifdef APP-PLUS  
isIos = (plus.os.name == "iOS")  
// #endif  

// 判断推送权限是否开启  
function judgeIosPermissionPush() {  
    var result = false;  
    var UIApplication = plus.ios.import("UIApplication");  
    var app = UIApplication.sharedApplication();  
    var enabledTypes = 0;  
    if (app.currentUserNotificationSettings) {  
        var settings = app.currentUserNotificationSettings();  
        enabledTypes = settings.plusGetAttribute("types");  
        console.log("enabledTypes1:" + enabledTypes);  
        if (enabledTypes == 0) {  
            console.log("推送权限没有开启");  
        } else {  
            result = true;  
            console.log("已经开启推送功能!")  
        }  
        plus.ios.deleteObject(settings);  
    } else {  
        enabledTypes = app.enabledRemoteNotificationTypes();  
        if (enabledTypes == 0) {  
            console.log("推送权限没有开启!");  
        } else {  
            result = true;  
            console.log("已经开启推送功能!")  
        }  
        console.log("enabledTypes2:" + enabledTypes);  
    }  
    plus.ios.deleteObject(app);  
    plus.ios.deleteObject(UIApplication);  
    return result;  
}  

// 判断定位权限是否开启  
function judgeIosPermissionLocation() {  
    var result = false;  
    var cllocationManger = plus.ios.import("CLLocationManager");  
    var status = cllocationManger.authorizationStatus();  
    result = (status != 2)  
    console.log("定位权限开启:" + result);  
    // 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation  
    /* var enable = cllocationManger.locationServicesEnabled();  
    var status = cllocationManger.authorizationStatus();  
    console.log("enable:" + enable);  
    console.log("status:" + status);  
    if (enable && status != 2) {  
        result = true;  
        console.log("手机定位服务已开启且已授予定位权限");  
    } else {  
        console.log("手机系统的定位没有打开或未给予定位权限");  
    } */  
    plus.ios.deleteObject(cllocationManger);  
    return result;  
}  

// 判断麦克风权限是否开启  
function judgeIosPermissionRecord() {  
    var result = false;  
    var avaudiosession = plus.ios.import("AVAudioSession");  
    var avaudio = avaudiosession.sharedInstance();  
    var permissionStatus = avaudio.recordPermission();  
    console.log("permissionStatus:" + permissionStatus);  
    if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {  
        console.log("麦克风权限没有开启");  
    } else {  
        result = true;  
        console.log("麦克风权限已经开启");  
    }  
    plus.ios.deleteObject(avaudiosession);  
    return result;  
}  

// 判断相机权限是否开启  
function judgeIosPermissionCamera() {  
    var result = false;  
    var AVCaptureDevice = plus.ios.import("AVCaptureDevice");  
    var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');  
    console.log("authStatus:" + authStatus);  
    if (authStatus == 3) {  
        result = true;  
        console.log("相机权限已经开启");  
    } else {  
        console.log("相机权限没有开启");  
    }  
    plus.ios.deleteObject(AVCaptureDevice);  
    return result;  
}  

// 判断相册权限是否开启  
function judgeIosPermissionPhotoLibrary() {  
    var result = false;  
    var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");  
    var authStatus = PHPhotoLibrary.authorizationStatus();  
    console.log("authStatus:" + authStatus);  
    if (authStatus == 3) {  
        result = true;  
        console.log("相册权限已经开启");  
    } else {  
        console.log("相册权限没有开启");  
    }  
    plus.ios.deleteObject(PHPhotoLibrary);  
    return result;  
}  

// 判断通讯录权限是否开启  
function judgeIosPermissionContact() {  
    var result = false;  
    var CNContactStore = plus.ios.import("CNContactStore");  
    var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);  
    if (cnAuthStatus == 3) {  
        result = true;  
        console.log("通讯录权限已经开启");  
    } else {  
        console.log("通讯录权限没有开启");  
    }  
    plus.ios.deleteObject(CNContactStore);  
    return result;  
}  

// 判断日历权限是否开启  
function judgeIosPermissionCalendar() {  
    var result = false;  
    var EKEventStore = plus.ios.import("EKEventStore");  
    var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);  
    if (ekAuthStatus == 3) {  
        result = true;  
        console.log("日历权限已经开启");  
    } else {  
        console.log("日历权限没有开启");  
    }  
    plus.ios.deleteObject(EKEventStore);  
    return result;  
}  

// 判断备忘录权限是否开启  
function judgeIosPermissionMemo() {  
    var result = false;  
    var EKEventStore = plus.ios.import("EKEventStore");  
    var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);  
    if (ekAuthStatus == 3) {  
        result = true;  
        console.log("备忘录权限已经开启");  
    } else {  
        console.log("备忘录权限没有开启");  
    }  
    plus.ios.deleteObject(EKEventStore);  
    return result;  
}  

// Android权限查询  
function requestAndroidPermission(permissionID) {  
    return new Promise((resolve, reject) => {  
        plus.android.requestPermissions(  
            permissionID.split(","),  
            // [permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装  
            function(resultObj) {  
                var result = 0;  
                for (var i = 0; i < resultObj.granted.length; i++) {  
                    var grantedPermission = resultObj.granted[i];  
                    console.log('已获取的权限:' + grantedPermission);  
                    result = 1  
                }  
                for (var i = 0; i < resultObj.deniedPresent.length; i++) {  
                    var deniedPresentPermission = resultObj.deniedPresent[i];  
                    console.log('拒绝本次申请的权限:' + deniedPresentPermission);  
                    result = 0  
                }  
                for (var i = 0; i < resultObj.deniedAlways.length; i++) {  
                    var deniedAlwaysPermission = resultObj.deniedAlways[i];  
                    console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);  
                    result = -1  
                }  
                resolve(result);  
                // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限  
                // if (result != 1) {  
                // gotoAppPermissionSetting()  
                // }  
            },  
            function(error) {  
                console.log('申请权限错误:' + error.code + " = " + error.message);  
                resolve({  
                    code: error.code,  
                    message: error.message  
                });  
            }  
        );  
    });  
}  

// 使用一个方法,根据参数判断权限  
function judgeIosPermission(permissionID) {  
    if (permissionID == "location") {  
        return judgeIosPermissionLocation()  
    } else if (permissionID == "camera") {  
        return judgeIosPermissionCamera()  
    } else if (permissionID == "photoLibrary") {  
        return judgeIosPermissionPhotoLibrary()  
    } else if (permissionID == "record") {  
        return judgeIosPermissionRecord()  
    } else if (permissionID == "push") {  
        return judgeIosPermissionPush()  
    } else if (permissionID == "contact") {  
        return judgeIosPermissionContact()  
    } else if (permissionID == "calendar") {  
        return judgeIosPermissionCalendar()  
    } else if (permissionID == "memo") {  
        return judgeIosPermissionMemo()  
    }  
    return false;  
}  

// 跳转到**应用**的权限页面  
function gotoAppPermissionSetting() {  
    if (isIos) {  
        var UIApplication = plus.ios.import("UIApplication");  
        var application2 = UIApplication.sharedApplication();  
        var NSURL2 = plus.ios.import("NSURL");  
        // var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");       
        var setting2 = NSURL2.URLWithString("app-settings:");  
        application2.openURL(setting2);  

        plus.ios.deleteObject(setting2);  
        plus.ios.deleteObject(NSURL2);  
        plus.ios.deleteObject(application2);  
    } else {  
        // console.log(plus.device.vendor);  
        var Intent = plus.android.importClass("android.content.Intent");  
        var Settings = plus.android.importClass("android.provider.Settings");  
        var Uri = plus.android.importClass("android.net.Uri");  
        var mainActivity = plus.android.runtimeMainActivity();  
        var intent = new Intent();  
        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);  
        var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);  
        intent.setData(uri);  
        mainActivity.startActivity(intent);  
    }  
}  

// 检查系统的设备服务是否开启  
// var checkSystemEnableLocation = async function () {  
function checkSystemEnableLocation() {  
    if (isIos) {  
        var result = false;  
        var cllocationManger = plus.ios.import("CLLocationManager");  
        var result = cllocationManger.locationServicesEnabled();  
        console.log("系统定位开启:" + result);  
        plus.ios.deleteObject(cllocationManger);  
        return result;  
    } else {  
        var context = plus.android.importClass("android.content.Context");  
        var locationManager = plus.android.importClass("android.location.LocationManager");  
        var main = plus.android.runtimeMainActivity();  
        var mainSvr = main.getSystemService(context.LOCATION_SERVICE);  
        var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);  
        console.log("系统定位开启:" + result);  
        return result  
    }  
}  

let permissionMap = {  
    "android": {  
        "CAMERA_EXTERNAL_STORAGE": {  
            "name": "android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE,android.permission.CAMERA",  
            "title": "相机/相册权限说明",  
            "content": "便于您使用该功能上传您的照片/图片/视频及用于更换头像、发布产品/需求、下载、与客服沟通等场景中读取和写入相册和文件内容"  
        },  
        "CAMERA": {  
            "name": "android.permission.CAMERA",  
            "title": "相机权限说明",  
            "content": "便于您使用该功能上传图片,用于与客服沟通等场景中发送拍摄图片"  
        },  
        "EXTERNAL_STORAGE": {  
            "name": "android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE",  
            "title": "相册权限说明",  
            "content": "便于您使用该功能上传您的照片/图片/视频及用于更换头像、发布产品/需求、下载、与客服沟通等场景中读取和写入相册和文件内容"  
        }  
    },  
    "ios": {}  
}  

let view = null;  

function showViewDesc(permission) {  
    let plat = isIos ? "ios" : "android";  
    view = new plus.nativeObj.View('per-modal', {  
        top: '0px',  
        left: '0px',  
        width: '100%',  
        backgroundColor: 'rgba(0,0,0,0.2)',  
        //opacity: '.9'     
    })  
    view.drawRect({  
        color: '#fff',  
        radius: '5px'  
    }, {  
        top: '30px',  
        left: '5%',  
        width: '90%',  
        height: "100px",  
    })  
    view.drawText(permissionMap[plat][permission]["title"], {  
        top: '40px',  
        left: "8%",  
        height: "30px"  
    }, {  
        align: "left",  
        color: "#000",  
    }, {  
        onClick: function(e) {  
            console.log(e);  
        }  
    })  
    view.drawText(permissionMap[plat][permission]["content"], {  
        top: '65px',  
        height: "60px",  
        left: "8%",  
        width: "84%"  
    }, {  
        whiteSpace: 'normal',  
        size: "14px",  
        align: "left",  
        color: "#656563"  
    })  
    view.show()  
}  

function premissionCheck(permission) {  
    return new Promise(async (resolve, reject) => {  
        let plat = isIos ? "ios" : "android";  
        if (isIos) { // ios  
            // const camera = permission.judgeIosPermission("camera");//判断ios是否给予摄像头权限  
            // //ios相册没权限,系统会自动弹出授权框  
            // //let photoLibrary = permission.judgeIosPermission("photoLibrary");//判断ios是否给予相册权限  
            // if(camera){  
            //     resolve();  
            // }else{  
            //     reject('需要开启相机使用权限');  
            // }  
            resolve(1)  
        } else { // android  
            let permission_arr = permissionMap[plat][permission]["name"].split(",");  
            let flag = true;  
            for(let i = 0;i<permission_arr.length;i++) {  
                let status = plus.navigator.checkPermission(permission_arr[i]);  
                if(status == "undetermined") {  
                    flag = false;  
                }  
            }  
            console.log("flag", flag)  
            if (flag == false) { // 未完全授权  
                showViewDesc(permission);  
                requestAndroidPermission(permissionMap[plat][permission]["name"]).then((res) => {  
                    view.close();  
                    if (res == -1) {  
                        uni.showModal({  
                            title: '提示',  
                            content: '操作权限已被拒绝,请手动前往设置',  
                            confirmText: "立即设置",  
                            success: (res) => {  
                                if (res.confirm) {  
                                    gotoAppPermissionSetting()  
                                }  
                            }  
                        })  
                    }  
                    resolve(res)  
                })  
            } else {  
                resolve(1)  
            }  
        }  
    })  
}  

module.exports = {  
    judgeIosPermission: judgeIosPermission,  
    requestAndroidPermission: requestAndroidPermission,  
    checkSystemEnableLocation: checkSystemEnableLocation,  
    gotoAppPermissionSetting: gotoAppPermissionSetting,  
    premissionCheck: premissionCheck  
}
继续阅读 »

近期关于华为应用市场上架过程中出现的【您的应用在运行时,未同步告知权限申请的使用目的,向用户索取(存储、拍照)等权限,不符合华为应用市场审核标准。】

请参考《审核指南》第7.21相关审核要求:https://developer.huawei.com/consumer/cn/doc/app/50104-07#h3-1683701612940-2
(应用申请的权限,都必须有明确、合理的使用场景和功能说明,禁止诱导或误导用户授权,应用使用的权限必须与申请所述的一致。在申请敏感权限时,需同步告知用户申请该权限的目的。未经用户同意,不得更改用户权限授权状态。)

针对以上问题,个人结合现有问题贴处理方式如下,仅供参考,各位可拿去按照自己需求进行细化调整,代码如下:

使用方式:
1.引入:

import permision from "@/js_sdk/permission.js"

2.判断:

var result = await permision.premissionCheck("EXTERNAL_STORAGE")  
if(result == 1) {  
    // 此处处理各自业务 如:uni.chooseImage  
}

3.permission.js源码参考如下

var isIos  
// #ifdef APP-PLUS  
isIos = (plus.os.name == "iOS")  
// #endif  

// 判断推送权限是否开启  
function judgeIosPermissionPush() {  
    var result = false;  
    var UIApplication = plus.ios.import("UIApplication");  
    var app = UIApplication.sharedApplication();  
    var enabledTypes = 0;  
    if (app.currentUserNotificationSettings) {  
        var settings = app.currentUserNotificationSettings();  
        enabledTypes = settings.plusGetAttribute("types");  
        console.log("enabledTypes1:" + enabledTypes);  
        if (enabledTypes == 0) {  
            console.log("推送权限没有开启");  
        } else {  
            result = true;  
            console.log("已经开启推送功能!")  
        }  
        plus.ios.deleteObject(settings);  
    } else {  
        enabledTypes = app.enabledRemoteNotificationTypes();  
        if (enabledTypes == 0) {  
            console.log("推送权限没有开启!");  
        } else {  
            result = true;  
            console.log("已经开启推送功能!")  
        }  
        console.log("enabledTypes2:" + enabledTypes);  
    }  
    plus.ios.deleteObject(app);  
    plus.ios.deleteObject(UIApplication);  
    return result;  
}  

// 判断定位权限是否开启  
function judgeIosPermissionLocation() {  
    var result = false;  
    var cllocationManger = plus.ios.import("CLLocationManager");  
    var status = cllocationManger.authorizationStatus();  
    result = (status != 2)  
    console.log("定位权限开启:" + result);  
    // 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation  
    /* var enable = cllocationManger.locationServicesEnabled();  
    var status = cllocationManger.authorizationStatus();  
    console.log("enable:" + enable);  
    console.log("status:" + status);  
    if (enable && status != 2) {  
        result = true;  
        console.log("手机定位服务已开启且已授予定位权限");  
    } else {  
        console.log("手机系统的定位没有打开或未给予定位权限");  
    } */  
    plus.ios.deleteObject(cllocationManger);  
    return result;  
}  

// 判断麦克风权限是否开启  
function judgeIosPermissionRecord() {  
    var result = false;  
    var avaudiosession = plus.ios.import("AVAudioSession");  
    var avaudio = avaudiosession.sharedInstance();  
    var permissionStatus = avaudio.recordPermission();  
    console.log("permissionStatus:" + permissionStatus);  
    if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {  
        console.log("麦克风权限没有开启");  
    } else {  
        result = true;  
        console.log("麦克风权限已经开启");  
    }  
    plus.ios.deleteObject(avaudiosession);  
    return result;  
}  

// 判断相机权限是否开启  
function judgeIosPermissionCamera() {  
    var result = false;  
    var AVCaptureDevice = plus.ios.import("AVCaptureDevice");  
    var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');  
    console.log("authStatus:" + authStatus);  
    if (authStatus == 3) {  
        result = true;  
        console.log("相机权限已经开启");  
    } else {  
        console.log("相机权限没有开启");  
    }  
    plus.ios.deleteObject(AVCaptureDevice);  
    return result;  
}  

// 判断相册权限是否开启  
function judgeIosPermissionPhotoLibrary() {  
    var result = false;  
    var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");  
    var authStatus = PHPhotoLibrary.authorizationStatus();  
    console.log("authStatus:" + authStatus);  
    if (authStatus == 3) {  
        result = true;  
        console.log("相册权限已经开启");  
    } else {  
        console.log("相册权限没有开启");  
    }  
    plus.ios.deleteObject(PHPhotoLibrary);  
    return result;  
}  

// 判断通讯录权限是否开启  
function judgeIosPermissionContact() {  
    var result = false;  
    var CNContactStore = plus.ios.import("CNContactStore");  
    var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);  
    if (cnAuthStatus == 3) {  
        result = true;  
        console.log("通讯录权限已经开启");  
    } else {  
        console.log("通讯录权限没有开启");  
    }  
    plus.ios.deleteObject(CNContactStore);  
    return result;  
}  

// 判断日历权限是否开启  
function judgeIosPermissionCalendar() {  
    var result = false;  
    var EKEventStore = plus.ios.import("EKEventStore");  
    var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);  
    if (ekAuthStatus == 3) {  
        result = true;  
        console.log("日历权限已经开启");  
    } else {  
        console.log("日历权限没有开启");  
    }  
    plus.ios.deleteObject(EKEventStore);  
    return result;  
}  

// 判断备忘录权限是否开启  
function judgeIosPermissionMemo() {  
    var result = false;  
    var EKEventStore = plus.ios.import("EKEventStore");  
    var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);  
    if (ekAuthStatus == 3) {  
        result = true;  
        console.log("备忘录权限已经开启");  
    } else {  
        console.log("备忘录权限没有开启");  
    }  
    plus.ios.deleteObject(EKEventStore);  
    return result;  
}  

// Android权限查询  
function requestAndroidPermission(permissionID) {  
    return new Promise((resolve, reject) => {  
        plus.android.requestPermissions(  
            permissionID.split(","),  
            // [permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装  
            function(resultObj) {  
                var result = 0;  
                for (var i = 0; i < resultObj.granted.length; i++) {  
                    var grantedPermission = resultObj.granted[i];  
                    console.log('已获取的权限:' + grantedPermission);  
                    result = 1  
                }  
                for (var i = 0; i < resultObj.deniedPresent.length; i++) {  
                    var deniedPresentPermission = resultObj.deniedPresent[i];  
                    console.log('拒绝本次申请的权限:' + deniedPresentPermission);  
                    result = 0  
                }  
                for (var i = 0; i < resultObj.deniedAlways.length; i++) {  
                    var deniedAlwaysPermission = resultObj.deniedAlways[i];  
                    console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);  
                    result = -1  
                }  
                resolve(result);  
                // 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限  
                // if (result != 1) {  
                // gotoAppPermissionSetting()  
                // }  
            },  
            function(error) {  
                console.log('申请权限错误:' + error.code + " = " + error.message);  
                resolve({  
                    code: error.code,  
                    message: error.message  
                });  
            }  
        );  
    });  
}  

// 使用一个方法,根据参数判断权限  
function judgeIosPermission(permissionID) {  
    if (permissionID == "location") {  
        return judgeIosPermissionLocation()  
    } else if (permissionID == "camera") {  
        return judgeIosPermissionCamera()  
    } else if (permissionID == "photoLibrary") {  
        return judgeIosPermissionPhotoLibrary()  
    } else if (permissionID == "record") {  
        return judgeIosPermissionRecord()  
    } else if (permissionID == "push") {  
        return judgeIosPermissionPush()  
    } else if (permissionID == "contact") {  
        return judgeIosPermissionContact()  
    } else if (permissionID == "calendar") {  
        return judgeIosPermissionCalendar()  
    } else if (permissionID == "memo") {  
        return judgeIosPermissionMemo()  
    }  
    return false;  
}  

// 跳转到**应用**的权限页面  
function gotoAppPermissionSetting() {  
    if (isIos) {  
        var UIApplication = plus.ios.import("UIApplication");  
        var application2 = UIApplication.sharedApplication();  
        var NSURL2 = plus.ios.import("NSURL");  
        // var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");       
        var setting2 = NSURL2.URLWithString("app-settings:");  
        application2.openURL(setting2);  

        plus.ios.deleteObject(setting2);  
        plus.ios.deleteObject(NSURL2);  
        plus.ios.deleteObject(application2);  
    } else {  
        // console.log(plus.device.vendor);  
        var Intent = plus.android.importClass("android.content.Intent");  
        var Settings = plus.android.importClass("android.provider.Settings");  
        var Uri = plus.android.importClass("android.net.Uri");  
        var mainActivity = plus.android.runtimeMainActivity();  
        var intent = new Intent();  
        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);  
        var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);  
        intent.setData(uri);  
        mainActivity.startActivity(intent);  
    }  
}  

// 检查系统的设备服务是否开启  
// var checkSystemEnableLocation = async function () {  
function checkSystemEnableLocation() {  
    if (isIos) {  
        var result = false;  
        var cllocationManger = plus.ios.import("CLLocationManager");  
        var result = cllocationManger.locationServicesEnabled();  
        console.log("系统定位开启:" + result);  
        plus.ios.deleteObject(cllocationManger);  
        return result;  
    } else {  
        var context = plus.android.importClass("android.content.Context");  
        var locationManager = plus.android.importClass("android.location.LocationManager");  
        var main = plus.android.runtimeMainActivity();  
        var mainSvr = main.getSystemService(context.LOCATION_SERVICE);  
        var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);  
        console.log("系统定位开启:" + result);  
        return result  
    }  
}  

let permissionMap = {  
    "android": {  
        "CAMERA_EXTERNAL_STORAGE": {  
            "name": "android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE,android.permission.CAMERA",  
            "title": "相机/相册权限说明",  
            "content": "便于您使用该功能上传您的照片/图片/视频及用于更换头像、发布产品/需求、下载、与客服沟通等场景中读取和写入相册和文件内容"  
        },  
        "CAMERA": {  
            "name": "android.permission.CAMERA",  
            "title": "相机权限说明",  
            "content": "便于您使用该功能上传图片,用于与客服沟通等场景中发送拍摄图片"  
        },  
        "EXTERNAL_STORAGE": {  
            "name": "android.permission.READ_EXTERNAL_STORAGE,android.permission.WRITE_EXTERNAL_STORAGE",  
            "title": "相册权限说明",  
            "content": "便于您使用该功能上传您的照片/图片/视频及用于更换头像、发布产品/需求、下载、与客服沟通等场景中读取和写入相册和文件内容"  
        }  
    },  
    "ios": {}  
}  

let view = null;  

function showViewDesc(permission) {  
    let plat = isIos ? "ios" : "android";  
    view = new plus.nativeObj.View('per-modal', {  
        top: '0px',  
        left: '0px',  
        width: '100%',  
        backgroundColor: 'rgba(0,0,0,0.2)',  
        //opacity: '.9'     
    })  
    view.drawRect({  
        color: '#fff',  
        radius: '5px'  
    }, {  
        top: '30px',  
        left: '5%',  
        width: '90%',  
        height: "100px",  
    })  
    view.drawText(permissionMap[plat][permission]["title"], {  
        top: '40px',  
        left: "8%",  
        height: "30px"  
    }, {  
        align: "left",  
        color: "#000",  
    }, {  
        onClick: function(e) {  
            console.log(e);  
        }  
    })  
    view.drawText(permissionMap[plat][permission]["content"], {  
        top: '65px',  
        height: "60px",  
        left: "8%",  
        width: "84%"  
    }, {  
        whiteSpace: 'normal',  
        size: "14px",  
        align: "left",  
        color: "#656563"  
    })  
    view.show()  
}  

function premissionCheck(permission) {  
    return new Promise(async (resolve, reject) => {  
        let plat = isIos ? "ios" : "android";  
        if (isIos) { // ios  
            // const camera = permission.judgeIosPermission("camera");//判断ios是否给予摄像头权限  
            // //ios相册没权限,系统会自动弹出授权框  
            // //let photoLibrary = permission.judgeIosPermission("photoLibrary");//判断ios是否给予相册权限  
            // if(camera){  
            //     resolve();  
            // }else{  
            //     reject('需要开启相机使用权限');  
            // }  
            resolve(1)  
        } else { // android  
            let permission_arr = permissionMap[plat][permission]["name"].split(",");  
            let flag = true;  
            for(let i = 0;i<permission_arr.length;i++) {  
                let status = plus.navigator.checkPermission(permission_arr[i]);  
                if(status == "undetermined") {  
                    flag = false;  
                }  
            }  
            console.log("flag", flag)  
            if (flag == false) { // 未完全授权  
                showViewDesc(permission);  
                requestAndroidPermission(permissionMap[plat][permission]["name"]).then((res) => {  
                    view.close();  
                    if (res == -1) {  
                        uni.showModal({  
                            title: '提示',  
                            content: '操作权限已被拒绝,请手动前往设置',  
                            confirmText: "立即设置",  
                            success: (res) => {  
                                if (res.confirm) {  
                                    gotoAppPermissionSetting()  
                                }  
                            }  
                        })  
                    }  
                    resolve(res)  
                })  
            } else {  
                resolve(1)  
            }  
        }  
    })  
}  

module.exports = {  
    judgeIosPermission: judgeIosPermission,  
    requestAndroidPermission: requestAndroidPermission,  
    checkSystemEnableLocation: checkSystemEnableLocation,  
    gotoAppPermissionSetting: gotoAppPermissionSetting,  
    premissionCheck: premissionCheck  
}
收起阅读 »

Uni-app 学习、交流群

uniapp

建立一个 Uni-app QQ、微信群

帮助开发者互相学习
即时解决问题
资源共享
后期的合作

QQqun:1004151935


微信群几天就过期了,可加我VX : bjawenfd 备注: 前端群 我单独拉入VX群

继续阅读 »

建立一个 Uni-app QQ、微信群

帮助开发者互相学习
即时解决问题
资源共享
后期的合作

QQqun:1004151935


微信群几天就过期了,可加我VX : bjawenfd 备注: 前端群 我单独拉入VX群

收起阅读 »

uni-app x,一个纯原生的Android App开发工具

uni_app x

uni-app x,下一代uni-app,一个神奇的产品。

用vue语法、uni的组件、api,以及uts语言,编译出了kotlin的app。不再使用js引擎和webview。纯纯的kotlin原生app。

uni-app x,让“跨平台开发性能不如原生”的这条曾广为流传的规则,扔进了历史的垃圾桶!

uni-app x原理简析

uni-app x整体推到你面前时,你可能觉得难以想象,怎么可能编译为纯原生App?uni-app过去之所以能跨平台,是因为js是跨平台的啊。

所谓,成也萧何败也萧何,而uni-app过去在app上性能不如原生,也很大程度是因为js和原生的交互通信阻塞问题。

简述下uni-app x的原理。

其实uni-app x使用的不是js,而是DCloud在2022年发布的uts语言。这是一种基于ts改造的语言,改造的目的就是让它可以全平台编译。

我们知道js和原生语言的差异性主要在于类型动态性。而ts已经为js提供了类型。uts是在ts的基础上,融合kotlinswift的特性,抽象出了一套全平台可用的编译型语言。

uts在不同平台,编译为不同的产物:

  • 在Web中编译为js
  • 在iOS中编译为swift
  • 在Android中编译为kotlin

然后DCloud又基于uts开发了uvue的ui引擎,可使用vue语法来开发界面,再补上uni的组件和api,最终形成了你目前看到的uni-app x。

让你使用熟悉的代码、跨平台的方式,却能写出和原生的功能性能完全一致的app。

而且有趣的是,我们与原生开发者交流,发现使用uni-app x开发应用,比原生开发要快非常多。

案例

很多开发者都在关注着uni-app x,等待先行者趟坑。

这几个月来,很多先行者已经发布了他们的基于uni-app x的产品。而HBuilderX也发布了里程碑的3.98正式版,已经可以支撑商业应用。

快亿商城

这是一个云端一体的、完整的电商项目。客户端、服务器,甚至还包括管理端:快亿商城管理端

t-uvue-ui

这是一个丰富的ui库,解救那些不擅长界面的开发者。当然即便是擅长界面开发的,使用这个组件库也能提升不少开发效率。

uXui

一款基于 uni-app x 的、免费、开源的 UI 框架。

快速体验

欢迎你也来体验uni-app x这个神奇的产品:用你熟悉的代码,开发出原生的Android App。

<template>  
    <view class="content">  
        <button @click="buttonClick">{{title}}</button>  
    </view>  
</template>  

<script> //注意这里编写的是uts,是ts的变种  
    export default {  
        data() {  
            return {  
                title: "Hello world"  
            }  
        },  
        onLoad() {  
            console.log('onLoad')  
        },  
        methods: {  
            buttonClick: function () {  
                uni.showModal({  
                    "showCancel": false,  
                    "content": "点了按钮"  
                })  
            }  
        }  
    }  
</script>  

<style>  
    .content {  
        width: 750rpx;  
        background-color: white;  
    }  
</style>

体验uni-app x的真实效果,在外部浏览器里下载hello uni-app x的apk,或扫描下方二维码。

hello uni-app x 演示了uni-app x目前支持的所有内置组件、API、以及诸多页面模版。

质量

uni-app x从源头重视产品质量,第一个版本就支持自动化测试。并已为uni-app x产品编写了几十个测试工程、数十万行测试例代码。

虽然这些工作导致uni-app x初期的迭代速度变慢。但让uni-app x的质量水平大幅提升。每天晚上DCloud内部众多机器在运行这些自动化测试代码,除了监控质量,还在监控启动速度、包体积大小、内存占用等各种关键指标。

插件大赛及生态

由于uts编译为kotlin,也就是kotlin在Android上能用的api、能用的三方sdk,uni-app x里都可以用。

<script>  
    import Build from 'android.os.Build';  
    export default {  
        onLoad() {  
            console.log(Build.MODEL); //调用原生对象,返回手机型号  
            console.log(uni.getSystemInfoSync().deviceModel); //调用uni API,返回手机型号。与上一行返回值相同  
        }  
    }  
</script>

上面的示例,在页面启动时打印了2行日志,显示手机型号。

  • uni.getSystemInfoSync,是uni的api
  • import的Build,是Android os的api

在uni-app x里,可以直接调用os的能力,不受限制,语法是uts的语法,但需要了解什么功能在原生里是哪个api。

使用uni.getSystemInfoSync则比较简单,看uni的文档即可,且可跨平台。

其实,uni.getSystemInfoSync 的内部实现就是一个uts模块,底层使用了一样的代码,也是import了android.os.Build。

uni.的api,大多是uts开发的,它们都开源在uni-api仓库

uni-app x作为一个原生应用,自然可以使用原生的各种sdk,包括flutter、react native、cocos、unity等原生sdk,均可集成使用。

在插件市场,有基于uni-app x的各种作品。

DCloud官方的:

三方项目:

ui库:

还有各种原生扩展的ui组件和api插件。

目前已有数百款适配uni-app x的插件。

随着插件大赛的开展,uni-app x周边生态在如火如荼的丰富中。

当然,也欢迎你来参加插件大赛,夺取丰厚的产品。插件大赛介绍详见:https://ask.dcloud.net.cn/article/40812

点击https://uniapp.dcloud.net.cn/uni-app-x/,阅读uni-app x的官方文档。

继续阅读 »

uni-app x,下一代uni-app,一个神奇的产品。

用vue语法、uni的组件、api,以及uts语言,编译出了kotlin的app。不再使用js引擎和webview。纯纯的kotlin原生app。

uni-app x,让“跨平台开发性能不如原生”的这条曾广为流传的规则,扔进了历史的垃圾桶!

uni-app x原理简析

uni-app x整体推到你面前时,你可能觉得难以想象,怎么可能编译为纯原生App?uni-app过去之所以能跨平台,是因为js是跨平台的啊。

所谓,成也萧何败也萧何,而uni-app过去在app上性能不如原生,也很大程度是因为js和原生的交互通信阻塞问题。

简述下uni-app x的原理。

其实uni-app x使用的不是js,而是DCloud在2022年发布的uts语言。这是一种基于ts改造的语言,改造的目的就是让它可以全平台编译。

我们知道js和原生语言的差异性主要在于类型动态性。而ts已经为js提供了类型。uts是在ts的基础上,融合kotlinswift的特性,抽象出了一套全平台可用的编译型语言。

uts在不同平台,编译为不同的产物:

  • 在Web中编译为js
  • 在iOS中编译为swift
  • 在Android中编译为kotlin

然后DCloud又基于uts开发了uvue的ui引擎,可使用vue语法来开发界面,再补上uni的组件和api,最终形成了你目前看到的uni-app x。

让你使用熟悉的代码、跨平台的方式,却能写出和原生的功能性能完全一致的app。

而且有趣的是,我们与原生开发者交流,发现使用uni-app x开发应用,比原生开发要快非常多。

案例

很多开发者都在关注着uni-app x,等待先行者趟坑。

这几个月来,很多先行者已经发布了他们的基于uni-app x的产品。而HBuilderX也发布了里程碑的3.98正式版,已经可以支撑商业应用。

快亿商城

这是一个云端一体的、完整的电商项目。客户端、服务器,甚至还包括管理端:快亿商城管理端

t-uvue-ui

这是一个丰富的ui库,解救那些不擅长界面的开发者。当然即便是擅长界面开发的,使用这个组件库也能提升不少开发效率。

uXui

一款基于 uni-app x 的、免费、开源的 UI 框架。

快速体验

欢迎你也来体验uni-app x这个神奇的产品:用你熟悉的代码,开发出原生的Android App。

<template>  
    <view class="content">  
        <button @click="buttonClick">{{title}}</button>  
    </view>  
</template>  

<script> //注意这里编写的是uts,是ts的变种  
    export default {  
        data() {  
            return {  
                title: "Hello world"  
            }  
        },  
        onLoad() {  
            console.log('onLoad')  
        },  
        methods: {  
            buttonClick: function () {  
                uni.showModal({  
                    "showCancel": false,  
                    "content": "点了按钮"  
                })  
            }  
        }  
    }  
</script>  

<style>  
    .content {  
        width: 750rpx;  
        background-color: white;  
    }  
</style>

体验uni-app x的真实效果,在外部浏览器里下载hello uni-app x的apk,或扫描下方二维码。

hello uni-app x 演示了uni-app x目前支持的所有内置组件、API、以及诸多页面模版。

质量

uni-app x从源头重视产品质量,第一个版本就支持自动化测试。并已为uni-app x产品编写了几十个测试工程、数十万行测试例代码。

虽然这些工作导致uni-app x初期的迭代速度变慢。但让uni-app x的质量水平大幅提升。每天晚上DCloud内部众多机器在运行这些自动化测试代码,除了监控质量,还在监控启动速度、包体积大小、内存占用等各种关键指标。

插件大赛及生态

由于uts编译为kotlin,也就是kotlin在Android上能用的api、能用的三方sdk,uni-app x里都可以用。

<script>  
    import Build from 'android.os.Build';  
    export default {  
        onLoad() {  
            console.log(Build.MODEL); //调用原生对象,返回手机型号  
            console.log(uni.getSystemInfoSync().deviceModel); //调用uni API,返回手机型号。与上一行返回值相同  
        }  
    }  
</script>

上面的示例,在页面启动时打印了2行日志,显示手机型号。

  • uni.getSystemInfoSync,是uni的api
  • import的Build,是Android os的api

在uni-app x里,可以直接调用os的能力,不受限制,语法是uts的语法,但需要了解什么功能在原生里是哪个api。

使用uni.getSystemInfoSync则比较简单,看uni的文档即可,且可跨平台。

其实,uni.getSystemInfoSync 的内部实现就是一个uts模块,底层使用了一样的代码,也是import了android.os.Build。

uni.的api,大多是uts开发的,它们都开源在uni-api仓库

uni-app x作为一个原生应用,自然可以使用原生的各种sdk,包括flutter、react native、cocos、unity等原生sdk,均可集成使用。

在插件市场,有基于uni-app x的各种作品。

DCloud官方的:

三方项目:

ui库:

还有各种原生扩展的ui组件和api插件。

目前已有数百款适配uni-app x的插件。

随着插件大赛的开展,uni-app x周边生态在如火如荼的丰富中。

当然,也欢迎你来参加插件大赛,夺取丰厚的产品。插件大赛介绍详见:https://ask.dcloud.net.cn/article/40812

点击https://uniapp.dcloud.net.cn/uni-app-x/,阅读uni-app x的官方文档。

收起阅读 »

uniapp 实现wx小程序 那种分段式半屏的文档在哪?

如视频中的 分段式弹窗、半屏路由等等关于Skyline的示例文档都没有吗?

如视频中的 分段式弹窗、半屏路由等等关于Skyline的示例文档都没有吗?

Failed to receiveTasks, instance (1) is not available. 可能原因

uniapp

uniapp热更新不会销毁旧实例导致内存里有奇奇怪怪的东西导致的,看看生命周期里有没有会导致内存泄露的东西。

uniapp热更新不会销毁旧实例导致内存里有奇奇怪怪的东西导致的,看看生命周期里有没有会导致内存泄露的东西。