目前微信公衆號程序開發已經至關火熱,客戶要求本身的系統有一個公衆號,已是一個很常見的須要。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有坑,這一塊目前還暫時沒時間去研究。因此,也沒法作答了。