前言:最近閑來無事,看了網上豆瓣的第三方客戶端,手有點癢,決定自己動手開發一個客戶端,比較了荔枝和喜馬拉雅,決定開發喜馬拉雅的第三方客戶端。
客戶端使用了WPF開發。
1.抓取接口;
首先得解決接口數據的問題,使用了手機端的喜馬拉雅,抓包看了接口。這里推薦使用fiddler2這個工具。從圖中可以看到接口信息,包括接口地址和參數的一些數據。
2.通過http獲取接口數據和轉換接口數據格式。
這里提供一個HttpWebRequestOpt類來獲取接口數據。

using System; using System.Collections.Specialized; using System.IO; using System.Net; using System.Text; namespace XIMALAYA.PCDesktop.Untils { /// <summary> /// 數據操作類 /// </summary> public class HttpWebRequestOpt { /// <summary> /// /// </summary> public string UserAgent { get; set; } /// <summary> /// cookie /// </summary> private CookieContainer Cookies { get; set; } private HttpWebRequestOpt() { //FileVersionInfo myFileVersion = FileVersionInfo.GetVersionInfo(Path.Combine(Directory.GetCurrentDirectory(), "XIMALAYA.PCDesktop.exe")); this.Cookies = new CookieContainer(); //this.UserAgent = string.Format("ting-ximalaya_v{0} name/ximalaya os/{1} osName/{2}", myFileVersion.FileVersion, OSInfo.Instance.OsInfo.VersionString, OSInfo.Instance.OsInfo.Platform.ToString()); //this.Cookies.Add(new Cookie("4&_token", "935&d63fef280403904a8c0a5ee0dbe228f2d064", "/", ".ximalaya.com")); } /// <summary> /// 添加cookie /// </summary> /// <param name="cookie"></param> public void SetCookies(Cookie cookie) { this.Cookies.Add(cookie); } /// <summary> /// 添加cookie /// </summary> /// <param name="cookie"></param> public void SetCookies(string key, string val) { this.Cookies.Add(new Cookie(key, val, "/", ".ximalaya.com")); } /// <summary> /// 通過POST方式發送數據 /// </summary> /// <param name="Url">url</param> /// <param name="postDataStr">Post數據</param> /// <returns></returns> public string SendDataByPost(string Url, string postDataStr) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url); request.CookieContainer = this.Cookies; request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postDataStr.Length; request.UserAgent = this.UserAgent; Stream myRequestStream = request.GetRequestStream(); StreamWriter myStreamWriter = new StreamWriter(myRequestStream, Encoding.GetEncoding("gb2312")); myStreamWriter.Write(postDataStr); myStreamWriter.Close(); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream myResponseStream = response.GetResponseStream(); StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.GetEncoding("utf-8")); string retString = myStreamReader.ReadToEnd(); myStreamReader.Close(); myResponseStream.Close(); return retString; } /// <summary> /// 通過GET方式發送數據 /// </summary> /// <param name="Url">url</param> /// <param name="postDataStr">GET數據</param> /// <returns></returns> public string SendDataByGET(string Url, string postDataStr) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url + (postDataStr == "" ? "" : "?") + postDataStr); request.CookieContainer = this.Cookies; request.Method = "GET"; request.ContentType = "text/html;charset=UTF-8"; request.UserAgent = this.UserAgent; HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream myResponseStream = response.GetResponseStream(); StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.GetEncoding("utf-8")); string retString = myStreamReader.ReadToEnd(); myStreamReader.Close(); myResponseStream.Close(); return retString; } /// <summary> /// 異步通過POST方式發送數據 /// </summary> /// <param name="Url">url</param> /// <param name="postDataStr">GET數據</param> /// <param name="async"></param> public void SendDataByPostAsyn(string Url, string postDataStr, AsyncCallback async) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url); request.CookieContainer = this.Cookies; request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postDataStr.Length; request.UserAgent = this.UserAgent; Stream myRequestStream = request.GetRequestStream(); StreamWriter myStreamWriter = new StreamWriter(myRequestStream, Encoding.GetEncoding("gb2312")); myStreamWriter.Write(postDataStr); myStreamWriter.Close(); myRequestStream.Close(); request.BeginGetResponse(async, request); } /// <summary> /// 異步通過GET方式發送數據 /// </summary> /// <param name="Url">url</param> /// <param name="postDataStr">GET數據</param> /// <param name="async"></param> /// <returns></returns> public void SendDataByGETAsyn(string Url, string postDataStr, AsyncCallback async) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url + (postDataStr == "" ? "" : "?") + postDataStr); request.CookieContainer = this.Cookies; request.Method = "GET"; request.ContentType = "text/html;charset=UTF-8"; request.UserAgent = this.UserAgent; request.BeginGetResponse(async, request); } /// <summary> /// 使用HttpWebRequest POST圖片等文件,帶參數 /// </summary> /// <param name="url"></param> /// <param name="file"></param> /// <param name="paramName"></param> /// <param name="contentType"></param> /// <param name="nvc"></param> /// <returns></returns> public string HttpUploadFile(string url, string file, string paramName, string contentType, NameValueCollection nvc) { string result = string.Empty; string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); byte[] boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n"); HttpWebRequest wr = (HttpWebRequest)WebRequest.Create(url); wr.ContentType = "multipart/form-data; boundary=" + boundary; wr.Method = "POST"; wr.KeepAlive = true; wr.Credentials = System.Net.CredentialCache.DefaultCredentials; Stream rs = wr.GetRequestStream(); string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}"; foreach (string key in nvc.Keys) { rs.Write(boundarybytes, 0, boundarybytes.Length); string formitem = string.Format(formdataTemplate, key, nvc[key]); byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem); rs.Write(formitembytes, 0, formitembytes.Length); } rs.Write(boundarybytes, 0, boundarybytes.Length); string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n"; string header = string.Format(headerTemplate, paramName, file, contentType); byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header); rs.Write(headerbytes, 0, headerbytes.Length); FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read); byte[] buffer = new byte[4096]; int bytesRead = 0; while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) { rs.Write(buffer, 0, bytesRead); } fileStream.Close(); byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n"); rs.Write(trailer, 0, trailer.Length); rs.Close(); WebResponse wresp = null; try { wresp = wr.GetResponse(); Stream stream2 = wresp.GetResponseStream(); StreamReader reader2 = new StreamReader(stream2); result = reader2.ReadToEnd(); } catch (Exception ex) { if (wresp != null) { wresp.Close(); wresp = null; } } finally { wr = null; } return result; } } }
接口地址:http://mobile.ximalaya.com/m/index_subjects;接口數據如下:
{"ret":0,"focusImages":{"ret":0,"list":[{"id":1384,"shortTitle":"DJ張羊 謝謝你的美好(感恩特輯)","longTitle":"DJ張羊 謝謝你的美好(感恩特輯)","pic":"http://fdfs.xmcdn.com/group5/M06/A8/1D/wKgDtlR1oKHSsFngAAFlxraThWc933.jpg","type":3,"trackId":4428642,"uid":1315711},{"id":1388,"shortTitle":"小清新女神11月榜","longTitle":"小清新女神11月榜","pic":"http://fdfs.xmcdn.com/group5/M04/A8/25/wKgDtlR1owzjFmE7AAF3pxnuNxg222.jpg","type":5},{"id":1383,"shortTitle":"王朔《你也不會年輕很久》 靜波播講","longTitle":"王朔《你也不會年輕很久》 靜波播講","pic":"http://fdfs.xmcdn.com/group5/M03/A8/1C/wKgDtlR1oE-xEoq6AAEfe5PJmt4656.jpg","type":3,"trackId":4417987,"uid":12512006},{"id":1382,"shortTitle":"楚老濕大課堂(長效圖-娛樂)","longTitle":"楚老濕大課堂(長效圖-娛樂)","pic":"http://fdfs.xmcdn.com/group5/M06/A8/19/wKgDtlR1n7ORluWTAAFuKujnTB0163.jpg","type":3,"trackId":4422955,"uid":8401915},{"id":1365,"shortTitle":"唱響喜瑪拉雅(活動圖)","longTitle":"唱響喜瑪拉雅(活動圖)","pic":"http://fdfs.xmcdn.com/group5/M06/A5/6C/wKgDtVR0VFXA3LWXAAMruRW5vnI973.png","type":8,"url":"http://activity.ximalaya.com/activity-web/activity/57?app=iting"},{"id":1363,"shortTitle":"歐萊雅廣告圖24、25、27、28","longTitle":"歐萊雅廣告圖24、25、27、28","pic":"http://fdfs.xmcdn.com/group5/M05/A0/32/wKgDtlRyla6AnGneAAF2kpKTc2I036.jpg","type":4,"url":"http://ma8.qq.com/wap/index.html?utm_source=xmly&utm_medium=113282464&utm_term=&utm_content=xmly01&utm_campaign=CPD_LRL_MEN_MA8%20Campaign_20141118_MO_other"}]},"categories":{"ret":0,"data":[]},"latest_special":{"title":"感恩的心 感謝有你","coverPathSmall":"http://fdfs.xmcdn.com/group5/M04/AA/9B/wKgDtlR2q_jxMbU-AATUrGYasdg092_mobile_small.jpg","coverPathBig":"http://fdfs.xmcdn.com/group5/M04/AA/9B/wKgDtlR2q_jxMbU-AATUrGYasdg092.jpg","coverPathBigPlus":null,"isHot":false},"latest_activity":{"title":"唱響喜馬拉雅-每年四季,打造你的音樂夢想","coverPathSmall":"http://fdfs.xmcdn.com/group5/M06/A4/DA/wKgDtlR0UQLik8xMABBJsD5tCNU868_mobile_small.jpg","isHot":true},"recommendAlbums":{"ret":0,"maxPageId":250,"count":1000,"list":[{"id":232357,"title":"今晚80后脫口秀 2014","coverSmall":"http://fdfs.xmcdn.com/group4/M01/19/5A/wKgDtFMsAq3COyRPAAUQ_GUt96k211_mobile_small.jpg","playsCounts":29318050},{"id":287570,"title":"大漠謠(風中奇緣)","coverSmall":"http://fdfs.xmcdn.com/group4/M07/7D/90/wKgDtFRGQFPzpmIsAAQ3HgQ6JRU598_mobile_small.jpg","playsCounts":669091},{"id":214706,"title":"段子來了 采采","coverSmall":"http://fdfs.xmcdn.com/group3/M04/64/9D/wKgDsVJ6DnSy_6Q7AAEXoFUKDKE679_mobile_small.jpg","playsCounts":29},{"id":233577,"title":"財經郎眼 2014","coverSmall":"http://fdfs.xmcdn.com/group2/M02/4E/2F/wKgDsFLTVG7RU3ZQAAPtxcqJYug831_mobile_small.jpg","playsCounts":8877870}]}}
有了數據就需要解析數據。接口數據為JSON格式,這里使用了FluentJson這個開源項目,可以把類與JSON數據互相轉換。官網上有相關的源碼和實例,可以下載看一下。下面介紹使用方法。
就針對上面的那個發現也接口我定義了一個類。

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using XIMALAYA.PCDesktop.Core.Models.Album; using XIMALAYA.PCDesktop.Core.Models.Category; using XIMALAYA.PCDesktop.Core.Models.FocusImage; using XIMALAYA.PCDesktop.Core.Models.Subject; using XIMALAYA.PCDesktop.Core.Models.User; namespace XIMALAYA.PCDesktop.Core.Models.Discover { public class SuperExploreIndexResult : BaseResult { /// <summary> /// 焦點圖 /// </summary> public FocusImageResult FocusImages { get; set; } /// <summary> /// 分類 /// </summary> public CategoryResult Categories { get; set; } /// <summary> /// 最后一個專題 /// </summary> public object LatestSpecial { get; set; } /// <summary> /// 最后一個活動 /// </summary> public object LatestActivity { get; set; } /// <summary> /// 推薦專輯 /// </summary> public AlbumInfoResult1 Albums { get; set; } public SuperExploreIndexResult() : base() { this.doAddMap(() => this.FocusImages, "focusImages"); this.doAddMap(() => this.Categories, "categories"); this.doAddMap(() => this.LatestActivity, "latest_activity"); this.doAddMap(() => this.LatestSpecial, "latest_special"); this.doAddMap(() => this.Albums, "recommendAlbums"); } } }
這個SuperExploreIndexResult類的構造函數對應了接口數據中的射影關系。
生成的映射類如下:

// <auto-generated> // 此代碼由工具生成。 // 對此文件的更改可能會導致不正確的行為,並且如果 // 重新生成代碼,這些更改將會丟失。 // 如存在本生成代碼外的新需求,請在相同命名空間下創建同名分部類實現 SuperExploreIndexResultConfigurationAppend 分部方法。 // </auto-generated> using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FluentJson.Configuration; using FluentJson; using XIMALAYA.PCDesktop.Core.Data.Decorator; using XIMALAYA.PCDesktop.Core.Models.Discover; namespace XIMALAYA.PCDesktop.Core.Data { /// <summary> /// SuperExploreIndexResult /// </summary> /// <typeparam name="T"></typeparam> public partial class SuperExploreIndexResultDecorator<T> : Decorator<T> { partial void doAddOtherConfig(); /// <summary> /// /// </summary> /// <typeparam name="result"></typeparam> public SuperExploreIndexResultDecorator(Result<T> result) : base(result) { } /// <summary> /// /// </summary> /// <typeparam name="result"></typeparam> public override void doAddConfig() { base.doAddConfig(); this.Config.MapType<SuperExploreIndexResult>(map => map .Field<System.Int32>(field => field.Ret, type => type.To("ret")) .Field<System.String>(field => field.Message, type => type.To("msg")) .Field<XIMALAYA.PCDesktop.Core.Models.FocusImage.FocusImageResult>(field => field.FocusImages, type => type.To("focusImages")) .Field<XIMALAYA.PCDesktop.Core.Models.Category.CategoryResult>(field => field.Categories, type => type.To("categories")) .Field<System.Object>(field => field.LatestActivity, type => type.To("latest_activity")) .Field<System.Object>(field => field.LatestSpecial, type => type.To("latest_special")) .Field<XIMALAYA.PCDesktop.Core.Models.Album.AlbumInfoResult1>(field => field.Albums, type => type.To("recommendAlbums")) ); this.doAddOtherConfig(); } } }
這里只列出了一個SuperExploreIndexResult類,還有CategoryResult,FocusImageResult,AlbumInfoResult1這三個類,也做了同樣的映射。這樣這個接口的數據最終就可以映射為SuperExploreIndexResult類了。總之,把接口中JSON數據中的對象是全部需要隱射的。
下面演示了如何調用上面的映射類。代碼中所有帶Decorator后綴的類都是映射類。采用了下裝飾模式。

using System; using System.ComponentModel.Composition; using FluentJson; using XIMALAYA.PCDesktop.Core.Data; using XIMALAYA.PCDesktop.Core.Data.Decorator; using XIMALAYA.PCDesktop.Core.Models.Discover; using XIMALAYA.PCDesktop.Core.Models.Tags; using XIMALAYA.PCDesktop.Untils; namespace XIMALAYA.PCDesktop.Core.Services { /// <summary> /// 發現頁接口數據 /// </summary> [Export(typeof(IExploreService))] class ExploreService : ServiceBase<SuperExploreIndexResult>, IExploreService { #region 屬性 private ServiceParams<SuperExploreIndexResult> SuperExploreIndexResult { get; set; } #endregion #region IExploreService 成員 /// <summary> /// 獲取發現首頁數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="act"></param> /// <param name="param"></param> public void GetData<T>(Action<object> act, T param) { Result<SuperExploreIndexResult> result = new Result<SuperExploreIndexResult>(); new SuperExploreIndexResultDecorator<SuperExploreIndexResult>(result); //分類 new CategoryResultDecorator<SuperExploreIndexResult>(result); new CategoryDataDecorator<SuperExploreIndexResult>(result); //焦點圖 new FocusImageResultDecorator<SuperExploreIndexResult>(result); new FocusImageDataDecorator<SuperExploreIndexResult>(result); //推薦用戶 //new UserDataDecorator<SuperExploreIndexResult>(result); //推薦專輯 new AlbumInfoResult1Decorator<SuperExploreIndexResult>(result); new AlbumData1Decorator<SuperExploreIndexResult>(result); //專題列表 //new SubjectListResultDecorator<SuperExploreIndexResult>(result); //new SubjectDataDecorator<SuperExploreIndexResult>(result); this.SuperExploreIndexResult = new ServiceParams<SuperExploreIndexResult>(Json.DecoderFor<SuperExploreIndexResult>(config => config.DeriveFrom(result.Config)), act); //this.Act = act; //this.Decoder = Json.DecoderFor<SuperExploreIndexResult>(config => config.DeriveFrom(result.Config)); try { this.Responsitory.Fetch(WellKnownUrl.SuperExploreIndex, param.ToString(), asyncResult => { this.GetDecodeData<SuperExploreIndexResult>(this.GetDataCallBack(asyncResult), this.SuperExploreIndexResult); }); } catch (Exception ex) { this.SuperExploreIndexResult.Act.BeginInvoke(new SuperExploreIndexResult { Ret = 500, Message = ex.Message }, null, null); } } #endregion } }
如上,只要配置好映射關系,通過T4模板我們可以生成對應的映射關系類。
下篇,客戶端使用了prism+mef這個框架,單獨開發模塊,最后組合的方式。未完待續。。。。