
手机软键盘弹起导致页面变形的一种解决方案
最近用 uniapp 开发 app,其中一个页面有十几个 input 输入框,在点击 input 输入时,软键盘弹起,导致页面往上顶,底部的按钮也全部弹到页面上面去了,布局全被打乱。
原来的样子:
软键盘弹出来后:
在开发APP时,通常情况下页面的宽度和高度都会设为 100%,即页面高度等于屏幕高度,页面宽度等于屏幕宽度。
当 input 获取焦点时,软键盘弹出,页面高度被挤压,此时页面高度 = 屏幕高度 - 软键盘高度。所以,页面高度缩小,元素都挤压在一起,布局被打乱。
一种可行的解决方案:给页面设置一个最小高度,即一个能让所有元素按原来布局排列的高度。
举例:
我开发的 APP 运行在 ipad上,横屏显示时,高度为 768px ,我可以把 768px 当做页面的最小高度。
.app {
min-height: 768px;
/* 原来定义的高度 100% */
height: 100vh;
}
软键盘还是会弹起,因为页面最小高度被设为了 768px,所以此时总高度为 768px + 软键盘高度,超出了屏幕高度(ipad横屏屏幕高度为768px)。如上图所示,此时原来页面的上半部分“消失”,就是被顶上去了,只显示原来页面的下半部分。但至少我们要的页面布局不变形已经实现了。等输入完,软键盘收起时,页面恢复原状。
ipad 的问题解决了,要是 APP 运行在其他手机端上呢?此时,CSS3 @media
属性就排上用场了。
假设要适配 iphone5 和 iphone6
/* iphone5 width:320; height:568*/
@media (min-width: 320px) {
.app {
min-height: 568px;
height: 100vh;
}
}
/* iphone6 width:375; height:667*/
@media (min-width: 375px) {
.app {
min-height: 667px;
height: 100vh;
}
}
这样设置即可适配 iphone5 和 iphone6
最近用 uniapp 开发 app,其中一个页面有十几个 input 输入框,在点击 input 输入时,软键盘弹起,导致页面往上顶,底部的按钮也全部弹到页面上面去了,布局全被打乱。
原来的样子:
软键盘弹出来后:
在开发APP时,通常情况下页面的宽度和高度都会设为 100%,即页面高度等于屏幕高度,页面宽度等于屏幕宽度。
当 input 获取焦点时,软键盘弹出,页面高度被挤压,此时页面高度 = 屏幕高度 - 软键盘高度。所以,页面高度缩小,元素都挤压在一起,布局被打乱。
一种可行的解决方案:给页面设置一个最小高度,即一个能让所有元素按原来布局排列的高度。
举例:
我开发的 APP 运行在 ipad上,横屏显示时,高度为 768px ,我可以把 768px 当做页面的最小高度。
.app {
min-height: 768px;
/* 原来定义的高度 100% */
height: 100vh;
}
软键盘还是会弹起,因为页面最小高度被设为了 768px,所以此时总高度为 768px + 软键盘高度,超出了屏幕高度(ipad横屏屏幕高度为768px)。如上图所示,此时原来页面的上半部分“消失”,就是被顶上去了,只显示原来页面的下半部分。但至少我们要的页面布局不变形已经实现了。等输入完,软键盘收起时,页面恢复原状。
ipad 的问题解决了,要是 APP 运行在其他手机端上呢?此时,CSS3 @media
属性就排上用场了。
假设要适配 iphone5 和 iphone6
/* iphone5 width:320; height:568*/
@media (min-width: 320px) {
.app {
min-height: 568px;
height: 100vh;
}
}
/* iphone6 width:375; height:667*/
@media (min-width: 375px) {
.app {
min-height: 667px;
height: 100vh;
}
}
这样设置即可适配 iphone5 和 iphone6
收起阅读 »
跨端框架深度评测:微信原生、wepy、mpvue、uni-app、taro、chameleon
说明:如下文章为2019年4月发布,2020年的测评版本也已出炉,最新评测点击:跨端开发框架深度横评之2020版
之前 Taro 团队发布了一篇《小程序多端框架全面测评》,让开发者对业界主流的跨端框架,有了初步认识。感谢 Taro 团队的付出。
不过横评这件事,要想得到更精确的结论,其实非常花费时间。它需要:
- 真实的动手写多个平台的测试demo,比较各个平台的功能、性能,它们的实际情况到底是不是如文档宣传的那样?
- 真实的学习每个框架,了解它们的学习曲线,在实际开发中遇到问题时,感受它们的文档、教程、社区生态和技服能力到底怎么样?
我们 uni-app
团队投入两周完成了这个深度评测,并定期更新数据。下面我们就分享下,实际开发不同框架的测试例时遇到的问题,以及在各端的兼容测试结果。在本文里,我们团队基于真实测试数据及各框架官网可采集到的公开数据,希望客观公正地评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待。
评测实验介绍
-
开发内容:开发一个仿微博小程序首页的复杂长列表,支持下拉刷新、上拉翻页、点赞。
-
界面如下:
-
开发版本:一共开发了6个版本,包括微信原生版、wepy版、mpvue版、taro版、uni-app版、chameleon版(以这些产品发布时间排序,下同),按照官网指引通过
cli
方式默认安装。 -
测试代码开源(Github仓库地址:https://github.com/dcloudio/test-framework),
Tips:若有同学觉得测试代码写法欠妥,欢迎提交 PR 或 Issus -
测试机型:红米 Redmi 6 Pro、MIUI 10.2.2.0 稳定版(最新版)、微信版本 7.0.3(最新版)
-
测试环境:每个框架开始测试前,杀掉各App进程、清空内存,保证测试机环境基本一致;每次从本地读取静态数据,屏蔽网络差异。
-
测试维度:
- 跨端支持度如何?
- 性能如何?
- 学习门槛
- 工具与周边生态
1. 跨端支持度如何
开发一次,到处运行,是每个程序员的梦想。但现实往往变成开发一次,到处调错。
各个待评测框架,是否真得如宣传的那样,一次开发、多端发布?
我们将上述仿微博App依次发布到各平台,验证每个框架在各端的兼容性,结果如下:
测试结果说明:
- ⭕ 表示支持且功能正常,❌ 表示不支持,其它则表示支持但存在部分bug或兼容问题
wepy
宣称计划在2.0版支持其他家小程序,但目前为alpha版。本测试基于wepy
官网指引安装的wepy-cli
正式版,版本为1.7.3,尚不支持多端- taro最新版支持了H5端的下拉刷新,但没有动画(本内容更新日期2019年5月20日)
chameleon
官网未找到stopPullDownRefresh
定义,停止页面下拉刷新需分平台编写
通过这个简单的例子可以看出,跨端支持度测评结论:uni-app
> taro
> chameleon
> mpvue
>wepy
、原生微信小程序
但是仅有上面的测试还不全面,实际业务要比这个测试例复杂很多。但我们没法开发很多复杂业务做评测,所以还需要再对照各家文档补充一些信息。
由于每个框架的文档中都描述了各种组件和API的跨端支持程度。我们过了几家的文档,发现各家基本是以微信小程序为基线,然后把各种组件和API在其他端实现了一遍:
taro
:H5端实现了大部分微信的API,App端和微信的差异比较大。uni-app
:组件、API、配置,大部分在各个端均已实现,个别API有说明在某些端不支持。可以看出uni-app是完整在H5端实现了一套微信模拟器,在App端实现了一套微信小程序引擎,才达到比较完善的平台兼容性。chameleon
:非常常用的一些组件和API在各端已经实现,这部分的平台差异较少。但大量组件和API需要开发者自己分平台写代码。
跨端框架,一方面要考虑框架提供的通用api跨端支持,同时还要考虑不同端的特色差异如何兼容。毕竟每个端都会有自己的特色,不可能完全一致。
taro
:提供了js环境变量判断和统一接口的多端文件,可以在组件、js、文件方面扩展多端,不支持其他环节的分平台处理。uni-app
:提供了条件编译模型,所有代码包括组件、js、css、配置json、文件、目录,均支持条件编译,可不受限的编写各端差异代码。chameleon
:提供了多态方案,可以在组件、js、文件方面扩展多端,不支持其他方式的分平台处理。
跨端框架,还涉及一个ui框架的跨端问题,评测结果如下:
taro
:官方提供了taro ui
,只支持微信小程序和H5两端,不支持App,详见uni-app
:官方提供了uni ui
,可全端运行;uni-app还有一个插件市场,里面有很多三方ui组件,详见chameleon
:官方提供了cml-ui
扩展组件库,可全端运行,但组件数量略少,详见
综合以上信息,本项的最终评测结论:uni-app
> taro
> chameleon
> mpvue
> wepy
、原生微信小程序
2. 跨端框架性能如何
跨端框架基本都是compiler
+ runtime
模式,引入的runtime
是否会降低运行性能?
尤其是与原生微信小程序开发相比性能怎么样,这是大家普遍关心的问题。
我们依然以上述仿微博小程序为例,测试2个容易出性能问题的点:长列表加载、大量点赞组件的响应。
2.1 长列表加载
仿微博的列表是一个包含很多组件的列表,这种复杂列表对性能的压力更大,很适合做性能测试。
从触发上拉加载到数据更新、页面渲染完成,需要准确计时。人眼视觉计时肯定不行,我们采用程序埋点的方式,制定了如下计时时机:
- 计时开始时机:交互事件触发,框架赋值之前,如:上拉加载(onReachBottom)函数开头
- 计时结束时机:页面渲染完毕(微信setData回调函数开头)
Tips:setData
回调函数开头可认为是页面渲染完成的时间,是因为微信setData
定义如下(微信规范):
字段 | 类型 | 必填 | 描述 | |
---|---|---|---|---|
data | Object | 是 | 这次要改变的数据 | |
callback | Function | 否 | setData引起的界面更新渲染完毕后的回调函数 |
测试方式:从页面空列表开始,通过程序自动触发上拉加载,每次新增20条列表,记录单次耗时;固定间隔连续触发 N 次上拉加载,使得页面达到 20*N 条列表,计算这 N 次触发上拉到渲染完成的平均耗时。
测试结果如下:
说明:以400条微博列表为例,从页面空列表开始,每隔1秒触发一次上拉加载(新增20条微博),记录单次耗时,触发20次后停止(页面达到400条微博),计算这20次的平均耗时,结果微信原生在这20次 触发上拉 -> 渲染完成
的平均耗时为876毫秒,最快的uni-app
是741毫秒,最慢的mpvue
是4493毫秒
大家初看这个数据,可能比较疑惑,别急,下方有详细说明
说明1:为何 mpvue/wepy 测试数据不完整?
mpvue
、wepy
诞生之初,微信小程序尚不支持自定义组件,无法进行组件化开发;mpvue
、wepy
为解决这个问题,将用户编写的Vue
组件,编译为WXML
中的模板(template),变相实现了组件化开发能力,提高代码复用性,这在当时的技术条件下是很棒的技术方案。
但如此方案,在页面复杂、组件较多的时,会大量增加页面 dom 节点数量,甚至超出微信的 dom 节点数限制。我们在 红米手机(Redmi 6 Pro)上实测,页面组件超过500个时,mpvue
、wepy
实现的仿微博App就会报出如下异常,并停止渲染,故这两个测试框架在组件较多时,测试数据不完整。这也就意味着,当页面组件太多时,无法使用这2个框架。
dom limit exceeded please check if there's any mistake you've made
Tips:wepy
在400条列表以内,为何性能高于微信原生框架,这个跟自定义组件管理开销及业务场景有关(wepy
编译为模板,不涉及组件创建及管理开销),后续对微博点赞,涉及组件数据传递时,微信原生框架的性能优势就提现出来了,详见下方测试数据。
说明2:为什么测试数据显示uni-app 会比微信原生框架的性能略好呢?
其实,在页面上有200条记录(200个组件)时,taro
性能数据也比微信原生框架更好。
微信原生框架耗时主要在setData
调用上,开发者若不单独优化,则每次都会传递大量数据;而 uni-app
、taro
都在调用setData
之前自动做diff
计算,每次仅传递变动的数据。
例如当前页面有20条数据,触发上拉加载时,会新加载20条数据,此时原生框架通过如下代码测试时,setData
会传输40条数据
data: {
listData: []
},
onReachBottom() { //上拉加载
let listData = this.data.listData;
listData.push(...Api.getNews());//新增数据
this.setData({
listData
}) //全量数据,发送数据到视图层
}
开发者使用微信原生框架,完全可以自己优化,精简传递数据,比如修改如下:
data: {
listData: []
},
onReachBottom() { //上拉加载
// 通过长度获取下一次渲染的索引
let index = this.data.listData.length;
let newData = {}; //新变更数据
Api.getNews().forEach((item) => {
newData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(newData) //增量数据,发送数据到视图层
}
经过如上优化修改后,再次测试,微信原生框架性能数据如下:
从测试结果可看出,如果开发者使用非uni-app开发,则必须手动处理setData的差量。这太麻烦了。
这个结果,和web开发类似,web开发也有原生js开发、vue、react框架等情况。如果不做特殊优化,原生js写的网页,性能经常还不如vue、react框架的性能。
也恰恰是因为Vue
、react
框架的优秀,性能好,开发体验好,所以原生js开发已经逐渐减少使用了。
复杂长列表加载下一页评测结论:微信原生开发手工优化
,uni-app
>微信原生开发未手工优化
,taro
> chameleon
> wepy
> mpvue
注:有人以为uni-app和mpvue是一样的,早期uni-app确实使用过mpvue,但后来因为性能和vue语法支持度问题已经重新开发了。
注:wepy在2.0 alpha里更改了组件编译模式,支持自定义组件,但由于仍是alpha状态,本次评测没有选择。
2.2 点赞组件响应速度
长列表中的某个组件,比如点赞组件,点击时是否能及时的修改未赞和已赞状态?是这项测试的评测点。
测试方式:
- 选中某微博,点击“点赞”按钮,实现点赞状态状态切换(已赞高亮、未赞灰色),
- 点赞按钮
onclick
函数开头开始计时,setData
回调函数开头结束计时;
在红米手机(Redmi 6 Pro)上进行多次测试,求其平均值,结果如下:
说明:也就是在列表数量为400时,微信原生开发的应用,点赞按钮从点击到状态变化需要111毫秒。
测试结果数据说明:
- wepy/mpvue 测试数据不完整的原因同上,在组件较多时,页面已经不再渲染了
- 基于微信自定义组件实现组件开发的框架(uni-app/taro/chameleon),组件数据通讯性能接近于微信原生框架,远高于基于
template
实现组件开发的框架(wepy/mpvue)性能
组件数据更新性能测评:微信原生开发
,uni-app
,taro
> chameleon
> wepy
> mpvue
综上,本性能测试做了2个测试,长列表加载和组件状态更新,综合2个实验,结论如下:
微信原生开发手工优化
,uni-app
>微信原生开发未手工优化
,taro
> chameleon
>> wepy
> mpvue
3. 学习门槛
DSL语法支持度
主流跨端框架基本都遵循React、Vue(类Vue)语法,其主要目的:复用工程师的现有技术栈,降低学习成本。此时,跨端框架对于原框架(React/Vue)语法的支持度就是一个重要的衡量标准,如果支持度较低、和原框架语法差异较大,则开发者无异于要学习一门新的框架,成本太高。
实际开发中发现,各个多端框架,都没有完全实现vue、react在web上的所有语法:
taro
对于 JSX
的语法支持是相对完善的,其文档中描述未来版本计划,
更多的 JSX 语法支持,1.3 之后限制生产力的语法只有只能用 map 创造循环组件一条
mpvue
、uni-app
框架基于 Vue.js
核心,通过修改 Vue.js
的 runtime
和 compiler
,实现了在小程序端的运行,支持绝大部分的Vue语法;uni-app
编译到微信端曾经使用过mpvue
,但后来重新编写,支持了更多vue语法如filter
、复杂 JavaScript
表达式等;
wepy
、chameleon
都是 类Vue
的实现,仅支持 Vue
的部分语法,开发时需要单独学习它们的规则;
DSL语法支持评测:uni-app
,taro
,> mpvue
> wepy
,chameleon
学习资料完善度
-
官方文档、问题搜索、示例demo的完备度方面:
-
wepy:文档只有2页,也无需搜索。仅支持微信,所以组件API等文档都直接看微信的文档。没有提供示例demo。详见
-
mpvue:文档较少,但其概念不复杂,也没有支持H5、App,所以组件API等文档都直接看微信的文档,学习难度低。问题搜索效果一般。没有提供示例demo。详见
-
taro:基础API文档完整,具体使用问题资源较少,问题搜索效果一般,示例demo只包含基础功能,仅发布了微信一端。详见
-
uni-app:基础文档和各种使用专题内容丰富,问题搜索效果较好,示例demo功能完备,并发布为7端上线。详见
-
chameleon:基础API文档完整,具体使用问题资源较少,问题搜索效果一般,示例demo只包含基础功能,仅发布了微信一端。详见
-
教学课程方面:
学习资料完善度评测:uni-app > mpvue , taro > chameleon > wepy
技术支持和社区活跃度
开发难免遇到问题,官方技术支持和社区活跃度很重要。
本次评测demo开发期间,我们的同学(同时掌握vue和react),在学习研究各个多端框架时,切实感受到由于语法、学习资料、社区的差异带来的学习门槛,吐出了很多槽。
综合评估,本项评测结论:uni-app
> taro
> mpvue
> wepy
> chameleon
Tips:本测评忽略React、Vue两框架自身的学习门槛
4. 工具和周边生态
工具
所有多端框架均支持cli
模式,可以在主流前端工具中开发。
各框架基本都带有d.ts的语法提示库。
由于mpvue
、uni-app
、taro
直接支持vue
、react
语法,配套的ide工具链较丰富,着色、校验、格式化完善,chameleon
针对部分编辑器推荐了插件,wepy
的有一些三方维护的vscode插件。
工具属性维度,明显高出一截的框架是uni-app
,其出品公司同时也是HBuilder的出品公司,DCloud.io。
HBuilder/HBuilderX系列是四大主流前端开发工具(可对比百度指数),其为uni-app
做了很多优化,故uni-app
的开发效率、易用性非其他框架可及。
当然对于不习惯HBuilderX的开发者而言,uni-app
的这个优势无法体现。
周边生态
一个底层框架,其周边配套非常重要,比如ui库、js库、项目模板。
- wepy:出现时间久,开源的项目示例较多。其没有自身的组件生态,沿用微信的生态。
- mpvue:发布时间也较早,历史积累较多,支持微信的组件生态,但会把微信的自定义组件编译为页面模板,导致性能问题。
- taro:官方提供了taro ui,github上有一些开源项目。不过taro ui只支持小程序和H5,不支持App端。taro提供了物料市场,但截至到19年10月28日,只有64个插件。
- uni-app:提供了插件市场,ui库、周边模板丰富。截至到19年10月28日,有850个插件。这些优质的插件已经超过了原生小程序的生态,比如原生小程序经常用到的wxParse、wx-echart、vant ui weapp等,在功能和性能上,均不如uni-app插件市场里的插件。可以另行参考https://ask.dcloud.net.cn/article/35489
- chameleon:还没有形成周边生态。
值得注意的是,uni-app
和mpvue
的插件生态是互通的,都是vue插件。所以双方还联合举办了插件大赛。这个联合生态的周边丰富度,是目前各个框架中最丰富的。
综上比较,工具和周边生态评测结论:uni-app
,mpvue
> wepy
> taro
> chameleon
其他常见评测指标
github star:
wepy | mpvue | taro | uni-app | chameleon | |
---|---|---|---|---|---|
6月20日 | 18053 | 17714 | 19584 | 8596 | 5369 |
10月28日 | 19.1k | 18.9k | 22.4k | 14.3k | 6.8k |
增幅 | 1047 | 1186 | 2816 | 5704 | 1431 |
github star 绝对值对比: taro
> wepy
>mpvue
> uni-app
> chameleon
增幅对比:uni-app
>taro
>mpvue
>chameleon
,wepy
由于uni-app
和chameleon
推出时间较晚,总star还不多,但从增速来看,最高的是uni-app
,其次是taro
。这比总star更能说明当前的情况。
百度指数
百度指数代表了开发者的搜索量和包含关键字的网页数量。如下是各跨端框架近7天(2019-03-24 ~ 2019-03-30)的百度指数:
Tips:
wepy
未被百度指数收录,说明其搜索量和包含该关键字的网页数量都不够多。
可以看出一个较大的冲突就是uni-app的star数量较少,而百度指数较高。
根据我们分析,star数量和产品发布时间有关,也和用户使用习惯有关。大多框架的交流互动主要是github的issus,而uni-app
的开发者在其问答社区交流,github页面访问量较低。
案例
我们对比了每个框架公布的案例,如下的框架名称的超链接已指向其案例页面,可以直接访问。
仅看发布到微信小程序的案例,数量和质量综合对比,uni-app >wepy > mpvue > taro > chameleon
如果看多端案例,综合对比,uni-app > taro > mpvue > wepy > chameleon
wepy
:出品较早,赶上了第一拨小程序开发需要脚手架的机会,知名案例较多,包括很多一线互联网公司。mpvue
、taro
:跨端框架的出品方本身为一线互联网公司,其内部项目会使用这些框架,经受过实战考验。除内部项目外,外部项目多以创业项目为主,鲜有其他一线互联网公司使用。uni-app
:案例很多,包括知名互联网公司、政府单位、创业者,官方数据已经超过10w+。各端案例都很丰富。chameleon
:案例不多,仅公布了几个微信小程序案例和百度小程序案例。
其他补充说明
1. App侧的补充说明
目前有taro
、uni-app
、chameleon
三家框架支持App端。但在App端大多是三方产品,比如taro
使用react native
,chameleon
使用weex
。
不管react native
还是weex
,其架构与小程序架构完全不同,从排版到API能力都差别很大,所以这类产品跨App端时兼容性较差。
uni-app
的App端,内置一个完整小程序引擎,并补充了可选的weex
引擎给对性能要求更高的开发者。这也是uni-app
在App端能够正常运行微信小程序代码的原因。
整个业内目前还不存在一个完全开源的小程序引擎(微信、百度、支付宝、头条的小程序引擎源码均未开源)。uni-app
的小程序引擎不是全开源,而是能力层开源,中控未开源。
所以可能各家的多端框架,在App端都有不完美的地方,需要开发者使用时注意。
其实App引擎并非前端领域,是原生领域的另一个竞技场。了解uni-app
与react native
、weex
、flutter
的差别,另见文档https://ask.dcloud.net.cn/article/36083。
2. 转换和混写
taro
提供了原生小程序转换为taro
工程的转换器,也支持在原生小程序里部分页面嵌入taro
编写的页面。
uni-app
有三方转换器,包括原生小程序、wepy框架小程序、mpvue框架小程序,都可以转换为uni-app。但不支持wxml混写。
chameleon
提供了转换的文档,没有官方转换工具,但uni-app
有三方开发的开源转换器。
结语
真实客观的永远是实验和数据,而不是结论。不同需求的开发者,可以根据上述实验数据,自行得出自己的选型结论。
但作为一篇完整的评测,我们也必须提供一份总结,虽然它可能加入了我们的主观感受:
-
如果你只开发微信小程序,不做多端,
uni-app
是更好的选择,除非你有兴趣手动优化原生小程序的代码,或者对react非常熟悉不愿意学习vue也可以使用taro
。另外注意,使用微信原生开发,对于webpack、各种预处理器、工程化流程的支持很不好,大公司很少用原生微信开发,还是用框架开发更合适。 -
如果你主要为了统一各家小程序,
uni-app
仍然是最好的选择,taro
次之。 -
如果你还需要跨端到H5侧,那么
uni-app
在跨端兼容方面会让你更省心。 -
如果你还需要跨端到App侧,那么
uni-app
是唯一可商用的选择。
当然,uni-app
也距离完美尚远,只是在参比框架中相对有优势。uni-app
团队仍将持续完善产品,帮助开发者提升投入产出、提升开发体验!
如有读者认为本文中任何评测失真,欢迎在这里报 issuse。
说明:如下文章为2019年4月发布,2020年的测评版本也已出炉,最新评测点击:跨端开发框架深度横评之2020版
之前 Taro 团队发布了一篇《小程序多端框架全面测评》,让开发者对业界主流的跨端框架,有了初步认识。感谢 Taro 团队的付出。
不过横评这件事,要想得到更精确的结论,其实非常花费时间。它需要:
- 真实的动手写多个平台的测试demo,比较各个平台的功能、性能,它们的实际情况到底是不是如文档宣传的那样?
- 真实的学习每个框架,了解它们的学习曲线,在实际开发中遇到问题时,感受它们的文档、教程、社区生态和技服能力到底怎么样?
我们 uni-app
团队投入两周完成了这个深度评测,并定期更新数据。下面我们就分享下,实际开发不同框架的测试例时遇到的问题,以及在各端的兼容测试结果。在本文里,我们团队基于真实测试数据及各框架官网可采集到的公开数据,希望客观公正地评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待。
评测实验介绍
-
开发内容:开发一个仿微博小程序首页的复杂长列表,支持下拉刷新、上拉翻页、点赞。
-
界面如下:
-
开发版本:一共开发了6个版本,包括微信原生版、wepy版、mpvue版、taro版、uni-app版、chameleon版(以这些产品发布时间排序,下同),按照官网指引通过
cli
方式默认安装。 -
测试代码开源(Github仓库地址:https://github.com/dcloudio/test-framework),
Tips:若有同学觉得测试代码写法欠妥,欢迎提交 PR 或 Issus -
测试机型:红米 Redmi 6 Pro、MIUI 10.2.2.0 稳定版(最新版)、微信版本 7.0.3(最新版)
-
测试环境:每个框架开始测试前,杀掉各App进程、清空内存,保证测试机环境基本一致;每次从本地读取静态数据,屏蔽网络差异。
-
测试维度:
- 跨端支持度如何?
- 性能如何?
- 学习门槛
- 工具与周边生态
1. 跨端支持度如何
开发一次,到处运行,是每个程序员的梦想。但现实往往变成开发一次,到处调错。
各个待评测框架,是否真得如宣传的那样,一次开发、多端发布?
我们将上述仿微博App依次发布到各平台,验证每个框架在各端的兼容性,结果如下:
测试结果说明:
- ⭕ 表示支持且功能正常,❌ 表示不支持,其它则表示支持但存在部分bug或兼容问题
wepy
宣称计划在2.0版支持其他家小程序,但目前为alpha版。本测试基于wepy
官网指引安装的wepy-cli
正式版,版本为1.7.3,尚不支持多端- taro最新版支持了H5端的下拉刷新,但没有动画(本内容更新日期2019年5月20日)
chameleon
官网未找到stopPullDownRefresh
定义,停止页面下拉刷新需分平台编写
通过这个简单的例子可以看出,跨端支持度测评结论:uni-app
> taro
> chameleon
> mpvue
>wepy
、原生微信小程序
但是仅有上面的测试还不全面,实际业务要比这个测试例复杂很多。但我们没法开发很多复杂业务做评测,所以还需要再对照各家文档补充一些信息。
由于每个框架的文档中都描述了各种组件和API的跨端支持程度。我们过了几家的文档,发现各家基本是以微信小程序为基线,然后把各种组件和API在其他端实现了一遍:
taro
:H5端实现了大部分微信的API,App端和微信的差异比较大。uni-app
:组件、API、配置,大部分在各个端均已实现,个别API有说明在某些端不支持。可以看出uni-app是完整在H5端实现了一套微信模拟器,在App端实现了一套微信小程序引擎,才达到比较完善的平台兼容性。chameleon
:非常常用的一些组件和API在各端已经实现,这部分的平台差异较少。但大量组件和API需要开发者自己分平台写代码。
跨端框架,一方面要考虑框架提供的通用api跨端支持,同时还要考虑不同端的特色差异如何兼容。毕竟每个端都会有自己的特色,不可能完全一致。
taro
:提供了js环境变量判断和统一接口的多端文件,可以在组件、js、文件方面扩展多端,不支持其他环节的分平台处理。uni-app
:提供了条件编译模型,所有代码包括组件、js、css、配置json、文件、目录,均支持条件编译,可不受限的编写各端差异代码。chameleon
:提供了多态方案,可以在组件、js、文件方面扩展多端,不支持其他方式的分平台处理。
跨端框架,还涉及一个ui框架的跨端问题,评测结果如下:
taro
:官方提供了taro ui
,只支持微信小程序和H5两端,不支持App,详见uni-app
:官方提供了uni ui
,可全端运行;uni-app还有一个插件市场,里面有很多三方ui组件,详见chameleon
:官方提供了cml-ui
扩展组件库,可全端运行,但组件数量略少,详见
综合以上信息,本项的最终评测结论:uni-app
> taro
> chameleon
> mpvue
> wepy
、原生微信小程序
2. 跨端框架性能如何
跨端框架基本都是compiler
+ runtime
模式,引入的runtime
是否会降低运行性能?
尤其是与原生微信小程序开发相比性能怎么样,这是大家普遍关心的问题。
我们依然以上述仿微博小程序为例,测试2个容易出性能问题的点:长列表加载、大量点赞组件的响应。
2.1 长列表加载
仿微博的列表是一个包含很多组件的列表,这种复杂列表对性能的压力更大,很适合做性能测试。
从触发上拉加载到数据更新、页面渲染完成,需要准确计时。人眼视觉计时肯定不行,我们采用程序埋点的方式,制定了如下计时时机:
- 计时开始时机:交互事件触发,框架赋值之前,如:上拉加载(onReachBottom)函数开头
- 计时结束时机:页面渲染完毕(微信setData回调函数开头)
Tips:setData
回调函数开头可认为是页面渲染完成的时间,是因为微信setData
定义如下(微信规范):
字段 | 类型 | 必填 | 描述 | |
---|---|---|---|---|
data | Object | 是 | 这次要改变的数据 | |
callback | Function | 否 | setData引起的界面更新渲染完毕后的回调函数 |
测试方式:从页面空列表开始,通过程序自动触发上拉加载,每次新增20条列表,记录单次耗时;固定间隔连续触发 N 次上拉加载,使得页面达到 20*N 条列表,计算这 N 次触发上拉到渲染完成的平均耗时。
测试结果如下:
说明:以400条微博列表为例,从页面空列表开始,每隔1秒触发一次上拉加载(新增20条微博),记录单次耗时,触发20次后停止(页面达到400条微博),计算这20次的平均耗时,结果微信原生在这20次 触发上拉 -> 渲染完成
的平均耗时为876毫秒,最快的uni-app
是741毫秒,最慢的mpvue
是4493毫秒
大家初看这个数据,可能比较疑惑,别急,下方有详细说明
说明1:为何 mpvue/wepy 测试数据不完整?
mpvue
、wepy
诞生之初,微信小程序尚不支持自定义组件,无法进行组件化开发;mpvue
、wepy
为解决这个问题,将用户编写的Vue
组件,编译为WXML
中的模板(template),变相实现了组件化开发能力,提高代码复用性,这在当时的技术条件下是很棒的技术方案。
但如此方案,在页面复杂、组件较多的时,会大量增加页面 dom 节点数量,甚至超出微信的 dom 节点数限制。我们在 红米手机(Redmi 6 Pro)上实测,页面组件超过500个时,mpvue
、wepy
实现的仿微博App就会报出如下异常,并停止渲染,故这两个测试框架在组件较多时,测试数据不完整。这也就意味着,当页面组件太多时,无法使用这2个框架。
dom limit exceeded please check if there's any mistake you've made
Tips:wepy
在400条列表以内,为何性能高于微信原生框架,这个跟自定义组件管理开销及业务场景有关(wepy
编译为模板,不涉及组件创建及管理开销),后续对微博点赞,涉及组件数据传递时,微信原生框架的性能优势就提现出来了,详见下方测试数据。
说明2:为什么测试数据显示uni-app 会比微信原生框架的性能略好呢?
其实,在页面上有200条记录(200个组件)时,taro
性能数据也比微信原生框架更好。
微信原生框架耗时主要在setData
调用上,开发者若不单独优化,则每次都会传递大量数据;而 uni-app
、taro
都在调用setData
之前自动做diff
计算,每次仅传递变动的数据。
例如当前页面有20条数据,触发上拉加载时,会新加载20条数据,此时原生框架通过如下代码测试时,setData
会传输40条数据
data: {
listData: []
},
onReachBottom() { //上拉加载
let listData = this.data.listData;
listData.push(...Api.getNews());//新增数据
this.setData({
listData
}) //全量数据,发送数据到视图层
}
开发者使用微信原生框架,完全可以自己优化,精简传递数据,比如修改如下:
data: {
listData: []
},
onReachBottom() { //上拉加载
// 通过长度获取下一次渲染的索引
let index = this.data.listData.length;
let newData = {}; //新变更数据
Api.getNews().forEach((item) => {
newData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(newData) //增量数据,发送数据到视图层
}
经过如上优化修改后,再次测试,微信原生框架性能数据如下:
从测试结果可看出,如果开发者使用非uni-app开发,则必须手动处理setData的差量。这太麻烦了。
这个结果,和web开发类似,web开发也有原生js开发、vue、react框架等情况。如果不做特殊优化,原生js写的网页,性能经常还不如vue、react框架的性能。
也恰恰是因为Vue
、react
框架的优秀,性能好,开发体验好,所以原生js开发已经逐渐减少使用了。
复杂长列表加载下一页评测结论:微信原生开发手工优化
,uni-app
>微信原生开发未手工优化
,taro
> chameleon
> wepy
> mpvue
注:有人以为uni-app和mpvue是一样的,早期uni-app确实使用过mpvue,但后来因为性能和vue语法支持度问题已经重新开发了。
注:wepy在2.0 alpha里更改了组件编译模式,支持自定义组件,但由于仍是alpha状态,本次评测没有选择。
2.2 点赞组件响应速度
长列表中的某个组件,比如点赞组件,点击时是否能及时的修改未赞和已赞状态?是这项测试的评测点。
测试方式:
- 选中某微博,点击“点赞”按钮,实现点赞状态状态切换(已赞高亮、未赞灰色),
- 点赞按钮
onclick
函数开头开始计时,setData
回调函数开头结束计时;
在红米手机(Redmi 6 Pro)上进行多次测试,求其平均值,结果如下:
说明:也就是在列表数量为400时,微信原生开发的应用,点赞按钮从点击到状态变化需要111毫秒。
测试结果数据说明:
- wepy/mpvue 测试数据不完整的原因同上,在组件较多时,页面已经不再渲染了
- 基于微信自定义组件实现组件开发的框架(uni-app/taro/chameleon),组件数据通讯性能接近于微信原生框架,远高于基于
template
实现组件开发的框架(wepy/mpvue)性能
组件数据更新性能测评:微信原生开发
,uni-app
,taro
> chameleon
> wepy
> mpvue
综上,本性能测试做了2个测试,长列表加载和组件状态更新,综合2个实验,结论如下:
微信原生开发手工优化
,uni-app
>微信原生开发未手工优化
,taro
> chameleon
>> wepy
> mpvue
3. 学习门槛
DSL语法支持度
主流跨端框架基本都遵循React、Vue(类Vue)语法,其主要目的:复用工程师的现有技术栈,降低学习成本。此时,跨端框架对于原框架(React/Vue)语法的支持度就是一个重要的衡量标准,如果支持度较低、和原框架语法差异较大,则开发者无异于要学习一门新的框架,成本太高。
实际开发中发现,各个多端框架,都没有完全实现vue、react在web上的所有语法:
taro
对于 JSX
的语法支持是相对完善的,其文档中描述未来版本计划,
更多的 JSX 语法支持,1.3 之后限制生产力的语法只有只能用 map 创造循环组件一条
mpvue
、uni-app
框架基于 Vue.js
核心,通过修改 Vue.js
的 runtime
和 compiler
,实现了在小程序端的运行,支持绝大部分的Vue语法;uni-app
编译到微信端曾经使用过mpvue
,但后来重新编写,支持了更多vue语法如filter
、复杂 JavaScript
表达式等;
wepy
、chameleon
都是 类Vue
的实现,仅支持 Vue
的部分语法,开发时需要单独学习它们的规则;
DSL语法支持评测:uni-app
,taro
,> mpvue
> wepy
,chameleon
学习资料完善度
-
官方文档、问题搜索、示例demo的完备度方面:
-
wepy:文档只有2页,也无需搜索。仅支持微信,所以组件API等文档都直接看微信的文档。没有提供示例demo。详见
-
mpvue:文档较少,但其概念不复杂,也没有支持H5、App,所以组件API等文档都直接看微信的文档,学习难度低。问题搜索效果一般。没有提供示例demo。详见
-
taro:基础API文档完整,具体使用问题资源较少,问题搜索效果一般,示例demo只包含基础功能,仅发布了微信一端。详见
-
uni-app:基础文档和各种使用专题内容丰富,问题搜索效果较好,示例demo功能完备,并发布为7端上线。详见
-
chameleon:基础API文档完整,具体使用问题资源较少,问题搜索效果一般,示例demo只包含基础功能,仅发布了微信一端。详见
-
教学课程方面:
学习资料完善度评测:uni-app > mpvue , taro > chameleon > wepy
技术支持和社区活跃度
开发难免遇到问题,官方技术支持和社区活跃度很重要。
本次评测demo开发期间,我们的同学(同时掌握vue和react),在学习研究各个多端框架时,切实感受到由于语法、学习资料、社区的差异带来的学习门槛,吐出了很多槽。
综合评估,本项评测结论:uni-app
> taro
> mpvue
> wepy
> chameleon
Tips:本测评忽略React、Vue两框架自身的学习门槛
4. 工具和周边生态
工具
所有多端框架均支持cli
模式,可以在主流前端工具中开发。
各框架基本都带有d.ts的语法提示库。
由于mpvue
、uni-app
、taro
直接支持vue
、react
语法,配套的ide工具链较丰富,着色、校验、格式化完善,chameleon
针对部分编辑器推荐了插件,wepy
的有一些三方维护的vscode插件。
工具属性维度,明显高出一截的框架是uni-app
,其出品公司同时也是HBuilder的出品公司,DCloud.io。
HBuilder/HBuilderX系列是四大主流前端开发工具(可对比百度指数),其为uni-app
做了很多优化,故uni-app
的开发效率、易用性非其他框架可及。
当然对于不习惯HBuilderX的开发者而言,uni-app
的这个优势无法体现。
周边生态
一个底层框架,其周边配套非常重要,比如ui库、js库、项目模板。
- wepy:出现时间久,开源的项目示例较多。其没有自身的组件生态,沿用微信的生态。
- mpvue:发布时间也较早,历史积累较多,支持微信的组件生态,但会把微信的自定义组件编译为页面模板,导致性能问题。
- taro:官方提供了taro ui,github上有一些开源项目。不过taro ui只支持小程序和H5,不支持App端。taro提供了物料市场,但截至到19年10月28日,只有64个插件。
- uni-app:提供了插件市场,ui库、周边模板丰富。截至到19年10月28日,有850个插件。这些优质的插件已经超过了原生小程序的生态,比如原生小程序经常用到的wxParse、wx-echart、vant ui weapp等,在功能和性能上,均不如uni-app插件市场里的插件。可以另行参考https://ask.dcloud.net.cn/article/35489
- chameleon:还没有形成周边生态。
值得注意的是,uni-app
和mpvue
的插件生态是互通的,都是vue插件。所以双方还联合举办了插件大赛。这个联合生态的周边丰富度,是目前各个框架中最丰富的。
综上比较,工具和周边生态评测结论:uni-app
,mpvue
> wepy
> taro
> chameleon
其他常见评测指标
github star:
wepy | mpvue | taro | uni-app | chameleon | |
---|---|---|---|---|---|
6月20日 | 18053 | 17714 | 19584 | 8596 | 5369 |
10月28日 | 19.1k | 18.9k | 22.4k | 14.3k | 6.8k |
增幅 | 1047 | 1186 | 2816 | 5704 | 1431 |
github star 绝对值对比: taro
> wepy
>mpvue
> uni-app
> chameleon
增幅对比:uni-app
>taro
>mpvue
>chameleon
,wepy
由于uni-app
和chameleon
推出时间较晚,总star还不多,但从增速来看,最高的是uni-app
,其次是taro
。这比总star更能说明当前的情况。
百度指数
百度指数代表了开发者的搜索量和包含关键字的网页数量。如下是各跨端框架近7天(2019-03-24 ~ 2019-03-30)的百度指数:
Tips:
wepy
未被百度指数收录,说明其搜索量和包含该关键字的网页数量都不够多。
可以看出一个较大的冲突就是uni-app的star数量较少,而百度指数较高。
根据我们分析,star数量和产品发布时间有关,也和用户使用习惯有关。大多框架的交流互动主要是github的issus,而uni-app
的开发者在其问答社区交流,github页面访问量较低。
案例
我们对比了每个框架公布的案例,如下的框架名称的超链接已指向其案例页面,可以直接访问。
仅看发布到微信小程序的案例,数量和质量综合对比,uni-app >wepy > mpvue > taro > chameleon
如果看多端案例,综合对比,uni-app > taro > mpvue > wepy > chameleon
wepy
:出品较早,赶上了第一拨小程序开发需要脚手架的机会,知名案例较多,包括很多一线互联网公司。mpvue
、taro
:跨端框架的出品方本身为一线互联网公司,其内部项目会使用这些框架,经受过实战考验。除内部项目外,外部项目多以创业项目为主,鲜有其他一线互联网公司使用。uni-app
:案例很多,包括知名互联网公司、政府单位、创业者,官方数据已经超过10w+。各端案例都很丰富。chameleon
:案例不多,仅公布了几个微信小程序案例和百度小程序案例。
其他补充说明
1. App侧的补充说明
目前有taro
、uni-app
、chameleon
三家框架支持App端。但在App端大多是三方产品,比如taro
使用react native
,chameleon
使用weex
。
不管react native
还是weex
,其架构与小程序架构完全不同,从排版到API能力都差别很大,所以这类产品跨App端时兼容性较差。
uni-app
的App端,内置一个完整小程序引擎,并补充了可选的weex
引擎给对性能要求更高的开发者。这也是uni-app
在App端能够正常运行微信小程序代码的原因。
整个业内目前还不存在一个完全开源的小程序引擎(微信、百度、支付宝、头条的小程序引擎源码均未开源)。uni-app
的小程序引擎不是全开源,而是能力层开源,中控未开源。
所以可能各家的多端框架,在App端都有不完美的地方,需要开发者使用时注意。
其实App引擎并非前端领域,是原生领域的另一个竞技场。了解uni-app
与react native
、weex
、flutter
的差别,另见文档https://ask.dcloud.net.cn/article/36083。
2. 转换和混写
taro
提供了原生小程序转换为taro
工程的转换器,也支持在原生小程序里部分页面嵌入taro
编写的页面。
uni-app
有三方转换器,包括原生小程序、wepy框架小程序、mpvue框架小程序,都可以转换为uni-app。但不支持wxml混写。
chameleon
提供了转换的文档,没有官方转换工具,但uni-app
有三方开发的开源转换器。
结语
真实客观的永远是实验和数据,而不是结论。不同需求的开发者,可以根据上述实验数据,自行得出自己的选型结论。
但作为一篇完整的评测,我们也必须提供一份总结,虽然它可能加入了我们的主观感受:
-
如果你只开发微信小程序,不做多端,
uni-app
是更好的选择,除非你有兴趣手动优化原生小程序的代码,或者对react非常熟悉不愿意学习vue也可以使用taro
。另外注意,使用微信原生开发,对于webpack、各种预处理器、工程化流程的支持很不好,大公司很少用原生微信开发,还是用框架开发更合适。 -
如果你主要为了统一各家小程序,
uni-app
仍然是最好的选择,taro
次之。 -
如果你还需要跨端到H5侧,那么
uni-app
在跨端兼容方面会让你更省心。 -
如果你还需要跨端到App侧,那么
uni-app
是唯一可商用的选择。
当然,uni-app
也距离完美尚远,只是在参比框架中相对有优势。uni-app
团队仍将持续完善产品,帮助开发者提升投入产出、提升开发体验!
如有读者认为本文中任何评测失真,欢迎在这里报 issuse。
收起阅读 »
MUI 结合 HTML5+ 实现的二维码扫描功能
一、说明
这里的布局排版不怎么直观,完整博客地址:MUI 结合 HTML5+ 实现的二维码扫描功能
二维码的扫描在手机APP的开发中是很常见的一个需求,毕竟用的也多嘛。html5+ 提供了 Barcode模块管理条码扫描,支持常见的条码(一维码及二维码)的扫描识别功能。可调用设备的摄像头对条码图片扫描进行数据输入,解码后返回码数据及码类型。通过plus.barcode可获取条码码管理对象。
二、主要实现代码逻辑
2.1,扫描实现
function startRecognize() {
try {
var filter;
//自定义的扫描控件样式
var styles = {
top: '100px',
left: '0px',
width: '100%',
height: '500px',
position: 'static',
}
//扫描控件构造
scan = plus.barcode.create('bcid', filter, styles);
scan.onmarked = onmarked;
scan.onerror = onerror;
plus.webview.currentWebview().append(scan);
scan.start();
//打开关闭闪光灯处理
var flag = false;
document.getElementById("turnTheLight").addEventListener('tap', function() {
if (flag == false) {
scan.setFlash(true);
flag = true;
} else {
scan.setFlash(false);
flag = false;
}
});
} catch (e) {
alert("出现错误啦:\n" + e);
}
};
2.2 从相册中选择二维码图片
function scanPicture() {
plus.gallery.pick(function(path) {
plus.barcode.scan(path, onmarked, function(error) {
plus.nativeUI.alert("无法识别此图片");
});
}, function(err) {
plus.nativeUI.alert("Failed: " + err.message);
});
}
一、说明
这里的布局排版不怎么直观,完整博客地址:MUI 结合 HTML5+ 实现的二维码扫描功能
二维码的扫描在手机APP的开发中是很常见的一个需求,毕竟用的也多嘛。html5+ 提供了 Barcode模块管理条码扫描,支持常见的条码(一维码及二维码)的扫描识别功能。可调用设备的摄像头对条码图片扫描进行数据输入,解码后返回码数据及码类型。通过plus.barcode可获取条码码管理对象。
二、主要实现代码逻辑
2.1,扫描实现
function startRecognize() {
try {
var filter;
//自定义的扫描控件样式
var styles = {
top: '100px',
left: '0px',
width: '100%',
height: '500px',
position: 'static',
}
//扫描控件构造
scan = plus.barcode.create('bcid', filter, styles);
scan.onmarked = onmarked;
scan.onerror = onerror;
plus.webview.currentWebview().append(scan);
scan.start();
//打开关闭闪光灯处理
var flag = false;
document.getElementById("turnTheLight").addEventListener('tap', function() {
if (flag == false) {
scan.setFlash(true);
flag = true;
} else {
scan.setFlash(false);
flag = false;
}
});
} catch (e) {
alert("出现错误啦:\n" + e);
}
};
2.2 从相册中选择二维码图片
function scanPicture() {
plus.gallery.pick(function(path) {
plus.barcode.scan(path, onmarked, function(error) {
plus.nativeUI.alert("无法识别此图片");
});
}, function(err) {
plus.nativeUI.alert("Failed: " + err.message);
});
}

uniapp 开发直播工功能使用的LivePusher接口会导致uniapp无限重启,在HbuilderX1.8.2.20190401版本时没有此问题,怎么解决,急
最近接到一个项目,需要开发直播功能,之前调用的LivePusher这个功能可实现,但更新了HbuilderX之后,这个功能就出现了BUG,也就是开发的app会无限次重启,直播页面一闪一闪的,最终手机死机!
最近接到一个项目,需要开发直播功能,之前调用的LivePusher这个功能可实现,但更新了HbuilderX之后,这个功能就出现了BUG,也就是开发的app会无限次重启,直播页面一闪一闪的,最终手机死机!

uniapp 在App端上传多张图片, 后端Java利用Spring的MultipartFile接收文件
提示: 由于在dcloud论坛发布的文章贴代码不便浏览,贴图片有些浏览器图片不能加载,所以本文章发布在简书了, 详情点击这里: uniapp在app端上传多图片,后端java接收
提示: 由于在dcloud论坛发布的文章贴代码不便浏览,贴图片有些浏览器图片不能加载,所以本文章发布在简书了, 详情点击这里: uniapp在app端上传多图片,后端java接收
收起阅读 »
Android动态权限申请
更新:插件市场已经提供了封装更完善版本:https://ext.dcloud.net.cn/plugin?id=594
从HBuilderX1.9.4及以上版本开始,Android平台默认targetSdkVersion从21(Android5.0)调整为23(Android6.0)。
Android动态权限申请机制
Android6.0(API23)及以后,系统对权限的管理更加严格,放弃了以往manifest中注册所需权限,用户只要安装APP,便获取了所有注册权限的权限管理机制,而是改为除了需manifest中注册,部分危险权限另需在用户使用某项特殊功能时,向用户动态申请的机制。
当用户手机系统为Android6.0及以上,APP的targetSdkVersion>=23时,新的动态权限申请机制将会被触发,其它所有情况(1.系统版本>=6.0,targetSdkVersion<23;2.系统版本<6.0,targetSdkVersion>=23;3.系统版本<6.0,targetSdkVersion<23)都不会触发动态权限申请机制,因此,如果你不想在APP中动态申请权限,可以将targetSdkVersion设置为小于23。如不然,你就需要在使用某些涉及危险权限的功能(如读取通讯录)时通过系统弹窗的形式向用户动态申请该权限。动态申请权限下,如果用户在权限申请弹窗中拒绝了该申请,则用户将不能使用需要该权限的功能,再次申请该权限时依然会弹窗向用户申请;若用户在权限申请弹窗中勾选了“不再提示”并拒绝,那么再次申请该权限的时候将不会弹出系统弹窗向用户申请权限,此时需要APP引导用户打开设置,在设置中给与APP所需权限。
注意:云端打包targetSdkVersion默认值为26
5+APP中动态权限申请机制的实现
5+APP各独立模块中已经集成了功能所需权限的动态申请机制,开发者无需另做处理。但是如果需要使用某些尚未集成的特殊功能,如通过native.js调用原生方法获取手机扫描到的wifi列表,由于android可以通过访问wifi获取位置信息,因此需要在使用原生方法前先动态申请该功能所需的ACCESS_FINE_LOCATION权限。正因为有这样的需求,DCloud在native.js中为Android提供了动态申请权限的功能。
开发者通过调用plus.android.requestPermissions申请权限。参数permissions为所需权限数组;resultCallback为申请结果回调,将会返回已获取的权限、拒绝本次申请的权限、永久拒绝申请的权限3种结果的权限列表,开发者可以读取各权限申请结果并做相应处理;errorCallback为权限参数格式错误时调用,返回错误信息。
代码举例
依然以获取wifi列表为例,使用该功能前需要开发者先申请所需权限ACCESS_FINE_LOCATION:
function requestPermission() {
plus.android.requestPermissions(
["android.permission.ACCESS_FINE_LOCATION"],
function(resultObj){
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:'+ grantedPermission);
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:'+ deniedPresentPermission );
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:'+ deniedAlwaysPermission);
}
// 若所需权限被永久拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
if (resultObj.deniedAlways.length > 0) {
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);
}
},
function(error){
console.log('申请权限错误:'+ error.code+ " = "+ error.message);
});
}
引导用户打开所需权限的方法分析
当需要引导用户打开特定权限时,最理想的情况是打开一个只有该权限开关的页面让用户开启权限,但是Android会将应用申请的所有权限集中在一个页面,因此从Android系统提供的功能的角度讲,最好是能引导用户进入应用的权限管理页面,在这个页面中让用户根据提示打开相应权限。然而,国内厂商早在Android未提供动态权限申请功能时就对Android应用的权限申请进行了改造和封装,这就使开发者无法通过统一的入口进入应用权限管理页面,而需要通过各个厂商自己的入口进入,如
// 华为
Intent intent = new Intent(packageName);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
intent.setComponent(comp);
mContext.startActivity(intent);
// 魅族
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", packageName);
mContext.startActivity(intent);
这无疑加大了开发难度。而且各厂商更可能随着版本的升级更改权限管理入口,这就加大了开发的不确定性。因此,我们推荐的最理想的引导用户打开权限的入口是应用设置页面,然后用户通过点击“权限管理”自主进入权限管理页面进行相关权限的设置。
5+APP中利用native.js打开应用设置页面的方法见上方代码举例。
附Android危险权限列表
-
SMS(短信)
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS -
STORAGE(存储卡,包括相册等)
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE -
CONTACTS(联系人)
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS -
PHONE(手机)
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS -
CALENDAR(日历)
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR -
CAMERA(相机)
android.permission.CAMERA -
LOCATION(位置)
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION -
SENSORS(传感器)
android.permission.BODY_SENSORS -
MICROPHONE(麦克风)
android.permission.RECORD_AUDIO
Android官方权限概述(需翻墙)
Android官方权限列表(需翻墙)
相关问题
更新:插件市场已经提供了封装更完善版本:https://ext.dcloud.net.cn/plugin?id=594
从HBuilderX1.9.4及以上版本开始,Android平台默认targetSdkVersion从21(Android5.0)调整为23(Android6.0)。
Android动态权限申请机制
Android6.0(API23)及以后,系统对权限的管理更加严格,放弃了以往manifest中注册所需权限,用户只要安装APP,便获取了所有注册权限的权限管理机制,而是改为除了需manifest中注册,部分危险权限另需在用户使用某项特殊功能时,向用户动态申请的机制。
当用户手机系统为Android6.0及以上,APP的targetSdkVersion>=23时,新的动态权限申请机制将会被触发,其它所有情况(1.系统版本>=6.0,targetSdkVersion<23;2.系统版本<6.0,targetSdkVersion>=23;3.系统版本<6.0,targetSdkVersion<23)都不会触发动态权限申请机制,因此,如果你不想在APP中动态申请权限,可以将targetSdkVersion设置为小于23。如不然,你就需要在使用某些涉及危险权限的功能(如读取通讯录)时通过系统弹窗的形式向用户动态申请该权限。动态申请权限下,如果用户在权限申请弹窗中拒绝了该申请,则用户将不能使用需要该权限的功能,再次申请该权限时依然会弹窗向用户申请;若用户在权限申请弹窗中勾选了“不再提示”并拒绝,那么再次申请该权限的时候将不会弹出系统弹窗向用户申请权限,此时需要APP引导用户打开设置,在设置中给与APP所需权限。
注意:云端打包targetSdkVersion默认值为26
5+APP中动态权限申请机制的实现
5+APP各独立模块中已经集成了功能所需权限的动态申请机制,开发者无需另做处理。但是如果需要使用某些尚未集成的特殊功能,如通过native.js调用原生方法获取手机扫描到的wifi列表,由于android可以通过访问wifi获取位置信息,因此需要在使用原生方法前先动态申请该功能所需的ACCESS_FINE_LOCATION权限。正因为有这样的需求,DCloud在native.js中为Android提供了动态申请权限的功能。
开发者通过调用plus.android.requestPermissions申请权限。参数permissions为所需权限数组;resultCallback为申请结果回调,将会返回已获取的权限、拒绝本次申请的权限、永久拒绝申请的权限3种结果的权限列表,开发者可以读取各权限申请结果并做相应处理;errorCallback为权限参数格式错误时调用,返回错误信息。
代码举例
依然以获取wifi列表为例,使用该功能前需要开发者先申请所需权限ACCESS_FINE_LOCATION:
function requestPermission() {
plus.android.requestPermissions(
["android.permission.ACCESS_FINE_LOCATION"],
function(resultObj){
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:'+ grantedPermission);
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:'+ deniedPresentPermission );
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:'+ deniedAlwaysPermission);
}
// 若所需权限被永久拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
if (resultObj.deniedAlways.length > 0) {
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);
}
},
function(error){
console.log('申请权限错误:'+ error.code+ " = "+ error.message);
});
}
引导用户打开所需权限的方法分析
当需要引导用户打开特定权限时,最理想的情况是打开一个只有该权限开关的页面让用户开启权限,但是Android会将应用申请的所有权限集中在一个页面,因此从Android系统提供的功能的角度讲,最好是能引导用户进入应用的权限管理页面,在这个页面中让用户根据提示打开相应权限。然而,国内厂商早在Android未提供动态权限申请功能时就对Android应用的权限申请进行了改造和封装,这就使开发者无法通过统一的入口进入应用权限管理页面,而需要通过各个厂商自己的入口进入,如
// 华为
Intent intent = new Intent(packageName);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
intent.setComponent(comp);
mContext.startActivity(intent);
// 魅族
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", packageName);
mContext.startActivity(intent);
这无疑加大了开发难度。而且各厂商更可能随着版本的升级更改权限管理入口,这就加大了开发的不确定性。因此,我们推荐的最理想的引导用户打开权限的入口是应用设置页面,然后用户通过点击“权限管理”自主进入权限管理页面进行相关权限的设置。
5+APP中利用native.js打开应用设置页面的方法见上方代码举例。
附Android危险权限列表
-
SMS(短信)
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS -
STORAGE(存储卡,包括相册等)
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE -
CONTACTS(联系人)
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS -
PHONE(手机)
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS -
CALENDAR(日历)
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR -
CAMERA(相机)
android.permission.CAMERA -
LOCATION(位置)
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION -
SENSORS(传感器)
android.permission.BODY_SENSORS -
MICROPHONE(麦克风)
android.permission.RECORD_AUDIO
Android官方权限概述(需翻墙)
Android官方权限列表(需翻墙)
相关问题
iOS权限检查见:https://ask.dcloud.net.cn/article/35915
收起阅读 »
应用云端打包国际化处理
云端打包配置国际化
从HBuilderX1.9.5开始,云端打包支持配置部分国际化功能,应用会根据当前系统设置的语言自动选择配置的国际化字符,如果不配置则使用应用默认配置的信息。
国际化支持的语言
应用内置支持以下语言:
- 英文
- 中文(简体)
国际化支持的内容
目前云端打包支持的国际化内容包括:
- 应用名称
在桌面显示的应用名称,配置国际化后,切换系统语言则会显示对应国际化配置的应用名称。 - iOS平台隐私访问描述信息
应用第一次使用涉及到用户隐私的功能是弹出授权确认框上显示的信息,提交App store审核时此信息必须准确描述获取此权限的原因。
切换系统语言时弹出的授权确认框会显示对应国际化配置的描述信息,不配置国际化则总是使用HBuilderX可视化界面配置的描述信息。(注意:系统授权弹窗国际化只受系统语言设置影响,app 内设置语言不受影响)
manifest.json国际化配置
HBuilderX暂时还不支持可视化界面配置,需在“源码视图”中手动添加配置。
打开项目的manifest.json文件,切换到“源码视图”。
uni-app项目在"app-plus"->"locales"节点,5+ APP项目在"plus"->"locales"节点配置以下信息:
"locales": {
"en": { // 英文
"name": "HBuilder", // 应用名称
"android": {
"strings": { //Android平台自定义字符串
"CustomKey": "CustomValue",
//...
}
},
"ios": {
"privacyDescription": { //iOS平台隐私访问描述信息
"NSPhotoLibraryUsageDescription": "access to the user’s photo library(read)",
//...
},
"infoPlist": { //iOS平台自定义InfoPlist.strings
"CustomKey": "CustomValue",
//...
}
}
}
"zh": { // 中文(简体)
}
}
其中locales下的键名(key)可以取值:
- 语言代码,通常为两个或三个字母,参考ISO 639规范,示例如下
语言名称 | 语言代码 |
---|---|
中文 | zh |
英文 | en |
日语 | ja |
韩语 | ko |
法语 | fr |
西班牙语 | es |
- 语言代码-地区代码,地区代码为两个字母,参考ISO 3166-2规范,示例如下
地区名称 | 地区代码 |
---|---|
中国 | CN |
中国台湾 | TW |
中国香港 | HK |
美国 | US |
英国 | GB |
日本 | JP |
韩国 | KR |
法国 | FR |
西班牙 | ES |
<a id="strings"/>
android -> strings
Android平台配置自定义strings.xml文件,HBuilderX2.5.0+版本支持。
用于配置strings.xml国际化文件,可在此节点下配置使用uni原生插件使用的自定义国际化键值对。
"strings": {
"CustomKey1": "CustomValue1",
"CustomKey2": "CustomValue2",
"CustomKey3": "CustomValue3"
}
也可以配置内部业务逻辑使用的国际化字符串
应用启动时引导用户允许权限的提示语
字符串键名 | 字符串键值 |
---|---|
dcloud_permission_write_external_storage_message | 引导用户开启“读写手机存储”权限提示语 |
dcloud_permission_read_phone_state_message | 引导用户开启“获取设备信息”权限提示语 |
配置应用启动时申请权限详细配置参考:https://ask.dcloud.net.cn/article/36549
html页面中input(type=file)打开的选择页面国际化字符串
字符串键名 | 字符串键值 |
---|---|
dcloud_choose_an_action | 选择页面标题,默认中文为“选择操作” |
图片选择plus.gallery.pick国际化字符串(多图)
字符串键名 | 字符串键值 |
---|---|
dcloud_gallery_library_name | 媒体选择器 |
dcloud_gallery_all_dir_name | 所有图片和视频 |
dcloud_gallery_all_video | 所有视频 |
dcloud_gallery_all_image | 所有图片 |
dcloud_gallery_select_title | 选择图片和视频 |
dcloud_gallery_select_video_title | 选择视频 |
dcloud_gallery_select_image_title | 选择图片 |
dcloud_gallery_video_dir_name | 所有视频 |
dcloud_gallery_msg_amount_limit | 已达到选择数量上限 |
dcloud_gallery_msg_size_limit | 请压缩和剪切后上传,文件最大只支持 |
dcloud_gallery_select_null | 请选择文件 |
dcloud_gallery_done | 完成 |
dcloud_gallery_count_string | 个 |
dcloud_gallery_preview | 预览 |
dcloud_gallery_select | 选择 |
dcloud_gallery_video | 视频 |
dcloud_gallery_cant_play_video | 没有可以播放的程序 |
dcloud_gallery_read_external_storage | 需要打开读取存储权限 |
<a id="privacyDescription"/>
ios -> privacyDescription
iOS平台配置隐私权限描述国际化。
建议将manifest.json页面切换到“模块权限配置”项,在“iOS隐私信息访问的许可描述”栏下配置应用需要使用到的隐私描述信息:
输入完成后切换到代码视图,uni-app项目在"app-plus"->"distribute"->"ios"->"privacyDescription"节点,5+ APP项目在"plus"->"distribute"->"apple"->"privacyDescription"节点下可看到输入的内容:
将"privacyDescription"节点下的内容拷贝到"locales"节点下要配置语言下的"ios"->"privacyDescription"节点下,并将值翻译为对应语言的描述。
完整可配置的隐私项可参考苹果官网https://developer.apple.com/documentation/bundleresources/information_property_list中以“NS”开头、“Description”结尾的项。
<a id="infoPlist"/>
ios -> infoPlist
iOS平台配置自定义InfoPlist.Strings文件,HBuilderX2.3.4+版本支持。
用于配置InfoPlist.Strings国际化文件, 可在此节点下配置使用uni原生插件使用的自定义国际化键值对。
"infoPlist": {
"CustomKey1": "CustomValue1",
"CustomKey2": "CustomValue2",
"CustomKey3": "CustomValue3"
}
应用内国际化处理
这里不描述应用如何动态切换语言(相关方法请参考其它标准H5方案),仅考虑如果获取当前系统的语言环境,业务代码根据语言环境进行国际化处理。
5+ APP项目
所有js都运行在系统Webview环境中,可以直接使用H5标准API获取当前系统设置的语言:
var lan = navigator.language||navigator.browserLanguage;
console.log(lan);
详细规范参考:https://developer.mozilla.org/zh-CN/docs/Web/API/NavigatorLanguage/languages
uni-app项目
可以调用5+ API(plus.os.language)获取当前系统设置的语言:
var lan = plus.os.language;
console.log(lan);
通常可以在App.vue页面的onLaunch中获取。
5+ API规范参考:http://www.html5plus.org/doc/zh_cn/device.html#plus.os.language
5+ API的国际化支持说明
大多数5+ API都不会涉及到国际化问题,除非有部分界面相关的API,5+ Runtime内部已经支持国际化,即根据当前系统设置的语言会自动处理国际化。
可能存在部分界面未完成国际化处理,我们会根据反馈情况确定优先级进行支持。
uni-app应用开发国际化说明参考https://ask.dcloud.net.cn/article/35872
云端打包配置国际化
从HBuilderX1.9.5开始,云端打包支持配置部分国际化功能,应用会根据当前系统设置的语言自动选择配置的国际化字符,如果不配置则使用应用默认配置的信息。
国际化支持的语言
应用内置支持以下语言:
- 英文
- 中文(简体)
国际化支持的内容
目前云端打包支持的国际化内容包括:
- 应用名称
在桌面显示的应用名称,配置国际化后,切换系统语言则会显示对应国际化配置的应用名称。 - iOS平台隐私访问描述信息
应用第一次使用涉及到用户隐私的功能是弹出授权确认框上显示的信息,提交App store审核时此信息必须准确描述获取此权限的原因。
切换系统语言时弹出的授权确认框会显示对应国际化配置的描述信息,不配置国际化则总是使用HBuilderX可视化界面配置的描述信息。(注意:系统授权弹窗国际化只受系统语言设置影响,app 内设置语言不受影响)
manifest.json国际化配置
HBuilderX暂时还不支持可视化界面配置,需在“源码视图”中手动添加配置。
打开项目的manifest.json文件,切换到“源码视图”。
uni-app项目在"app-plus"->"locales"节点,5+ APP项目在"plus"->"locales"节点配置以下信息:
"locales": {
"en": { // 英文
"name": "HBuilder", // 应用名称
"android": {
"strings": { //Android平台自定义字符串
"CustomKey": "CustomValue",
//...
}
},
"ios": {
"privacyDescription": { //iOS平台隐私访问描述信息
"NSPhotoLibraryUsageDescription": "access to the user’s photo library(read)",
//...
},
"infoPlist": { //iOS平台自定义InfoPlist.strings
"CustomKey": "CustomValue",
//...
}
}
}
"zh": { // 中文(简体)
}
}
其中locales下的键名(key)可以取值:
- 语言代码,通常为两个或三个字母,参考ISO 639规范,示例如下
语言名称 | 语言代码 |
---|---|
中文 | zh |
英文 | en |
日语 | ja |
韩语 | ko |
法语 | fr |
西班牙语 | es |
- 语言代码-地区代码,地区代码为两个字母,参考ISO 3166-2规范,示例如下
地区名称 | 地区代码 |
---|---|
中国 | CN |
中国台湾 | TW |
中国香港 | HK |
美国 | US |
英国 | GB |
日本 | JP |
韩国 | KR |
法国 | FR |
西班牙 | ES |
<a id="strings"/>
android -> strings
Android平台配置自定义strings.xml文件,HBuilderX2.5.0+版本支持。
用于配置strings.xml国际化文件,可在此节点下配置使用uni原生插件使用的自定义国际化键值对。
"strings": {
"CustomKey1": "CustomValue1",
"CustomKey2": "CustomValue2",
"CustomKey3": "CustomValue3"
}
也可以配置内部业务逻辑使用的国际化字符串
应用启动时引导用户允许权限的提示语
字符串键名 | 字符串键值 |
---|---|
dcloud_permission_write_external_storage_message | 引导用户开启“读写手机存储”权限提示语 |
dcloud_permission_read_phone_state_message | 引导用户开启“获取设备信息”权限提示语 |
配置应用启动时申请权限详细配置参考:https://ask.dcloud.net.cn/article/36549
html页面中input(type=file)打开的选择页面国际化字符串
字符串键名 | 字符串键值 |
---|---|
dcloud_choose_an_action | 选择页面标题,默认中文为“选择操作” |
图片选择plus.gallery.pick国际化字符串(多图)
字符串键名 | 字符串键值 |
---|---|
dcloud_gallery_library_name | 媒体选择器 |
dcloud_gallery_all_dir_name | 所有图片和视频 |
dcloud_gallery_all_video | 所有视频 |
dcloud_gallery_all_image | 所有图片 |
dcloud_gallery_select_title | 选择图片和视频 |
dcloud_gallery_select_video_title | 选择视频 |
dcloud_gallery_select_image_title | 选择图片 |
dcloud_gallery_video_dir_name | 所有视频 |
dcloud_gallery_msg_amount_limit | 已达到选择数量上限 |
dcloud_gallery_msg_size_limit | 请压缩和剪切后上传,文件最大只支持 |
dcloud_gallery_select_null | 请选择文件 |
dcloud_gallery_done | 完成 |
dcloud_gallery_count_string | 个 |
dcloud_gallery_preview | 预览 |
dcloud_gallery_select | 选择 |
dcloud_gallery_video | 视频 |
dcloud_gallery_cant_play_video | 没有可以播放的程序 |
dcloud_gallery_read_external_storage | 需要打开读取存储权限 |
<a id="privacyDescription"/>
ios -> privacyDescription
iOS平台配置隐私权限描述国际化。
建议将manifest.json页面切换到“模块权限配置”项,在“iOS隐私信息访问的许可描述”栏下配置应用需要使用到的隐私描述信息:
输入完成后切换到代码视图,uni-app项目在"app-plus"->"distribute"->"ios"->"privacyDescription"节点,5+ APP项目在"plus"->"distribute"->"apple"->"privacyDescription"节点下可看到输入的内容:
将"privacyDescription"节点下的内容拷贝到"locales"节点下要配置语言下的"ios"->"privacyDescription"节点下,并将值翻译为对应语言的描述。
完整可配置的隐私项可参考苹果官网https://developer.apple.com/documentation/bundleresources/information_property_list中以“NS”开头、“Description”结尾的项。
<a id="infoPlist"/>
ios -> infoPlist
iOS平台配置自定义InfoPlist.Strings文件,HBuilderX2.3.4+版本支持。
用于配置InfoPlist.Strings国际化文件, 可在此节点下配置使用uni原生插件使用的自定义国际化键值对。
"infoPlist": {
"CustomKey1": "CustomValue1",
"CustomKey2": "CustomValue2",
"CustomKey3": "CustomValue3"
}
应用内国际化处理
这里不描述应用如何动态切换语言(相关方法请参考其它标准H5方案),仅考虑如果获取当前系统的语言环境,业务代码根据语言环境进行国际化处理。
5+ APP项目
所有js都运行在系统Webview环境中,可以直接使用H5标准API获取当前系统设置的语言:
var lan = navigator.language||navigator.browserLanguage;
console.log(lan);
详细规范参考:https://developer.mozilla.org/zh-CN/docs/Web/API/NavigatorLanguage/languages
uni-app项目
可以调用5+ API(plus.os.language)获取当前系统设置的语言:
var lan = plus.os.language;
console.log(lan);
通常可以在App.vue页面的onLaunch中获取。
5+ API规范参考:http://www.html5plus.org/doc/zh_cn/device.html#plus.os.language
5+ API的国际化支持说明
大多数5+ API都不会涉及到国际化问题,除非有部分界面相关的API,5+ Runtime内部已经支持国际化,即根据当前系统设置的语言会自动处理国际化。
可能存在部分界面未完成国际化处理,我们会根据反馈情况确定优先级进行支持。
uni-app应用开发国际化说明参考https://ask.dcloud.net.cn/article/35872
收起阅读 »
h5模拟导航栏滑动渐变
由于原生的导航栏自定义有限,使用plus.nativeObj.View自己来画又太费劲了,所以使用h5来实现一个,分享给大家。
-
修改pages.json,去年原生导航栏
{ "path": "nav-bar2/nav-bar2", "style": { "navigationBarTitleText": "NavBar 导航栏", "app-plus": { "titleNView": false } } }
2、页面
<template> <view style="display: flex; flex: 1;justify-content: center; background: #00BFFF;"> <view style="position: fixed; width: 100%; background-color: #000000; " :style="{height: searchBgHeight + 'px', opacity:searchBgOpcity}" ></view> <view class="header" :style="{'margin-top':statusBarHeight}" style="background: #fff; position: fixed; width: 90%; display: flex;flex: 1;justify-content: center;box-shadow: none;border: 1px solid transparent;border-radius: 10upx;"> <uni-nav-bar style="box-shadow:none;" color="#333333" @click-left="showCity" @click-right="scan"> <block slot="left"> <view class="city"> <view>北京北京</view> <uni-icon type="arrowdown" color="#333333" size="22"></uni-icon> </view> </block> <view class="input-view"> <input confirm-type="search" @confirm="confirm" class="input" type="text" placeholder="输入搜索关键词" /> <uni-icon type="search" size="22" color="#666666"></uni-icon> </view> </uni-nav-bar> </view> <view class="content"> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> </view> </view> </template>
<script>
import uniStatusBar from '@/components/uni-status-bar/uni-status-bar.vue'
import uniNavBar from '@/components/uni-nav-bar2/uni-nav-bar2.vue'
import uniIcon from '@/components/uni-icon/uni-icon.vue'
var statusBarHeight = uni.getSystemInfoSync().statusBarHeight + 'px';
export default {
components: {
uniNavBar,
uniIcon
},
data() {
return {
statusBarHeight: statusBarHeight,
searchBgHeight: '',
searchBgOpcity: '',
city: '北京北京'
}
},
created() {
let _t = this;
setTimeout(() => { //获取状态栏高度,setTimeout后才能调用uni.
uni.getSystemInfo({
success: function(res) {
_t.searchBgHeight = res.statusBarHeight + 50
_t.searchBgOpcity = 0.0
console.log(_t.statusBarHeight);
}
});
}, 1);
} ,
methods: {
back() {
uni.navigateBack({
delta: 1
})
},
showMenu() {
uni.showToast({
title: '菜单'
})
},
clickLeft() {
uni.showToast({
title: '左侧按钮'
})
},
search() {
uni.showToast({
title: '搜索'
})
},
showCity() {
uni.showToast({
title: '选择城市'
})
},
scan() {
uni.showToast({
title: '扫码'
})
},
confirm() {
uni.showToast({
title: '搜索'
})
}
},
onPullDownRefresh() {
console.log('onPullDownRefresh')
setTimeout(function() {
uni.stopPullDownRefresh()
console.log('stopPullDownRefresh')
}, 1000)
},
onLoad() {
const pages = getCurrentPages();
const page = pages[pages.length - 1];
var that = this;
page.onPageScroll = function(data) {
let opcity = data.scrollTop / 100;
if (opcity > 1) {
opcity = 1.0
}
that.searchBgOpcity = opcity;
console.log("page......searchBgOpcity==" + that.searchBgOpcity);
}
}
}
</script>
<style>
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff
}
view {
font-size: 28upx;
line-height: inherit
}
..uni-navbar--shadow {
box-shadow: none;
}
.example {
padding: 0 30upx 30upx
}
.example-title {
font-size: 32upx;
line-height: 32upx;
color: #777;
margin: 40upx 25upx;
position: relative
}
.example .example-title {
margin: 40upx 0
}
.example-body {
padding: 0 40upx
}
.uni-common-mt {
color: #7a7e83;
font-size: 28upx;
padding: 30upx;
}
.title {
font-size: 15px;
line-height: 20px;
color: #333333;
padding: 15px;
}
.city {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
margin-left: 8px;
white-space: nowrap;
}
.input-view {
width: 92%;
display: flex;
height: 30px;
border-radius: 15px;
padding: 0 4%;
flex-wrap: nowrap;
margin: 3px 0;
line-height: 30px;
}
.input-view .uni-icon {
line-height: 30px !important;
}
.input-view .input {
height: 30px;
line-height: 30px;
width: 94%;
padding: 0 3%;
}
</style>
3、修改到Hello-uni-app中的一个uni-nav-bar.vue,修改名称叫uni-nav-bar2.vue
<template>
<view class="uni-navbar">
<view class="uni-navbar__content" :class="{'uni-navbar--fixed': !!fixed}" :style="{'background-color':backgroundColor}" style="width: 100%;">
<view class="uni-navbar__header" :style="{color:color}" >
<view class="uni-navbar__header-btns" @tap="onClickLeft">
<view v-if="leftIcon.length">
<uni-icon :type="leftIcon" :color="color" size="24"></uni-icon>
</view>
<view v-if="leftText.length" class="uni-navbar-btn-text" :class="{'uni-navbar-btn-icon-left':!leftIcon.length}">{{leftText}}</view>
<slot name="left"></slot>
</view>
<view class="uni-navbar__header-container">
<view v-if="title.length" class="uni-navbar__header-container-inner">{{title}}</view>
<!-- 标题插槽 -->
<slot></slot>
</view>
<!-- <view class="uni-navbar__header-btns" @tap="onClickRight">
<view v-if="rightIcon.length">
<uni-icon :type="rightIcon" :color="color" size="24"></uni-icon>
</view>
优先显示图标
<view v-if="rightText.length&&!rightIcon.length" class="uni-navbar-btn-text">{{rightText}}</view>
<slot name="right"></slot>
</view> -->
</view>
</view>
<view class="uni-navbar__placeholder" v-if="fixed">
<uni-status-bar v-if="statusBar"></uni-status-bar>
<view class="uni-navbar__placeholder-view"></view>
</view>
</view>
</template>
<script>
import uniStatusBar from '../uni-status-bar/uni-status-bar.vue'
import uniIcon from '../uni-icon/uni-icon.vue'
export default {
name: 'uni-nav-bar',
components: {
uniStatusBar,
uniIcon
},
props: {
title: {
type: String,
default: ''
},
leftText: {
type: String,
default: ''
},
rightText: {
type: String,
default: ''
},
leftIcon: {
type: String,
default: ''
},
rightIcon: {
type: String,
default: ''
},
fixed: {
type: [Boolean, String],
default: false
},
color: {
type: String,
default: '#000000'
},
backgroundColor: {
type: String,
default: 'transparent'
},
statusBar: {
type: [Boolean, String],
default: false
},
shadow: {
type: [String, Boolean],
default: true
},
border: {
type: [String, Boolean],
default: true
}
},
methods: {
onClickLeft() {
this.$emit('click-left')
},
onClickRight() {
this.$emit('click-right')
}
}
}
</script>
<style>
@charset "UTF-8";
.uni-navbar__content {
display: block;
position: relative;
width: 100%;
/* background-color: #fff; */
overflow: hidden
}
.uni-navbar__content view {
line-height: 44px
}
.uni-navbar__header {
display: flex;
flex-direction: row;
width: 100%;
height: 38px;
line-height: 38px;
font-size: 16px
}
.uni-navbar__header-btns {
display: inline-flex;
flex-wrap: nowrap;
flex-shrink: 0;
width: 120upx;
padding: 0 12upx
}
.uni-navbar__header-btns:first-child {
padding-left: 0
}
.uni-navbar__header-btns:last-child {
width: 60upx
}
.uni-navbar__header-container {
width: 100%;
margin: 0 10upx
}
.uni-navbar__header-container-inner {
font-size: 30upx;
text-align: center;
padding-right: 60upx
}
.uni-navbar__placeholder-view {
height: 38px
}
.uni-navbar--fixed {
position: fixed;
z-index: 998
}
.uni-navbar--shadow {
box-shadow: 0 1px 6px #ccc
}
.uni-navbar--border:after {
position: absolute;
z-index: 3;
bottom: 0;
left: 0;
right: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
background-color: #c8c7cc
}
</style>
由于原生的导航栏自定义有限,使用plus.nativeObj.View自己来画又太费劲了,所以使用h5来实现一个,分享给大家。
-
修改pages.json,去年原生导航栏
{ "path": "nav-bar2/nav-bar2", "style": { "navigationBarTitleText": "NavBar 导航栏", "app-plus": { "titleNView": false } } }
2、页面
<template> <view style="display: flex; flex: 1;justify-content: center; background: #00BFFF;"> <view style="position: fixed; width: 100%; background-color: #000000; " :style="{height: searchBgHeight + 'px', opacity:searchBgOpcity}" ></view> <view class="header" :style="{'margin-top':statusBarHeight}" style="background: #fff; position: fixed; width: 90%; display: flex;flex: 1;justify-content: center;box-shadow: none;border: 1px solid transparent;border-radius: 10upx;"> <uni-nav-bar style="box-shadow:none;" color="#333333" @click-left="showCity" @click-right="scan"> <block slot="left"> <view class="city"> <view>北京北京</view> <uni-icon type="arrowdown" color="#333333" size="22"></uni-icon> </view> </block> <view class="input-view"> <input confirm-type="search" @confirm="confirm" class="input" type="text" placeholder="输入搜索关键词" /> <uni-icon type="search" size="22" color="#666666"></uni-icon> </view> </uni-nav-bar> </view> <view class="content"> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> <view style="margin: 20upx;">1111</view> </view> </view> </template>
<script>
import uniStatusBar from '@/components/uni-status-bar/uni-status-bar.vue'
import uniNavBar from '@/components/uni-nav-bar2/uni-nav-bar2.vue'
import uniIcon from '@/components/uni-icon/uni-icon.vue'
var statusBarHeight = uni.getSystemInfoSync().statusBarHeight + 'px';
export default {
components: {
uniNavBar,
uniIcon
},
data() {
return {
statusBarHeight: statusBarHeight,
searchBgHeight: '',
searchBgOpcity: '',
city: '北京北京'
}
},
created() {
let _t = this;
setTimeout(() => { //获取状态栏高度,setTimeout后才能调用uni.
uni.getSystemInfo({
success: function(res) {
_t.searchBgHeight = res.statusBarHeight + 50
_t.searchBgOpcity = 0.0
console.log(_t.statusBarHeight);
}
});
}, 1);
} ,
methods: {
back() {
uni.navigateBack({
delta: 1
})
},
showMenu() {
uni.showToast({
title: '菜单'
})
},
clickLeft() {
uni.showToast({
title: '左侧按钮'
})
},
search() {
uni.showToast({
title: '搜索'
})
},
showCity() {
uni.showToast({
title: '选择城市'
})
},
scan() {
uni.showToast({
title: '扫码'
})
},
confirm() {
uni.showToast({
title: '搜索'
})
}
},
onPullDownRefresh() {
console.log('onPullDownRefresh')
setTimeout(function() {
uni.stopPullDownRefresh()
console.log('stopPullDownRefresh')
}, 1000)
},
onLoad() {
const pages = getCurrentPages();
const page = pages[pages.length - 1];
var that = this;
page.onPageScroll = function(data) {
let opcity = data.scrollTop / 100;
if (opcity > 1) {
opcity = 1.0
}
that.searchBgOpcity = opcity;
console.log("page......searchBgOpcity==" + that.searchBgOpcity);
}
}
}
</script>
<style>
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff
}
view {
font-size: 28upx;
line-height: inherit
}
..uni-navbar--shadow {
box-shadow: none;
}
.example {
padding: 0 30upx 30upx
}
.example-title {
font-size: 32upx;
line-height: 32upx;
color: #777;
margin: 40upx 25upx;
position: relative
}
.example .example-title {
margin: 40upx 0
}
.example-body {
padding: 0 40upx
}
.uni-common-mt {
color: #7a7e83;
font-size: 28upx;
padding: 30upx;
}
.title {
font-size: 15px;
line-height: 20px;
color: #333333;
padding: 15px;
}
.city {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
margin-left: 8px;
white-space: nowrap;
}
.input-view {
width: 92%;
display: flex;
height: 30px;
border-radius: 15px;
padding: 0 4%;
flex-wrap: nowrap;
margin: 3px 0;
line-height: 30px;
}
.input-view .uni-icon {
line-height: 30px !important;
}
.input-view .input {
height: 30px;
line-height: 30px;
width: 94%;
padding: 0 3%;
}
</style>
3、修改到Hello-uni-app中的一个uni-nav-bar.vue,修改名称叫uni-nav-bar2.vue
<template>
<view class="uni-navbar">
<view class="uni-navbar__content" :class="{'uni-navbar--fixed': !!fixed}" :style="{'background-color':backgroundColor}" style="width: 100%;">
<view class="uni-navbar__header" :style="{color:color}" >
<view class="uni-navbar__header-btns" @tap="onClickLeft">
<view v-if="leftIcon.length">
<uni-icon :type="leftIcon" :color="color" size="24"></uni-icon>
</view>
<view v-if="leftText.length" class="uni-navbar-btn-text" :class="{'uni-navbar-btn-icon-left':!leftIcon.length}">{{leftText}}</view>
<slot name="left"></slot>
</view>
<view class="uni-navbar__header-container">
<view v-if="title.length" class="uni-navbar__header-container-inner">{{title}}</view>
<!-- 标题插槽 -->
<slot></slot>
</view>
<!-- <view class="uni-navbar__header-btns" @tap="onClickRight">
<view v-if="rightIcon.length">
<uni-icon :type="rightIcon" :color="color" size="24"></uni-icon>
</view>
优先显示图标
<view v-if="rightText.length&&!rightIcon.length" class="uni-navbar-btn-text">{{rightText}}</view>
<slot name="right"></slot>
</view> -->
</view>
</view>
<view class="uni-navbar__placeholder" v-if="fixed">
<uni-status-bar v-if="statusBar"></uni-status-bar>
<view class="uni-navbar__placeholder-view"></view>
</view>
</view>
</template>
<script>
import uniStatusBar from '../uni-status-bar/uni-status-bar.vue'
import uniIcon from '../uni-icon/uni-icon.vue'
export default {
name: 'uni-nav-bar',
components: {
uniStatusBar,
uniIcon
},
props: {
title: {
type: String,
default: ''
},
leftText: {
type: String,
default: ''
},
rightText: {
type: String,
default: ''
},
leftIcon: {
type: String,
default: ''
},
rightIcon: {
type: String,
default: ''
},
fixed: {
type: [Boolean, String],
default: false
},
color: {
type: String,
default: '#000000'
},
backgroundColor: {
type: String,
default: 'transparent'
},
statusBar: {
type: [Boolean, String],
default: false
},
shadow: {
type: [String, Boolean],
default: true
},
border: {
type: [String, Boolean],
default: true
}
},
methods: {
onClickLeft() {
this.$emit('click-left')
},
onClickRight() {
this.$emit('click-right')
}
}
}
</script>
<style>
@charset "UTF-8";
.uni-navbar__content {
display: block;
position: relative;
width: 100%;
/* background-color: #fff; */
overflow: hidden
}
.uni-navbar__content view {
line-height: 44px
}
.uni-navbar__header {
display: flex;
flex-direction: row;
width: 100%;
height: 38px;
line-height: 38px;
font-size: 16px
}
.uni-navbar__header-btns {
display: inline-flex;
flex-wrap: nowrap;
flex-shrink: 0;
width: 120upx;
padding: 0 12upx
}
.uni-navbar__header-btns:first-child {
padding-left: 0
}
.uni-navbar__header-btns:last-child {
width: 60upx
}
.uni-navbar__header-container {
width: 100%;
margin: 0 10upx
}
.uni-navbar__header-container-inner {
font-size: 30upx;
text-align: center;
padding-right: 60upx
}
.uni-navbar__placeholder-view {
height: 38px
}
.uni-navbar--fixed {
position: fixed;
z-index: 998
}
.uni-navbar--shadow {
box-shadow: 0 1px 6px #ccc
}
.uni-navbar--border:after {
position: absolute;
z-index: 3;
bottom: 0;
left: 0;
right: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
background-color: #c8c7cc
}
</style>
收起阅读 »

关于小米手机中 plus.ui.Toast 存在被自动添加 HBuilder 字样的解决方法
前几天就遇到了这个问题,没有时间去处理这个小问题,所以就放过一边等待最后去解决,今天有闲下来了就一并解决这些小问题
遇到的小问题:
解决以后:
解决方法:
var Toast = plus.android.importClass("android.widget.Toast");
var mToast = Toast.makeText(plus.android.runtimeMainActivity(), null, Toast.LENGTH_SHORT);
mToast.setText('Hello World!');
mToast.show();
前几天就遇到了这个问题,没有时间去处理这个小问题,所以就放过一边等待最后去解决,今天有闲下来了就一并解决这些小问题
遇到的小问题:
解决以后:
解决方法:
var Toast = plus.android.importClass("android.widget.Toast");
var mToast = Toast.makeText(plus.android.runtimeMainActivity(), null, Toast.LENGTH_SHORT);
mToast.setText('Hello World!');
mToast.show();
收起阅读 »

解决上拉加载
当总页数小于等于pageSize时,首次加载需要禁用上拉加载,一般使用mui('#pullrefreshContainer').pullRefresh().disablePullupToRefresh();但是该方法时好时坏,经常有解决办法是使用定时器延时执行该方法,实际上仍不能解决mui存在的bug;以下是我的解决办法,供大家参考:
1、定义全局变量pullupToRefresh = true;//能否上拉加载
2、在ajax的回调函数里判断,如果总页数是否小于等于pageSize,则pullupToRefresh = false,否则pullupToRefresh = true;
3、在上拉加载方法里首先判断pullupToRefresh的值,当pullupToRefresh = true时才执行下拉刷新
当总页数小于等于pageSize时,首次加载需要禁用上拉加载,一般使用mui('#pullrefreshContainer').pullRefresh().disablePullupToRefresh();但是该方法时好时坏,经常有解决办法是使用定时器延时执行该方法,实际上仍不能解决mui存在的bug;以下是我的解决办法,供大家参考:
1、定义全局变量pullupToRefresh = true;//能否上拉加载
2、在ajax的回调函数里判断,如果总页数是否小于等于pageSize,则pullupToRefresh = false,否则pullupToRefresh = true;
3、在上拉加载方法里首先判断pullupToRefresh的值,当pullupToRefresh = true时才执行下拉刷新

接兼职\外包\咨询类
uni框架1年经验(熟悉vue生态,熟读源码),nodejs经验2年(express+mongodb+redis)
熟悉js原生开发与安卓原生插件开发
可接外包:
- 网站项目(静态/动态)
- uniapp项目
- 服务器应用(数据库的Restful api设计或websocket服务器设计)
- 普通组件
- 第三方原生lib转化为uniapp的原生插件
- 应用中接入ai功能
- BUG消除
可全部typescript开发,可以个人或公司名义合作,可签合同
如有需求请联系报价
联系方式:qq527501080 email :snowolfjay@qq.com