前景
目前微信公眾號程序開發已經相當火熱,客戶要求自己的系統有一個公眾號,已經是一個很常見的需要。
使用公眾號可以很方便的便於項目干系人查看信息和進行互動,還可以很方便錄入一些電腦端不便於錄入的數據,如照片等。
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有坑,這一塊目前還暫時沒時間去研究。所以,也無法做答了。
