前景
目前微信公众号程序开发已经相当火热,客户要求自己的系统有一个公众号,已经是一个很常见的需要。
使用公众号可以很方便的便于项目干系人查看信息和进行互动,还可以很方便录入一些电脑端不便于录入的数据,如照片等。
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中调试微信公众号,可能会很痛苦。早些时候,我们经历了反复的卸载,重装过程。
现在项目一阶段已经完结,重视这个问题,解决方案也已经出来,并实施:
-
前面说到,请求SAP的首页面,会通过后端的拦截器进行拦截以进行权限验证(java)。如果使用DotNet的童鞋,可以使用ashx去处理这个请求。在这里,可以使用两种做法,当html页面不大时,可以在response请求头中,加上“Cache-Control:no-cache”;另外,我们还可以配置上当前公众号的版本,response时,redirect url时拼上querySetring: ?v=[version];
-
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注入的代码进行的。如果这个问题不解决,前面那些都是白搭。
那么怎么办呢?经调查,我们发现,在mainjs中,负责加载其它页面模块的代码如下:
如果能加上红框中的代码,就可以实现延迟加载的模块也能因版本升级而更新缓存。所以,直接的做法是,每次编译新版本后,手动更改mainjs中的代码。
当然,这样做太不优雅了,万一哪天疏忽了,可是要出大问题的。那么,怎么让我们每次直接编译成这样的代码呢?
我们知道,这些代码,都是webpack在编译时,注入进来的。那么,这些代码必然在webpack中存在,所以,在node_modules/webpack中搜上面44-48行的代码,你会很快定位到一个文件:webpack/lib/JsonpMainTemplatePlugin.js。原来,webpack把注入的这些代码,放在了一个模板中:(放入上下文)解析模板生成对应代码再注入。
我们将模板进行更改一下,这样就可以避免每次生成mainjs后再手动更改了:
调试一下看看,效果如预期:
8 Ionic引入自定义图标(2018-01-31)
参考:https://yannbraga.com/2017/06/28/how-to-use-custom-icons-on-ionic-3/
.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有坑,这一块目前还暂时没时间去研究。所以,也无法做答了。