ios 关于支付宝添加桌面快捷方式的探究

前言

最近公司产品有个需求,为我们app中每个H5应用实现添加桌面快捷方式的功能,与支付宝健康码添加到桌面一样。所以特地研究了下相关产品中该功能的实现方式,如哈罗单车和支付宝。

原理

在 app 里面通过 OpenURL 方式 跳转到Safari 浏览器,打开一个引导页面,然后点击添加到主屏幕,如下图:

1241595556274_.pic.jpg

哈罗单车的实现

548341-fb5e56e2ad03a85a.png

哈罗单车在Safari中展示的是一整张引导图片,为啥这里说一整张图呢,因为支付宝展示的引导页是由多张图片拼接而成。

哈罗单车不同的业务使用快捷方式,需要UI每次做一张引导图,或者做一张通用图片当引导图。

探索支付宝添加桌面快捷方式的实现

  1. 在关闭网络的情况下,在支付宝健康码中点击"添加到桌面",跳转到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

  1. 支付宝小程序也同样有添加到桌面的功能,同样的方式获取到小程序跳转到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

  1. 我们将支付宝健康码页面的url在mac上的Safari 浏览器打开,查看元素


    截屏2020-07-23 下午6.49.10.png

可以看到页面主要有index.html,index.js,还有几张图片


截屏2020-07-23 下午6.54.49.png

从这张图我们可以猜到,支付宝是通过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端开发,自己单独来实现呢?
如果自己单独实现,需要解决哪些问题呢?

  1. 首先我从mac端浏览器中可以获取到index.html和index.js和图片等资源文件
  2. 我们需要将支付宝的index.html文件中原本通过url中获取参数的形式改为app本地替换

步骤

1. 开发引导页面

我们首先在app项目工程的根目录下创建一个文件夹Web,文件夹下存放引导页面


截屏2020-07-23 下午7.44.32.png

content.html

content.html是拷贝的是支付宝index.html文件,对其做了修改。

  1. 支付宝的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, "&amp;");
    myStr = myStr.replace(/'/g, "&#39;");
    myStr = myStr.replace(/"/g, "&quot;");
    myStr = myStr.replace(/</g, "&lt;");
    myStr = myStr.replace(/>/g, "&gt;");
    myStr = myStr.replace(/\s/g, '&nbsp;');
    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 方法来重新进行内容替换,就可以展示我们本地的引导页了。此时展示就是非全屏展示,跟从快捷键打开不一样。

可能遇到的问题

  1. 在Safari中出现如下错误提示 414 Request-URI Too Large

这是因为我们的url超过了nginx 服务器的限制,找Web端开发人员设置下nginx 服务器的配置即可。
参考https://blog.csdn.net/qq_41198398/article/details/83618204

  1. 添加到桌面的图标有黑边
    这是因为我们传入的appIcon图标有圆角,appIcon用大图512x512或1024x1024,不要圆角

参考资料

  1. 哈罗单车乘车码添加桌面快捷方式
    https://www.jianshu.com/p/15e36a3bd22c
  2. 支付宝健康码和小程序添加桌面快捷方式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349