
通过Flink实现个推海量消息数据的实时统计
背景
消息报表主要用于统计消息任务的下发情况。比如,单条推送消息下发APP用户总量有多少,成功推送到手机的数量有多少,又有多少APP用户点击了弹窗通知并打开APP等。通过消息报表,我们可以很直观地看到消息推送的流转情况、消息下发到达成功率、用户对消息的点击情况等。
个推在提供消息推送服务时,为了更好地了解每天的推送情况,会从不同的维度进行数据统计,生成消息报表。个推每天下发的消息推送数巨大,可以达到数百亿级别,原本我们采用的离线统计系统已不能满足业务需求。随着业务能力的不断提升,我们选择了Flink作为数据处理引擎,以满足对海量消息推送数据的实时统计。
本文将主要阐述选择Flink的原因、Flink的重要特性以及优化后的实时计算方法。
离线计算平台架构
在消息报表系统的初期,我们采用的是离线计算的方式,主要采用spark作为计算引擎,原始数据存放在HDFS中,聚合数据存放在Solr、Hbase和Mysql中:
查询的时候,先根据筛选条件,查询的维度主要有三个:
- appId
- 下发时间
- taskGroupName
根据不同维度可以查询到taskId的列表,然后根据task查询hbase获取相应的结果,获取下发、展示和点击相应的指标数据。在我们考虑将其改造为实时统计时,会存在着一系列的难点:
- 原始数据体量巨大,每天数据量达到几百亿规模,需要支持高吞吐量;
- 需要支持实时的查询;
- 需要对多份数据进行关联;
- 需要保证数据的完整性和数据的准确性。
Why Flink
Flink是什么
Flink 是一个针对流数据和批数据的分布式处理引擎。它主要是由 Java 代码实现。目前主要还是依靠开源社区的贡献而发展。
对 Flink 而言,其所要处理的主要场景就是流数据。Flink 的前身是柏林理工大学一个研究性项目, 在 2014 被 Apache 孵化器所接受,然后迅速地成为了 ASF(Apache Software Foundation)的顶级项目之一。
方案对比
为了实现个推消息报表的实时统计,我们之前考虑使用spark streaming作为我们的实时计算引擎,但是我们在考虑了spark streaming、storm和flink的一些差异点后,还是决定使用Flink作为计算引擎:
针对上面的业务痛点,Flink能够满足以下需要:
-
Flink以管道推送数据的方式,可以让Flink实现高吞吐量。
-
Flink是真正意义上的流式处理,延时更低,能够满足我们消息报表统计的实时性要求。
-
Flink可以依靠强大的窗口功能,实现数据的增量聚合;同时,可以在窗口内进行数据的join操作。
-
我们的消息报表涉及到金额结算,因此对于不允许存在误差,Flink依赖自身的exact once机制,保证了我们数据不会重复消费和漏消费。
Flink的重要特性
下面我们来具体说说Flink中一些重要的特性,以及实现它的原理:
1)低延时、高吞吐
Flink速度之所以这么快,主要是在于它的流处理模型。
Flink 采用 Dataflow 模型,和 Lambda 模式不同。Dataflow 是纯粹的节点组成的一个图,图中的节点可以执行批计算,也可以是流计算,也可以是机器学习算法。流数据在节点之间流动,被节点上的处理函数实时 apply 处理,节点之间是用 netty 连接起来,两个 netty 之间 keepalive,网络 buffer 是自然反压的关键。
经过逻辑优化和物理优化,Dataflow 的逻辑关系和运行时的物理拓扑相差不大。这是纯粹的流式设计,时延和吞吐理论上是最优的。
简单来说,当一条数据被处理完成后,序列化到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理。
2)Checkpoint
Flink是通过分布式快照来实现checkpoint,能够支持Exactly-Once语义。
分布式快照是基于Chandy和Lamport在1985年设计的一种算法,用于生成分布式系统当前状态的一致性快照,不会丢失信息且不会记录重复项。
Flink使用的是Chandy Lamport算法的一个变种,定期生成正在运行的流拓扑的状态快照,并将这些快照存储到持久存储中(例如:存储到HDFS或内存中文件系统)。检查点的存储频率是可配置的。
3)backpressure
back pressure出现的原因是为了应对短期数据尖峰。
旧版本Spark Streaming的back pressure通过限制最大消费速度实现,对于基于Receiver 形式,我们可以通过配置spark.streaming. receiver.maxRate参数来限制每个 receiver 每秒最大可以接收的记录的数据。
对于 Direct Approach 的数据接收,我们可以通过配置spark.streaming. kafka.maxRatePerPartition 参数来限制每次作业中每个 Kafka 分区最多读取的记录条数。
但这样是非常不方便的,在实际上线前,还需要对集群进行压测,来决定参数的大小。
Flink运行时的构造部件是operators以及streams。每一个operator消费一个中间/过渡状态的流,对它们进行转换,然后生产一个新的流。
描述这种机制最好的类比是:Flink使用有效的分布式阻塞队列来作为有界的缓冲区。如同Java里通用的阻塞队列跟处理线程进行连接一样,一旦队列达到容量上限,一个相对较慢的接受者将拖慢发送者。
消息报表的实时计算
优化之后,架构升级成如下:
可以看出,我们做了以下几点优化:
- Flink替换了之前的spark,进行消息报表的实时计算;
- ES替换了之前的Solr。
对于Flink进行实时计算,我们的关注点主要有以下4个方面:
- ExactlyOnce保证了数据只会被消费一次
- 状态管理的能力
- 强大的时间窗口
- 流批一体
为了实现我们实时统计报表的需求,主要依靠Flink的增量聚合功能。
首先,我们设置了Event Time作为时间窗口的类型,保证了只会计算当天的数据;同时,我们每隔一分钟增量统计当日的消息报表,因此分配1分钟的时间窗口。
然后我们使用.aggregate (AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用AggregateFunction提前聚合掉数据,减少 state 的存储压力。之后,我们将增量聚合后的数据写入到ES和Hbase中。
流程如下所示:
同时,在查询的时候,我们通过taskID、日期等维度进行查询,先从ES中获取taskID的集合,之后通过taskID查询hbase,得出统计结果。
总结
通过使用Flink,我们实现了对消息推送数据的实时统计,能够实时查看消息下发、展示、点击等数据指标,同时,借助FLink强大的状态管理功能,服务的稳定性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并将Flink引入到其他的业务线中,以满足一些实时性要求高的业务场景需求。
背景
消息报表主要用于统计消息任务的下发情况。比如,单条推送消息下发APP用户总量有多少,成功推送到手机的数量有多少,又有多少APP用户点击了弹窗通知并打开APP等。通过消息报表,我们可以很直观地看到消息推送的流转情况、消息下发到达成功率、用户对消息的点击情况等。
个推在提供消息推送服务时,为了更好地了解每天的推送情况,会从不同的维度进行数据统计,生成消息报表。个推每天下发的消息推送数巨大,可以达到数百亿级别,原本我们采用的离线统计系统已不能满足业务需求。随着业务能力的不断提升,我们选择了Flink作为数据处理引擎,以满足对海量消息推送数据的实时统计。
本文将主要阐述选择Flink的原因、Flink的重要特性以及优化后的实时计算方法。
离线计算平台架构
在消息报表系统的初期,我们采用的是离线计算的方式,主要采用spark作为计算引擎,原始数据存放在HDFS中,聚合数据存放在Solr、Hbase和Mysql中:
查询的时候,先根据筛选条件,查询的维度主要有三个:
- appId
- 下发时间
- taskGroupName
根据不同维度可以查询到taskId的列表,然后根据task查询hbase获取相应的结果,获取下发、展示和点击相应的指标数据。在我们考虑将其改造为实时统计时,会存在着一系列的难点:
- 原始数据体量巨大,每天数据量达到几百亿规模,需要支持高吞吐量;
- 需要支持实时的查询;
- 需要对多份数据进行关联;
- 需要保证数据的完整性和数据的准确性。
Why Flink
Flink是什么
Flink 是一个针对流数据和批数据的分布式处理引擎。它主要是由 Java 代码实现。目前主要还是依靠开源社区的贡献而发展。
对 Flink 而言,其所要处理的主要场景就是流数据。Flink 的前身是柏林理工大学一个研究性项目, 在 2014 被 Apache 孵化器所接受,然后迅速地成为了 ASF(Apache Software Foundation)的顶级项目之一。
方案对比
为了实现个推消息报表的实时统计,我们之前考虑使用spark streaming作为我们的实时计算引擎,但是我们在考虑了spark streaming、storm和flink的一些差异点后,还是决定使用Flink作为计算引擎:
针对上面的业务痛点,Flink能够满足以下需要:
-
Flink以管道推送数据的方式,可以让Flink实现高吞吐量。
-
Flink是真正意义上的流式处理,延时更低,能够满足我们消息报表统计的实时性要求。
-
Flink可以依靠强大的窗口功能,实现数据的增量聚合;同时,可以在窗口内进行数据的join操作。
-
我们的消息报表涉及到金额结算,因此对于不允许存在误差,Flink依赖自身的exact once机制,保证了我们数据不会重复消费和漏消费。
Flink的重要特性
下面我们来具体说说Flink中一些重要的特性,以及实现它的原理:
1)低延时、高吞吐
Flink速度之所以这么快,主要是在于它的流处理模型。
Flink 采用 Dataflow 模型,和 Lambda 模式不同。Dataflow 是纯粹的节点组成的一个图,图中的节点可以执行批计算,也可以是流计算,也可以是机器学习算法。流数据在节点之间流动,被节点上的处理函数实时 apply 处理,节点之间是用 netty 连接起来,两个 netty 之间 keepalive,网络 buffer 是自然反压的关键。
经过逻辑优化和物理优化,Dataflow 的逻辑关系和运行时的物理拓扑相差不大。这是纯粹的流式设计,时延和吞吐理论上是最优的。
简单来说,当一条数据被处理完成后,序列化到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理。
2)Checkpoint
Flink是通过分布式快照来实现checkpoint,能够支持Exactly-Once语义。
分布式快照是基于Chandy和Lamport在1985年设计的一种算法,用于生成分布式系统当前状态的一致性快照,不会丢失信息且不会记录重复项。
Flink使用的是Chandy Lamport算法的一个变种,定期生成正在运行的流拓扑的状态快照,并将这些快照存储到持久存储中(例如:存储到HDFS或内存中文件系统)。检查点的存储频率是可配置的。
3)backpressure
back pressure出现的原因是为了应对短期数据尖峰。
旧版本Spark Streaming的back pressure通过限制最大消费速度实现,对于基于Receiver 形式,我们可以通过配置spark.streaming. receiver.maxRate参数来限制每个 receiver 每秒最大可以接收的记录的数据。
对于 Direct Approach 的数据接收,我们可以通过配置spark.streaming. kafka.maxRatePerPartition 参数来限制每次作业中每个 Kafka 分区最多读取的记录条数。
但这样是非常不方便的,在实际上线前,还需要对集群进行压测,来决定参数的大小。
Flink运行时的构造部件是operators以及streams。每一个operator消费一个中间/过渡状态的流,对它们进行转换,然后生产一个新的流。
描述这种机制最好的类比是:Flink使用有效的分布式阻塞队列来作为有界的缓冲区。如同Java里通用的阻塞队列跟处理线程进行连接一样,一旦队列达到容量上限,一个相对较慢的接受者将拖慢发送者。
消息报表的实时计算
优化之后,架构升级成如下:
可以看出,我们做了以下几点优化:
- Flink替换了之前的spark,进行消息报表的实时计算;
- ES替换了之前的Solr。
对于Flink进行实时计算,我们的关注点主要有以下4个方面:
- ExactlyOnce保证了数据只会被消费一次
- 状态管理的能力
- 强大的时间窗口
- 流批一体
为了实现我们实时统计报表的需求,主要依靠Flink的增量聚合功能。
首先,我们设置了Event Time作为时间窗口的类型,保证了只会计算当天的数据;同时,我们每隔一分钟增量统计当日的消息报表,因此分配1分钟的时间窗口。
然后我们使用.aggregate (AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用AggregateFunction提前聚合掉数据,减少 state 的存储压力。之后,我们将增量聚合后的数据写入到ES和Hbase中。
流程如下所示:
同时,在查询的时候,我们通过taskID、日期等维度进行查询,先从ES中获取taskID的集合,之后通过taskID查询hbase,得出统计结果。
总结
通过使用Flink,我们实现了对消息推送数据的实时统计,能够实时查看消息下发、展示、点击等数据指标,同时,借助FLink强大的状态管理功能,服务的稳定性也得到了一定的保障。未来,个推也将持续优化消息推送服务,并将Flink引入到其他的业务线中,以满足一些实时性要求高的业务场景需求。
收起阅读 »
面试官:同学,说说 Applink 的使用以及原理
简介
通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。
流程
在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下
快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。
在 AS 中已经有详细的使用步骤了,总共分为 4 步
add URL intent filters
创建一个 URL
或者也可以点击 “How it works” 按钮
Add logic to handle the intent
选择通过 applink 启动的入口 activity。
点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest
<activity android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="http"
android:host="geyan.getui.com" />
</intent-filter>
</activity>
此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。
另外一个改动点是
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
// ATTENTION: This was auto-generated to handle app links.
Intent appLinkIntent = getIntent();
String appLinkAction = appLinkIntent.getAction();
Uri appLinkData = appLinkIntent.getData();
}
applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。
Associate website
这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下
基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.lenny.myapplication",
"sha256_cert_fingerprints":
["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]
}
}]
sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。
最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。
若一个host需要配置多个app,assetlinks.json添加多个app的信息。
若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json
有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的?
后续讲原理的时候会涉及到,这里先不细说。
Test device
最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如
https://yourdomain.com/products/123?coupon=save90
其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。
那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了
adb shell am start
-W -a android.intent.action.VIEW
-d "https://yourdomain.com/products/123?coupon=save90"
[包名]
使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。
原理
上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。
上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~
也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。
一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。
PackageMmanagerService.class
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
final int installFlags = args.installFlags;
<!--开始验证applink-->
startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
...
}
private void startIntentFilterVerifications(int userId, boolean replacing,
PackageParser.Package pkg) {
...
mHandler.removeMessages(START_INTENT_FILTER_VERIFICATIONS);
final Message msg = mHandler.obtainMessage(START_INTENT_FILTER_VERIFICATIONS);
msg.obj = new IFVerificationParams(pkg, replacing, userId, verifierUid);
mHandler.sendMessage(msg);
}
可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 verifyIntentFiltersIfNeeded。
private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,
PackageParser.Package pkg) {
...
<!--检查是否有Activity设置了AppLink-->
final boolean hasDomainURLs = hasDomainURLs(pkg);
if (!hasDomainURLs) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"No domain URLs, so no need to verify any IntentFilter!");
return;
}
<!--是否autoverigy-->
boolean needToVerify = false;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
<!--needsVerification是否设置autoverify -->
if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
needToVerify = true;
break;
}
}
}
<!--如果有搜集需要验证的Activity信息及scheme信息-->
if (needToVerify) {
final int verificationId = mIntentFilterVerificationToken++;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Verification needed for IntentFilter:" + filter.toString());
mIntentFilterVerifier.addOneIntentFilterVerification(
verifierUid, userId, verificationId, filter, packageName);
count++;
} } } } }
<!--开始验证-->
if (count > 0) {
mIntentFilterVerifier.startVerifications(userId);
}
}
对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证
PMS#IntentVerifierProxy.class
public void startVerifications(int userId) {
...
sendVerificationRequest(userId, verificationId, ivs);
}
mCurrentIntentFilterVerifications.clear();
}
private void sendVerificationRequest(int userId, int verificationId,
IntentFilterVerificationState ivs) {
Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,
verificationId);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,
getDefaultScheme());
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,
ivs.getHostsString());
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
ivs.getPackageName());
verificationIntent.setComponent(mIntentFilterVerifierComponent);
verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
UserHandle user = new UserHandle(userId);
mContext.sendBroadcastAsUser(verificationIntent, user);
}
目前 Android 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方
DirectStatementRetriever.class
@Override
public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
if (source instanceof AndroidAppAsset) {
return retrieveFromAndroid((AndroidAppAsset) source);
} else if (source instanceof WebAsset) {
return retrieveFromWeb((WebAsset) source);
} else {
throw new AssociationServiceException("Namespace is not supported.");
}
}
private Result retrieveFromWeb(WebAsset asset)
throws AssociationServiceException {
return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
}
private String computeAssociationJsonUrl(WebAsset asset) {
try {
return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
WELL_KNOWN_STATEMENT_PATH)
.toExternalForm();
} catch (MalformedURLException e) {
throw new AssertionError("Invalid domain name in database.");
}
}
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,
AbstractAsset source)
throws AssociationServiceException {
List<Statement> statements = new ArrayList<Statement>();
if (maxIncludeLevel < 0) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
WebContent webContent;
try {
URL url = new URL(urlString);
if (!source.followInsecureInclude()
&& !url.getProtocol().toLowerCase().equals("https")) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
<!--通过网络请求获取配置-->
webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
} catch (IOException | InterruptedException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
try {
ParsedStatement result = StatementParser
.parseStatementList(webContent.getContent(), source);
statements.addAll(result.getStatements());
<!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->
for (String delegate : result.getDelegates()) {
statements.addAll(
retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
.getStatements());
}
<!--发送结果-->
return Result.create(statements, webContent.getExpireTimeMillis());
} catch (JSONException | IOException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
}
到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数
private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
缺点
- 只能在 Android M 系统上支持
在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。 - 要使用App Links开发者必须维护一个与app相关联的网站
对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。 - 对 ink 域名不太友善
在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。
机型 | 版本 | 是否识别ink | 是否识别com |
---|---|---|---|
小米 | MI6 Android 8.0 MIUI 9.5 | 否 | 是 |
小米 | MI5 Android 7.0 MIUI 9.5 | 否 | 是 |
魅族 | PRO 7 Android 7.0 Flyme 6.1.3.1A | 否 | 是 |
三星 | S8 Android 7.0 | 是,弹框 | 是 |
华为 | HonorV10 Android 8.0 EMUI 8.0 | 是 | 是 |
oppo R11s Android 7.1.1 ColorOS 3.2 | 是 | 是 | |
oppo | A59s Android 5.1 ColorOS 3.0 | 是,不能跳转到app | 是,不能跳转到app |
vivo | X6Plus A Android 5.0.2 Funtouch OS_2.5 | 否 | 是 |
vivo | 767 Android 6.0 Funtouch OS_2.6 | 是,不能跳转到app | 是,不能跳转到app |
vivo | X9 Android 7.1.1 Funtouch OS_3.1 | 是,不能跳转到app | 是,不能跳转到app |
参考
1.官方文档: https://developer.android.com/studio/write/app-link-indexing.html
作者:哈哈将
行业前沿、移动开发、数据建模等干货内容,尽在公众号:个推技术学院
简介
通过 Link这个单词我们可以看出这个是一种链接,使用此链接可以直接跳转到 APP,常用于应用拉活,跨应用启动,推送通知启动等场景。
流程
在AS 上其实已经有详细的使用步骤解析了,这里给大家普及下
快速点击 shift 两次,输入 APPLink 即可找到 AS 提供的集成教程。
在 AS 中已经有详细的使用步骤了,总共分为 4 步
add URL intent filters
创建一个 URL
或者也可以点击 “How it works” 按钮
Add logic to handle the intent
选择通过 applink 启动的入口 activity。
点击完成后,AS 会自动在两个地方进行修改,一个是 AndroidManifest
<activity android:name=".TestActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="http"
android:host="geyan.getui.com" />
</intent-filter>
</activity>
此处多了一个 data,看到这个 data 标签,我们可以大胆的猜测,也许这个 applink 的是一个隐式启动。
另外一个改动点是
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
// ATTENTION: This was auto-generated to handle app links.
Intent appLinkIntent = getIntent();
String appLinkAction = appLinkIntent.getAction();
Uri appLinkData = appLinkIntent.getData();
}
applink 的值即为之前配置的 url 链接,此处是为了接收数据用的,不再多说了。
Associate website
这一步最关键了,需要根据 APP 的证书生成一个 json 文件, APP 安装的时候会去联网进行校验。选择你的线上证书,然后点击生成会得到一个 assetlinks.json 的文件,需要把这个文件放到服务器指定的目录下
基于安全原因,这个文件必须通过 SSL 的 GET 请求获取,JSON 格式如下:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.lenny.myapplication",
"sha256_cert_fingerprints":
["E7:E8:47:2A:E1:BF:63:F7:A3:F8:D1:A5:E1:A3:4A:47:88:0F:B5:F3:EA:68:3F:5C:D8:BC:0B:BA:3E:C2:D2:61"]
}
}]
sha256_cert_fingerprints 这个参数可以通过 keytool 命令获取,这里不再多说了。
最后把这个文件上传到 你配置的地址/.well-know/statements/json,为了避免今后每个 app 链接请求都访问网络,安卓只会在 app 安装的时候检查这个文件。,如果你能在请求 https://yourdomain.com/.well-known/statements.json 的时候看到这个文件(替换成自己的域名),那么说明服务端的配置是成功的。目前可以通过 http 获得这个文件,但是在M最终版里则只能通过 HTTPS 验证。确保你的 web 站点支持 HTTPS 请求。
若一个host需要配置多个app,assetlinks.json添加多个app的信息。
若一个 app 需要配置多个 host,每个 host 的 .well-known 下都要配置assetlinks.json
有没有想过 url 的后缀是不是一定要写成 /.well-know/statements/json 的?
后续讲原理的时候会涉及到,这里先不细说。
Test device
最后我们本质仅是拿到一个 URL,大多数的情况下,我们会在 url 中拼接一些参数,比如
https://yourdomain.com/products/123?coupon=save90
其中 ./products/123?coupon=save90 是我们之前在第二步填写的 path。
那测试方法多种多样,可以使用通知,也可以使用短信,或者使用 adb 直接模拟,我这边图省事就直接用 adb 模拟了
adb shell am start
-W -a android.intent.action.VIEW
-d "https://yourdomain.com/products/123?coupon=save90"
[包名]
使用这个命令就会自动打开 APP。前提是 yourdomain.com 网站上存在了 web-app 关联文件。
原理
上述这些都简单的啦,依葫芦画瓢就行,下面讲些深层次的东西,不仅要知道会用,还得知道为什么可以这么用,不然和咸鱼有啥区别。
上诉也说了,我们配置的域名是在 activity 的 data 标签的,那是否是可以认为 applink 是一种隐式启动,应用安装的时候根据 data 的内容到这个网页下面去获取 assetlinks.json 进行校验,如果符合条件则把 这个 url 保存在本地,当点击 webview 或者短信里面的 url的时候,系统会自动与本地库中的域名相匹配, 如果匹配失败则会被自动认为是 deeplink 的连接。确认过眼神对吧~~~
也就说在第一次安装 APP 的时候是会去请求 data 标签下面的域名的,并且去请求所获得的域名,那 安装->初次启动 的体验自然会想到是在源码中 PackageManagerService 实现。
一个 APk 的安装过程是极其复杂的,涉及到非常多的底层知识,这里不细说,直接找到校验 APPLink 的入口 PackageManagerService 的 installPackageLI 方法。
PackageMmanagerService.class
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
final int installFlags = args.installFlags;
<!--开始验证applink-->
startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
...
}
private void startIntentFilterVerifications(int userId, boolean replacing,
PackageParser.Package pkg) {
...
mHandler.removeMessages(START_INTENT_FILTER_VERIFICATIONS);
final Message msg = mHandler.obtainMessage(START_INTENT_FILTER_VERIFICATIONS);
msg.obj = new IFVerificationParams(pkg, replacing, userId, verifierUid);
mHandler.sendMessage(msg);
}
可以看到这边发送了一个 message 为 START_INTENT_FILTER_VERIFICATIONS 的 handler 消息,在 handle 的 run 方法里又会接着调用 verifyIntentFiltersIfNeeded。
private void verifyIntentFiltersIfNeeded(int userId, int verifierUid, boolean replacing,
PackageParser.Package pkg) {
...
<!--检查是否有Activity设置了AppLink-->
final boolean hasDomainURLs = hasDomainURLs(pkg);
if (!hasDomainURLs) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"No domain URLs, so no need to verify any IntentFilter!");
return;
}
<!--是否autoverigy-->
boolean needToVerify = false;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
<!--needsVerification是否设置autoverify -->
if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
needToVerify = true;
break;
}
}
}
<!--如果有搜集需要验证的Activity信息及scheme信息-->
if (needToVerify) {
final int verificationId = mIntentFilterVerificationToken++;
for (PackageParser.Activity a : pkg.activities) {
for (ActivityIntentInfo filter : a.intents) {
if (filter.handlesWebUris(true) && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Verification needed for IntentFilter:" + filter.toString());
mIntentFilterVerifier.addOneIntentFilterVerification(
verifierUid, userId, verificationId, filter, packageName);
count++;
} } } } }
<!--开始验证-->
if (count > 0) {
mIntentFilterVerifier.startVerifications(userId);
}
}
对 APPLink 进行了检查,搜集,验证,主要是对 scheme 的校验是否是 http/https,以及是否有 flag 为 Intent.ACTION_DEFAULT与Intent.ACTION_VIEW 的参数,接着是开启验证
PMS#IntentVerifierProxy.class
public void startVerifications(int userId) {
...
sendVerificationRequest(userId, verificationId, ivs);
}
mCurrentIntentFilterVerifications.clear();
}
private void sendVerificationRequest(int userId, int verificationId,
IntentFilterVerificationState ivs) {
Intent verificationIntent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,
verificationId);
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,
getDefaultScheme());
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,
ivs.getHostsString());
verificationIntent.putExtra(
PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
ivs.getPackageName());
verificationIntent.setComponent(mIntentFilterVerifierComponent);
verificationIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
UserHandle user = new UserHandle(userId);
mContext.sendBroadcastAsUser(verificationIntent, user);
}
目前 Android 的实现是通过发送一个广播来进行验证的,也就是说,这是个异步的过程,验证是需要耗时的(网络请求),发出去的广播会被 IntentFilterVerificationReceiver 接收到。这个类又会再次 start DirectStatementService,在这个 service 里面又会去调用 DirectStatementRetriever 类。在此类的 retrieveStatementFromUrl 方法中才是真正请求网络的地方
DirectStatementRetriever.class
@Override
public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
if (source instanceof AndroidAppAsset) {
return retrieveFromAndroid((AndroidAppAsset) source);
} else if (source instanceof WebAsset) {
return retrieveFromWeb((WebAsset) source);
} else {
throw new AssociationServiceException("Namespace is not supported.");
}
}
private Result retrieveFromWeb(WebAsset asset)
throws AssociationServiceException {
return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
}
private String computeAssociationJsonUrl(WebAsset asset) {
try {
return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
WELL_KNOWN_STATEMENT_PATH)
.toExternalForm();
} catch (MalformedURLException e) {
throw new AssertionError("Invalid domain name in database.");
}
}
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,
AbstractAsset source)
throws AssociationServiceException {
List<Statement> statements = new ArrayList<Statement>();
if (maxIncludeLevel < 0) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
WebContent webContent;
try {
URL url = new URL(urlString);
if (!source.followInsecureInclude()
&& !url.getProtocol().toLowerCase().equals("https")) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
<!--通过网络请求获取配置-->
webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
} catch (IOException | InterruptedException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
try {
ParsedStatement result = StatementParser
.parseStatementList(webContent.getContent(), source);
statements.addAll(result.getStatements());
<!--如果有一对多的情况,或者说设置了“代理”,则循环获取配置-->
for (String delegate : result.getDelegates()) {
statements.addAll(
retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
.getStatements());
}
<!--发送结果-->
return Result.create(statements, webContent.getExpireTimeMillis());
} catch (JSONException | IOException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
}
到了这里差不多就全部讲完了,本质就是通过 HTTPURLConnection 去发起来一个请求。之前还留了个问题,是不是一定要要 /.well-known/assetlinks.json,到这里是不是可以完全明白了,就是 WELL_KNOWN_STATEMENT_PATH 参数
private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
缺点
- 只能在 Android M 系统上支持
在配置好了app对App Links的支持之后,只有运行Android M的用户才能正常工作。之前安卓版本的用户无法直接点击链接进入app,而是回到浏览器的web页面。 - 要使用App Links开发者必须维护一个与app相关联的网站
对于小的开发者来说这个有点困难,因为他们没有能力为app维护一个网站,但是它们仍然希望通过web链接获得流量。 - 对 ink 域名不太友善
在测试中发现,国内各大厂商对 .ink 域名不太友善,很多的是被支持了 .com 域名,但是不支持 .ink 域名。
机型 | 版本 | 是否识别ink | 是否识别com |
---|---|---|---|
小米 | MI6 Android 8.0 MIUI 9.5 | 否 | 是 |
小米 | MI5 Android 7.0 MIUI 9.5 | 否 | 是 |
魅族 | PRO 7 Android 7.0 Flyme 6.1.3.1A | 否 | 是 |
三星 | S8 Android 7.0 | 是,弹框 | 是 |
华为 | HonorV10 Android 8.0 EMUI 8.0 | 是 | 是 |
oppo R11s Android 7.1.1 ColorOS 3.2 | 是 | 是 | |
oppo | A59s Android 5.1 ColorOS 3.0 | 是,不能跳转到app | 是,不能跳转到app |
vivo | X6Plus A Android 5.0.2 Funtouch OS_2.5 | 否 | 是 |
vivo | 767 Android 6.0 Funtouch OS_2.6 | 是,不能跳转到app | 是,不能跳转到app |
vivo | X9 Android 7.1.1 Funtouch OS_3.1 | 是,不能跳转到app | 是,不能跳转到app |
参考
1.官方文档: https://developer.android.com/studio/write/app-link-indexing.html
作者:哈哈将
行业前沿、移动开发、数据建模等干货内容,尽在公众号:个推技术学院

uni-app h5使用wxjssdk开发中遇到的各种坑---持续更新,如果有可以解决的在评论区回复我,谢谢分享

关于ios运行自定义基座 安装失败 return code=-402620395
一定要使用 开发(Development)证书及profile文件打包生成自定义基座
并且需要添加测试机的UDID
使用发布证书不可以
一定要使用 开发(Development)证书及profile文件打包生成自定义基座
并且需要添加测试机的UDID
使用发布证书不可以

uni-ad App端打包注意及plus.ad使用指南
HBuilderX 2.5.3 版本起,DCloud广告联盟升级为uni-ad。uni-app、5+ App、wap2app等项目全都支持使用uni-ad。
概述
uni-ad聚合目前国内流行的广告平台优量汇、穿山甲、快手、百度等广告渠道,支持开屏广告和信息流、Banner广告。
使用uni-ad前,需登录uni-ad广告联盟申请开通
参考教程:https://ask.dcloud.net.cn/article/36769
注意问题
Android平台
Android权限问题
我们目前测试发现在华为Android8以上手机,使用穿山甲(今日头条)广告时如果没有给应用读取设备信息权限,只会显示抖音广告。
建议开发者开通广告时,最好能引导用户允许读取设备信息权限,这样给用户下发的广告会更精准,可以获得更高的CPM。
由于2019年底开始工业和信息化部展开App侵犯用户权益专项整治行动,不允许App在用户拒绝授权后应用退出或关闭。因此建议开发者根据以下方案进行选择:
- 如果不提交到各应用商店
建议开发者配置应用每次启动都申请读取设备信息权限,并且用户必须允许,即permissionPhoneState下的request配置为"always",详情参考:https://ask.dcloud.net.cn/article/36549#phonestate - 如果提交应用市场
建议开发者申请穿山甲广告,同时申请开通优量汇、快手、百度等渠道广告,uni-ad会自动优化选择展现效果好的广告,最大化提升广告的CPM。
华为应用市场审核问题
目前已知开通广告后提交华为应用市场可能无法通过审核
反馈信息:“未通过原因:您的应用点闪屏广告或者弹窗广告立即下载,请修复
这是因为穿山甲和优量汇的app推广广告,用户点击后不会弹窗提示确认,直接下载apk导致的。如果要修改为提示用户确认后再下载,需人工向穿山甲和优量汇广告平台申请操作。
如碰到此问题,请邮件联系:uniad@dcloud.io
邮件标题:华为应用市场点击广告立即下载问题
并提供以下内容:
应用标识:__UNI__XXXXXX
谷歌应用(google play)市场审核问题
谷歌应用市场最新政策要求应用中不能包含直接下载apk的逻辑,只能通过google play更新/安装应用。目前国内广告厂商(穿山甲、优量汇)都是直接下载apk安装,所以无法通过谷歌应用市场的审核。
如果要提交谷歌应用市场,请不要勾选第三方广告联盟(穿山甲、优量汇),广告基础功能不受影响
后续我们会接入Google Ads、Facebook Ads等。
iOS平台
为了提升广告效果,如果勾选了优量汇、穿山甲、快手中的任何一个广告平台,则一定会使用广告标识(IDFA)
此时提交AppStore审核时注意需要勾选使用广告标识符,详情参考:https://ask.dcloud.net.cn/article/36107
注意:从HBuilder X- 3.1.13版本之后,iOS14.5的手机会在流量中显示 App Tracking Transparency 授权来获取IDFA,所以需要在manifest.json -> "App权限配置"中配置NSUserTrackingUsageDescription,描述获取IDFA的用途,否则会导致App Store审核不过或者导致应用运行闪退。例如:

配置uni-ad广告模块
云端打包时选择要集成的广告平台sdk。
-
方式1:
打开项目的manifest.json文件,在“App模块配置”页的“uni-ad”项下勾选需要集成的广告平台:
-
方式2:云打包界面直接选中,然后打包
勾选App需要支持的广告平台,提交云端打包生效
不勾选广告平台,打包将不会把对应的广告SDK打进去,也就无法显示对应平台的广告
使用广告
开屏广告
在uni-ad广告联盟申请开通“开屏广告”后重新提交云端打包即可。
开屏广告无需编程,可直接使用。
<a id="splash"/>
自定义开屏广告界面
开屏广告界面屏幕顶部85%区域显示广告内容,屏幕底部15%区域默认显示应用图标及名称,支持自定义底部区域显示内容:
- uni-app项目
在manifest.json文件的 "app-plus" -> "splashscreen" 下添加ads节点 - 5+ APp项目
在manifest.json文件的 "plus" -> "splashscreen" 下添加ads节点
"splashscreen" : {
"ads" : {
"background" : "#FF0000",
"image" : "static/logo.png"
},
//...
},
其中 background 配置背景颜色,格式为“#RRGGBB”;
image 配置底部区域显示的图片,配置不再显示应用图标及名称,图片路径相对应用资源目录路径,不支持网络地址,建议分辨率720x256(要求png格式,背景透明,留出边距,在不同分辨率手机上会自动等比例缩放处理。
注意:
- Android平台
自定义开屏广告界面更新应用资源生效 - iOS平台
开屏广告显示在默认开屏界面splash上,仅在使用通用启动界面才支持自定义底部区域显示内容,且需提交云端打包后生效。
如果使用自定义storyboard启动界面,则以上配置失效,显示自定义storyboard启动界面底部15%区域。
更多开屏界面配置项,请登录uni-ad广告联盟,在应用详情的开屏配置页面修改:
<a id="splash_fs"/>
开屏广告是否全屏展示
HBuilderX2.8.12及以下版本,应用仅支持费全屏开屏广告。
HBuilderX2.8.7及以上版本,新增支持应用设置开屏广告全屏显示,默认非全屏显示,如需全屏显示,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中打开“是否全屏展示”开关。
<a id="splash_fr"/>
应用从后台切回到前台是否显示广告
HBuilderX2.8.12及以下版本,应用仅在启动时显示开屏广告,应用从后台切换到前台不会显示开屏广告。
HBuilderX2.9.0及以上版本,应用支持从后台切换到前台时显示开屏广告,如果应用开通了开屏广告功能,默认也会开通后台切换到前台显示开屏广告,如需关闭此功能,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中关闭“从后台切回后是否展示”开关。
信息流、Banner广告
信息流和banner,需要编程。
使用信息流前,需在uni-ad广告联盟申请获取广告位标识(adpid)
HBuilderX标准基座真机运行测试信息流广告位标识(adpid)为:1111111111
注意:AdView为原生组件,仅支持页面级的滚动。
uni-app项目
使用已经封装好的 ad 组件。
详情参考:https://uniapp.dcloud.io/component/ad
5+ App(WAP2APP)项目
5+ APP中使用原生AdView原生控件渲染广告数据,其层级要高于所有DOM元素,使用时务必注意层级问题。
使用信息流、banner广告时,按以下步骤操作:
- 在DOM中创建占位div
var adDom = null; // DOMContentloaded事件处理 document.addEventListener('DOMContentLoaded', function(){ //获取占位div元素 adDom = document.getElementById('ad'); }, false); //在html中要显示广告的位置放置占位div <div id="ad" style="width:100%;height:0px;"></div>
- 获取广告数据
var adata = null; function getAdData(){ //获取广告数据 plus.ad.getAds({adpid:'1111111111', //替换为自己申请获取的广告位标识,此广告位标识仅在HBuilderX标准基座中有效 width:'100%', //广告将要显示的宽度 count:1 //注意实际业务中建议一次请求3-5条广告,避免请求到相同的广告 }, function(e){ console.log('获取广告成功: '+JSON.stringify(e)); if(!e || !e.ads || e.ads.length<1){ console.log('无广告数据!'); }else{ console.log('更新广告数据!'); adata = e.ads[0]; //这里只使用一条广告数据 } }, function(e){ console.log('获取广告失败: '+JSON.stringify(e)); }); }
注意:获取广告数据时需传入广告展现时真实的宽度,便于向广告平台获取合适的广告数据
- 创建广告控件,监听渲染和关闭事件,绑定并渲染广告数据
var adView = null; function showAdView(){ //创建AdView控件 adView = plus.ad.createAdView({ top:adDom.offsetTop+'px', left:'0px', width:'100%', height:'0px', position: 'static' }); //将AdView添加到Webview窗口中 plus.webview.currentWebview().append(adView); //监听AdView渲染完成事件,动态调整高度 adView.setRenderingListener(function(e){ console.log('渲染广告完成: '+JSON.stringify(e)); if(0 != e.result){ console.log('渲染失败!'); }else{ //调整广告控件高度,显示广告内容;调整广告控件的top值,避免渲染过程中top值发生变化导致的广告位置不对的问题。 adView.setStyle({top: adDom.offsetTop + 'px', height: e.height + 'px' }); //调整占位div高度,避免被广告控件盖住DOM元素 adDom.style.height = e.height+'px'; } }); //监听用户关闭广告控件事件 adView.setDislikeListener(function(e){ console.log('用户关闭广告: '+JSON.stringify(e)); adView.close(),adView=null; //调整占位div高度,避免关闭广告控件后显示空白区域 adDom.style.height = '0px'; }); //绑定并渲染广告数据 adView.renderingBind(adata); }
激励视频广告
HBuilderX标准基座真机运行测试激励视频广告位标识(adpid)为:1507000689
uni-app项目
使用已经封装成uni API。
详情参考:https://uniapp.dcloud.io/api/ad/rewarded-video-ad
5+ App(WAP2APP)项目
激励视频广告由RewardedVideoAd管理。
操作步骤如下:
- 创建激励视频广告对象 plus.ad.createRewardedVideoAd,需传入在uni-ad平台申请的广告位标识adpid
- 监听激励视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
- 监听激励视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
- 监听激励视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告,发放奖励
- 加载激励视频广告:调用广告对象load
示例如下:
//视频激励广告
var adReward = null;
function rewardedVideoAd(){
if(adReward){
outLine('正在加载激励视频广告');
return;
}
console.log('#视频激励广告#');
adReward = plus.ad.createRewardedVideoAd({adpid:'1507000689'}); // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效
adReward.onLoad(function(){
console.log('加载成功')
adReward.show();
});
adReward.onError(function(e){
console.log('加载失败: '+JSON.stringify(e));
adReward.destroy();
adReward = null;
});
adReward.onClose(function(e){
if(e.isEnded){
console.log('激励视频播放完成');
plus.nativeUI.toast('激励视频播放完成');
}else{
console.log('激励视频未播放完成关闭!')
}
adReward.destroy();
adReward = null;
});
adReward.load();
}
<a id="fullscreenvideo"/>
全屏视频广告
HBuilderX标准基座真机运行测试全屏视频广告位标识(adpid)为:1507000611
全屏视频广告与激励视频广告效果类似,在应用中可以在激励视频广告填充不足时用全屏视频广告来代替。
全屏视频与激励视频广告的差别:
- 不支持视频服务器回调校验事件
- 全屏视频在播放结束前(通常播放6-10秒)可以关闭,用户关闭视频不会提醒用户无法获取奖励
uni-app项目
使用已经封装成uni API。
详情参考:https://uniapp.dcloud.net.cn/api/a-d/full-screen-video
5+ App(WAP2APP)项目
全屏视频广告由FullScreenVideoAd管理。
操作步骤如下:
- 创建全屏视频广告对象 plus.ad.createFullScreenVideoAd,需传入在uni-ad平台申请的广告位标识adpid
- 监听全屏视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
- 监听全屏视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
- 监听全屏视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告
- 加载激励视频广告:调用广告对象load
示例如下:
//视全屏激励广告
var adFull = null;
function fullVideoAd(){
if(adFull){
outLine('正在加载全屏视频广告');
return;
}
console.log('#全屏视频广告#');
adFull = plus.ad.createFullScreenVideoAd({adpid:'1507000611'}); // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效
adFull.onLoad(function(){
console.log('加载成功')
adFull.show();
});
adFull.onError(function(e){
console.log('加载失败: '+JSON.stringify(e));
adFull.destroy();
adFull = null;
});
adFull.onClose(function(e){
if(e.isEnded){
console.log('全屏视频播放完成');
plus.nativeUI.toast('全屏视频播放完成');
}else{
console.log('全屏视频未播放完成关闭!')
}
adFull.destroy();
adFull = null;
});
adFull.load();
}
沉浸视频流广告
也称为Draw视频信息流广告
沉浸视频流广告为媒体提供了竖屏视频信息流广告样式,适合在全屏的竖屏视频中使用。目前仅提供了nvue的方式使用,使用方式可以参考文档:https://uniapp.dcloud.net.cn/component/ad-draw
HBuilderX3.0.0及以上版本开始支持。
内容联盟
内容联盟提供了单独的接口接入视频流供接入方接入。是一种支持用户上下滑动的切换视频的内容形式。
使用步骤:
- 通过使用plus.ad.showContentPage观看内容。
示例:
plus.ad.showContentPage({
adpid:"1111111112", // 1111111112为测试广告位,使用时请替换成自己的。
}, function(e) {
console.log("成功")
}, function(e) {
console.log(JSON.stringify(e))
});
<a id="rewarderror"/>
<a id="videoerror"/>
广告错误码
激励视频及全屏视频广告常见错误及处理建议:
- "-5001"
广告位标识adpid为空,请传入有效的adpid
请到广告平台申请广告位并获取adpid。 - "-5002"
无效的广告位标识adpid,请使用正确的adpid
请到广告平台确认使用的广告位标识adpid是否正确。 - "-5003"
未开通广告,请在广告平台申请并确保已审核通过
当前应用还没有开通广告,或者广告还没有审核通过,请到广告平台查看申请状态。 - "-5004"
无广告模块,打包时请配置要使用的广告模块
云端打包时没有勾选广告平台SDK,请参考前面“配置uni-ad广告模块”方法勾选后重新提交云端打包。 - "-5005"
广告加载失败,请稍后重试
加载视频激励广告失败,返回此错误时建议过一段时间再重新加载一次。 - "-5006"
广告未加载完成无法播放,请加载完成后再调show播放 - "-5007"
无法获取广告配置数据,请尝试重试
返回此错误时建议重新加载一次。 - "-5100"
其他错误,聚合广告商内部错误。 - 其他错误码及详细介绍请参考[https://uniapp.dcloud.net.cn/uni-ad/ad-error-code.html]
本地离线打包
Android平台参考:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/androidModuleConfig/uniad
iOS平台参数:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/iOSModuleConfig/uniad
HBuilderX 2.5.3 版本起,DCloud广告联盟升级为uni-ad。uni-app、5+ App、wap2app等项目全都支持使用uni-ad。
概述
uni-ad聚合目前国内流行的广告平台优量汇、穿山甲、快手、百度等广告渠道,支持开屏广告和信息流、Banner广告。
使用uni-ad前,需登录uni-ad广告联盟申请开通
参考教程:https://ask.dcloud.net.cn/article/36769
注意问题
Android平台
Android权限问题
我们目前测试发现在华为Android8以上手机,使用穿山甲(今日头条)广告时如果没有给应用读取设备信息权限,只会显示抖音广告。
建议开发者开通广告时,最好能引导用户允许读取设备信息权限,这样给用户下发的广告会更精准,可以获得更高的CPM。
由于2019年底开始工业和信息化部展开App侵犯用户权益专项整治行动,不允许App在用户拒绝授权后应用退出或关闭。因此建议开发者根据以下方案进行选择:
- 如果不提交到各应用商店
建议开发者配置应用每次启动都申请读取设备信息权限,并且用户必须允许,即permissionPhoneState下的request配置为"always",详情参考:https://ask.dcloud.net.cn/article/36549#phonestate - 如果提交应用市场
建议开发者申请穿山甲广告,同时申请开通优量汇、快手、百度等渠道广告,uni-ad会自动优化选择展现效果好的广告,最大化提升广告的CPM。
华为应用市场审核问题
目前已知开通广告后提交华为应用市场可能无法通过审核
反馈信息:“未通过原因:您的应用点闪屏广告或者弹窗广告立即下载,请修复
这是因为穿山甲和优量汇的app推广广告,用户点击后不会弹窗提示确认,直接下载apk导致的。如果要修改为提示用户确认后再下载,需人工向穿山甲和优量汇广告平台申请操作。
如碰到此问题,请邮件联系:uniad@dcloud.io
邮件标题:华为应用市场点击广告立即下载问题
并提供以下内容:
应用标识:__UNI__XXXXXX
谷歌应用(google play)市场审核问题
谷歌应用市场最新政策要求应用中不能包含直接下载apk的逻辑,只能通过google play更新/安装应用。目前国内广告厂商(穿山甲、优量汇)都是直接下载apk安装,所以无法通过谷歌应用市场的审核。
如果要提交谷歌应用市场,请不要勾选第三方广告联盟(穿山甲、优量汇),广告基础功能不受影响
后续我们会接入Google Ads、Facebook Ads等。
iOS平台
为了提升广告效果,如果勾选了优量汇、穿山甲、快手中的任何一个广告平台,则一定会使用广告标识(IDFA)
此时提交AppStore审核时注意需要勾选使用广告标识符,详情参考:https://ask.dcloud.net.cn/article/36107
注意:从HBuilder X- 3.1.13版本之后,iOS14.5的手机会在流量中显示 App Tracking Transparency 授权来获取IDFA,所以需要在manifest.json -> "App权限配置"中配置NSUserTrackingUsageDescription,描述获取IDFA的用途,否则会导致App Store审核不过或者导致应用运行闪退。例如:
配置uni-ad广告模块
云端打包时选择要集成的广告平台sdk。
-
方式1:
打开项目的manifest.json文件,在“App模块配置”页的“uni-ad”项下勾选需要集成的广告平台:
-
方式2:云打包界面直接选中,然后打包
勾选App需要支持的广告平台,提交云端打包生效
不勾选广告平台,打包将不会把对应的广告SDK打进去,也就无法显示对应平台的广告
使用广告
开屏广告
在uni-ad广告联盟申请开通“开屏广告”后重新提交云端打包即可。
开屏广告无需编程,可直接使用。
<a id="splash"/>
自定义开屏广告界面
开屏广告界面屏幕顶部85%区域显示广告内容,屏幕底部15%区域默认显示应用图标及名称,支持自定义底部区域显示内容:
- uni-app项目
在manifest.json文件的 "app-plus" -> "splashscreen" 下添加ads节点 - 5+ APp项目
在manifest.json文件的 "plus" -> "splashscreen" 下添加ads节点
"splashscreen" : {
"ads" : {
"background" : "#FF0000",
"image" : "static/logo.png"
},
//...
},
其中 background 配置背景颜色,格式为“#RRGGBB”;
image 配置底部区域显示的图片,配置不再显示应用图标及名称,图片路径相对应用资源目录路径,不支持网络地址,建议分辨率720x256(要求png格式,背景透明,留出边距,在不同分辨率手机上会自动等比例缩放处理。
注意:
- Android平台
自定义开屏广告界面更新应用资源生效 - iOS平台
开屏广告显示在默认开屏界面splash上,仅在使用通用启动界面才支持自定义底部区域显示内容,且需提交云端打包后生效。
如果使用自定义storyboard启动界面,则以上配置失效,显示自定义storyboard启动界面底部15%区域。
更多开屏界面配置项,请登录uni-ad广告联盟,在应用详情的开屏配置页面修改:
<a id="splash_fs"/>
开屏广告是否全屏展示
HBuilderX2.8.12及以下版本,应用仅支持费全屏开屏广告。
HBuilderX2.8.7及以上版本,新增支持应用设置开屏广告全屏显示,默认非全屏显示,如需全屏显示,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中打开“是否全屏展示”开关。
<a id="splash_fr"/>
应用从后台切回到前台是否显示广告
HBuilderX2.8.12及以下版本,应用仅在启动时显示开屏广告,应用从后台切换到前台不会显示开屏广告。
HBuilderX2.9.0及以上版本,应用支持从后台切换到前台时显示开屏广告,如果应用开通了开屏广告功能,默认也会开通后台切换到前台显示开屏广告,如需关闭此功能,请登录uni-ad广告联盟,进入应用详情页面,在修改开屏配置中关闭“从后台切回后是否展示”开关。
信息流、Banner广告
信息流和banner,需要编程。
使用信息流前,需在uni-ad广告联盟申请获取广告位标识(adpid)
HBuilderX标准基座真机运行测试信息流广告位标识(adpid)为:1111111111
注意:AdView为原生组件,仅支持页面级的滚动。
uni-app项目
使用已经封装好的 ad 组件。
详情参考:https://uniapp.dcloud.io/component/ad
5+ App(WAP2APP)项目
5+ APP中使用原生AdView原生控件渲染广告数据,其层级要高于所有DOM元素,使用时务必注意层级问题。
使用信息流、banner广告时,按以下步骤操作:
- 在DOM中创建占位div
var adDom = null; // DOMContentloaded事件处理 document.addEventListener('DOMContentLoaded', function(){ //获取占位div元素 adDom = document.getElementById('ad'); }, false); //在html中要显示广告的位置放置占位div <div id="ad" style="width:100%;height:0px;"></div>
- 获取广告数据
var adata = null; function getAdData(){ //获取广告数据 plus.ad.getAds({adpid:'1111111111', //替换为自己申请获取的广告位标识,此广告位标识仅在HBuilderX标准基座中有效 width:'100%', //广告将要显示的宽度 count:1 //注意实际业务中建议一次请求3-5条广告,避免请求到相同的广告 }, function(e){ console.log('获取广告成功: '+JSON.stringify(e)); if(!e || !e.ads || e.ads.length<1){ console.log('无广告数据!'); }else{ console.log('更新广告数据!'); adata = e.ads[0]; //这里只使用一条广告数据 } }, function(e){ console.log('获取广告失败: '+JSON.stringify(e)); }); }
注意:获取广告数据时需传入广告展现时真实的宽度,便于向广告平台获取合适的广告数据
- 创建广告控件,监听渲染和关闭事件,绑定并渲染广告数据
var adView = null; function showAdView(){ //创建AdView控件 adView = plus.ad.createAdView({ top:adDom.offsetTop+'px', left:'0px', width:'100%', height:'0px', position: 'static' }); //将AdView添加到Webview窗口中 plus.webview.currentWebview().append(adView); //监听AdView渲染完成事件,动态调整高度 adView.setRenderingListener(function(e){ console.log('渲染广告完成: '+JSON.stringify(e)); if(0 != e.result){ console.log('渲染失败!'); }else{ //调整广告控件高度,显示广告内容;调整广告控件的top值,避免渲染过程中top值发生变化导致的广告位置不对的问题。 adView.setStyle({top: adDom.offsetTop + 'px', height: e.height + 'px' }); //调整占位div高度,避免被广告控件盖住DOM元素 adDom.style.height = e.height+'px'; } }); //监听用户关闭广告控件事件 adView.setDislikeListener(function(e){ console.log('用户关闭广告: '+JSON.stringify(e)); adView.close(),adView=null; //调整占位div高度,避免关闭广告控件后显示空白区域 adDom.style.height = '0px'; }); //绑定并渲染广告数据 adView.renderingBind(adata); }
激励视频广告
HBuilderX标准基座真机运行测试激励视频广告位标识(adpid)为:1507000689
uni-app项目
使用已经封装成uni API。
详情参考:https://uniapp.dcloud.io/api/ad/rewarded-video-ad
5+ App(WAP2APP)项目
激励视频广告由RewardedVideoAd管理。
操作步骤如下:
- 创建激励视频广告对象 plus.ad.createRewardedVideoAd,需传入在uni-ad平台申请的广告位标识adpid
- 监听激励视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
- 监听激励视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
- 监听激励视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告,发放奖励
- 加载激励视频广告:调用广告对象load
示例如下:
//视频激励广告
var adReward = null;
function rewardedVideoAd(){
if(adReward){
outLine('正在加载激励视频广告');
return;
}
console.log('#视频激励广告#');
adReward = plus.ad.createRewardedVideoAd({adpid:'1507000689'}); // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效
adReward.onLoad(function(){
console.log('加载成功')
adReward.show();
});
adReward.onError(function(e){
console.log('加载失败: '+JSON.stringify(e));
adReward.destroy();
adReward = null;
});
adReward.onClose(function(e){
if(e.isEnded){
console.log('激励视频播放完成');
plus.nativeUI.toast('激励视频播放完成');
}else{
console.log('激励视频未播放完成关闭!')
}
adReward.destroy();
adReward = null;
});
adReward.load();
}
<a id="fullscreenvideo"/>
全屏视频广告
HBuilderX标准基座真机运行测试全屏视频广告位标识(adpid)为:1507000611
全屏视频广告与激励视频广告效果类似,在应用中可以在激励视频广告填充不足时用全屏视频广告来代替。
全屏视频与激励视频广告的差别:
- 不支持视频服务器回调校验事件
- 全屏视频在播放结束前(通常播放6-10秒)可以关闭,用户关闭视频不会提醒用户无法获取奖励
uni-app项目
使用已经封装成uni API。
详情参考:https://uniapp.dcloud.net.cn/api/a-d/full-screen-video
5+ App(WAP2APP)项目
全屏视频广告由FullScreenVideoAd管理。
操作步骤如下:
- 创建全屏视频广告对象 plus.ad.createFullScreenVideoAd,需传入在uni-ad平台申请的广告位标识adpid
- 监听全屏视频加载成功事件:调用广告对象的onLoad方法监听,加载成功后调用其 show 方法播放视频
- 监听全屏视频错误事件(可选):调用广告对象的onError方法监听错误,发生错误时可以尝试重新加载一次,如果还失败可以释放广告对象
- 监听全屏视频播放完成:调用广告对象的onClose发监听播放完成,触发此事件说用户已看完广告
- 加载激励视频广告:调用广告对象load
示例如下:
//视全屏激励广告
var adFull = null;
function fullVideoAd(){
if(adFull){
outLine('正在加载全屏视频广告');
return;
}
console.log('#全屏视频广告#');
adFull = plus.ad.createFullScreenVideoAd({adpid:'1507000611'}); // 注意替换为自己申请的adpid,此广告位标识仅在HBuilderX标准基座中有效
adFull.onLoad(function(){
console.log('加载成功')
adFull.show();
});
adFull.onError(function(e){
console.log('加载失败: '+JSON.stringify(e));
adFull.destroy();
adFull = null;
});
adFull.onClose(function(e){
if(e.isEnded){
console.log('全屏视频播放完成');
plus.nativeUI.toast('全屏视频播放完成');
}else{
console.log('全屏视频未播放完成关闭!')
}
adFull.destroy();
adFull = null;
});
adFull.load();
}
沉浸视频流广告
也称为Draw视频信息流广告
沉浸视频流广告为媒体提供了竖屏视频信息流广告样式,适合在全屏的竖屏视频中使用。目前仅提供了nvue的方式使用,使用方式可以参考文档:https://uniapp.dcloud.net.cn/component/ad-draw
HBuilderX3.0.0及以上版本开始支持。
内容联盟
内容联盟提供了单独的接口接入视频流供接入方接入。是一种支持用户上下滑动的切换视频的内容形式。
使用步骤:
- 通过使用plus.ad.showContentPage观看内容。
示例:
plus.ad.showContentPage({
adpid:"1111111112", // 1111111112为测试广告位,使用时请替换成自己的。
}, function(e) {
console.log("成功")
}, function(e) {
console.log(JSON.stringify(e))
});
<a id="rewarderror"/>
<a id="videoerror"/>
广告错误码
激励视频及全屏视频广告常见错误及处理建议:
- "-5001"
广告位标识adpid为空,请传入有效的adpid
请到广告平台申请广告位并获取adpid。 - "-5002"
无效的广告位标识adpid,请使用正确的adpid
请到广告平台确认使用的广告位标识adpid是否正确。 - "-5003"
未开通广告,请在广告平台申请并确保已审核通过
当前应用还没有开通广告,或者广告还没有审核通过,请到广告平台查看申请状态。 - "-5004"
无广告模块,打包时请配置要使用的广告模块
云端打包时没有勾选广告平台SDK,请参考前面“配置uni-ad广告模块”方法勾选后重新提交云端打包。 - "-5005"
广告加载失败,请稍后重试
加载视频激励广告失败,返回此错误时建议过一段时间再重新加载一次。 - "-5006"
广告未加载完成无法播放,请加载完成后再调show播放 - "-5007"
无法获取广告配置数据,请尝试重试
返回此错误时建议重新加载一次。 - "-5100"
其他错误,聚合广告商内部错误。 - 其他错误码及详细介绍请参考[https://uniapp.dcloud.net.cn/uni-ad/ad-error-code.html]
本地离线打包
Android平台参考:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/androidModuleConfig/uniad
iOS平台参数:https://nativesupport.dcloud.net.cn/AppDocs/usemodule/iOSModuleConfig/uniad

用Apple Developer申请苹果开发者账号新方式(支持支付宝微信付款申请)
因为近期苹果开发者账号在网页申请无法付款,所以苹果推出了Apple Developer 应用,支持在苹果手机申请开发者账号并且付款,支付宝微信银行卡都可以付款!
无法在网页申请的可以尝试这个方式去申请,亲测是可以申请成功,陆陆续续也有人申请成功,也有部分反馈说付款不了,具体可以看下面教程直接尝试下!
相关说明
1、需要更新到12.4以上,并且手机开通了密码或者指纹解锁
2、之前在网页提交申请但无法付款的apple id无法使用,需要新注册的开好双重未提交申请的苹果账号。
3、需要身份证号、人脸识别、绑定支付信息
4、付款成功后账号无需审核,马上生效使用。
5、以前旧的开发者续费也可以使用这个方式尝试下
因为近期苹果开发者账号在网页申请无法付款,所以苹果推出了Apple Developer 应用,支持在苹果手机申请开发者账号并且付款,支付宝微信银行卡都可以付款!
无法在网页申请的可以尝试这个方式去申请,亲测是可以申请成功,陆陆续续也有人申请成功,也有部分反馈说付款不了,具体可以看下面教程直接尝试下!
相关说明
1、需要更新到12.4以上,并且手机开通了密码或者指纹解锁
2、之前在网页提交申请但无法付款的apple id无法使用,需要新注册的开好双重未提交申请的苹果账号。
3、需要身份证号、人脸识别、绑定支付信息
4、付款成功后账号无需审核,马上生效使用。
5、以前旧的开发者续费也可以使用这个方式尝试下
收起阅读 »
VUE搭建脚手架Cli入门新手篇
一、那么我们就从最简单的环境搭建开始:
安装node.js,从node.js官网下载并安装node,安装过程很简单,一路“下一步”就可以了(傻瓜式安装)。安装完成之后,打开命令行工具(win+r,然后输入cmd),输入 node -v,如下图,如果出现相应的版本号,则说明安装成功。
这里需要说明下,因为在官网下载安装node.js后,就已经自带npm(包管理工具)了,另需要注意的是npm的版本最好是3.x.x以上,以免对后续产生影响。
安装淘宝镜像,打开命令行工具,把这个(npm install -g cnpm --registry= https://registry.npm.taobao.org)复制(这里要手动复制就是用鼠标右键那个,具体为啥不多解释),安装这里是因为我们用的npm的服务器是外国,有的时候我们安装“依赖”的时候很很慢很慢超级慢,所以就用这个cnpm来安装我们说需要的“依赖”。安装完成之后输入 cnpm -v,如下图,如果出现相应的版本号,则说明安装成功。
安装webpack,打开命令行工具输入:npm install webpack -g,安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。
安装vue-cli脚手架构建工具,打开命令行工具输入:npm install vue-cli -g,安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。
二、通过以上四步,我们需要准备的环境和工具都准备好了,接下来就开始使用vue-cli来构建项目
在硬盘上找一个文件夹放工程用的。这里有两种方式指定到相关目录:①cd 目录路径 ②如果以安装git的,在相关目录右键选择Git Bash Here
安装vue脚手架输入:vue init webpack exprice ,注意这里的“exprice” 是项目的名称可以说是随便的起名,但是需要主要的是“不能用中文”。
$ vue init webpack exprice --------------------- 这个是那个安装vue脚手架的命令
This will install Vue 2.x version of the template. ---------------------这里说明将要创建一个vue 2.x版本的项目
For Vue 1.x use: vue init webpack#1.0 exprice
? Project name (exprice) ---------------------项目名称
? Project name exprice
? Project description (A Vue.js project) ---------------------项目描述
? Project description A Vue.js project
? Author Datura --------------------- 项目创建者
? Author Datura
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n) --------------------- 是否安装Vue路由,也就是以后是spa(但页面应用需要的模块)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n ---------------------是否启用eslint检测规则,这里个人建议选no
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? (Y/n)
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? (Y/n)
? Setup e2e tests with Nightwatch? Yes
vue-cli · Generated "exprice".
To get started: --------------------- 这里说明如何启动这个服务
cd exprice
npm install
npm run dev
如下图:
cd 命令进入创建的工程目录,首先cd exprice(这里是自己建工程的名字);
安装项目依赖:npm install,因为自动构建过程中已存在package.json文件,所以这里直接安装依赖就行。不要从国内镜像cnpm安装(会导致后面缺了很多依赖库),但是但是如果真的安装“个把”小时也没成功那就用:cnpm install 吧
安装 vue 路由模块 vue-router 和网络请求模块 vue-resource,输入:cnpm install vue-router vue-resource --save。
创建完成的“exprice”目录如下:
下面我简单的说明下各个目录都是干嘛的:
启动项目,输入:npm run dev。服务启动成功后浏览器会默认打开一个“欢迎页面”,如下图:
注意:这里是默认服务启动的是本地的8080端口,所以请确保你的8080端口不被别的程序所占用。
一、那么我们就从最简单的环境搭建开始:
安装node.js,从node.js官网下载并安装node,安装过程很简单,一路“下一步”就可以了(傻瓜式安装)。安装完成之后,打开命令行工具(win+r,然后输入cmd),输入 node -v,如下图,如果出现相应的版本号,则说明安装成功。
这里需要说明下,因为在官网下载安装node.js后,就已经自带npm(包管理工具)了,另需要注意的是npm的版本最好是3.x.x以上,以免对后续产生影响。
安装淘宝镜像,打开命令行工具,把这个(npm install -g cnpm --registry= https://registry.npm.taobao.org)复制(这里要手动复制就是用鼠标右键那个,具体为啥不多解释),安装这里是因为我们用的npm的服务器是外国,有的时候我们安装“依赖”的时候很很慢很慢超级慢,所以就用这个cnpm来安装我们说需要的“依赖”。安装完成之后输入 cnpm -v,如下图,如果出现相应的版本号,则说明安装成功。
安装webpack,打开命令行工具输入:npm install webpack -g,安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。
安装vue-cli脚手架构建工具,打开命令行工具输入:npm install vue-cli -g,安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。
二、通过以上四步,我们需要准备的环境和工具都准备好了,接下来就开始使用vue-cli来构建项目
在硬盘上找一个文件夹放工程用的。这里有两种方式指定到相关目录:①cd 目录路径 ②如果以安装git的,在相关目录右键选择Git Bash Here
安装vue脚手架输入:vue init webpack exprice ,注意这里的“exprice” 是项目的名称可以说是随便的起名,但是需要主要的是“不能用中文”。
$ vue init webpack exprice --------------------- 这个是那个安装vue脚手架的命令
This will install Vue 2.x version of the template. ---------------------这里说明将要创建一个vue 2.x版本的项目
For Vue 1.x use: vue init webpack#1.0 exprice
? Project name (exprice) ---------------------项目名称
? Project name exprice
? Project description (A Vue.js project) ---------------------项目描述
? Project description A Vue.js project
? Author Datura --------------------- 项目创建者
? Author Datura
? Vue build (Use arrow keys)
? Vue build standalone
? Install vue-router? (Y/n) --------------------- 是否安装Vue路由,也就是以后是spa(但页面应用需要的模块)
? Install vue-router? Yes
? Use ESLint to lint your code? (Y/n) n ---------------------是否启用eslint检测规则,这里个人建议选no
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? (Y/n)
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? (Y/n)
? Setup e2e tests with Nightwatch? Yes
vue-cli · Generated "exprice".
To get started: --------------------- 这里说明如何启动这个服务
cd exprice
npm install
npm run dev
如下图:
cd 命令进入创建的工程目录,首先cd exprice(这里是自己建工程的名字);
安装项目依赖:npm install,因为自动构建过程中已存在package.json文件,所以这里直接安装依赖就行。不要从国内镜像cnpm安装(会导致后面缺了很多依赖库),但是但是如果真的安装“个把”小时也没成功那就用:cnpm install 吧
安装 vue 路由模块 vue-router 和网络请求模块 vue-resource,输入:cnpm install vue-router vue-resource --save。
创建完成的“exprice”目录如下:
下面我简单的说明下各个目录都是干嘛的:
启动项目,输入:npm run dev。服务启动成功后浏览器会默认打开一个“欢迎页面”,如下图:
注意:这里是默认服务启动的是本地的8080端口,所以请确保你的8080端口不被别的程序所占用。
收起阅读 »
iOS tabbar毛玻璃适配
一直对iOS的毛玻璃效果很热衷,体验也确实不错。Android平台虽然也能实现,但是效果感觉一直没有iOS好,而且用的App好像也不多。
最近在用flutter写地图应用,flutter对毛玻璃支持较好。但是我的应用是一个地图应用,想在地图上面覆盖一层毛玻璃,显示一些图标文字,想想挺漂亮,居然实现不了。估计和地图是platformview有关系。
看到官方在最新的版本中加入了iOS tabbar毛玻璃效果,立马拿过来用一用。
发现一个问题:之前全局设置了安全区域safearea,iPhoneX等刘海机型能很好的适配。
现在为了毛玻璃效果,需要去掉safeara底部适配,这是全局性质的,导致其他页面都没有safeara底部适配了,每个页面都需要单独设置,挺麻烦。
没办法,只能每个页面单独设置了,下面是我的方法,有点麻烦。小伙伴有更好的方法希望能分享一下哦~
最外面的一个container加个padding,如果底部有悬浮按钮,为了避免在按钮下面穿透,需要也加个padding。
<view class="container"></view>
.container {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
# 如果底部有悬浮按钮,需要这样处理,加一个padding
.bottom-review {
background-color: white;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 10px 10px;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
border-top: #f6f6f6 solid 1px;
}
这是效果图:
一直对iOS的毛玻璃效果很热衷,体验也确实不错。Android平台虽然也能实现,但是效果感觉一直没有iOS好,而且用的App好像也不多。
最近在用flutter写地图应用,flutter对毛玻璃支持较好。但是我的应用是一个地图应用,想在地图上面覆盖一层毛玻璃,显示一些图标文字,想想挺漂亮,居然实现不了。估计和地图是platformview有关系。
看到官方在最新的版本中加入了iOS tabbar毛玻璃效果,立马拿过来用一用。
发现一个问题:之前全局设置了安全区域safearea,iPhoneX等刘海机型能很好的适配。
现在为了毛玻璃效果,需要去掉safeara底部适配,这是全局性质的,导致其他页面都没有safeara底部适配了,每个页面都需要单独设置,挺麻烦。
没办法,只能每个页面单独设置了,下面是我的方法,有点麻烦。小伙伴有更好的方法希望能分享一下哦~
最外面的一个container加个padding,如果底部有悬浮按钮,为了避免在按钮下面穿透,需要也加个padding。
<view class="container"></view>
.container {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
# 如果底部有悬浮按钮,需要这样处理,加一个padding
.bottom-review {
background-color: white;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 10px 10px;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
border-top: #f6f6f6 solid 1px;
}
这是效果图:
收起阅读 »
vue多个组件合并到一个页面,组件共
把test1.vue,test2合并到test3中一起显示
test1.vue
<template>
<view>
test1
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
test2.vue
<template>
<view>
test2
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
test3.vue
<template>
<view>
test333
<test1></test1>
<test2></test2>
</view>
</template>
<script>
import test1 from '../test1/test1'
import test2 from '../test2/test2'
export default {
components: {
test1,
test2
},
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
把test1.vue,test2合并到test3中一起显示
test1.vue
<template>
<view>
test1
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
test2.vue
<template>
<view>
test2
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
test3.vue
<template>
<view>
test333
<test1></test1>
<test2></test2>
</view>
</template>
<script>
import test1 from '../test1/test1'
import test2 from '../test2/test2'
export default {
components: {
test1,
test2
},
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>
收起阅读 »

ios 获取本地文件失败
<body>
<video onclick="aa()" class='my-video' playsinline="" webkit-playsinline="" autoplay='autoplay' controls='controls' style='height: 300px !important;width: 100%;'>
<source src='file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV' type='video/mp4'></source>
</video>
</body>
<script type="text/javascript">
function aa(){
var filepath = "file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV";
//var filepath = "_doc/filecache/124APPLE/IMG_4156.MOV";
var fpath = decodeURI(filepath);
alert(fpath);
docompress(fpath);
}
function docompress(path) {
plus.zip.compress(path,path,function(){
alert("success:"+path);
uploadVideoZip(path);
},function(error) {
alert("fail:"+JSON.stringify(error));
});
}
</script>
[结果]
{"code":-4,"message":"文件不存在"}
<body>
<video onclick="aa()" class='my-video' playsinline="" webkit-playsinline="" autoplay='autoplay' controls='controls' style='height: 300px !important;width: 100%;'>
<source src='file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV' type='video/mp4'></source>
</video>
</body>
<script type="text/javascript">
function aa(){
var filepath = "file:///var/mobile/Media/DCIM/124APPLE/IMG_4156.MOV";
//var filepath = "_doc/filecache/124APPLE/IMG_4156.MOV";
var fpath = decodeURI(filepath);
alert(fpath);
docompress(fpath);
}
function docompress(path) {
plus.zip.compress(path,path,function(){
alert("success:"+path);
uploadVideoZip(path);
},function(error) {
alert("fail:"+JSON.stringify(error));
});
}
</script>
[结果]
{"code":-4,"message":"文件不存在"}
收起阅读 »
uni微信app支付
//微信app支付 demo
var orderinfo = dat.data;
uni.requestPayment({
provider:"wxpay",
orderInfo:JSON.stringify(orderinfo),
success:function(res){
uni.showToast({
title:"支付成功",
icon:"success",
duration:2000,
complete:function(){
vm.goback();
}
});
},
fail:function(res){
uni.showToast({
title: '支付失败,请重新支付',
icon: "none",
duration: 2000,
});
console.log(JSON.stringify(res));
}
});
// 返回数据为
"data": {
"code": 1,
"message": "Success",
"timestamp": 1576656961,
"data": {
"package": "Sign=WXPay",
"out_trade_no": "02ci00110000i",
"appid": "*************************",
"sign": "0DC67CB535F781E6DAF5D809281C5725",
"partnerid": "1480005102",
"prepayid": "wx18161601019540994e6f2cf31556221700",
"noncestr": "ce4dda6d083686058663bf27cb58f704",
"timestamp": "1576656961"
}
},
注:本地测试可能包名有问题 ,建议云打包测试,包名与微信申请的包名填写一致就可以了。
//微信app支付 demo
var orderinfo = dat.data;
uni.requestPayment({
provider:"wxpay",
orderInfo:JSON.stringify(orderinfo),
success:function(res){
uni.showToast({
title:"支付成功",
icon:"success",
duration:2000,
complete:function(){
vm.goback();
}
});
},
fail:function(res){
uni.showToast({
title: '支付失败,请重新支付',
icon: "none",
duration: 2000,
});
console.log(JSON.stringify(res));
}
});
// 返回数据为
"data": {
"code": 1,
"message": "Success",
"timestamp": 1576656961,
"data": {
"package": "Sign=WXPay",
"out_trade_no": "02ci00110000i",
"appid": "*************************",
"sign": "0DC67CB535F781E6DAF5D809281C5725",
"partnerid": "1480005102",
"prepayid": "wx18161601019540994e6f2cf31556221700",
"noncestr": "ce4dda6d083686058663bf27cb58f704",
"timestamp": "1576656961"
}
},
注:本地测试可能包名有问题 ,建议云打包测试,包名与微信申请的包名填写一致就可以了。
收起阅读 »

关于HBuilderX自定义字体的说明
设置字体
点击菜单【工具】【设置】【常用配置】,然后选择相应字体
如何自定义字体?
字体列表,包含了操作系统内所有已安装的字体。
自定义,只能填写操作系统内已有的字体;
如需要自定义,则需要先安装相应字体。
示例
以source-code-pro为例。
下载字体后,点击安装,安装成功后就会出现字体列表中。
设置字体
点击菜单【工具】【设置】【常用配置】,然后选择相应字体
如何自定义字体?
字体列表,包含了操作系统内所有已安装的字体。
自定义,只能填写操作系统内已有的字体;
如需要自定义,则需要先安装相应字体。
示例
以source-code-pro为例。
下载字体后,点击安装,安装成功后就会出现字体列表中。
收起阅读 »