HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

hbuilderX编辑器貌似有点low

HBuilder

HBuilderX编辑器下载后,点击运行,出现卡死的界面。

我的是win10 64位机器。且发现官方给的windows下载版本是32位的。。

官方是不是有点太草率了。连一个编辑器安装都出现各种奇葩问题。。。

HBuilderX编辑器下载后,点击运行,出现卡死的界面。

我的是win10 64位机器。且发现官方给的windows下载版本是32位的。。

官方是不是有点太草率了。连一个编辑器安装都出现各种奇葩问题。。。

uni-app使用总结

uniapp

本文主要uni-app开发项目遇到的一些问题的总结,关于uni-app具体的介绍和使用请查看uni-app官网
笔者编译使用的HBuilderX版本为2.4.2.20191115。

样式

1.1 无法设置背景图片

直接使用图片,并且图片大于40kb,官网有说明,主要是针对web之外的平台存在限制。

1.2 根据官方文档设置窗口背景颜色无效(在pages.json设置窗口默认backgroundColor或者单独设置窗口style中的backgroundColor都没有效果)

在页面中设置

page {  
    background-color: #ccc;  
}

1.3 设置页面navigationBarShadow导航栏阴影无效

关于导航栏的一些问题只能自己编写自定义组件或者nvue组件来解决,具体可以阅读[官方文档](uni-app官网

1.4 使用flex布局,如果嵌套了scroll-view,flex布局会失效

<view class="content">  
        <view class="fixed-item"></view>  
        <view class="flex-item">  
            <scroll-view></scroll-view>  
        </view>  
 </view>
.content {  
    display: flex;  
    flex-direction: column;  
    align-items: center;  
    justify-content: center;  
    position: absolute;  
    top: 0;  
    bottom: 0;  
    width: 100%;  
}  

.fixed-item {  
    width: 100%;  
    height: 100px;  
    background-color: #007AFF;  
}  
.flex-item {  
    flex: 1;     
}

1.5 原生端给组件设置margin无效,margin不会出现合并的情况(使用flex时)

原因未知

vue语法

2.1 在原生端App.vue没有vue的生命周期,但是有页面的生命周期,但是web端两者都存在

原因未知

2.2 vuex在原生端是不支持使用命名空间的,但是在web端是支持的

原因未知

2.3 $attrs无效

2.4 不支持vue-router

使用uni.navigateTo和uni.switchTab代替router.push方法实现页面跳转,uni.swtichTab在跳转tab页面时使用,但是不能再tab页面使用uni.redirectTo,不然跳转的目标页面也会出现底部tab栏

2.5 app端不支持v-slot传值

原因未知

2.6 v-for方法遍历数字时,web端从1开始,但是原生端从0开始

原因未知

2.7 原生端ref无法获取uni原生组件,web端可行

原因未知

2.8 onShow第一次触发时,$refs的内容为空

在this.$nextTick(() => {})中使用$refs

组件与接口

3.1 组件不可控,无法通过event修改组件的显示值(input、switch组件)

Web端可以通过$refs设置(switch通过$refs获取然后设置switchChecked的值),但原生端无法通过$refs获取组件无法使用这个方法实现

3.2 uni-request无法设置cookie

将cookie数据放在header字段中

3.3 监听subNVue的显示和隐藏

SubNVue无法监听显示和隐藏,显示可以在调用show时在回调函数中触发。可以通过下列方式监听:

const qrcode = uni.getSubNVueById(‘qr_code’)   
qrcode.addEventListener(‘hide’, () => {   
    console.log(‘hideQrCode’)   
})   
qrcode.addEventListener('show', () => {   
    console.log(‘showQrCode’)   
}) 

3.4 disableScroll: true无法禁止页面整体滚动

设置

“App-plus”: {  
    “bounce”: “none”  
}

3.5 使用subNVues,点击遮罩将无法关闭popup

style样式中的background不能设置为除transparent的其他值

3.6 CanvasContext.draw在App中无法执行回调函数

在vue页面中可以使用,在nvue中无法执行回调函数

3.7 修改导航栏

var wv = this.$mp.page.$getAppWebview();  
        wv.setStyle({    
            titleNView: {    
                “buttons”: [  
                {  
                  “fontSrc”: “/static/uni.ttf”,  
                  “fontSize”: “14”,  
                  “color”: “#FFFFFF”,  
                  “text”: `\ue333 ${address.city}`,  
                  "background": "rgba(0,0,0,0)”,  
                  "float": “left”,  
                  “width”: “96px”  
                }  
              ]  
            }    
        });  

具体可以参考[官网](uni-app在App端动态修改原生导航栏 - DCloud问答

其他

  1. 在原生端不同平台也会存在差异。
  2. 针对很多比较奇怪的问题可以选择重新编译。
继续阅读 »

本文主要uni-app开发项目遇到的一些问题的总结,关于uni-app具体的介绍和使用请查看uni-app官网
笔者编译使用的HBuilderX版本为2.4.2.20191115。

样式

1.1 无法设置背景图片

直接使用图片,并且图片大于40kb,官网有说明,主要是针对web之外的平台存在限制。

1.2 根据官方文档设置窗口背景颜色无效(在pages.json设置窗口默认backgroundColor或者单独设置窗口style中的backgroundColor都没有效果)

在页面中设置

page {  
    background-color: #ccc;  
}

1.3 设置页面navigationBarShadow导航栏阴影无效

关于导航栏的一些问题只能自己编写自定义组件或者nvue组件来解决,具体可以阅读[官方文档](uni-app官网

1.4 使用flex布局,如果嵌套了scroll-view,flex布局会失效

<view class="content">  
        <view class="fixed-item"></view>  
        <view class="flex-item">  
            <scroll-view></scroll-view>  
        </view>  
 </view>
.content {  
    display: flex;  
    flex-direction: column;  
    align-items: center;  
    justify-content: center;  
    position: absolute;  
    top: 0;  
    bottom: 0;  
    width: 100%;  
}  

.fixed-item {  
    width: 100%;  
    height: 100px;  
    background-color: #007AFF;  
}  
.flex-item {  
    flex: 1;     
}

1.5 原生端给组件设置margin无效,margin不会出现合并的情况(使用flex时)

原因未知

vue语法

2.1 在原生端App.vue没有vue的生命周期,但是有页面的生命周期,但是web端两者都存在

原因未知

2.2 vuex在原生端是不支持使用命名空间的,但是在web端是支持的

原因未知

2.3 $attrs无效

2.4 不支持vue-router

使用uni.navigateTo和uni.switchTab代替router.push方法实现页面跳转,uni.swtichTab在跳转tab页面时使用,但是不能再tab页面使用uni.redirectTo,不然跳转的目标页面也会出现底部tab栏

2.5 app端不支持v-slot传值

原因未知

2.6 v-for方法遍历数字时,web端从1开始,但是原生端从0开始

原因未知

2.7 原生端ref无法获取uni原生组件,web端可行

原因未知

2.8 onShow第一次触发时,$refs的内容为空

在this.$nextTick(() => {})中使用$refs

组件与接口

3.1 组件不可控,无法通过event修改组件的显示值(input、switch组件)

Web端可以通过$refs设置(switch通过$refs获取然后设置switchChecked的值),但原生端无法通过$refs获取组件无法使用这个方法实现

3.2 uni-request无法设置cookie

将cookie数据放在header字段中

3.3 监听subNVue的显示和隐藏

SubNVue无法监听显示和隐藏,显示可以在调用show时在回调函数中触发。可以通过下列方式监听:

const qrcode = uni.getSubNVueById(‘qr_code’)   
qrcode.addEventListener(‘hide’, () => {   
    console.log(‘hideQrCode’)   
})   
qrcode.addEventListener('show', () => {   
    console.log(‘showQrCode’)   
}) 

3.4 disableScroll: true无法禁止页面整体滚动

设置

“App-plus”: {  
    “bounce”: “none”  
}

3.5 使用subNVues,点击遮罩将无法关闭popup

style样式中的background不能设置为除transparent的其他值

3.6 CanvasContext.draw在App中无法执行回调函数

在vue页面中可以使用,在nvue中无法执行回调函数

3.7 修改导航栏

var wv = this.$mp.page.$getAppWebview();  
        wv.setStyle({    
            titleNView: {    
                “buttons”: [  
                {  
                  “fontSrc”: “/static/uni.ttf”,  
                  “fontSize”: “14”,  
                  “color”: “#FFFFFF”,  
                  “text”: `\ue333 ${address.city}`,  
                  "background": "rgba(0,0,0,0)”,  
                  "float": “left”,  
                  “width”: “96px”  
                }  
              ]  
            }    
        });  

具体可以参考[官网](uni-app在App端动态修改原生导航栏 - DCloud问答

其他

  1. 在原生端不同平台也会存在差异。
  2. 针对很多比较奇怪的问题可以选择重新编译。
收起阅读 »

uni-app商业级应用实战!

uniapp uni_app

需要资料加企鹅:3340563142

需要资料加企鹅:3340563142

加载字体文件过大的问题,不是icon,而是fontFamily

App iconfont

目前在写app时遇到ttf文件加载太大了,放本地打包比app还大(差不多了),放服务器加载太慢,于是乎想要看看能不能把这个ttf字体包搞小一点。
首先是加载字体文件的loadFontFace,插件市场也有很好的字体引入插件。但是各种字体文件的通病就是ttf字体包很小的不支持汉字,支持全汉字等的文件比较大(动不动就支持拉丁,日,韩,朝等各种文字,还有各种生僻字,总共5-6万字的字体库)。
后来有人说,这些字体在html中可以这样加载,但是这样加载是不是相当于从字体包中挑出相应的文字重新生成一个文件,这样不能满足我的需求,因为我的文字有新闻类的文章,总不能每次都请求一个字体文件啊(或许这样也可以)。
然后就是类似于阿里icon的在线字体生成,我觉得是不是这样的也许能满足我的需求,直接把汉字常用字生成对应的字体文件,但是发现文字数量有限制,并不现实(某些文本确定的地方这样生成的文件更合适)。
最后,找到了这个将原有字体文件多余文字抽离,精简字体包的方法,精简ttf的方法,按照这个教程,我实现了3500常用字,7600常用字和8000常用字的字体包的生成。
为什么要发这个帖子,效果太显著了,有下面几点,1、我使用的思源宋体,原otf大概23M,精简版ttf只有3M左右(7600字),加载速度大大提升;2、最后的精简字体方法中有坑:首先,FontCreator工具收费,仅有win版,FontSubsetGUI工具仅支持ttf,对于otf支持度不够等;3、最后就是FontSubsetGUI的下载源收费,而且需要.net环境的依赖,各种字体文件大家随便去各种字体网站找一找都有下载的。
##############
刚又发现了,Nvue中引入的字体没有生效,weex引入,loadFontface引入都不行,css引入还是不行oo原来路径的问题啊。比如我的代码‘/static/font/siyuanSimSun.ttf’这个路径,还有就是引入的fontfamily不要用‘string’,倒是和Vue页面内的相反。
var domModule = weex.requireModule('dom');
domModule.addRule('fontFace', {
'fontFamily': "siyuanSimSun",
'src': "url('/static/font/siyuanSimSun.ttf')"
});
##############
刚测试苹果机,注意一下ttf的路径,原安卓的路径(/static/font/siyuanSimSun.ttf)比较随意,倒是iOS相对路径(./static/font/siyuanSimSun.ttf)一定要写准确,也可以转本地绝对路径'src': 'url("'+"file:/" + plus.io.convertLocalFileSystemURL("_www/static/font/siyuanSimSun.ttf")+'")'。

##############
又发现一个问题,之前的环境都是V3引擎,现在我发现关闭V3后Nvue里的字体引入方式失效了,我会重新找方案。

##############
最近把字体的引入搞差不多了,有两个点,V3没有问题,尽量不要在app里引Vue页面的字体,分别引入。Nvue比较好一点,但是不论Nue还是Vue的input,textarea,在安卓端都没办法把内容字体使用指定字体。

##############
本身随笔性质的,也没描述太清楚,既然官方大佬来了,我要把这个字体精简工具放出来,里边有好几个,具体是什么我也不懂,反正按上边的教程,很傻瓜的。

##############
突然发现忘记了一点,我在app.vue里放了一个全局字体
.siyuanSimSun{
font-family: siyuanSimSun; //这里不要带引号
}

继续阅读 »

目前在写app时遇到ttf文件加载太大了,放本地打包比app还大(差不多了),放服务器加载太慢,于是乎想要看看能不能把这个ttf字体包搞小一点。
首先是加载字体文件的loadFontFace,插件市场也有很好的字体引入插件。但是各种字体文件的通病就是ttf字体包很小的不支持汉字,支持全汉字等的文件比较大(动不动就支持拉丁,日,韩,朝等各种文字,还有各种生僻字,总共5-6万字的字体库)。
后来有人说,这些字体在html中可以这样加载,但是这样加载是不是相当于从字体包中挑出相应的文字重新生成一个文件,这样不能满足我的需求,因为我的文字有新闻类的文章,总不能每次都请求一个字体文件啊(或许这样也可以)。
然后就是类似于阿里icon的在线字体生成,我觉得是不是这样的也许能满足我的需求,直接把汉字常用字生成对应的字体文件,但是发现文字数量有限制,并不现实(某些文本确定的地方这样生成的文件更合适)。
最后,找到了这个将原有字体文件多余文字抽离,精简字体包的方法,精简ttf的方法,按照这个教程,我实现了3500常用字,7600常用字和8000常用字的字体包的生成。
为什么要发这个帖子,效果太显著了,有下面几点,1、我使用的思源宋体,原otf大概23M,精简版ttf只有3M左右(7600字),加载速度大大提升;2、最后的精简字体方法中有坑:首先,FontCreator工具收费,仅有win版,FontSubsetGUI工具仅支持ttf,对于otf支持度不够等;3、最后就是FontSubsetGUI的下载源收费,而且需要.net环境的依赖,各种字体文件大家随便去各种字体网站找一找都有下载的。
##############
刚又发现了,Nvue中引入的字体没有生效,weex引入,loadFontface引入都不行,css引入还是不行oo原来路径的问题啊。比如我的代码‘/static/font/siyuanSimSun.ttf’这个路径,还有就是引入的fontfamily不要用‘string’,倒是和Vue页面内的相反。
var domModule = weex.requireModule('dom');
domModule.addRule('fontFace', {
'fontFamily': "siyuanSimSun",
'src': "url('/static/font/siyuanSimSun.ttf')"
});
##############
刚测试苹果机,注意一下ttf的路径,原安卓的路径(/static/font/siyuanSimSun.ttf)比较随意,倒是iOS相对路径(./static/font/siyuanSimSun.ttf)一定要写准确,也可以转本地绝对路径'src': 'url("'+"file:/" + plus.io.convertLocalFileSystemURL("_www/static/font/siyuanSimSun.ttf")+'")'。

##############
又发现一个问题,之前的环境都是V3引擎,现在我发现关闭V3后Nvue里的字体引入方式失效了,我会重新找方案。

##############
最近把字体的引入搞差不多了,有两个点,V3没有问题,尽量不要在app里引Vue页面的字体,分别引入。Nvue比较好一点,但是不论Nue还是Vue的input,textarea,在安卓端都没办法把内容字体使用指定字体。

##############
本身随笔性质的,也没描述太清楚,既然官方大佬来了,我要把这个字体精简工具放出来,里边有好几个,具体是什么我也不懂,反正按上边的教程,很傻瓜的。

##############
突然发现忘记了一点,我在app.vue里放了一个全局字体
.siyuanSimSun{
font-family: siyuanSimSun; //这里不要带引号
}

收起阅读 »

地点搜索功能分享,用来弥补uniapp 使用uni.chooseLocation无法选择其他城市地点的不足

自己在使用uni.chooseLocation时发现只能搜索到当前城市的地点,无法搜索到其他城市的地点,这样导致实现地点搜索时限制很多,因此分享给出自己结合HTML5+实现的地点选择。

<template>  
    <view class="full-wrap">  
        <view id="map">  
        </view>  
        <view class="now-pos" @tap="searchPos(posCity)">  
            定位城市:{{posCity}}  
        </view>  
        <view class="choose-city flex-box">  
            <view class="city-item flex-box">  
                <text class="city flex-box" @tap="chooseCity">{{nowCity}}</text>  
                <image src="../../static/down.png" class="down-ico"></image>  
            </view>  
            <view class="search-box">  
                <input placeholder="搜索地点" type="text" name="input" @input="searchPos(nowCity)" style="width: 100% ;" v-model="searchText"></input>  
            </view>  
        </view>  
        <view class="search-result-wrap">  
            <view class="cu-item padding" :key="index" v-for="(item,index) in cityList" @click="chooseHandle(item)">  
                <view class="search-result-content">  
                    <text class="list-city-name">{{item.name}}</text>  
                    <text class="list-city-address text-gray">{{item.city+item.address}}</text>  
                </view>  
            </view>  
        </view>  
        <!-- 城市选择 该插件从插件市场下载 -->  
        <mpvue-picker :themeColor="themeColor" ref="mpvuePicker" :mode="mode" :deepLength="deepLength" :pickerValueDefault="pickerValueDefault"  
         @onConfirm="onConfirm" @="" :pickerValueArray="pickerValueArray"></mpvue-picker>  
    </view>  
</template>  

<script>  
    import cityData from '../../commonjs/city.data.js'  
    import mpvuePicker from '../../components/mpvue-picker/mpvuePicker.vue'  
    export default {  
        components: {  
            mpvuePicker  
        },  
        data() {  
            return {  
                lat: " 39.915",  
                lng: '116.404',  
                posCity: "",  
                nowCity: '',  
                searchText: "",  
                cityList: [],  
                pickerValueDefault: [0, 0],  
                themeColor: '#007AFF',  
                mode: 'multiLinkageSelector',  
                deepLength: 2,  
                pickerValueArray: cityData  
            }  
        },  
        mounted() {  
            // 默认以当前位置为中心  
            uni.getLocation({  
                geocode: 'true',  
                success: res => {  
                    this.posCity = res.address.city;  
                    this.nowCity = res.address.city;  
                    this.lat = res.latitude;  
                    this.lng = res.longitude;  
                    this.searchPos(res.address.city);  
                }  
            });  
        },  
        methods: {  
            searchPos(cityName) {  
                let map = this.$refs.map;  
                let searchObj = new plus.maps.Search(map);  
                searchObj.onPoiSearchComplete = (state, result)=> {  
                    if (state == 0) {  
                        if (result.currentNumber <= 0) {  
                            uni.showToast({  
                                title: '没有检索到结果',  
                                icon: 'none'  
                            });  
                        }  
                        this.cityList = [];  
                        for (let i = 0; i < result.currentNumber; i++) {  
                            let pos = result.getPosition(i);  
                            this.cityList.push(pos);  
                        }  
                    } else {  
                        uni.showToast({  
                            title: '检索失败',  
                            icon: 'none'  
                        });  
                    }  
                }  
                let pt = new plus.maps.Point(this.lng, this.lat);  
                // 默认搜索火车站  
                let nowPosCIty = this.searchText ? this.searchText : '火车站';  
                searchObj.poiSearchInCity(cityName, nowPosCIty);  
            },  
            chooseCity() {  
                this.$refs.mpvuePicker.show();  
            },  
            onCancel(e) {  
                console.log(e);  
            },  
            onConfirm(e) {  
                let pickerText = e.label.split('-');  
                this.searchPos(pickerText[1])  
            },  
            chooseHandle(e) {  
                console.log('选择地点的地点信息:');  
                console.log(e);  
            }  
        }  
    }  
</script>  

<style scoped>  
    #map1 {  
        height: 0;  
    }  
    .flex-box {  
        display: flex;  
    }  
.now-pos {  
    font-size: 30upx;  
    padding: 25upx;  
}  
    .choose-city {  
        background-color: #FFFFFF;  
        border-bottom: 0.5px solid #ddd;  
    }  

    .city-item {  
        width: 160upx;  
        color: grey;  
        padding: 20upx 15upx;  
        margin-left: 15upx;  
        border-right: 0.5px solid #ddd;  
        border-radius: 5upx;  
    }  

    .city {  
        font-size: 30upx;  
        margin-top: 8upx;  
    }  
.down-ico {  
    width: 30upx;  
    height: 30upx;  
    position: relative;  
    top:16upx;  
}  
    .search-result-wrap {  
        padding: 10upx;  
    }  

    .search-result-content {  
        display: flex;  
        flex-direction: column;  
        padding: 10upx;  
        border-bottom: 0.5px solid #ddd;  
    }  

    .search-box {  
        font-size: 30upx;  
        width: 550upx;  
        padding: 20upx 15upx;  
        background-color: #FFFFFF;  
    }  

    .list-city-name {  
        font-size: 32upx;  
        margin-bottom: 0;  
        padding: 0;  
    }  

    .list-city-address {  
        font-size: 24upx;  
        color: gray;  
        margin-top: 0;  
    }  
</style>  

其中使用了城市选择插件,结合自己的需要自行选择,我直接在插件市场找了一个使用了,如有侵权,请联系本人QQ:1414901782;
演示视频:见附件
项目地址:uniapp选择地点

继续阅读 »

自己在使用uni.chooseLocation时发现只能搜索到当前城市的地点,无法搜索到其他城市的地点,这样导致实现地点搜索时限制很多,因此分享给出自己结合HTML5+实现的地点选择。

<template>  
    <view class="full-wrap">  
        <view id="map">  
        </view>  
        <view class="now-pos" @tap="searchPos(posCity)">  
            定位城市:{{posCity}}  
        </view>  
        <view class="choose-city flex-box">  
            <view class="city-item flex-box">  
                <text class="city flex-box" @tap="chooseCity">{{nowCity}}</text>  
                <image src="../../static/down.png" class="down-ico"></image>  
            </view>  
            <view class="search-box">  
                <input placeholder="搜索地点" type="text" name="input" @input="searchPos(nowCity)" style="width: 100% ;" v-model="searchText"></input>  
            </view>  
        </view>  
        <view class="search-result-wrap">  
            <view class="cu-item padding" :key="index" v-for="(item,index) in cityList" @click="chooseHandle(item)">  
                <view class="search-result-content">  
                    <text class="list-city-name">{{item.name}}</text>  
                    <text class="list-city-address text-gray">{{item.city+item.address}}</text>  
                </view>  
            </view>  
        </view>  
        <!-- 城市选择 该插件从插件市场下载 -->  
        <mpvue-picker :themeColor="themeColor" ref="mpvuePicker" :mode="mode" :deepLength="deepLength" :pickerValueDefault="pickerValueDefault"  
         @onConfirm="onConfirm" @="" :pickerValueArray="pickerValueArray"></mpvue-picker>  
    </view>  
</template>  

<script>  
    import cityData from '../../commonjs/city.data.js'  
    import mpvuePicker from '../../components/mpvue-picker/mpvuePicker.vue'  
    export default {  
        components: {  
            mpvuePicker  
        },  
        data() {  
            return {  
                lat: " 39.915",  
                lng: '116.404',  
                posCity: "",  
                nowCity: '',  
                searchText: "",  
                cityList: [],  
                pickerValueDefault: [0, 0],  
                themeColor: '#007AFF',  
                mode: 'multiLinkageSelector',  
                deepLength: 2,  
                pickerValueArray: cityData  
            }  
        },  
        mounted() {  
            // 默认以当前位置为中心  
            uni.getLocation({  
                geocode: 'true',  
                success: res => {  
                    this.posCity = res.address.city;  
                    this.nowCity = res.address.city;  
                    this.lat = res.latitude;  
                    this.lng = res.longitude;  
                    this.searchPos(res.address.city);  
                }  
            });  
        },  
        methods: {  
            searchPos(cityName) {  
                let map = this.$refs.map;  
                let searchObj = new plus.maps.Search(map);  
                searchObj.onPoiSearchComplete = (state, result)=> {  
                    if (state == 0) {  
                        if (result.currentNumber <= 0) {  
                            uni.showToast({  
                                title: '没有检索到结果',  
                                icon: 'none'  
                            });  
                        }  
                        this.cityList = [];  
                        for (let i = 0; i < result.currentNumber; i++) {  
                            let pos = result.getPosition(i);  
                            this.cityList.push(pos);  
                        }  
                    } else {  
                        uni.showToast({  
                            title: '检索失败',  
                            icon: 'none'  
                        });  
                    }  
                }  
                let pt = new plus.maps.Point(this.lng, this.lat);  
                // 默认搜索火车站  
                let nowPosCIty = this.searchText ? this.searchText : '火车站';  
                searchObj.poiSearchInCity(cityName, nowPosCIty);  
            },  
            chooseCity() {  
                this.$refs.mpvuePicker.show();  
            },  
            onCancel(e) {  
                console.log(e);  
            },  
            onConfirm(e) {  
                let pickerText = e.label.split('-');  
                this.searchPos(pickerText[1])  
            },  
            chooseHandle(e) {  
                console.log('选择地点的地点信息:');  
                console.log(e);  
            }  
        }  
    }  
</script>  

<style scoped>  
    #map1 {  
        height: 0;  
    }  
    .flex-box {  
        display: flex;  
    }  
.now-pos {  
    font-size: 30upx;  
    padding: 25upx;  
}  
    .choose-city {  
        background-color: #FFFFFF;  
        border-bottom: 0.5px solid #ddd;  
    }  

    .city-item {  
        width: 160upx;  
        color: grey;  
        padding: 20upx 15upx;  
        margin-left: 15upx;  
        border-right: 0.5px solid #ddd;  
        border-radius: 5upx;  
    }  

    .city {  
        font-size: 30upx;  
        margin-top: 8upx;  
    }  
.down-ico {  
    width: 30upx;  
    height: 30upx;  
    position: relative;  
    top:16upx;  
}  
    .search-result-wrap {  
        padding: 10upx;  
    }  

    .search-result-content {  
        display: flex;  
        flex-direction: column;  
        padding: 10upx;  
        border-bottom: 0.5px solid #ddd;  
    }  

    .search-box {  
        font-size: 30upx;  
        width: 550upx;  
        padding: 20upx 15upx;  
        background-color: #FFFFFF;  
    }  

    .list-city-name {  
        font-size: 32upx;  
        margin-bottom: 0;  
        padding: 0;  
    }  

    .list-city-address {  
        font-size: 24upx;  
        color: gray;  
        margin-top: 0;  
    }  
</style>  

其中使用了城市选择插件,结合自己的需要自行选择,我直接在插件市场找了一个使用了,如有侵权,请联系本人QQ:1414901782;
演示视频:见附件
项目地址:uniapp选择地点

收起阅读 »

在uni-app中使用pdf.js预览 pdf文件

分享 uniapp pdf

主要根据 掘金上的这篇博文 跨平台(uni-app)文件在线预览解决方案 设置,但是遇到了几个坑,和大家分享下:

  • 出现跨域问题
    可以将 view.js 的
       if (origin !== viewerOrigin && protocol !== 'blob:') {  
        throw new Error('file origin does not match viewer\'s');  
      }

注释掉

  • 无法读取文件流
    uniapp一读取文件流就闪退。可以下载后读取。但是下载后按照原方法,会被读取view.js下的路径,需要用H5+的api转换成绝对路径
        onLoad() {  
            uni.downloadFile({  
                url: 'http://192.168.18.28:8280/webupload/service/sys/file/upload/download?appKey=accessKey&prodID=prodID&fileID=3494049602848768',  
                success: (result) => {  
                    var tempFilePath = result.tempFilePath  
                    let fileUrl = plus.io.convertLocalFileSystemURL(tempFilePath)  
                    this.allUrl = this.viewerUrl + '?file=' + fileUrl  
                }  
            })  
        }
继续阅读 »

主要根据 掘金上的这篇博文 跨平台(uni-app)文件在线预览解决方案 设置,但是遇到了几个坑,和大家分享下:

  • 出现跨域问题
    可以将 view.js 的
       if (origin !== viewerOrigin && protocol !== 'blob:') {  
        throw new Error('file origin does not match viewer\'s');  
      }

注释掉

  • 无法读取文件流
    uniapp一读取文件流就闪退。可以下载后读取。但是下载后按照原方法,会被读取view.js下的路径,需要用H5+的api转换成绝对路径
        onLoad() {  
            uni.downloadFile({  
                url: 'http://192.168.18.28:8280/webupload/service/sys/file/upload/download?appKey=accessKey&prodID=prodID&fileID=3494049602848768',  
                success: (result) => {  
                    var tempFilePath = result.tempFilePath  
                    let fileUrl = plus.io.convertLocalFileSystemURL(tempFilePath)  
                    this.allUrl = this.viewerUrl + '?file=' + fileUrl  
                }  
            })  
        }
收起阅读 »

APICloud终于承认侵权并向DCloud道歉了(附最新产品对比)

APICloud

​在北京市高级人民法院的法令要求下,APICloud终于承认了侵犯DCloud著作权,并在官网和公众号向DCloud道歉。

2015年,DCloud起诉APICloud侵权后,APICloud不但否认侵权,还污蔑DCloud在造谣,在开发者和媒体处混淆视听。如今,这份迟来了4年的道歉函,终于让真相大白!

但令人遗憾的是,与海底捞等诚信企业的真挚道歉相比,APICloud的道歉行为缺少诚意。

首先从表达形式上,这篇道歉函堪称反SEO经典教材。

  1. 网页以图片方式显示标题,防止爬虫抓取
  2. 链接:https://www.apicloud.com/static_pages/qz.html,不能直接访问,必须从APICloud官网首页点那个图片来跳转
  3. 禁止搜索引擎爬虫访问(robots:noindex,nofollow)
  4. 道歉函页面没有title
  5. 道歉函源码没有一句正文,所有内容使用js编写

网友纷纷表示学习了,这反SEO做的太用心了。
有这份心思,花在正道上,岂不是更好?

道歉形式权当调侃玩笑了。但DCloud不能接收的,是道歉内容本身,完全看不出APICloud真正反思了自己的错误,看不出它的道歉诚意。
下为APICloud道歉函正文:

注:apicloud官网的致歉函已经下线,可在微信公众号搜索“apicloud”,查询2019年12月3日的历史文章,了解致歉函内容。

DCloud已通过法律途径和微信公众号明确提出了我们对致歉内容的意见,但APICloud仍拒不更改致歉函。

在此,DCloud再次公开提出我们的3点道歉要求:

  • 要求1

APICloud持续多个版本破解DCloud产品、抄袭源码、盗用DLL文件和图片。在APICloud的致歉函中,不能以“开发人员的不慎”,轻描淡写的敷衍过去了。
破解反编译产品是费时费力的事情,我们很难相信普通开发人员,在没有领导的安排下,会长时间去做这么冒险的事情。管理层应该反思自己,而不是甩锅给不知名的开发人员。

  • 要求2

在DCloud起诉APICloud后,APICloud反而污蔑DCloud在造谣,数次中伤DCloud声誉、讽刺DCloud管理层。APICloud需收回自己的错误言论,并为此向DCloud道歉。

  • 要求3

APICloud误导、欺瞒开发者,使开发者做出错误判断,APICloud不仅应该向DCloud道歉,也应该向被误导和欺瞒的开发者道歉。

如果APICloud真正认识到自己的错误,请认真反思,回应DCloud的3点要求,重新给出一份有诚意的致歉函。

后记

在DCloud发出本文的公众号后,不少开发者在后台留言:

  • 有人依然为侵权行为做掩护,并嘲笑使用正版软件的人虚伪。
  • 有不太了解DCloud产品,希望详细了解产品区别。

前者我们懒得理他,并且不欢迎他进入DCloud社区。
后者的问题,我们在此整理一下最新版比较

  1. 产品覆盖
    DCloud有HBuilder和uni-app两个产品,HBuilder是前端开发工具,有400多万开发者,月活百万;uni-app是多端应用开发框架,有手机端月活高达8.4亿。
    可以详细了解DCloud的案例,很多知名开发商:https://uniapp.dcloud.io/case
    使用uni-app,可以同时开发跨平台App、H5网页,以及微信/阿里/百度/头条/QQ等小程序。

    • 技术人员掌握uni-app,可以完成老板交代的任何项目
    • 开发商使用uni-app,可以大幅提升开发效率,降低开发成本,获得更多用户
      而apicloud只能开发App,无法覆盖其他平台。
  2. 发展趋势
    从百度指数可以看出,apicloud这些年基本在原地踏步

    iOS13发布,UIWebview被列为私有API,苹果强推Apple登录。DCloud第一时间提供了wkwebview白屏恢复等各种方案,以及Apple登录。
    Android10发布,无法获取IMEI等设备信息。DCloud及时提供了OAID等业内新标准。
    而APICloud落后几个月才参考DCloud方案更新版本,维护力度很弱。

  3. 收费
    APICloud有大量收费点,详见:https://www.apicloud.com/vipservice/price
    一个应用装机量超5万收费、打渠道包收费、打包总次数收费、创建应用多了收费、一个应用多人协作收费...到处是收费点。基本策略就是初期免费引用户进来,应用做大就会开始收费。
    而DCloud全免费,不管你的应用做到多大,哪怕用户过亿,DCloud的产品也仍然是免费的。并且以后也永远不会做这种养韭菜、割韭菜的事情,永不变更目前的免费策略。
    有人说DCloud云打包也收费,并非如此。只是经受过恶意流量攻击,DCloud的云打包会控制每日的不合理打包次数,正常使用肯定不会收费。而且DCloud支持完全免费的离线打包,更自由。
    除了上述收费点,APICloud把大量重要功能从基础产品中剥离,放到插件市场销售。
    比如原生视频、直播,比如跨多家手机厂商的统一推送,DCloud均包含在基础产品中免费提供,而APICloud要单独购买插件。
    开发一个应用,使用APICloud要比DCloud付出更多成本。

  4. App端的性能体验
    uni-app支持nvue,使用纯原生渲染,性能体验、内存占用优于竞品更多。
    即便是使用uni-app的webview渲染,由于它是一个小程序引擎,体验更加优秀,不管是新页面加载速度,还是Android手机输入框弹出的感受,都和原生一样顺滑。
    uni-app自带一个独立的js引擎,运行更快,可以直接运行所有es6语法,而不需要转换适配低版本WebView。
    包括软键盘弹出等很多细节体验,apicloud都做不细做不好。

  5. 生态
    DCloud的周边插件和培训生态丰富。
    DCloud的插件市场虽然推出较晚,但目前已有一千多款插件。详见:https://ext.dcloud.net.cn/
    DCloud基础产品的功能较多,支持Native.js调用各种手机端API,支持丰富的小程序SDK直接在App端使用,又有各种丰富的原生插件,形成了非常完善的插件生态。
    uni-app还支持小程序生态的组件、sdk在app里直接使用,通过renderjs支持web组件生态库在app端的使用,可以说汇聚了最多的生态力度,可以找到各种优秀轮子。
    uni-app插件市场优秀的原生插件作者,月收入可过万。
    插件市场还有大量行业应用模板,如电商、电子书、漫画、音乐等整套免费模板,可拿来就用。
    开发者在DCloud的生态中寻找轮子,不说应有尽有,但也更加丰富,而且更加便宜、ui定制更灵活。
    vue的一个重要创新,就是组件化。uni-app将其发扬光大,还新增了easycom技术,市场中各种组件可以极快的完成应用的搭建。
    而uni-app的培训生态也非常完善,腾讯课堂官方为uni-app录制视频教程,腾讯课堂、网易云课堂、哔哩哔哩中有各种收费或免费的视频教程。

  6. 社区
    DCloud的问答社区,月活百万,每日几百篇帖子。QQ/微信群,累计70多个,其中uni-app有几十个2千人大群,每日几十万聊天记录。
    在csdn、掘金等社区也有大量开发者的经验分享。
    在更活跃、更开放的社区里,开发者可以快速进步。
    DCloud官方也投入大量精力,听取社区反馈意见,每月积极更新产品,持续帮助开发者更加成功。

uni-app当然也还不完美,官方也在持续升级。但整体而言,比某些竞品要更优秀、更良心。
DCloud无意垄断市场,欢迎竞争。开发者可以在DCloud、react native、flutter等各种优秀的跨平台开发工具中选择,但不要被某些并无诚意的产品蒙蔽。

继续阅读 »

​在北京市高级人民法院的法令要求下,APICloud终于承认了侵犯DCloud著作权,并在官网和公众号向DCloud道歉。

2015年,DCloud起诉APICloud侵权后,APICloud不但否认侵权,还污蔑DCloud在造谣,在开发者和媒体处混淆视听。如今,这份迟来了4年的道歉函,终于让真相大白!

但令人遗憾的是,与海底捞等诚信企业的真挚道歉相比,APICloud的道歉行为缺少诚意。

首先从表达形式上,这篇道歉函堪称反SEO经典教材。

  1. 网页以图片方式显示标题,防止爬虫抓取
  2. 链接:https://www.apicloud.com/static_pages/qz.html,不能直接访问,必须从APICloud官网首页点那个图片来跳转
  3. 禁止搜索引擎爬虫访问(robots:noindex,nofollow)
  4. 道歉函页面没有title
  5. 道歉函源码没有一句正文,所有内容使用js编写

网友纷纷表示学习了,这反SEO做的太用心了。
有这份心思,花在正道上,岂不是更好?

道歉形式权当调侃玩笑了。但DCloud不能接收的,是道歉内容本身,完全看不出APICloud真正反思了自己的错误,看不出它的道歉诚意。
下为APICloud道歉函正文:

注:apicloud官网的致歉函已经下线,可在微信公众号搜索“apicloud”,查询2019年12月3日的历史文章,了解致歉函内容。

DCloud已通过法律途径和微信公众号明确提出了我们对致歉内容的意见,但APICloud仍拒不更改致歉函。

在此,DCloud再次公开提出我们的3点道歉要求:

  • 要求1

APICloud持续多个版本破解DCloud产品、抄袭源码、盗用DLL文件和图片。在APICloud的致歉函中,不能以“开发人员的不慎”,轻描淡写的敷衍过去了。
破解反编译产品是费时费力的事情,我们很难相信普通开发人员,在没有领导的安排下,会长时间去做这么冒险的事情。管理层应该反思自己,而不是甩锅给不知名的开发人员。

  • 要求2

在DCloud起诉APICloud后,APICloud反而污蔑DCloud在造谣,数次中伤DCloud声誉、讽刺DCloud管理层。APICloud需收回自己的错误言论,并为此向DCloud道歉。

  • 要求3

APICloud误导、欺瞒开发者,使开发者做出错误判断,APICloud不仅应该向DCloud道歉,也应该向被误导和欺瞒的开发者道歉。

如果APICloud真正认识到自己的错误,请认真反思,回应DCloud的3点要求,重新给出一份有诚意的致歉函。

后记

在DCloud发出本文的公众号后,不少开发者在后台留言:

  • 有人依然为侵权行为做掩护,并嘲笑使用正版软件的人虚伪。
  • 有不太了解DCloud产品,希望详细了解产品区别。

前者我们懒得理他,并且不欢迎他进入DCloud社区。
后者的问题,我们在此整理一下最新版比较

  1. 产品覆盖
    DCloud有HBuilder和uni-app两个产品,HBuilder是前端开发工具,有400多万开发者,月活百万;uni-app是多端应用开发框架,有手机端月活高达8.4亿。
    可以详细了解DCloud的案例,很多知名开发商:https://uniapp.dcloud.io/case
    使用uni-app,可以同时开发跨平台App、H5网页,以及微信/阿里/百度/头条/QQ等小程序。

    • 技术人员掌握uni-app,可以完成老板交代的任何项目
    • 开发商使用uni-app,可以大幅提升开发效率,降低开发成本,获得更多用户
      而apicloud只能开发App,无法覆盖其他平台。
  2. 发展趋势
    从百度指数可以看出,apicloud这些年基本在原地踏步

    iOS13发布,UIWebview被列为私有API,苹果强推Apple登录。DCloud第一时间提供了wkwebview白屏恢复等各种方案,以及Apple登录。
    Android10发布,无法获取IMEI等设备信息。DCloud及时提供了OAID等业内新标准。
    而APICloud落后几个月才参考DCloud方案更新版本,维护力度很弱。

  3. 收费
    APICloud有大量收费点,详见:https://www.apicloud.com/vipservice/price
    一个应用装机量超5万收费、打渠道包收费、打包总次数收费、创建应用多了收费、一个应用多人协作收费...到处是收费点。基本策略就是初期免费引用户进来,应用做大就会开始收费。
    而DCloud全免费,不管你的应用做到多大,哪怕用户过亿,DCloud的产品也仍然是免费的。并且以后也永远不会做这种养韭菜、割韭菜的事情,永不变更目前的免费策略。
    有人说DCloud云打包也收费,并非如此。只是经受过恶意流量攻击,DCloud的云打包会控制每日的不合理打包次数,正常使用肯定不会收费。而且DCloud支持完全免费的离线打包,更自由。
    除了上述收费点,APICloud把大量重要功能从基础产品中剥离,放到插件市场销售。
    比如原生视频、直播,比如跨多家手机厂商的统一推送,DCloud均包含在基础产品中免费提供,而APICloud要单独购买插件。
    开发一个应用,使用APICloud要比DCloud付出更多成本。

  4. App端的性能体验
    uni-app支持nvue,使用纯原生渲染,性能体验、内存占用优于竞品更多。
    即便是使用uni-app的webview渲染,由于它是一个小程序引擎,体验更加优秀,不管是新页面加载速度,还是Android手机输入框弹出的感受,都和原生一样顺滑。
    uni-app自带一个独立的js引擎,运行更快,可以直接运行所有es6语法,而不需要转换适配低版本WebView。
    包括软键盘弹出等很多细节体验,apicloud都做不细做不好。

  5. 生态
    DCloud的周边插件和培训生态丰富。
    DCloud的插件市场虽然推出较晚,但目前已有一千多款插件。详见:https://ext.dcloud.net.cn/
    DCloud基础产品的功能较多,支持Native.js调用各种手机端API,支持丰富的小程序SDK直接在App端使用,又有各种丰富的原生插件,形成了非常完善的插件生态。
    uni-app还支持小程序生态的组件、sdk在app里直接使用,通过renderjs支持web组件生态库在app端的使用,可以说汇聚了最多的生态力度,可以找到各种优秀轮子。
    uni-app插件市场优秀的原生插件作者,月收入可过万。
    插件市场还有大量行业应用模板,如电商、电子书、漫画、音乐等整套免费模板,可拿来就用。
    开发者在DCloud的生态中寻找轮子,不说应有尽有,但也更加丰富,而且更加便宜、ui定制更灵活。
    vue的一个重要创新,就是组件化。uni-app将其发扬光大,还新增了easycom技术,市场中各种组件可以极快的完成应用的搭建。
    而uni-app的培训生态也非常完善,腾讯课堂官方为uni-app录制视频教程,腾讯课堂、网易云课堂、哔哩哔哩中有各种收费或免费的视频教程。

  6. 社区
    DCloud的问答社区,月活百万,每日几百篇帖子。QQ/微信群,累计70多个,其中uni-app有几十个2千人大群,每日几十万聊天记录。
    在csdn、掘金等社区也有大量开发者的经验分享。
    在更活跃、更开放的社区里,开发者可以快速进步。
    DCloud官方也投入大量精力,听取社区反馈意见,每月积极更新产品,持续帮助开发者更加成功。

uni-app当然也还不完美,官方也在持续升级。但整体而言,比某些竞品要更优秀、更良心。
DCloud无意垄断市场,欢迎竞争。开发者可以在DCloud、react native、flutter等各种优秀的跨平台开发工具中选择,但不要被某些并无诚意的产品蒙蔽。

收起阅读 »

uniapp做一个前端,大概30个页面,最好是个人开发者。


可以加微信详谈,样式只是大概,样式全部使用colorui拼接,不求优化适配全部设备。只求速度上线开发,能做的我们细谈。


可以加微信详谈,样式只是大概,样式全部使用colorui拼接,不求优化适配全部设备。只求速度上线开发,能做的我们细谈。

个推 实现个推绑定、解绑别名。。。

uni-app极光推送:https://blog.csdn.net/dashenid/article/details/103437459

由于html5+没有提供绑定、解绑别名的方法,

用native.js去实现,点用原生的个推方法

igexinTool.js

function igexinTool() {  
    var isAndorid, PushManager, context, Instance, GeTuiSdk;  

    if(plus.os.name == 'Android') {  
        isAndorid = true;  
    } else {  
        isAndorid = false;  
    }  

    if(isAndorid) {  
        PushManager = plus.android.importClass("com.igexin.sdk.PushManager");  
        context = plus.android.runtimeMainActivity().getContext();  
        Instance = PushManager.getInstance();  
    } else {  
        GeTuiSdk = plus.ios.importClass("GeTuiSdk");  
    }  

    this.bindAlias = function(alias) {  
        if(isAndorid) {  
            Instance.bindAlias(context, alias);  
        } else {  
            GeTuiSdk.bindAliasandSequenceNum(alias, alias);  
        }  
    }  

    this.unbindAlias = function(alias) {  
        if(isAndorid) {  
            Instance.unBindAlias(context, alias, true);  
        } else {  
            GeTuiSdk.unbindAliasandSequenceNumandIsSelf(alias, alias, true);  
        }  
    }  

    this.getVersion = function() {  
        if(isAndorid) {  
            return Instance.getVersion(context);  
        } else {  
            return GeTuiSdk.version;  
        }  
    }  

    //开启推送  
    this.turnOnPush = function() {  
        if(isAndorid) {  
            Instance.turnOnPush(context);  
        } else {  
            GeTuiSdk.setPushModeForOff(false);  
        }  
    }  

    //关闭推送  
    this.turnOffPush = function() {  
        if(isAndorid) {  
            Instance.turnOffPush(context);  
        } else {  
            GeTuiSdk.setPushModeForOff(true);  
        }  
    }  

}

使用:
var tool = new igexinTool();
tool.bindAlias("ykj");

继续阅读 »

uni-app极光推送:https://blog.csdn.net/dashenid/article/details/103437459

由于html5+没有提供绑定、解绑别名的方法,

用native.js去实现,点用原生的个推方法

igexinTool.js

function igexinTool() {  
    var isAndorid, PushManager, context, Instance, GeTuiSdk;  

    if(plus.os.name == 'Android') {  
        isAndorid = true;  
    } else {  
        isAndorid = false;  
    }  

    if(isAndorid) {  
        PushManager = plus.android.importClass("com.igexin.sdk.PushManager");  
        context = plus.android.runtimeMainActivity().getContext();  
        Instance = PushManager.getInstance();  
    } else {  
        GeTuiSdk = plus.ios.importClass("GeTuiSdk");  
    }  

    this.bindAlias = function(alias) {  
        if(isAndorid) {  
            Instance.bindAlias(context, alias);  
        } else {  
            GeTuiSdk.bindAliasandSequenceNum(alias, alias);  
        }  
    }  

    this.unbindAlias = function(alias) {  
        if(isAndorid) {  
            Instance.unBindAlias(context, alias, true);  
        } else {  
            GeTuiSdk.unbindAliasandSequenceNumandIsSelf(alias, alias, true);  
        }  
    }  

    this.getVersion = function() {  
        if(isAndorid) {  
            return Instance.getVersion(context);  
        } else {  
            return GeTuiSdk.version;  
        }  
    }  

    //开启推送  
    this.turnOnPush = function() {  
        if(isAndorid) {  
            Instance.turnOnPush(context);  
        } else {  
            GeTuiSdk.setPushModeForOff(false);  
        }  
    }  

    //关闭推送  
    this.turnOffPush = function() {  
        if(isAndorid) {  
            Instance.turnOffPush(context);  
        } else {  
            GeTuiSdk.setPushModeForOff(true);  
        }  
    }  

}

使用:
var tool = new igexinTool();
tool.bindAlias("ykj");

收起阅读 »

uni-app 使用axios 分享 ,解决: adapter is not a function

uni-app 使用axios 真机会提示: adapter is not a function 。我也不懂,我也不会,就菜鸟一个。

然后跟了下问题所在,好像调用不到xhr.js ,
翻到这里 var adapter = config.adapter || defaults.adapter; 报错。

参考:
https://juejin.im/post/5dbd8c64e51d4529fc3facd4
https://blog.csdn.net/u013704958/article/details/90713386 我借鉴5楼提供的方法,用uni.request 来调用
https://ext.dcloud.net.cn/plugin?id=930

添加了一段代码,重新引用
//真机获取
axios.defaults.adapter = function (config) {....}

下面axios封装来自大佬的 HzyAdmin 项目
https://gitee.com/hzy6/HzyAdminSpa

import axios from 'axios';  
import qs from 'qs';  
import tools from './tools';  

let loading;  
let isloading = true;  
//http request 拦截器  
axios.interceptors.request.use(config => {  
        if (isloading) {  
            uni.showLoading();  
            loading=true;  
        }  
        var cookie = uni.getStorageSync('Authorization');  
        config.headers['x-requested-width'] = 'XMLHttpRequest';  
        config.headers['Authorization'] = cookie;  
        config.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';  

        if (!config.data) return config;  

        if (config.data.isUpload)  
            config.headers['Content-Type'] = 'multipart/form-data';  
        else  
            config.data = qs.stringify(config.data); //如果是非上传类型 则 将数据重新组装  

        return config;  
    },  
    error => {  
        console.log(error);  
        return Promise.reject(error);  
    });  

//http response 拦截器  
axios.interceptors.response.use(response => {  
        if (loading) {  
            uni.hideLoading();  
            loading=false;  
        }  
        var data = response.data;  

        if (data.hasOwnProperty('status')) {  
            // public enum EMessageBoxStatus  
            // {  
            //     接口授权码无效 = -3,  
            //     服务端异常 = -2,  
            //     自定义 = -1,  
            //     失败 = 0,  
            //     成功 = 1,  
            // }  

            if (data.status == -3) { //接口授权码无效global.$router.push("/Login")  
                tools.alert(data.msg + '请重新登录!', () => uni.navigateTo({url:'../pages/Login'}));  
                return;  
            }  
            if (data.status == -2) { //服务端异常  
                tools.alert(data.msg);  
                return;  
            }  
            if (data.status == 0) { //失败  
                tools.msg(data.msg, '错误');  
                return;  
            }  
        }  

        return response;  
    },  
    error => {  
        console.log(error);  
        uni.hideLoading();  
        if (error.response.status === 401) {  
            if (loading) {  
                loading=false;  
            }  
            global.tools.notice("无权访问!", "错误");  
            return uni.navigateTo({url:'../pages/Login'}) //global.$router.push('/Login');  
        } else {  
            return Promise.reject(error)  
        }  
    });  

//真机获取  
axios.defaults.adapter = function (config) {  
    return new Promise((resolve, reject) => {  
        console.log(config)  
        var settle = require('axios/lib/core/settle');  
        var buildURL = require('axios/lib/helpers/buildURL');  
        uni.request({  
            method: config.method.toUpperCase(),  
            url: buildURL(config.url, config.params, config.paramsSerializer),  
            header: config.headers,  
            data: config.data,  
            dataType: config.dataType,  
            responseType: config.responseType,  
            sslVerify: config.sslVerify,  
            complete:function complete(response){  
                response = {  
                  data: response.data,  
                  status: response.statusCode,  
                  errMsg: response.errMsg,  
                  header: response.header,  
                  config: config  
                };  

            settle(resolve, reject, response);  
            }  
        })  
    })  
}  

/**  
 * 封装get方法  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param headers 头部信息  
 * @returns {Promise}  
 */  
export function get(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    url =`${url}?${qs.stringify(data)}`;  
    return new Promise((resolve, reject) => {  
        axios.get(url, config)  
            .then(response => {  
                resolve(response);  
            })  
            .catch(err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装post请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function post(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.post(url, data, config)  
            .then(response => {  
                if (response != undefined) {  
                    resolve(response);  
                }  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装 post 请求 用于上传文件   
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function upload(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    if (!data) data = {};  
    data.isUpload = true;  
    return new Promise((resolve, reject) => {  
        axios.post(url, data, config)  
            .then(response => {  
                if (response != undefined) {  
                    resolve(response);  
                }  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装 get请求 用于下载文件  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @returns {Promise}  
 */  
export function download(url, data = {}, loading = true) {  
    this.get(url, data, loading, {  
        // responseType: 'stream',  
        responseType: 'blob',  
        // responseType: 'arraybuffer',  
    }).then(res => {  
        var data = res.data;  
        var headers = res.headers;  
        //"attachment; filename=6a9c13bc-e214-44e4-8456-dbca9fcd2367.xls;filename*=UTF-8''6a9c13bc-e214-44e4-8456-dbca9fcd2367.xls"  
        var contentDisposition = headers['content-disposition'];  
        var contentType = headers['content-type'];  
        var attachmentInfoArrary = contentDisposition.split(';');  
        var fileName = '';  
        if (attachmentInfoArrary.length > 1) {  
            fileName = attachmentInfoArrary[1].split('=')[1];  
        }  
        var blob = new Blob([data], { type: contentType });  

        if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE  
            window.navigator.msSaveOrOpenBlob(blob, fileName);  
        } else {  
            let url = (window.URL || window.webkitURL).createObjectURL(blob);  
            // window.open(url, "_blank"); //下载  
            // window.URL.revokeObjectURL(url) // 只要映射存在,Blob就不能进行垃圾回收,因此一旦不再需要引用,就必须小心撤销URL,释放掉blob对象。  

            let a = document.createElement('a');  
            a.style.display = 'none';  
            a.href = url;  
            a.setAttribute('download', fileName);  
            document.body.appendChild(a);  
            a.click()  
            document.body.removeChild(a); // 下载完成移除元素  
            // window.location.href = url  
            window.URL.revokeObjectURL(url); // 只要映射存在,Blob就不能进行垃圾回收,因此一旦不再需要引用,就必须小心撤销URL,释放掉blob对象。  

        }  
    });  
}  

/**  
 * 封装patch请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function patch(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.patch(url, data, config)  
            .then(response => {  
                resolve(response);  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装put请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function put(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.put(url, data, config)  
            .then(response => {  
                resolve(response);  
            }, err => {  
                reject(err)  
            })  
    })  
}
继续阅读 »

uni-app 使用axios 真机会提示: adapter is not a function 。我也不懂,我也不会,就菜鸟一个。

然后跟了下问题所在,好像调用不到xhr.js ,
翻到这里 var adapter = config.adapter || defaults.adapter; 报错。

参考:
https://juejin.im/post/5dbd8c64e51d4529fc3facd4
https://blog.csdn.net/u013704958/article/details/90713386 我借鉴5楼提供的方法,用uni.request 来调用
https://ext.dcloud.net.cn/plugin?id=930

添加了一段代码,重新引用
//真机获取
axios.defaults.adapter = function (config) {....}

下面axios封装来自大佬的 HzyAdmin 项目
https://gitee.com/hzy6/HzyAdminSpa

import axios from 'axios';  
import qs from 'qs';  
import tools from './tools';  

let loading;  
let isloading = true;  
//http request 拦截器  
axios.interceptors.request.use(config => {  
        if (isloading) {  
            uni.showLoading();  
            loading=true;  
        }  
        var cookie = uni.getStorageSync('Authorization');  
        config.headers['x-requested-width'] = 'XMLHttpRequest';  
        config.headers['Authorization'] = cookie;  
        config.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';  

        if (!config.data) return config;  

        if (config.data.isUpload)  
            config.headers['Content-Type'] = 'multipart/form-data';  
        else  
            config.data = qs.stringify(config.data); //如果是非上传类型 则 将数据重新组装  

        return config;  
    },  
    error => {  
        console.log(error);  
        return Promise.reject(error);  
    });  

//http response 拦截器  
axios.interceptors.response.use(response => {  
        if (loading) {  
            uni.hideLoading();  
            loading=false;  
        }  
        var data = response.data;  

        if (data.hasOwnProperty('status')) {  
            // public enum EMessageBoxStatus  
            // {  
            //     接口授权码无效 = -3,  
            //     服务端异常 = -2,  
            //     自定义 = -1,  
            //     失败 = 0,  
            //     成功 = 1,  
            // }  

            if (data.status == -3) { //接口授权码无效global.$router.push("/Login")  
                tools.alert(data.msg + '请重新登录!', () => uni.navigateTo({url:'../pages/Login'}));  
                return;  
            }  
            if (data.status == -2) { //服务端异常  
                tools.alert(data.msg);  
                return;  
            }  
            if (data.status == 0) { //失败  
                tools.msg(data.msg, '错误');  
                return;  
            }  
        }  

        return response;  
    },  
    error => {  
        console.log(error);  
        uni.hideLoading();  
        if (error.response.status === 401) {  
            if (loading) {  
                loading=false;  
            }  
            global.tools.notice("无权访问!", "错误");  
            return uni.navigateTo({url:'../pages/Login'}) //global.$router.push('/Login');  
        } else {  
            return Promise.reject(error)  
        }  
    });  

//真机获取  
axios.defaults.adapter = function (config) {  
    return new Promise((resolve, reject) => {  
        console.log(config)  
        var settle = require('axios/lib/core/settle');  
        var buildURL = require('axios/lib/helpers/buildURL');  
        uni.request({  
            method: config.method.toUpperCase(),  
            url: buildURL(config.url, config.params, config.paramsSerializer),  
            header: config.headers,  
            data: config.data,  
            dataType: config.dataType,  
            responseType: config.responseType,  
            sslVerify: config.sslVerify,  
            complete:function complete(response){  
                response = {  
                  data: response.data,  
                  status: response.statusCode,  
                  errMsg: response.errMsg,  
                  header: response.header,  
                  config: config  
                };  

            settle(resolve, reject, response);  
            }  
        })  
    })  
}  

/**  
 * 封装get方法  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param headers 头部信息  
 * @returns {Promise}  
 */  
export function get(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    url =`${url}?${qs.stringify(data)}`;  
    return new Promise((resolve, reject) => {  
        axios.get(url, config)  
            .then(response => {  
                resolve(response);  
            })  
            .catch(err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装post请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function post(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.post(url, data, config)  
            .then(response => {  
                if (response != undefined) {  
                    resolve(response);  
                }  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装 post 请求 用于上传文件   
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function upload(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    if (!data) data = {};  
    data.isUpload = true;  
    return new Promise((resolve, reject) => {  
        axios.post(url, data, config)  
            .then(response => {  
                if (response != undefined) {  
                    resolve(response);  
                }  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装 get请求 用于下载文件  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @returns {Promise}  
 */  
export function download(url, data = {}, loading = true) {  
    this.get(url, data, loading, {  
        // responseType: 'stream',  
        responseType: 'blob',  
        // responseType: 'arraybuffer',  
    }).then(res => {  
        var data = res.data;  
        var headers = res.headers;  
        //"attachment; filename=6a9c13bc-e214-44e4-8456-dbca9fcd2367.xls;filename*=UTF-8''6a9c13bc-e214-44e4-8456-dbca9fcd2367.xls"  
        var contentDisposition = headers['content-disposition'];  
        var contentType = headers['content-type'];  
        var attachmentInfoArrary = contentDisposition.split(';');  
        var fileName = '';  
        if (attachmentInfoArrary.length > 1) {  
            fileName = attachmentInfoArrary[1].split('=')[1];  
        }  
        var blob = new Blob([data], { type: contentType });  

        if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE  
            window.navigator.msSaveOrOpenBlob(blob, fileName);  
        } else {  
            let url = (window.URL || window.webkitURL).createObjectURL(blob);  
            // window.open(url, "_blank"); //下载  
            // window.URL.revokeObjectURL(url) // 只要映射存在,Blob就不能进行垃圾回收,因此一旦不再需要引用,就必须小心撤销URL,释放掉blob对象。  

            let a = document.createElement('a');  
            a.style.display = 'none';  
            a.href = url;  
            a.setAttribute('download', fileName);  
            document.body.appendChild(a);  
            a.click()  
            document.body.removeChild(a); // 下载完成移除元素  
            // window.location.href = url  
            window.URL.revokeObjectURL(url); // 只要映射存在,Blob就不能进行垃圾回收,因此一旦不再需要引用,就必须小心撤销URL,释放掉blob对象。  

        }  
    });  
}  

/**  
 * 封装patch请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function patch(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.patch(url, data, config)  
            .then(response => {  
                resolve(response);  
            }, err => {  
                reject(err)  
            })  
    })  
}  

/**  
 * 封装put请求  
 * @param url  
 * @param data  
 * @param loading 是否有加载效果  
 * @param config config信息  
 * @returns {Promise}  
 */  
export function put(url, data = {}, loading = true, config = {}) {  
    isloading = loading;  
    return new Promise((resolve, reject) => {  
        axios.put(url, data, config)  
            .then(response => {  
                resolve(response);  
            }, err => {  
                reject(err)  
            })  
    })  
}
收起阅读 »

打包时未添加OAuth模块

Appstore

真是服了uniapp了,我的应用里不需要第三方登录,所有打包时没有勾选OAuth模块,结果打包时总是提示“未添加OAuth模块”,最坑爹的是打包成IOS的appstore版本也会出现这个提示,导致我的app被拒,真坑爹。

真是服了uniapp了,我的应用里不需要第三方登录,所有打包时没有勾选OAuth模块,结果打包时总是提示“未添加OAuth模块”,最坑爹的是打包成IOS的appstore版本也会出现这个提示,导致我的app被拒,真坑爹。

APP可视化埋点大揭秘,附源码解析

一、背景

运营者能够对用户行为进行分析的前提,是对大量数据的掌握。在以往,这个数据通常是由开发者在控件点击、页面等事件中,一行行地编写埋点代码来完成数据收集的。然而传统的操作模式每当升级改版时,开发和测试人员就需要重复不断对代码进行更新,整个流程耗时长,无法满足业务的需求。

为帮助开发者解决这一痛点,个推应用统计“个数”推出“可视化埋点”这一技术来更高效地实现这个这一过程。“个数”的可视化埋点灵活、方便,开发者不需对数据追踪点添加任何代码,只需要连接管理台并圈选页面中需要埋点的元素,即可添加随时生效的界面追踪点。

本文将结合个数实践经验,对可视化埋点中的两大关键技术点即控件唯一标识和事件采集进行分析并提供解决方案。

二、可视化埋点关键技术点

可视化埋点的难点,或者说核心就是如何在开发者不编写任何代码的情况下,SDK 如何确定任意一个控件在该应用内的唯一性,以及如何监听控件的点击和页面的切换。

标识

为了防止不同页面中的控件标识重复出现,控件的唯一标识一般由页面标识加上控件标识生成。

页面标识生成

页面标识可以直接使用页面的名称,即 Activity name。其获取方式比较多,这里介绍一种比较通用的方法,即通过注册 Application.ActivityLifecycleCallbacks ,开发者可以在以下生命周期的回调中,轻松地拿到当前的 Activity 对象。此方法适用于一个 Activity 并无 Fragment 存在的情形。

代码详见下图:

获取方式也是比较多,不过较于 Activity 的获取会相对麻烦一些,因为系统没有直接提供 API ,因而需要稍微转个弯:通过 Gradle 插桩的方式,获取 Fragment 的生命周期,以及 Fragment 实例对象:

如果该应用的页面存在一个 Activity 中嵌套多个 Fragment 的情况,单单一个 Activity name则可能无法精准地定位到某个页面,因而还需要加上 Fragment 的名称。Fragment的获取可以通过 Gradle 插桩法来实现,即根据 Fragment 的生命周期来获取Fragment 实例对象。

1.2控件标识生成

理想的情况下页面中的每个控件都有属于自己的唯一 id,SDK 直接获取控件的 id 当做控件标识即可。但现实情况却是,一个页面中往往存在多个相同 id 的控件,或者是没有 id 的控件,比如 Listview 的 item ,开发者不可能给listview的每个item 设置不同的 id。

因此需要转变一下思路。我们可以从控件路径这个除id 外比较独特的性质着手来生成控件标识。开发者可以通过给控件的路径加上控件角标的结构方式,生成控件的唯一标识。下图是Github 上一个仿 B 站的应用。我们对这个应用进行一下控件树分析。首先我们使用 Android Studio 自带的 UI Automator Viewer 工具查看该页面的布局结构:

接下来,我们可以从Application.ActivityLifecycleCallbacks 的回调中拿到 Activity 实例,再使用 activity.getWindow().getDecorView().getRootView() 方法来获取当前页面的控件树。

例如图中的文字控件是 TextView,且无兄弟布局,则可以标记为 TextView[0] 。它的父布局是 LinearLayout 且排在兄弟布局中的第二位,那么就可以写成是 LinearLayout[1],然后使用自己定义的符号拼接,像是 LinearLayout[1]/TextView[0] 。之后以此类推、循环遍历、层层递进,将所有经过的控件以及它们的下标都拼接起来,组成控件在该页面中的唯一标识。

对于一些可复用的 View ,我们则需要采取一些特殊处理。例如对于 RecyclerView、ListView、 ViewPager 等复用控件,我们都需要采取不同的处理方式,去获取当前 View 在该控件中的具体下标。如果没有进行特殊处理,则会导致子控件错位,数据统计不准确。

采集

在以往的处理中,如果需要知道一个按钮的点击次数,开发者就要在该控件的click事件中加入对应的打点代码。这种重复劳作,无疑增加了开发者的开发负担。对此,我们可以采用动态代理方式或Gradle 插桩方式来改善这个问题。

动态代理方式

使用安卓自带的辅助功能 View.AccessibilityDelegate 。前文提到当页面变化时,我们可以通过 Application.ActivityLifecycleCallbacks 获取到 Activity 的实例对象,接着根据activity.getWindow().getDecorView().getRootView() 来获取到控件树。由于控件树可能会实时发生变化,我们则需要通过 ViewTreeObserver.OnGlobalLayoutListener 的方法监听视图变化,从而在该回调中拿到变化的控件。接着我们 要根据递归判断该控件是否为 ViewGroup、是否可以点击、是否能够显示等,继而给符合条件的 View 设置 sendAccessibilityEvent();此外,我们还要在继承了 View.AccessibilityDelegate 的定义类中,对以下这些方法添加 SDK 的代理:

当对应的控件被点击时,系统就会自动调用设置过代理的方法,存储或者上报对应数据。

Gradle 插桩的方式

Android Gradle 工具在1.5.0 版本后提供了 Transfrom API , 该API 允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件。在编译期,开发者可以通过onClick、onItemClick等方法(详见下图)进行监听,这相当于是正则匹配。

当上述监听的方法被编译的时候,就可以将埋点的代理操作插入这些方法中,实现自动化埋点的流程。网上相关流程也是非常详细,有兴趣的可以自行搜索学习。

三、结语

以上就是APP端可视化埋点实现过程中的关键点,特别需要注意的是控件唯一标识那一块,由于布局千变万化,开发者针对很多特定的布局都需要采取对应的处理方式。

目前个推应用统计——个数这个产品只需要一行初始化代码就可以自动帮助开发者采集包括页面统计、事件埋点、新增活跃等多维度信息。

借着万圣节,悄悄跟你说个恐怖的故事:除个推应用统计服务之外,VIP消息推送、用户画像,现在申请均可以免费用一年!一键认证服务还可以享受充10万条送5万条的优惠哦 !点击https://www.getui.com/2019devfest,“码“上申请吧!

行业前沿、面试宝典,更多技术干货,尽在个推技术学院。

继续阅读 »

一、背景

运营者能够对用户行为进行分析的前提,是对大量数据的掌握。在以往,这个数据通常是由开发者在控件点击、页面等事件中,一行行地编写埋点代码来完成数据收集的。然而传统的操作模式每当升级改版时,开发和测试人员就需要重复不断对代码进行更新,整个流程耗时长,无法满足业务的需求。

为帮助开发者解决这一痛点,个推应用统计“个数”推出“可视化埋点”这一技术来更高效地实现这个这一过程。“个数”的可视化埋点灵活、方便,开发者不需对数据追踪点添加任何代码,只需要连接管理台并圈选页面中需要埋点的元素,即可添加随时生效的界面追踪点。

本文将结合个数实践经验,对可视化埋点中的两大关键技术点即控件唯一标识和事件采集进行分析并提供解决方案。

二、可视化埋点关键技术点

可视化埋点的难点,或者说核心就是如何在开发者不编写任何代码的情况下,SDK 如何确定任意一个控件在该应用内的唯一性,以及如何监听控件的点击和页面的切换。

标识

为了防止不同页面中的控件标识重复出现,控件的唯一标识一般由页面标识加上控件标识生成。

页面标识生成

页面标识可以直接使用页面的名称,即 Activity name。其获取方式比较多,这里介绍一种比较通用的方法,即通过注册 Application.ActivityLifecycleCallbacks ,开发者可以在以下生命周期的回调中,轻松地拿到当前的 Activity 对象。此方法适用于一个 Activity 并无 Fragment 存在的情形。

代码详见下图:

获取方式也是比较多,不过较于 Activity 的获取会相对麻烦一些,因为系统没有直接提供 API ,因而需要稍微转个弯:通过 Gradle 插桩的方式,获取 Fragment 的生命周期,以及 Fragment 实例对象:

如果该应用的页面存在一个 Activity 中嵌套多个 Fragment 的情况,单单一个 Activity name则可能无法精准地定位到某个页面,因而还需要加上 Fragment 的名称。Fragment的获取可以通过 Gradle 插桩法来实现,即根据 Fragment 的生命周期来获取Fragment 实例对象。

1.2控件标识生成

理想的情况下页面中的每个控件都有属于自己的唯一 id,SDK 直接获取控件的 id 当做控件标识即可。但现实情况却是,一个页面中往往存在多个相同 id 的控件,或者是没有 id 的控件,比如 Listview 的 item ,开发者不可能给listview的每个item 设置不同的 id。

因此需要转变一下思路。我们可以从控件路径这个除id 外比较独特的性质着手来生成控件标识。开发者可以通过给控件的路径加上控件角标的结构方式,生成控件的唯一标识。下图是Github 上一个仿 B 站的应用。我们对这个应用进行一下控件树分析。首先我们使用 Android Studio 自带的 UI Automator Viewer 工具查看该页面的布局结构:

接下来,我们可以从Application.ActivityLifecycleCallbacks 的回调中拿到 Activity 实例,再使用 activity.getWindow().getDecorView().getRootView() 方法来获取当前页面的控件树。

例如图中的文字控件是 TextView,且无兄弟布局,则可以标记为 TextView[0] 。它的父布局是 LinearLayout 且排在兄弟布局中的第二位,那么就可以写成是 LinearLayout[1],然后使用自己定义的符号拼接,像是 LinearLayout[1]/TextView[0] 。之后以此类推、循环遍历、层层递进,将所有经过的控件以及它们的下标都拼接起来,组成控件在该页面中的唯一标识。

对于一些可复用的 View ,我们则需要采取一些特殊处理。例如对于 RecyclerView、ListView、 ViewPager 等复用控件,我们都需要采取不同的处理方式,去获取当前 View 在该控件中的具体下标。如果没有进行特殊处理,则会导致子控件错位,数据统计不准确。

采集

在以往的处理中,如果需要知道一个按钮的点击次数,开发者就要在该控件的click事件中加入对应的打点代码。这种重复劳作,无疑增加了开发者的开发负担。对此,我们可以采用动态代理方式或Gradle 插桩方式来改善这个问题。

动态代理方式

使用安卓自带的辅助功能 View.AccessibilityDelegate 。前文提到当页面变化时,我们可以通过 Application.ActivityLifecycleCallbacks 获取到 Activity 的实例对象,接着根据activity.getWindow().getDecorView().getRootView() 来获取到控件树。由于控件树可能会实时发生变化,我们则需要通过 ViewTreeObserver.OnGlobalLayoutListener 的方法监听视图变化,从而在该回调中拿到变化的控件。接着我们 要根据递归判断该控件是否为 ViewGroup、是否可以点击、是否能够显示等,继而给符合条件的 View 设置 sendAccessibilityEvent();此外,我们还要在继承了 View.AccessibilityDelegate 的定义类中,对以下这些方法添加 SDK 的代理:

当对应的控件被点击时,系统就会自动调用设置过代理的方法,存储或者上报对应数据。

Gradle 插桩的方式

Android Gradle 工具在1.5.0 版本后提供了 Transfrom API , 该API 允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件。在编译期,开发者可以通过onClick、onItemClick等方法(详见下图)进行监听,这相当于是正则匹配。

当上述监听的方法被编译的时候,就可以将埋点的代理操作插入这些方法中,实现自动化埋点的流程。网上相关流程也是非常详细,有兴趣的可以自行搜索学习。

三、结语

以上就是APP端可视化埋点实现过程中的关键点,特别需要注意的是控件唯一标识那一块,由于布局千变万化,开发者针对很多特定的布局都需要采取对应的处理方式。

目前个推应用统计——个数这个产品只需要一行初始化代码就可以自动帮助开发者采集包括页面统计、事件埋点、新增活跃等多维度信息。

借着万圣节,悄悄跟你说个恐怖的故事:除个推应用统计服务之外,VIP消息推送、用户画像,现在申请均可以免费用一年!一键认证服务还可以享受充10万条送5万条的优惠哦 !点击https://www.getui.com/2019devfest,“码“上申请吧!

行业前沿、面试宝典,更多技术干货,尽在个推技术学院。

收起阅读 »