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有坑,这一块目前还暂时没时间去研究。所以,也无法做答了。

posted @ 2017-05-04 15:57  Caption  阅读(7497)  评论(37编辑  收藏  举报