前言
最近公司产品有个需求,为我们app中每个H5应用实现添加桌面快捷方式的功能,与支付宝健康码添加到桌面一样。所以特地研究了下相关产品中该功能的实现方式,如哈罗单车和支付宝。
原理
在 app 里面通过 OpenURL 方式 跳转到Safari 浏览器,打开一个引导页面,然后点击添加到主屏幕,如下图:
哈罗单车的实现
哈罗单车在Safari中展示的是一整张引导图片,为啥这里说一整张图呢,因为支付宝展示的引导页是由多张图片拼接而成。
哈罗单车不同的业务使用快捷方式,需要UI每次做一张引导图,或者做一张通用图片当引导图。
探索支付宝添加桌面快捷方式的实现
- 在关闭网络的情况下,在支付宝健康码中点击"添加到桌面",跳转到Safari浏览器,得到url
https://render.alipay.com/p/s/shortcut/index?appId=20000067&appName=杭州健康码&appIcon=https://gw.alicdn.com/tfs/TB1nUmbzUT1gK0jSZFhXXaAtVXa-1024-1024.png&appUrl=https%3A%2F%2Fh5.dingtalk.com%2FhealthAct%2Findex.html
可以看出url为https://render.alipay.com/p/s/shortcut/index,后面参数有appId,appName,appIcon,appUrl
- 支付宝小程序也同样有添加到桌面的功能,同样的方式获取到小程序跳转到Safari浏览器中的url,这里以"体育服务"小程序为例
https://render.alipay.com/p/s/shortcut/index?appId=2018073060792690&appName=体育服务&appIcon=https://appstoreisvpic.alipayobjects.com/prod/40ad067a-899a-4d31-89ba-c8758c405d36.png@120w.png
-
我们将支付宝健康码页面的url在mac上的Safari 浏览器打开,查看元素
可以看到页面主要有index.html,index.js,还有几张图片
从这张图我们可以猜到,支付宝是通过html和js将appIcon动态嵌入到上图相应的位置,以此来实现safari中的引导页面。
比较
支付宝的实现方式更加高明,采用动态拼接图片的方式,业务方只需要传入appIcon。
但是哈罗单车主要是靠app客户端实现的,支付宝则是靠Web端来实现。
支付宝总体实现
通过对index.html和index.js的分析,得出支付宝实现方式如下:
app客户端请求站点https://render.alipay.com/p/s/shortcut/index,并将appId,appName,appIcon,appUrl等传递过去。站点下index.html中接收url中携带的参数值,生成对应的base64。
这样app客户端实现起来很简单,只需要打开url并将参数传递过去即可。剩下的交由Web端开发来做。
//支付宝实现应用桌面快捷方式
//url中不含中文时,可以不对url进行unicode转码,使用https://render.alipay.com/p/s/shortcut/index?appId=20000067&appName=123&appIcon=https://gw.alicdn.com/tfs/TB1nUmbzUT1gK0jSZFhXXaAtVXa-1024-1024.png&appUrl=https://Fh5.dingtalk.com/FhealthAct/Findex.html也可以
NSURL *URL = [NSURL URLWithString:@"https://render.alipay.com/p/s/shortcut/index?appId=20000067&appName=123&appIcon=https://gw.alicdn.com/tfs/TB1nUmbzUT1gK0jSZFhXXaAtVXa-1024-1024.png&appUrl=https%3A%2F%2Fh5.dingtalk.com%2FhealthAct%2Findex.html"];
UIApplication *application = [UIApplication sharedApplication];
NSURL *URL = [NSURL URLWithString:urlString];
if (@available(iOS 10.0, *)) {
[application openURL:URL options:@{}
completionHandler:^(BOOL success) {
}];
} else {
[application openURL:URL];
}
但是我身为一个ios开发,可不可以不借助Web端开发,自己单独来实现呢?
如果自己单独实现,需要解决哪些问题呢?
- 首先我从mac端浏览器中可以获取到index.html和index.js和图片等资源文件
- 我们需要将支付宝的index.html文件中原本通过url中获取参数的形式改为app本地替换
步骤
1. 开发引导页面
我们首先在app项目工程的根目录下创建一个文件夹Web,文件夹下存放引导页面
content.html
content.html是拷贝的是支付宝index.html文件,对其做了修改。
- 支付宝的index.html文件通过从浏览器的url中获取参数,通过js函数getQueryParams实现。因为我的参数没有放在URL中,所以通过getQueryParams函数获取到的参数都是空的。可以给appId等参数赋个默认值,这样就实现了本地参数替换。
var query = getQueryParams(location.href);
var { appId = "TransitCodeAppId", appName= "TransitCodeAppTitle", appIcon = "TransitCodeAppIcon", appUrl = "ezvizsaas://123", scheme = "TransitCodeAppScheme" } = query;
修改过后的content.html代码如下:
将其中的"企业萤石云"改成自己的主app名称即可,也可使用字符串进行动态替换。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title id="J_desktopTitle">企业萤石云</title>
<meta http-equiv="cache-control" content="no-cache" />
<meta content="yes" name="apple-touch-fullscreen" />
<meta content="yes" name="apple-mobile-web-app-capable" />
<meta content="white" name="apple-mobile-web-app-status-bar-style" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, minimal-ui" />
<meta name="data-aspm" content="a192" />
<link id="J_desktopIcon" sizes="114x114" rel="apple-touch-icon-precomposed" />
<a href="TransitCodeAppScheme" id="qbt" style="display:none"></a>
<script>
var Base64 = function() {
var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var _utf8_encode = function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
this.encode = function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = _utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
return output;
};
};
var getQuery = function(url) {
var query = {};
var hashParts = url.split('#');
var urlParts = hashParts[0].split('?');
if (urlParts.length > 1) {
// 有query
var queryParts = urlParts[1].split('&');
for (var i = 0, len = queryParts.length; i < len; i++) {
var items = queryParts[i].split('=');
query[items[0]] = decodeURIComponent(items[1]);
}
}
return query;
};
var htmlEncode = function(myStr) {
myStr = myStr || '';
//myStr = myStr.replace(/&/g, "&");
myStr = myStr.replace(/'/g, "'");
myStr = myStr.replace(/"/g, """);
myStr = myStr.replace(/</g, "<");
myStr = myStr.replace(/>/g, ">");
myStr = myStr.replace(/\s/g, ' ');
return myStr;
};
var iOSversion = function() {
if (/iP(hone|od|ad)/.test(navigator.platform)) {
var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)];
}
}
var base64 = new Base64();
var query = getQuery(location.href);
var { appId = "TransitCodeAppId", appName= "TransitCodeAppTitle", appIcon = "TransitCodeAppIcon", appUrl = "ezvizsaas://123", scheme = "TransitCodeAppScheme" } = query;
<!-- var { appId, appName, appIcon, appUrl, scheme } = query;-->
var sysVer = iOSversion();
appName = htmlEncode(appName);
appIcon = htmlEncode(appIcon);
if ((appId || scheme) && appName && appIcon) {
if (sysVer[0] < 14) {
// 14 以下的版本走离线版
var htmlTpl = "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title id=\"J_desktopTitle\">\u652F\u4ED8\u5B9D<\/title><meta content=\"yes\" name=\"apple-touch-fullscreen\"><meta content=\"yes\" name=\"apple-mobile-web-app-capable\"><meta content=\"white\" name=\"apple-mobile-web-app-status-bar-style\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,minimal-ui\"><meta name=\"data-aspm\" content=\"a192\"><link id=\"J_desktopIcon\" sizes=\"114x114\" rel=\"apple-touch-icon-precomposed\"><a href=\"TransitCodeAppScheme" id=\"qbt\" style=\"display:none\"><\/a>
<\/head><script>window._to={autoExpo:!0},document.documentElement.style.fontSize=100*document.documentElement.clientWidth\/375+\"px\"<\/script><script>!function(r){function n(t){if(a[t])return a[t].exports;var e=a[t]={exports:{},id:t,loaded:!1};return r[t].call(e.exports,e,e.exports,n),e.loaded=!0,e.exports}var a={};n.m=r,n.c=a,n.p=\"\",n(0)}([function(t,e){\"use strict\";!function(){if(!window.Tracert){for(var o={_isInit:!0,_readyToRun:[],_guid:function(){return\"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(\/[xy]\/g,function(t){var e=16*Math.random()|0;return(\"x\"===t?e:3&e|8).toString(16)})},get:function(t){if(\"pageId\"!==t)return this[t];if(window._tracert_loader_cfg=window._tracert_loader_cfg||{},window._tracert_loader_cfg.pageId)return window._tracert_loader_cfg.pageId;var e=document.querySelectorAll(\"meta[name=data-aspm]\"),r=e&&e[0].getAttribute(\"content\"),n=document.body&&document.body.getAttribute(\"data-aspm\"),a=r&&n?r+\".\"+n+\"_\"+o._guid()+\"_\"+Date.now():\"-_\"+o._guid()+\"_\"+Date.now();return window._tracert_loader_cfg.pageId=a},call:function(){var e,r=arguments;try{e=[].slice.call(r,0)}catch(t){var n=r.length;e=[];for(var a=0;a<n;a++)e.push(r[a])}o.addToRun(function(){o.call.apply(o,e)})},addToRun:function(t){var e=t;\"function\"==typeof e&&(e._logTimer=new Date-0,o._readyToRun.push(e))}},t=[\"config\",\"logPv\",\"info\",\"err\",\"click\",\"expo\",\"pageName\",\"pageState\",\"time\",\"timeEnd\",\"parse\",\"checkExpo\",\"stringify\",\"report\",\"set\",\"before\"],e=0;e<t.length;e++){!function(t){o[t]=function(){var e,r=arguments;try{e=[].slice.call(r,0)}catch(t){var n=r.length;e=[];for(var a=0;a<n;a++)e.push(r[a])}e.unshift(t),o.addToRun(function(){o.call.apply(o,e)})}}(t[e])}window.Tracert=o}}()}])<\/script><script src=\"index.js\"><\/script><style>*{margin:0;padding:0}body,html{height:100%;width:100%;overflow:hidden;background:#f3f2f2;text-align:center}.backguide{height:100%;width:100%}.backguide .enter{position:absolute;height:.44rem;width:3.43rem;background:#108ee9;border:0;font-size:.18rem;color:#fff;bottom:.52rem;left:.16rem;border-radius:.02rem}.backguide .tips{margin-top:1.29rem;font-size:.14rem;line-height:.2rem;color:#999}.backguide .icon{height:.71rem;width:.71rem;border-radius:.14rem}.backguide .appname{font-size:.18rem;line-height:.25rem;color:#333;margin-top:-.14rem}.main{color:#333;text-align:center}.subject{margin-top:.3rem;font-size:.2rem}.preview{margin-top:.36rem;position:relative}.preview .content{position:relative;margin:0 .55rem}.preview .bg{width:2.5rem;margin-left:.05rem;position:relative}.preview .icon{position:absolute;display:block;border-radius:21%}.preview .title{position:absolute;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.preview .icon.large{width:1.25rem;height:1.25rem;left:1.22rem;top:.42rem}.preview .title.large{width:1.5rem;left:1.13rem;top:1.7rem;font-size:.17rem}.preview .icon.small{width:.14rem;height:.14rem;left:.8rem;top:1.125rem}.preview .title.small{width:.16rem;left:.78rem;top:1.266rem;font-size:.025rem}.guide{width:100%;position:absolute;left:0;bottom:.3rem}.guide .content{position:relative;z-index:20;width:3.5rem;padding-top:.16rem;padding-bottom:.06rem;margin:0 auto;border-radius:.04rem;box-shadow:0 6px 15px rgba(0,0,0,.13);background:#fff;font-size:.14rem}.guide .tips{position:relative;z-index:20}.guide .icon{width:.2rem;height:.24rem;margin:0 .035rem .02rem;vertical-align:bottom}.guide .toolbar{width:100%;height:auto;margin-top:-.12rem;position:relative;z-index:10}.guide .arrow{width:.27rem;height:auto;position:absolute;left:50%;bottom:-.26rem;margin-left:-.135rem;z-index:10}<\/style><body data-aspm=\"b15502\"><div id=\"B_container\" class=\"backguide\" style=\"display:none\"><div class=\"tips\">\u4F60\u5373\u5C06\u8FDB\u5165<\/div><img class=\"icon\" src=\"" + appIcon + "\"><div class=\"appname\">" + appName + "<\/div><button class=\"enter\" onclick=\"jumpSchema()\" data-aspm=\"c37816.d76317\" data-aspm-expo data-aspm-param=\"appId=" + appId + "\">\u7ACB\u5373\u8FDB\u5165<\/button><\/div><div class=\"main\" id=\"J_container\" style=\"display:none\"><div class=\"subject\">\u6DFB\u52A0\u670D\u52A1\u5230\u684C\u9762<\/div><div class=\"preview\"><div class=\"content\"><img class=\"bg\" src=\"https:\/\/gw.alipayobjects.com\/zos\/rmsportal\/ceOiTFtaubdHkhSrNfPj.jpg\"> <img class=\"icon small\" src=\"" + appIcon + "\"> <img class=\"icon large\" src=\"" + appIcon + "\"><div class=\"title large\">" + appName + "<\/div><\/div><\/div><div class=\"guide\"><div class=\"content\"><p class=\"tips\">\u70B9\u51FB\u4E0B\u65B9\u5DE5\u5177\u680F\u4E0A\u7684<img class=\"icon\" src=\"https:\/\/gw.alipayobjects.com\/zos\/rmsportal\/XEbFrgamEdvSxVFOBeuZ.png\"><\/p><p class=\"tips\">\u5E76\u9009\u62E9<img class=\"icon\" src=\"https:\/\/gw.alipayobjects.com\/zos\/rmsportal\/IkKEhyTLQpYtqXMZBYtQ.png\">\u201C<strong>\u6DFB\u52A0\u5230\u4E3B\u5C4F\u5E55<\/strong>\u201D<\/p><img class=\"toolbar\" src=\"https:\/\/gw.alipayobjects.com\/zos\/rmsportal\/oFNuXVhPJYvBDJPXJTmt.jpg\"><\/div><img class=\"arrow\" src=\"https:\/\/gw.alipayobjects.com\/zos\/rmsportal\/FlBEnTRnlhMyLyVhlfZT.png\"><\/div><\/div><script>function jumpSchema(){window.location.href=window.AlipaySchema;window.Tracert.click(\"c37816.d76317\",{appId:'" + (appId || "") + "'})}function getIOSversion(){if(\/iP(hone|od|ad)\/.test(navigator.platform)){var e=navigator.appVersion.match(\/OS (\\d+)_(\\d+)_?(\\d+)?\/);return[parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3]||0,10)]}}var appId='" + (appId || "") + "',appUrl='" + (appUrl || "") + "',scheme='" + (scheme || "") + "',href=\"\";if(href=scheme||\"TransitCodeAppScheme:\/\/platformapi\/startapp?appId=" + appId + "&chInfo=ch_desktop\"+(appUrl=appUrl?\"&url=\"+encodeURIComponent(appUrl):\"\"),window.AlipaySchema=href,window.navigator.standalone){var v=getIOSversion();13<=v[0]&&(document.getElementById(\"B_container\").style.display=\"block\",window.Tracert.call(\"expoCheck\")),window.location.href=window.AlipaySchema}else document.getElementById(\"J_container\").style.display=\"block\",document.getElementById(\"J_desktopTitle\").textContent=\"" + appName + "\",document.getElementById(\"J_desktopIcon\").setAttribute(\"href\",\"" + appIcon + "\")<\/script><\/body><\/html>";;
var base64Tpl = base64.encode(htmlTpl);
location.href = 'data:text/html;base64,' + base64Tpl;
}
//} else { // iOS 13 以上用在线页面当快捷方式,解决 -1 屏搜索点击不动的兼容性问题
// location.href = '' + '?appId=' + appId + '&scheme=' + scheme + '&appUrl=' + appUrl + '&appName=' + appName + '&appIcon=' + appIcon;
//}
} else {
alert('参数错误');
}
</script>
<script>
window._to = { autoExpo: true };
document.documentElement.style.fontSize = document.documentElement.clientWidth * 100 / 375 + 'px';
</script>
<script>!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";!function(){if(!window.Tracert){for(var Tracert={_isInit:!0,_readyToRun:[],_guid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=16*Math.random()|0,v="x"===c?r:3&r|8;return v.toString(16)})},get:function(key){if("pageId"===key){if(window._tracert_loader_cfg=window._tracert_loader_cfg||{},window._tracert_loader_cfg.pageId)return window._tracert_loader_cfg.pageId;var metaa=document.querySelectorAll("meta[name=data-aspm]"),spma=metaa&&metaa[0].getAttribute("content"),spmb=document.body&&document.body.getAttribute("data-aspm"),pageId=spma&&spmb?spma+"."+spmb+"_"+Tracert._guid()+"_"+Date.now():"-_"+Tracert._guid()+"_"+Date.now();return window._tracert_loader_cfg.pageId=pageId,pageId}return this[key]},call:function(){var argsList,args=arguments;try{argsList=[].slice.call(args,0)}catch(ex){var argsLen=args.length;argsList=[];for(var i=0;i<argsLen;i++)argsList.push(args[i])}Tracert.addToRun(function(){Tracert.call.apply(Tracert,argsList)})},addToRun:function(_fn){var fn=_fn;"function"==typeof fn&&(fn._logTimer=new Date-0,Tracert._readyToRun.push(fn))}},fnlist=["config","logPv","info","err","click","expo","pageName","pageState","time","timeEnd","parse","checkExpo","stringify","report","set","before"],i=0;i<fnlist.length;i++){var fn=fnlist[i];!function(fn){Tracert[fn]=function(){var argsList,args=arguments;try{argsList=[].slice.call(args,0)}catch(ex){var argsLen=args.length;argsList=[];for(var i=0;i<argsLen;i++)argsList.push(args[i])}argsList.unshift(fn),Tracert.addToRun(function(){Tracert.call.apply(Tracert,argsList)})}}(fn)}window.Tracert=Tracert}}()}]);</script>
<script src="index.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
html,body{
height: 100%;
width: 100%;
overflow: hidden;
background: #F3F2F2;
text-align: center;
}
.backguide {
height: 100%;
width: 100%;
}
.backguide .enter {
position: absolute;
height: 0.44rem;
width: 3.43rem;
background: #108EE9;
border: 0;
font-size: 0.18rem;
color: white;
bottom: 0.52rem;
left: 0.16rem;
border-radius: 0.02rem;
}
.backguide .tips {
margin-top: 1.29rem;
font-size: 0.14rem;
line-height: 0.2rem;
color: #999999;
}
.backguide .icon {
height: 0.71rem;
width: 0.71rem;
border-radius: 0.14rem;
}
.backguide .appname {
font-size: 0.18rem;
line-height: 0.25rem;
color: #333333;
margin-top: -0.14rem;
}
.main {
color: #333;
text-align: center;
}
.subject {
margin-top: .3rem;
font-size: 0.2rem;
}
.preview {
margin-top:0.36rem;
position: relative;
}
.preview .content {
position: relative;
margin: 0 0.55rem;
}
.preview .bg {
width: 2.5rem;
margin-left: 0.05rem;
position: relative;
}
.preview .icon {
position: absolute;
display: block;
border-radius: 21%;
}
.preview .title {
position: absolute;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview .icon.large {![548341-fb5e56e2ad03a85a.png](https://upload-images.jianshu.io/upload_images/1679132-bcfab3ad95c4a826.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
width: 1.25rem;
height: 1.25rem;
left: 1.22rem;
top: .42rem
}
.preview .title.large {
width: 1.5rem;
left: 1.13rem;
top: 1.70rem;
font-size: 0.17rem;
}
.preview .icon.small {
width: 0.14rem;
height: 0.14rem;
left: 0.8rem;
top: 1.125rem;
}
.preview .title.small {
width: 0.16rem;
left: 0.78rem;
top: 1.266rem;
font-size: 0.025rem;
}
.guide {
width: 100%;
position: absolute;
left: 0;
bottom: .30rem;
}
.guide .content {
position: relative;
z-index: 20;
width: 3.50rem;
padding-top: 0.16rem;
padding-bottom: 0.06rem;
margin: 0 auto;
border-radius: 0.04rem;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.13);
background: #fff;
font-size: .14rem;
}
.guide .tips {
position: relative;
z-index: 20;
}
.guide .icon {
width: 0.20rem;
height: 0.24rem;
margin: 0 0.035rem 0.02rem;
vertical-align: bottom;
}
.guide .toolbar {
width: 100%;
height: auto;
margin-top: -0.12rem;
position: relative;
z-index: 10;
}
.guide .arrow {
width: 0.27rem;
height: auto;
position: absolute;
left: 50%;
bottom: -0.26rem;
margin-left: -0.135rem;
z-index: 10;
}
</style>
</head>
<body data-aspm="b15502">
<div id="B_container" class="backguide" style="display: none;">
<div class="tips">你即将进入</div>
<img id="B_icon" class="icon" src=""></img>
<div id="B_appname" class="appname">企业萤石云</div>
<button class="enter" onclick="jumpSchema()" data-aspm="c37816.d76317" data-aspm-expo data-aspm-param="appId=${appId}">立即进入</button>
</div>
<div class="main" id="J_container" style="display: none;">
<div class="subject">添加服务到桌面</div>
<div class="preview">
<div class="content">
<img class="bg" src="https://gw.alipayobjects.com/zos/rmsportal/ceOiTFtaubdHkhSrNfPj.jpg">
<img id="J_iconsmall" class="icon small" src="">
<img id="J_iconlarge" class="icon large" src="">
<div id="J_appname" class="title large">企业萤石云</div>
</div>
</div>
<div class="guide">
<div class="content">
<p class="tips">点击下方工具栏上的<img class="icon" src="https://gw.alipayobjects.com/zos/rmsportal/XEbFrgamEdvSxVFOBeuZ.png"></p>
<p class="tips">并选择<img class="icon" src="https://gw.alipayobjects.com/zos/rmsportal/IkKEhyTLQpYtqXMZBYtQ.png">“<strong>添加到主屏幕</strong>”</p>
<img class="toolbar" src="https://gw.alipayobjects.com/zos/rmsportal/oFNuXVhPJYvBDJPXJTmt.jpg">
</div>
<img class="arrow" src="https://gw.alipayobjects.com/zos/rmsportal/FlBEnTRnlhMyLyVhlfZT.png">
</div>
</div>
<script>
function jumpSchema() {
var lnk = document.getElementById("qbt").click();
<!-- window.location.href = window.AlipaySchema;-->
<!-- var appId = window.AlipayShortCutAppId;-->
<!-- window.Tracert.click('c37816.d76317', {appId: appId});-->
}
function getQueryParams(url) {
var query = {};
var hashParts = url.split('#');
var urlParts = hashParts[0].split('?');
if (urlParts.length > 1) {
// 有query
var queryParts = urlParts[1].split('&');
for (var i = 0, len = queryParts.length; i < len; i++) {
var items = queryParts[i].split('=');
query[items[0]] = decodeURIComponent(items[1]);
}
}
return query;
};
function getIOSversion() {
if (/iP(hone|od|ad)/.test(navigator.platform)) {
var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)];
}
}
var query = getQueryParams(location.href);
var { appId = "TransitCodeAppId", appName= "TransitCodeAppTitle", appIcon = "TransitCodeAppIcon", appUrl = "ezvizsaas://123", scheme = "TransitCodeAppScheme" } = query;
window.AlipayShortCutAppId = appId;
var href = '';
if (scheme) {
href = scheme;
} else {
appUrl = appUrl ? '&url=' + encodeURIComponent(appUrl) : '';
href = `TransitCodeAppScheme`;
}
window.AlipaySchema = href;
if (window.navigator.standalone) {
var v = getIOSversion();
if (v[0] >= 13) {
document.getElementById('B_container').style.display = 'block';
document.getElementById('B_icon').setAttribute('src', appIcon);
document.getElementById('B_appname').textContent = appName;
window.Tracert.call('expoCheck');
}
var lnk = document.getElementById("qbt").click();
<!-- window.location.href = window.AlipaySchema;-->
} else {
document.getElementById('J_container').style.display = 'block';
document.getElementById('J_desktopTitle').textContent = appName;
document.getElementById('J_desktopIcon').setAttribute('href', appIcon);
document.getElementById('J_iconsmall').setAttribute('src', appIcon);
document.getElementById('J_iconlarge').setAttribute('src', appIcon);
document.getElementById('J_appname').textContent = appName;
}
</script>
</body>
</html>
代码中包含的一些字符串含义如下:
TransitCodeAppIcon:快捷方式在桌面的图标
TransitCodeAppTitle:快捷方式的名称
TransitCodeAppScheme:跳转页面对应的 scheme,比如 ezvizsaas://web/app?appId=1
通过这几个字符串代替实际内容,就可以做到动态替换。"企业萤石云"替换为自己的主app名称即可。
这里通过 window.navigator.standalone 可以知道引导页是否是全屏展示,如果是全屏那么跳转到 app 对应页面,非全屏则插入具体 HTML 内容,展示引导内容。
index.js
index.js中代码过长,由于简书对内容长度的限制,会导致无法发布,这里就不贴出来了。
index.js用的是支付宝的代码,其中许多代码是无用的,不过不影响我们的业务,可以不用删减。js中的一些关于"支付宝"的文本可以替换成我们的。
2. 使用 Data URI Scheme 技术将引导页转为一个字符串
代码如下
//iconUrl:桌面图标 appTitle:默认app名称
- (NSString *)oppcreateDesktopWithPreUrl:(NSString *)preUrl iconUrl:(NSString *)iconUrl appTitle:(NSString *)title scheme:(NSString *)scheme {
if ([preUrl length] == 0) {
return nil;
}
NSString *contentHtmlString = [self contentHtmlWithIconImageString:iconUrl title:title appScheme:scheme];
NSData *data = [contentHtmlString dataUsingEncoding:NSUTF8StringEncoding];
contentHtmlString = [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; // base64格式的字符串;
NSString *DataURIString = [NSString stringWithFormat:@"data:text/html;charset=utf-8;base64,%@",contentHtmlString];
NSString *urlString = [NSString stringWithFormat:@"%@%@", preUrl, DataURIString];
return urlString;
}
-(NSString *)contentHtmlWithIconImageString:(NSString *)iconImageString title:(NSString *)title appScheme:(NSString *)scheme{
NSString *contentHtmlPath = [self getcontentHTMLTempletPath];
NSString *contentHtmlString = [NSString stringWithContentsOfFile:contentHtmlPath encoding:NSUTF8StringEncoding error:nil];
/*替换字符串*/
contentHtmlString = [contentHtmlString stringByReplacingOccurrencesOfString:@"TransitCodeAppIcon" withString:iconImageString];
contentHtmlString = [contentHtmlString stringByReplacingOccurrencesOfString:@"TransitCodeAppTitle" withString:title];
contentHtmlString = [contentHtmlString stringByReplacingOccurrencesOfString:@"TransitCodeAppScheme" withString:scheme];
return contentHtmlString;
}
- (NSString *)getcontentHTMLTempletPath{
NSString * path = [[NSBundle mainBundle] pathForResource:@"content" ofType:@"html"];
return path;
}
Data URI Scheme 技术就是将一个文件,图片或者HTML文件等等,通过 Base64 加密等转为字符串放到 HTML 里面直接加载,这样就不用请求服务器了。这里我们需要将上面介绍的本地的引导 HTML 页面转为一个字符串,后面要将这个字符串拼到服务器HTML url 后面。
看代码可以知道,通过 getcontentHTMLTempletPath 方法拿到本地 HTML 文件路径,然后将里面内容进行替换为我们应用的快捷方式相关内容。然后将 HTML 内容转为 base64 编码。最后通过将 base64 编码按照特定格式拼接即可,这里是
[NSString stringWithFormat:@"data:text/html;charset=utf-8;base64,%@",contentHtmlString];以 data:text/html;charset=utf-8;base64, 开头,后面我们可以看到存储的时候也是保存的这个。
3. 打开服务器 HTML 页,用本地引导页替换
用上面生成的字符串拼到服务器 HTML 页面url (这里我是直接将 HTML 文件发给Web端同事,放到 Web网站上面然后拿到链接)后面,代码如下
//下面是添加快捷方式到桌面的相关代码
- (void)addShortcutToDesk {
NSString *baseUrl = @"https://www.baidu.com"; //这里将https://www.baidu.com替换为你自己的shortcut-middle.html所在的Web服务器地址
NSString *preUrl = [NSString stringWithFormat:@"%@/shortcut-middle.html",baseUrl];
preUrl = [NSString stringWithFormat:@"%@?dataurl=", preUrl];
NSString *title = [self.webAppParams objectForKey:@"appName"];
NSString *scheme = [NSString stringWithFormat:@"ezvizsaas://web/app?appId=%ld",self.webAppId];
NSString *iconUrl = [self.webAppParams objectForKey:@"appLogo"];
NSString *urlString = [self oppcreateDesktopWithPreUrl:preUrl iconUrl:iconUrl appTitle:title scheme:scheme];
UIApplication *application = [UIApplication sharedApplication];
NSURL *URL = [NSURL URLWithString:urlString];
if (@available(iOS 10.0, *)) {
[application openURL:URL options:@{}
completionHandler:^(BOOL success) {
}];
} else {
[application openURL:URL];
}
}
这里服务器保存的 HTML页面shortcut-middle.html很简单,加载后主要就是通过上面拼接的链接获取本地引导页内容,然后加载引导页,shortcut-middle.html代码如下
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
</body>
<script type="text/javascript">
const dataurl = '';
var url = location.search;
if (url && url.length > 1 && url.indexOf('?dataurl=') !== -1) {
url = url.replace("?dataurl=", "");
window.location.replace(url);
} else {
window.location.replace(dataurl);
}
</script>
</html>
通过 location.search 方法可以获取到拼接好的url内容,然后拿到我们拼上去的内容,使用 window.location.replace 方法来重新进行内容替换,就可以展示我们本地的引导页了。此时展示就是非全屏展示,跟从快捷键打开不一样。
可能遇到的问题
- 在Safari中出现如下错误提示 414 Request-URI Too Large
这是因为我们的url超过了nginx 服务器的限制,找Web端开发人员设置下nginx 服务器的配置即可。
参考https://blog.csdn.net/qq_41198398/article/details/83618204
- 添加到桌面的图标有黑边
这是因为我们传入的appIcon图标有圆角,appIcon用大图512x512或1024x1024,不要圆角
参考资料
- 哈罗单车乘车码添加桌面快捷方式
https://www.jianshu.com/p/15e36a3bd22c - 支付宝健康码和小程序添加桌面快捷方式