(原創,轉載請注明出處)
一、微信網頁版前端結構
微信網頁版為angular應用。
angular應用啟動代碼
angular.bootstrap(document, ["webwxApp"])
angular應用主要Module
angular.module("Services")
angular.module("Controllers")
angular.module("Directives")
angular應用主要模版文件
/readMenu.html
angular應用配置代碼
微信網頁版采用了ui-route模塊來配置路由,與頁面相對應,聊天二級頁對應'chat' state,聯系人二級頁對應'contact' state, 公眾號二級頁對應'read' state。
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)
}
})
}]
}
}
})
}]);
二、微信網頁版全局數據初始化
全局控制器appController
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賦值。
appFactory的init方法
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。
<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。
<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>
公眾號正文窗口右上角的按鈕模版:
<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>
其中,涉及公眾號數據初始化的代碼如下(見全局數據初始化代碼):
S.init(n.MPSubscribeMsgList)
S為subscribeMsgService, subscribeMsgService定義如下。
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 來獲取。
HTTP請求包分析
通過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
請求數據Palyload
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)
},
HTTP響應包
響應頭
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。