• Ionic2 + Angular4 + JSSDK开发中的若干问题汇总


    前景

    目前微信公众号程序开发已经相当火热,客户要求自己的系统有一个公众号,已经是一个很常见的需要。

    使用公众号可以很方便的便于项目干系人查看信息和进行互动,还可以很方便录入一些电脑端不便于录入的数据,如照片等。

    ionic是一个移动端开发框架,使用hybird技术,只要使用前端开发技术就可以开发出电脑端,安卓端和ios端的站点程序。由于其内置了很多仿移动端Native的控件,使用此框架进行移动端开发,既可以减少控件和样式开发成本,又可以很方便将已经开发的程序打包成安卓或ios程序。

    最近尝试使用ionic2 + angular4 + jssdk对xxx平台移动端进行开发,遇到过一些技术上的坑,记录如下(持续更新)。

    问题

    1 ios中选择照片后,在图片集中出现不了。

    升级jssdk到1.2以上。

    2 使用foreach方式,一次同时上传多张图片,容易失败。

    应该使用同步上传的方式,一张图片传完再传下一张,也不会慢多少, 示例如下:

        let i = 0;
        let img = this.images[i];
    
        let uploadImg = function () {
          wx.uploadImage({
            localId: img.picUrl, // 需要上传的图片的本地ID,由chooseImage接口获得
            isShowProgressTips: 0, // 默认为1,显示进度提示
            success: uploadSuccess,
            fail: function () {
              alert('上传失败');
              me.loader.dismiss();
            }
          });
        };
    
        let uploadSuccess = function (res: any) {
          me.uploadedImageIds.push(res.serverId);
          img = me.images[++i];
          img ? uploadImg() : me.submitImage();
        };
    
        uploadImg();
    

       

    3 第一次加载的时候,总是容易出现“invalid signature"。

    signature的计算,需要使用appKey, account和页面URL等。经测试,使用jssdk的页面的URL必须严格和计算signature使用的URL一致:地址 + queryString。#参数不用管。

    我们从微信公众号访问SPA程序时,可能会带上一些权限的参数,如authCode等,这些参数会影响到signature的计算

    现在我们的做法是完全在服务器端计算signature,但是客户端必须告诉服务器端当前的url。需要经过这两步:

    var subUrl = window.location.href.split('#')[0];
    var signatureUrl = '/[subpath]/signature?url=' + encodeURIComponent(subUrl);
    

      全部代码如下:

    var xhr = new XMLHttpRequest();
    var subUrl = window.location.href.split('#')[0];
    var signatureUrl = '/wxgzh/signature?url=' + encodeURIComponent(subUrl);
    var configWeixin = function () {
      var response = JSON.parse(this.response);
      wx.config({
        debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
        appId: response.appId, //'wx03605b6ba300b93b', // 必填,公众号的唯一标识
        timestamp: response.timestamp, // 必填,生成签名的时间戳
        nonceStr: response.nonceStr, // 必填,生成签名的随机串
        signature: response.signature,// 必填,签名,见附录1
        jsApiList: ['chooseImage', 'scanQRCode', 'uploadImage'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
      });
    };
    
    xhr.open('get', signatureUrl);
    xhr.addEventListener("load", configWeixin, false);
    xhr.send();
    

       

    4 SPA和验证redirect问题

    服务器端有可能会在我们第一次登录的时候,将我们的页面重定向到一个登录界面。登录好后再重定向回来。

    服务器端第一次重定向到登录界面时,会记录第二次要重定向回来的地址。

    经实验和查证,在服务器端无法获取客户端的#参数,如果index.html#photoSet,服务器只能获取到index.html。在SPA程序中,丢失了#参数,客户端将不能直接进入对应页面。

    目前我们的解决办法是,链接的地址中,#参数写成query string, 如index.html#photoSet,写成index.html?hash=photoSet。服务器端第二次重定向前,会检查query string中是否有hash参数,如果有,将地址拼装成index.html#photoSet后再执行重定向。

    4.1 前端站点的部署问题(2018-01-31补充)

    路由配置

    下面有园友评论中提到了,不使用#路由,而直接使用标准路由。直接使用标准路由,对部署会有一些限制。因为标准路由是基于文件路径的,直接使用http-server一类的服务,会直接出现404的错误:原因是因为,我们现在的url并不是一个文件路径了。

    解决办法:IIS的实现 :https://blogs.msdn.microsoft.com/premier_developer/2017/06/14/tips-for-running-an-angular-app-in-iis。tomcat也有类似的设置。总体原则是通过rewrite规则,让资源服务器虽然识别的是一个完整路径,但响应的时候会根据文件是否存在,而只使用特定的资源去响应,比如“二级路径/index.html"。

    部署环境的选择

    很多刚接触的同行经常会问,前端站点搞好了,放哪呢?

    放哪都可以,只要提供http静态资源服务就行了。

    值得一提的是,如果要调试jssdk, 需要在微信管理界面配置所谓的“信任域名”,然后你需要将站点绑定信任域名,才可能调成功。

    我们知道,域名绑定需要公网ip,而我们开发环境在局域网甚至localhost。除非公司给你作端口映射,将公司公网映射至你的电脑,否则调试会成为一件很麻烦的事。一般,我们会将自己的代码,通过一定的方式发布到服务器上调试jssdk,所以发布的方便程度,会直接影响jssdk的高度效率。

    个人实践下来:感觉阿里云的sso算是不错的选择,特别是想做个人公众号的园友。它满足几个特点:1同步方便;2网速极快;3可以估CDN;4价格实惠。再多说会有广告嫌疑了,我没有必要给它们打广告,它们也不会给我报酬,呵呵。

    5 DatePicker控件的时间差问题

    第一次使用DatePicker时,发现录入的时间总会超前8小时。指定了控件的timeOffset属性后,就正确了。

     

    6 性能问题

    从默认的sample到现在的程序,在性能上,我们主要做了两点优化:

    6.1 延迟加载页面(可自行google 关键字ionic3 lazy load page)

    当需要使用某个页面的时候,再单独加载某个页面,而不是一开始全部加载好了缓存。这对于页面比较多的SPA,作用会比较明显。以我们现在系统为为例,也可以节省约一秒的时间。

    延迟加载的关键技术有两点:给每个page组件标记上IonicPage,注明name和segment,给每个页面一个单独的module,import和export这个页面。

    如果此页面使用了普通组件,页面module需要将组件所在module import进来。

    6.2 build --prod

    在使用ionic-app-scripts对ionic2程序进行编译时,可以使用--prod命令对编译进行优化。

    使用--prod进行编译时,会发现可能一些原先执行ionic serve时可以通过的代码,此时无法编译通过,比如字符串的interpolation, 及其它一些不严格的写法,改过来就好了。

    prod编译的优化主要使用AOT技术, 会将模板解析成js代码,让DOM操作更加高效。另外,这样编译出来的代码也更精简,更难读懂(安全)。

    另外,angular的enableProdMode也要在main.ts中开启。

    7 缓存问题

    开发过程中,缓存是一个很麻烦的问题。

    在android系统中,公众号页面更新后,可以在微信浏览器中打开debugx5.qq.com页面来清除缓存。但是在IOS中,这一招不行。所以,在IOS中调试微信公众号,可能会很痛苦。早些时候,我们经历了反复的卸载,重装过程。

    现在项目一阶段已经完结,重视这个问题,解决方案也已经出来,并实施:

    1. 前面说到,请求SAP的首页面,会通过后端的拦截器进行拦截以进行权限验证(java)。如果使用DotNet的童鞋,可以使用ashx去处理这个请求。在这里,可以使用两种做法,当html页面不大时,可以在response请求头中,加上“Cache-Control:no-cache”;另外,我们还可以配置上当前公众号的版本,response时,redirect url时拼上querySetring: ?v=[version];

    2. html页面的缓存问题解决了,接下来解决js的缓存问题。一般而言,我们这样解决js的缓存问题:

            <script src="test.js?v=[版本]"></script>
            <script src="test.js?rndstr=[随机数字]"></script>

            我比较倾向于第一种,因为版本号不变时,缓存功能还是有用的。为了动态使用版本号(不在html中写死),首页中加入如下js片段:   

    (function(){
          window.BIMRUN_VERSION = 1.1;
    var loadJs = function(name){
    var dom = document.createElement("script");
    dom.async = false;
    dom.src = "build/"+name+".js?v="+BIMRUN_VERSION;
    document.body.appendChild(dom);
    return dom;
    };
    loadJs("polyfills");
    loadJs("main");
    })();

         分别用于引用polyfills和main更新后的强制缓存更新。

    正常情况下,到这里就应该算解决了。但是,我们引入了page的lazy loading技术,而延迟请求模块js文件,是ionic script调用webpack注入的代码进行的。如果这个问题不解决,前面那些都是白搭。

    blob.png

    那么怎么办呢?经调查,我们发现,在mainjs中,负责加载其它页面模块的代码如下:

    blob.png

    如果能加上红框中的代码,就可以实现延迟加载的模块也能因版本升级而更新缓存。所以,直接的做法是,每次编译新版本后,手动更改mainjs中的代码。

    当然,这样做太不优雅了,万一哪天疏忽了,可是要出大问题的。那么,怎么让我们每次直接编译成这样的代码呢?

    我们知道,这些代码,都是webpack在编译时,注入进来的。那么,这些代码必然在webpack中存在,所以,在node_modules/webpack中搜上面44-48行的代码,你会很快定位到一个文件:webpack/lib/JsonpMainTemplatePlugin.js。原来,webpack把注入的这些代码,放在了一个模板中:(放入上下文)解析模板生成对应代码再注入。

    我们将模板进行更改一下,这样就可以避免每次生成mainjs后再手动更改了:

    blob.png

    调试一下看看,效果如预期:

    blob.png

     

    8 Ionic引入自定义图标(2018-01-31)

    参考:https://yannbraga.com/2017/06/28/how-to-use-custom-icons-on-ionic-3/

    核心原理:
    在icon的使用中,<ion-icon name="fa-glass"></ion-icon> 会默认使用ion-ios-fa-glass或ion-md-fa-glass样式。
    所以,对于自定义图标(以font-awesome中的fa-glass为例),我们添加上ion-ios-fa-glass和ion-md-fa-glass两个样式就行了,如下:
    .fa-glass:before,
    .ion-ios-fa-glass:before,
    .ion-md-fa-glass:before {
      content: "f000";
    }

    另外,我们需要统一为之指定字体:

    ion-icon[class*="ion-ios-fa"],
    ion-icon[class*="ion-md-fa"]{
      font-family: FontAwesome;
    }

    当然,手动添加这些比较麻烦,所以,我写了一段程序来做这些事:

    var file = IoEx.GetSelectFilePath();
    if (string.IsNullOrEmpty(file)) return;
    
    var sb =new StringBuilder();
    var lines = File.ReadAllLines(file);
    foreach (var line in lines)
    {
        if (line.EndsWith(":before {"))
        {
            var className = line.Replace(":before {", "").TrimStart('.');
    
            if (!className.EndsWith("-o"))
            {
                sb.AppendLine($".ion-ios-{className}:before,");
                sb.AppendLine($".ion-md-{className}:before,");
            }
            else
            {
                className = className.Substring(0, className.Length - 2);
                sb.AppendLine($".ion-ios-{className}-outline:before,");
                sb.AppendLine($".ion-md-{className}-outline:before,");
            }
    
        } 
        sb.AppendLine(line); 
    }
    
    var savePath = IoEx.GetSaveFilePath();
    if (!string.IsNullOrEmpty(savePath))
    {
        File.WriteAllText(savePath,sb.ToString());
    }

    IoEx中的代码为调用系统FileSaveDialog和FileSelectDialog。

    对于阿里图库导出的样式,判断逻辑有差别,但不大。

    9 部分微信jssdk至Observable对象的封装(2018-01-31)

    下面的代码主要给各位,尤其是对Observable不太熟和园友一个示例,不全,风格也未必你喜欢,见谅。

    export interface WxImgSelectResult {
      sourceType: string;
      localIds: string[];
      errMsg: string;
    }
    
    export interface WxImgUploadResult {
      serverId: string;
      mediaUrl: string; // empty string
      errMsg: string; // uploadImage:ok
    }
    
    export interface  QrCodeResult{
      resultStr:string,
      errmsg:string
    }
    
    /*
     Generated class for the WxProvider provider.
    
     See https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1445241432
     */
    @Injectable()
    export class WxProvider {
      PATH_JSCONFIG = API_SERVICE_DOMAIN + "/api/wx/jsconfig";
    
      initialized = false;
    
      get wx(): any {
        return window['wx'];
      }
    
      constructor(public http: HttpClient,
                  public toastCtrl: ToastController,
                  private auth: AuthService
      ) {
        this.auth.authenticated();
      }
    
      // 为充分利用加载时间,这部分在index中调用...这段代码我一般不用,而是直接在index.html中裸写。因为加载完Index到程序初始化完成,有一大段时间。
      configWx() {
        let url = window.location.href.split('#')[0];
        // if(url[url.length-1] === '/')url=url.substr(0,url.length-1);
        // url =encodeURIComponent(url);
    
        this.http.post(this.PATH_JSCONFIG, {url})
          .subscribe((response: any) => {
            this.wx.config({
              debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
              appId: response.data.appId, //'wx03605b6ba300b93b', // 必填,公众号的唯一标识
              timestamp: response.data.timestamp, // 必填,生成签名的时间戳
              nonceStr: response.data.nonceStr, // 必填,生成签名的随机串
              signature: response.data.signature,// 必填,签名,见附录1
              jsApiList: ['chooseImage', 'scanQRCode', 'uploadImage'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
            });
    
            this.initialized = true;
          })
      }
    
      scanQr(): Observable<QrCodeResult> {
        return Observable.create(observer => {
          this.wx.scanQRCode({
            needResult: 1,
            scanType: ["qrCode"], // 可以指定扫二维码还是一维码,默认二者都有, "barCode"
            success: res => {
              observer.next(res);
              observer.complete();
            },
            fail: observer.error
          });
        })
      }
    
      // https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
    
      /**
       * 返回选择并上传到微信服务器端的照片的serverId, 用于后端从微信服务器端拉取。
       *
       * 因为从微信服务器获取图片需要使用到公众号的secretKey等敏感信息,所以,我们只能多传一次了。
       * @returns {any}
       */
      choosePictures(): Observable<string> {
        const me = this;
    
        return Observable.create(observer => {
          this.wx.chooseImage({
            count: 9, // 最多可以选择的图片张数,默认9
            sizeType: ['original', 'compressed'], // original 原图,compressed 压缩图,默认二者都有
            sourceType: ['album', 'camera'], // album 从相册选图,camera 使用相机,默认二者都有
            success: (res: WxImgSelectResult)=> {
              // 如果没有选择任何图片
              if (!res.localIds.length) {
                observer.complete();
                return;
              }
    
              let i = 0;
              let img = res.localIds[i];
    
              // 上传图片至微信服务器
              let uploadImg = () => {
                me.wx.uploadImage({
                  localId: img, // 需要上传的图片的本地ID,由chooseImage接口获得
                  isShowProgressTips: 0, // 默认为1,显示进度提示
                  success: uploadSuccess,
                  fail: observer.error
                });
              };
    
              // 传完一张再传下一张,否则会挂掉一些。
              let uploadSuccess = (upd: WxImgUploadResult) => {
                observer.next(upd.serverId);
                img = res.localIds[++i];
                img ? uploadImg() : observer.complete();
              }
    
              uploadImg();
            },
            fail: observer.error
          })
        });
      }
    }
    

     使用示例:

        this.pictures = [];
    
        this.wx.choosePictures()
          .switchMap(id => this.wxService.WeixinApi_Media([new VmMedia({id})]))
          .subscribe(pics=> {
            pics.forEach(it => {
              it.picPath = API_SERVICE_DOMAIN + it.picPath;
              it.picPathThumb = API_SERVICE_DOMAIN + it.picPathThumb;
            })
            this.pictures = this.pictures.concat(pics)
          });
    

       

    待解决/未完全解决的问题

    1 条件编译

    开发中,经常会遇到不同环境的问题,比如dev环境,为了绕过验证,我们可能采用p_auth验证,将用户名和密码放在请求头中,这一段代码往往写在httpConfig中。而且,dev时,由于代码在本地,接口在服务器端,域名不一致,还需要服务器端通过Nginx统一添加跨域请求头,但生产环境肯定不会这样了。

    对于一些简单的,不太敏感的策略,新建一个app.config.ts里面export const ENVIRONMENT  = "DEV/TEST/RC2/PROD"就行了。其它的地方写上和环境对应的代码,比如main.ts中可能会写上:

    ENVIRONMENT == 'PROD' && enableProdMode();
    

    但是,对于一些敏感信息,我们不能这样做。如果没有每件编译,意味着我们得每次注释掉一些代码后再build,这样不优雅。

    ionic script使用tsc对ts文件进行编译,如果使用typescript-plus,可以有条件编译的功能。问题是,ionic的命令行,把这些都整合死了,如果要改。。。算了,我还是老老实实的注释了发布吧。

    也许不久,tsc就会支持条件编译了,但愿吧。  

     

    ---不定期更新

    --tab中的navCtrl有坑,这一块目前还暂时没时间去研究。所以,也无法做答了。

  • 相关阅读:
    python 3.x报错:No module named 'cookielib'或No module named 'urllib2'
    Xshell实现Windows和使用跳板机跳转的远程Linux互传文件
    Linux scp常用命令
    正则表达式
    [NBUT 1458 Teemo]区间第k大问题,划分树
    [hdu5416 CRB and Tree]树上路径异或和,dfs
    [vijos P1008 篝火晚会]置换
    [hdu5411 CRB and Puzzle]DP,矩阵快速幂
    [hdu4713 Permutation]DP
    [hdu4710 Balls Rearrangement]分段统计
  • 原文地址:https://www.cnblogs.com/caption/p/6807613.html
Copyright © 2020-2023  润新知