(原創,轉載請註明出處)javascript
微信網頁版爲angular應用。css
angular.bootstrap(document, ["webwxApp"])
angular.module("Services") angular.module("Controllers") angular.module("Directives")
/readMenu.html
微信網頁版採用了ui-route模塊來配置路由,與頁面相對應,聊天二級頁對應'chat' state,聯繫人二級頁對應'contact' state, 公衆號二級頁對應'read' state。html
angular.module("webwxApp", ["ui.router", "ngAnimate", "Services", "Controllers", "Directives", "Filters", "ngDialog", "jQueryScrollbar", "ngClipboard", "exceptionOverride"]).run(["$rootScope", "$state", "$stateParams", function(e, t, o) { e.$state = t, e.$stateParams = o }]).factory("httpInterceptor", ["accountFactory", function(e) { return { request: function(t) { if (!t.cache && t.url.indexOf(".html") < 0 && (t.params || (t.params = {}), t.params.pass_ticket = e.getPassticket()), t.url.indexOf(".html") < 0) { var o = location.href.match(/(\?|&)lang=([^&#]+)/); if (o) { var n = o[2]; t.params || (t.params = {}), t.params.lang = n } } return t } } }]).config(["$sceProvider", "$httpProvider", "$logProvider", "$stateProvider", "$urlRouterProvider", "ngClipProvider", function(e, t, o, n, r, a) { e.enabled(!1), o.debugEnabled(!0), a.setPath(window.MMSource.copySwfPath), t.interceptors.push("httpInterceptor"); var i = document.domain.indexOf("qq.com") < 0; i || (document.domain = "qq.com"); var c; n.state("chat", { url: "", params: { userName: "" }, views: { navView: { controller: ["$stateParams", "chatFactory", "contactFactory", "stateManageService", "$rootScope", function(e, t, o, n, r) { function a() { var n = o.getContact(e.userName, "", !0); r.$broadcast("root:statechange"), t.setCurrentUserName(e.userName), t.addChatList([n || { FromUserName: e.userName }]), e.userName = "" } if (n.change("navChat:active", !0), e.userName) { var i = o.getContact(e.userName, "", !0); i ? a() : o.addBatchgetContact({ UserName: e.userName, ChatRoomId: "" }, !0).then(function(e) { a(), console.log("addBatchgetContact now ok", e) }, function(e) { console.error("addBatchgetContact now err", e) }) } }] }, contentView: { templateUrl: "contentChat.html", controller: "contentChatController" } } }).state("contact", { url: "", views: { navView: { controller: ["stateManageService", function(e) { e.change("navContact:active", !0) }] }, contentView: { templateUrl: "contentContact.html", controller: "contentContactController" } } }).state("read", { url: "", params: { readItem: "" }, views: { navView: { controller: ["stateManageService", function(e) { e.change("navRead:active", !0) }] }, contentView: { templateUrl: "contentRead.html", controller: ["$scope", "$stateParams", "subscribeMsgService", "mmpop", function(e, t, o, n) { if (t.readItem) c = e.readItem = t.readItem; else { var r = o.getSubscribeMsgs()[0]; e.readItem = c || r && r.MPArticleList[0] } e.optionMenu = function() { n.toggleOpen({ templateUrl: "readMenu.html", container: angular.element(document.querySelector(".read_list_header")), controller: "readMenuController", singletonId: "mmpop_reader_menu", className: "reader_menu" }) }, i || $("#reader").load(function() { var e = $(this).contents().find("body"), t = e.find("#js_view_source"); if (t.length > 0) { e.css({ position: "relative" }); var o = $('<a href="javascript:;" onclick="var url = window.msg_source_url || window.location.href; var win = window.top.open(url, \'_blank\'); win.focus();" style="position: absolute; bottom: 20px; left: 15px; width: 4em; height: 25px; background: #FFFFFF;">閱讀原文</a>'); e.append(o) } }) }] } } }) }]);
angular.module("Controllers").controller("appController", ["$rootScope", "$scope", "$timeout", "$log", "$state", "$window", "ngDialog", "mmpop", "appFactory", "loginFactory", "contactFactory", "accountFactory", "chatFactory", "confFactory", "contextMenuFactory", "notificationFactory", "utilFactory", "reportService", "actionTrack", "surviveCheckService", "subscribeMsgService", "stateManageService", function(e, t, o, n, r, a, i, c, s, l, u, f, d, g, m, p, h, M, y, C, S, v) { //controller初始化 }
window._appTiming = {}, r.go("chat"), e.CONF = g, t.isUnLogin = !window.MMCgi.isLogin, t.debug = !0, t.isShowReader = /qq\.com/gi.test(location.href), window.MMCgi.isLogin && (T(), h.browser.chrome && !MMDEV && (window.onbeforeunload = function(e) { return e = e || window.event, e && (e.returnValue = "關閉瀏覽器聊天內容將會丟失。"), "關閉瀏覽器聊天內容將會丟失。" })), t.$on("newLoginPage", function(e, t) { console.log("newLoginPage", t), f.setSkey(t.SKey), f.setSid(t.Sid), f.setUin(t.Uin), f.setPassticket(t.Passticket), T() });
經過分析以上代碼,T()纔是初始化的具體執行方法。前端
function T() { t.isLoaded = !0, t.isUnLogin = !1, M.report(M.ReportType.timing, { timing: { initStart: Date.now() } }), s.init().then(function(n) { if (h.log("initData", n), n.BaseResponse && "0" != n.BaseResponse.Ret) return console.log("BaseResponse.Ret", n.BaseResponse.Ret), void(l.timeoutDetect(n.BaseResponse.Ret) || i.openConfirm({ className: "default ", templateUrl: "comfirmTips.html", controller: ["$scope", function(e) { e.title = MM.context("02d9819"), e.content = MM.context("0d2fc2c"), M.report(M.ReportType.initError, { text: "程序初始化失敗,點擊確認刷新頁面", code: n.BaseResponse.Ret, cookie: document.cookie }), e.callback = function() { document.location.reload(!0) } }] })); f.setUserInfo(n.User), f.setSkey(n.SKey), f.setSyncKey(n.SyncKey), u.addContact(n.User), u.addContacts(n.ContactList), d.initChatList(n.ChatSet), d.notifyMobile(f.getUserName(), g.StatusNotifyCode_INITED), S.init(n.MPSubscribeMsgList), e.$broadcast("root:pageInit:success"), h.setCheckUrl(f), h.log("getUserInfo", f.getUserInfo()), t.$broadcast("updateUser"), M.report(M.ReportType.timing, { timing: { initEnd: Date.now() } }); var r = n.ClickReportInterval || 3e5; setTimeout(function a() { y.report(), setTimeout(a, r) }, r), o(function() { function e(o) { u.initContact(o).then(function(o) { u.addContacts(o.MemberList), M.report(M.ReportType.timing, { timing: { initContactEnd: Date.now() }, needSend: !0 }), 16 >= t && o.Seq && 0 != o.Seq && (t++, e(o.Seq)) }) } M.report(M.ReportType.timing, { timing: { initContactStart: Date.now() } }); var t = 1; e(0) }, 0), t.account = u.getContact(f.getUserName()), E() }) }
經過分析以上代碼,能夠看到,s.init()爲初始化主要方法,其中s爲appFactory;初始化後,經過各類set方法來爲各個model賦值。java
angular.module("Services").factory("appFactory", ["$http", "$q", "confFactory", "accountFactory", "loginFactory", "utilFactory", "reportService", "mmHttp", function(e, t, o, n, r, a, i, c) { var s = { globalData: { chatList: [] }, init: function() { var e = t.defer(); return c({ method: "POST", url: o.API_webwxinit, MMRetry: { count: 1, timeout: 1 }, data: { BaseRequest: { Uin: n.getUin(), Sid: n.getSid(), Skey: n.getSkey(), DeviceID: n.getDeviceID() } } }).success(function(t) { e.resolve(t) }).error(function(t) { e.reject("error:" + t) }), e.promise }, sync: function() { var e = t.defer(); return c({ method: "POST", MMRetry: { serial: !0 }, url: o.API_webwxsync + "?" + ["sid=" + n.getSid(), "skey=" + n.getSkey()].join("&"), data: angular.extend(n.getBaseRequest(), { SyncKey: n.getSyncKey(), rr: ~new Date }) }).success(function(t) { e.resolve(t), a.getCookie("webwx_data_ticket") || i.report(i.ReportType.cookieError, { text: "webwx_data_ticket 票據丟失", cookie: document.cookie }) }).error(function(t) { e.reject("error:" + t), a.log("sync error") }), e.promise }, syncCheck: function() { var e = t.defer(), c = this, s = o.API_synccheck + "?" + ["r=" + a.now(), "skey=" + encodeURIComponent(n.getSkey()), "sid=" + encodeURIComponent(n.getSid()), "uin=" + n.getUin(), "deviceid=" + n.getDeviceID(), "synckey=" + encodeURIComponent(n.getFormateSyncCheckKey())].join("&"); return window.synccheck && (window.synccheck.selector = 0), $.ajax({ url: s, dataType: "script", timeout: 35e3 }).done(function() { window.synccheck && "0" == window.synccheck.retcode ? "0" != window.synccheck.selector ? c.sync().then(function(t) { e.resolve(t) }, function(e) { console.log("syncCheck sync nothing", e) }) : e.reject(window.synccheck && window.synccheck.selector) : !window.synccheck || "1101" != window.synccheck.retcode && "1102" != window.synccheck.retcode ? window.synccheck && "1100" == window.synccheck.retcode ? r.loginout(0) : (e.reject("syncCheck net error"), i.report(i.ReportType.netError, { text: "syncCheck net error", url: s })) : r.loginout(1) }), e.promise }, report: function() {} }; return s }])
涉及公衆號的ui-route state爲read。包含有兩個view,分別爲navView和contentView。navView爲概覽導航,contentView爲正文閱讀。根據contentView的定義,能夠看到正文閱讀的模版爲ContentRead.html。jquery
<script type="text/ng-template" id="contentRead.html"><div class="box reader"> <div class="box_hd with_border read_list_header"> <div class="ext" ng-if="readItem"> <a href="javascript:;" ng-click="optionMenu();"> <i class="titlebar_menuicon"></i> </a> </div> <div class="title_wrap"> <div class="title">{{readItem.AppName}}</div> </div> </div> <div class="box_bd"> <iframe ng-src="{{readItem.Url}}" frameborder="0" class="iframe" id="reader"></iframe> </div> </div></script>
涉及公衆號的Directive爲navReadDirective,對應的模版爲navRead.html。web
<script type="text/ng-template" id="navRead.html"><!--BEGIN chat list--> <div jquery-scrollbar class="read_list scrollbar-dynamic" id="J_NavReadScrollBody"> <p class="ico_loading" ng-show="subscribeMsgs.defaultValue"><img src="https://res.wx.qq.com/zh_CN/htmledition/v2/images/icon/ico_loading31e225.gif" alt=""/>加載中...</p> <p class="ico_loading" ng-show="!subscribeMsgs.defaultValue && subscribeMsgs.length == 0">暫無文章...</p> <div data-no-cache="true" mm-repeat="readItem in articleList" data-height-calc="heightCalc" data-buffer-height="200" mm-repeat-keyboard mm-repeat-keyboard-scroll-selector="#J_NavReadScrollBody"> <div class="just_for_bg" ng-if="readItem.UserName" ng-class="{first: readItem._index === 0}"> <div class="read_item_hd"> <p class="date">{{readItem.Time|timeFormat}}</p> <div class="avatar"> <img class="img" src="https://res.wx.qq.com/zh_CN/htmledition/v2/images/img31e225.gif" mm-src="{{readItem.HeadImgUrl}}" alt=""/> </div> <p class="info"> <span class="username">{{readItem.NickName}}</span> </p> </div> </div> <div ng-if="!readItem.UserName" class="read_item slide-left" ng-click="itemClick(readItem)" ng-class="{'active': (readItem == currentItem)}" > <div class="cont"> <h3 class="title">{{readItem.Title}}</h3> </div> <div class="ext"> <div class="cover"> <div class="img" ng-style="{'background-image': 'url('+ readItem.Cover +')'}"></div> </div> </div> </div> </div> </div>
公衆號正文窗口右上角的按鈕模版:ajax
<script type="text/ng-template" id="readMenu.html"><ul class="dropdown_menu"> <li> <a href="javascript:;" title="複製網頁連接" clip-copy="copyLink()" clip-click="copyCallback()"> <i class="menuicon_copylink"></i> 複製網頁連接 </a> </li> <!--<li>--> <!--<a href="javascript:;" title="轉發" ng-click="forwarding()">轉發</a>--> <!--</li>--> <li class="last_child"> <a href="javascript:;" title="新窗口中打開" ng-click="openTab()"> <i class="menuicon_newtab"></i> 新窗口中打開 </a> </li> </ul> </script>
其中,涉及公衆號數據初始化的代碼以下(見全局數據初始化代碼):chrome
S.init(n.MPSubscribeMsgList)
S爲subscribeMsgService, subscribeMsgService定義以下。json
angular.module("Services").factory("subscribeMsgService", ["$rootScope", "contactFactory", "accountFactory", "confFactory", "utilFactory", function(e, t, o, n, r) { var a = [], i = { current: null, changeFlag: 0, init: function(e) { this.changeFlag = Date.now(), this.add(e) }, getSubscribeMsgs: function() { return a }, add: function(e) { e.length > 0 && (this.changeFlag = Date.now()); for (var t = 0, n = e.length; n > t; t++) { var i = e[t]; i.HeadImgUrl = i.HeadImgUrl = r.getContactHeadImgUrl({ UserName: i.UserName, Skey: o.getSkey() }); for (var c = i.MPArticleList, s = 0; s < c.length; s++) { var l = c[s]; l.AppName = i.NickName, /dev\.web\.weixin/.test(location.href) || (l.Url = l.Url.replace(/^http:\/\//, "https://")) } a.push(i) } } }; return i }])
經過實際運行測試,以及代碼分析,能夠看到,微信網頁版僅在頁面載入時初始化公衆號文章。
一旦載入,再也不刷新,除非刷新頁面。
但凡使用到公衆號的數據的地方,都是調用的subscribeMsgService的getSubscribeMsgs方法,這個方法直接返回的是subscribeMsgService內部的變量a。
經過訪問 cgi-bin/mmwebwx-bin/webwxinit 來獲取。
經過chrome的network選項卡,能夠看到該請求。
a. Request URL: https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1311101047 b. Request Method: POST c. Status Code: 200 OK d. Remote Address: 101.226.76.164:443
a. Accept: application/json, text/plain, */* b. Accept-Encoding: gzip, deflate, br c. Accept-Language: zh,zh-CN;q=0.8 d. Cache-Control: no-cache e. Connection: keep-alive f. Content-Length: 100 g. Content-Type: application/json;charset=UTF-8 h. Cookie: xxxxxxxxxxxxx i. Host: wx.qq.com j. Origin: https://wx.qq.com k. Pragma: no-cache l. Referer: https://wx.qq.com/?mmdebug m. User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36
BaseRequest:{Uin: "xxxxxx", Sid: "xxxxxx", Skey: "", DeviceID: "e085070410028608"}
如下經過代碼說明。
c({ method: "POST", url: o.API_webwxinit, MMRetry: { count: 1, timeout: 1 }, data: { BaseRequest: { Uin: n.getUin(), Sid: n.getSid(), Skey: n.getSkey(), DeviceID: n.getDeviceID() } } })
請求爲POST方法,url爲o.API_webwxinit方法獲取,payload數據爲一個JSON對象。
其中API_webxinit方法爲
"/cgi-bin/mmwebwx-bin/webwxinit?r=" + ~new Date
JSON對象中,Uin和Sid都是cookie中獲取的,DeviceID爲臨時生成,具體參見getDeviceID()。
getDeviceID: function() { return "e" + ("" + Math.random().toFixed(15)).substring(2, 17) },
a. Connection: keep-alive b. Content-Encoding: gzip c. Content-Length: 30065 d. Content-Type: text/plain
{ "MPSubscribeMsgList": [{ "UserName": "@5a655a99997e77131aeec13f150dea45", "MPArticleCount": 2, "MPArticleList": [{ "Title": "xxx", "Digest": "xxx", "Cover": "http://mmbiz.qpic.cn/mmbiz_jpg/oZ9tOATGCKc1OIicaT9SGz2O3vUorCj7IdrCr0Al8F6cTMzvsMkVsgwS6iaablSEibLDrsjoUNvlc8Q7RxEqnLfibA/640?wxtype=jpeg&wxfrom=0", "Url": "http://mp.weixin.qq.com/s?__biz=MzA4NDc2MzIwNA==&mid=2656521000&idx=1&sn=d30318e4b975a806a71abfca19d11128&chksm=8441dc03b3365515536c5bcacea1796d438c68eff39172537c78c0f27c1a13003e8050f2056f&scene=0#rd" }, { "Title": "xxxx", "Digest": "xxxx", "Cover": "http://mmbiz.qpic.cn/mmbiz_jpg/oZ9tOATGCKc1OIicaT9SGz2O3vUorCj7IdrjZg89vPLkg1gcHN2iaYD35WVVaVZ4AnicRKUBBxmBZcWgX5PslHmAA/300?wxtype=jpeg&wxfrom=0", "Url": "http://mp.weixin.qq.com/s?__biz=MzA4NDc2MzIwNA==&mid=2656521000&idx=2&sn=7e8fab0fd99b7aa32c971164887e4da9&chksm=8441dc03b33655150ffc203bcb7d7eac2ddb8694a98ed3eededa18c532908a01757cd534ba7f&scene=0#rd" }], "Time": 1483759210, "NickName": "xxxx" }] }
微信網頁版前端的調試和生產環境在同一個庫文件中,根據URL地址的請求參數來判斷是否調試環境。若是是生產環境,則將console.log賦值爲null,這樣,在chrome的F12下將看不到調試信息。
開啓微信網頁版調試模式,在微信網頁版的地址欄後增長/?mmdebug。