目前微信公众号程序开发已经至关火热,客户要求本身的系统有一个公众号,已是一个很常见的须要。javascript
使用公众号能够很方便的便于项目干系人查看信息和进行互动,还能够很方便录入一些电脑端不便于录入的数据,如照片等。html
ionic是一个移动端开发框架,使用hybird技术,只要使用前端开发技术就能够开发出电脑端,安卓端和ios端的站点程序。因为其内置了不少仿移动端Native的控件,使用此框架进行移动端开发,既能够减小控件和样式开发成本,又能够很方便将已经开发的程序打包成安卓或ios程序。前端
最近尝试使用ionic2 + angular4 + jssdk对xxx平台移动端进行开发,遇到过一些技术上的坑,记录以下(持续更新)。java
升级jssdk到1.2以上。node
应该使用同步上传的方式,一张图片传完再传下一张,也不会慢多少, 示例以下:android
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();
signature的计算,须要使用appKey, account和页面URL等。经测试,使用jssdk的页面的URL必须严格和计算signature使用的URL一致:地址 + queryString。#参数不用管。webpack
咱们从微信公众号访问SPA程序时,可能会带上一些权限的参数,如authCode等,这些参数会影响到signature的计算。ios
如今咱们的作法是彻底在服务器端计算signature,可是客户端必须告诉服务器端当前的url。须要通过这两步:web
var subUrl = window.location.href.split('#')[0]; var signatureUrl = '/[subpath]/signature?url=' + encodeURIComponent(subUrl);
所有代码以下:typescript
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();
服务器端有可能会在咱们第一次登陆的时候,将咱们的页面重定向到一个登陆界面。登陆好后再重定向回来。
服务器端第一次重定向到登陆界面时,会记录第二次要重定向回来的地址。
经实验和查证,在服务器端没法获取客户端的#参数,若是index.html#photoSet,服务器只能获取到index.html。在SPA程序中,丢失了#参数,客户端将不能直接进入对应页面。
目前咱们的解决办法是,连接的地址中,#参数写成query string, 如index.html#photoSet,写成index.html?hash=photoSet。服务器端第二次重定向前,会检查query string中是否有hash参数,若是有,将地址拼装成index.html#photoSet后再执行重定向。
路由配置
下面有园友评论中提到了,不使用#路由,而直接使用标准路由。直接使用标准路由,对部署会有一些限制。由于标准路由是基于文件路径的,直接使用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价格实惠。再多说会有广告嫌疑了,我没有必要给它们打广告,它们也不会给我报酬,呵呵。
第一次使用DatePicker时,发现录入的时间总会超前8小时。指定了控件的timeOffset属性后,就正确了。
从默认的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中开启。
开发过程当中,缓存是一个很麻烦的问题。
在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后再手动更改了:
调试一下看看,效果如预期:
参考: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。
对于阿里图库导出的样式,判断逻辑有差异,但不大。
下面的代码主要给各位,尤为是对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) });
开发中,常常会遇到不一样环境的问题,好比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有坑,这一块目前还暂时没时间去研究。因此,也没法作答了。