在微信開(kāi)發(fā)者工具中,編譯運(yùn)行你的小程序項(xiàng)目,然后打開(kāi)控制臺(tái),輸入 document 并回車(chē),就可以看到小程序運(yùn)行時(shí),WebView 加載的完整的 page-frame.html,如下圖:

通過(guò)分析這個(gè) HTML 文件,我們可以得到小程序的啟動(dòng)執(zhí)行流程大致如下:

此圖來(lái)自上述文章,我們這里不再重復(fù)贅述這些流程,下面我們來(lái)看一下其中的 App() 和 Page() 的細(xì)節(jié)。這兩個(gè)函數(shù)在小程序框架 WAService.js 中定義,并在 app.js 和每個(gè)頁(yè)面的 page.js 中進(jìn)行調(diào)用實(shí)例化。
在微信開(kāi)發(fā)者工具的控制臺(tái)中執(zhí)行 openVendor() 方法,可以打開(kāi)小程序框架所在目錄,如下:
/Users/用戶(hù)名/Library/Application Support/微信web開(kāi)發(fā)者工具/WeappVendor/基礎(chǔ)庫(kù)版本號(hào)目錄
本文以 1.9.94 基礎(chǔ)庫(kù)為例進(jìn)行分析。WAService.js 文件的結(jié)構(gòu)如下:
;(function(global) {
// WeixinJSBridge 的定義和加載
// NativeBuffer 的定義和加載
// wxConsole 的定義和加載
// WeixinWorker 的定義和加載
// Reporter 的定義和加載
// __appServiceSDK__ 的定義和加載
wx = __appServiceSDK__.wx,
// exparser 的定義和加載
// __virtualDOM__ 的定義和加載
// __appServiceEngine__ 的定義和加載
Page = __appServiceEngine__.Page,
Component = __appServiceEngine__.Component,
Behavior = __appServiceEngine__.Behavior,
__webview_engine_version__ = .02,
App = __appServiceEngine__.App,
getApp = __appServiceEngine__.getApp,
getCurrentPages = __appServiceEngine__.getCurrentPages,
__createPluginGlobal = __appServiceEngine__.__createPluginGlobal,
// __wxModule__ 的定義和加載
definePlugin = __wxModule__.definePlugin,
requirePlugin = __wxModule__.requirePlugin;
// define 方法的定義
// require 方法的定義
global.App = App;
global.Page = Page;
global.Component = Component;
global.Behavior = Behavior;
global.__webview_engine_version__ = 0.02;
global.getApp = getApp;
global.getCurrentPages = getCurrentPages;
global.wx = wx;
global.definePlugin = __wxModule__.definePlugin;
global.requirePlugin = __wxModule__.requirePlugin;
})(this);
我們發(fā)現(xiàn),WAService.js 中定義了 WeixinJSBridge 和 wx 這兩個(gè)基礎(chǔ) API 集合,同時(shí)也包含的其他一些框架核心,如 exparser,__virtualDOM__,__appServiceEngine__ 等。其中__appServiceEngine__ 提供了框架最基本的對(duì)外接口,如 App,Page,Component,Behavior 等方法;exparser 提供了框架底層的能力,如實(shí)例化組件,數(shù)據(jù)變化監(jiān)聽(tīng),View 層與邏輯層的交互等;__virtualDOM__ 則起著連接 __appServiceEngine__ 和 exparser 的作用,如對(duì)開(kāi)發(fā)者傳入 Page 方法的對(duì)象進(jìn)行格式化再傳入 exparser 的對(duì)應(yīng)方法處理。(此段分析摘自上述文章)
由上可知,本文要分析的全局函數(shù) App() 和 Page() 是對(duì) WAService.js 中定義的 __appServiceEngine__ 對(duì)象同名方法的引用。下面我們簡(jiǎn)要分析一下它們的內(nèi)部實(shí)現(xiàn)和初始化流程。
根據(jù)微信小程序開(kāi)發(fā)文檔,App() 函數(shù)用來(lái)注冊(cè)一個(gè)小程序,接收一個(gè) object 對(duì)象參數(shù),其指定小程序的生命周期函數(shù)等。我們從微信開(kāi)發(fā)者工具的函數(shù)提示可以知道,App() 函數(shù)的聲明如下:
function App(options: _AppOptions): void
對(duì)于入?yún)?nbsp;object 對(duì)象(_AppOptions)的屬性說(shuō)明如下:

此外,全局的 getApp() 函數(shù)可以用來(lái)獲取到小程序?qū)嵗穆暶魅缦拢?/p>
function getApp(): object
內(nèi)部實(shí)現(xiàn)
在 __appServiceEngine__ 對(duì)象中,對(duì) App 和 getApp 屬性的定義如下:
// 其中的 t 就是 __appServiceEngine__ 對(duì)象
var i = n(17);
Object.defineProperty(t, "App", {
enumerable: !0,
get: function() {
return i.appHolder
}
}),
Object.defineProperty(t, "getApp", {
enumerable: !0,
get: function() {
return i.getApp
}
}),
而這兩個(gè)屬性對(duì)應(yīng)的實(shí)現(xiàn)分別為 appHolder() 和 getApp() 方法,定義如下:
l = void 0,
t.appHolder = (0, i.surroundByTryCatch)(function(e) {
l = new y(e)
}, "create app instance"),
t.getApp = function() {
return l
},
由上可知,在 appHolder() 方法中,把外部傳入的 object 對(duì)象傳給 y(...) 方法進(jìn)行初始化一個(gè)小程序?qū)嵗龑?duì)象,并把結(jié)果賦給變量 l 緩存起來(lái),而在 getApp() 方法中則直接 return l,返回當(dāng)前小程序?qū)ο蟆?/p>
在上述 page-frame.html 中,我們知道,在 app.js 被加載完后,小程序框架會(huì)立即執(zhí)行 require('app.js') 進(jìn)行注冊(cè)小程序?qū)嵗?,即?duì) App() 函數(shù)進(jìn)行調(diào)用(開(kāi)發(fā)者已經(jīng)在 app.js 中定義好了入?yún)?duì)象),如下:
<script src="./app.js"></script>
<script>require("app.js")</script>
在 App() 函數(shù)中,最終會(huì)調(diào)用 y(...) 方法進(jìn)行初始化,其中 y(...) 的定義比較長(zhǎng),我們這里不再貼出代碼,詳情請(qǐng)自行查閱 WAService.js,它的處理流程如下:
聲明 App.getCurrentPage 方法將被廢棄,請(qǐng)使用 getCurrentPages() 全局方法;
綁定生命周期函數(shù),即把外部入?yún)?duì)象定義的屬性綁定到小程序?qū)嵗龑?duì)象中,包括 onLaunch,onShow,onHide,onUnlaunch 和 onPageNotFound;
綁定開(kāi)發(fā)者自定義的其他屬性(包括數(shù)據(jù)和方法),并校驗(yàn)屬性名是否為 “getCurrentPage”,如果是則警告;
根據(jù)外部是否有定義 onError 屬性判斷是否注冊(cè)錯(cuò)誤上報(bào);
檢查啟動(dòng)參數(shù)(取自__wxConfig.appLaunchInfo)并依次調(diào)用 onLaunch 和 onShow 方法;
注冊(cè)前后臺(tái)切換回調(diào) onShow 和 onHide;
注冊(cè)找不到頁(yè)面的回調(diào) onPageNotFound;
返回實(shí)例給 App() 函數(shù)進(jìn)行緩存。
根據(jù)文檔,Page() 函數(shù)用來(lái)注冊(cè)一個(gè)頁(yè)面,接收一個(gè) object 對(duì)象參數(shù),其指定頁(yè)面的初始數(shù)據(jù)、生命周期函數(shù)、事件處理函數(shù)等。Page() 函數(shù)的聲明如下:
function Page(page: PageOptions): void

此外,getCurrentPages() 函數(shù)用于獲取當(dāng)前頁(yè)面棧的實(shí)例,以數(shù)組形式按棧的順序給出,第一個(gè)元素為首頁(yè),最后一個(gè)元素為當(dāng)前頁(yè)面。它的聲明如下:
function getCurrentPages(): object[]
同樣地,在 __appServiceEngine__ 對(duì)象中,對(duì) Page 和 getCurrentPages 屬性的定義如下:
var r = n(2);
Object.defineProperty(t, "Page", {
enumerable: !0,
get: function() {
return r.pageHolder
}
}),
Object.defineProperty(t, "getCurrentPages", {
enumerable: !0,
get: function() {
return r.getCurrentPages
}
}),
而這兩個(gè)屬性對(duì)應(yīng)的實(shí)現(xiàn)分別為 pageHolder() 和 getCurrentPages() 方法,定義如下:
var k = void 0, // 保存當(dāng)前顯示的頁(yè)面(棧頂)
x = [], // 保存已加載過(guò)的頁(yè)面歷史棧數(shù)組
// 其中的 t 就是 __appServiceEngine__ 對(duì)象
t.getCurrentPage = function() {
return k
},
t.getCurrentPages = function() {
var e = [];
return x.forEach(function(t) {
e.push(t.page)
}),
e
},
M = {}, // 緩存所有已經(jīng)注冊(cè)的頁(yè)面
t.pageHolder = function(e) {
if (!__wxRouteBegin) throw (0, f.error)("Page 注冊(cè)錯(cuò)誤", "Please do not register multiple Pages in " + __wxRoute + ".js"),
new a.AppServiceEngineKnownError("Please do not register multiple Pages in " + __wxRoute + ".js");
__wxRouteBegin = !1;
var t = __wxRoute;
if (!A(t)) throw (0, f.error)("Page 注冊(cè)錯(cuò)誤", __wxRoute + " has not been declared in app.json."),
new a.AppServiceEngineKnownError(__wxRoute + " has not been declared in app.json.");
var n = "undefined" != typeof __wxAppCode__ ? __wxAppCode__[t + ".json"] || {}: {};
if ("Object" !== (0, f.getDataType)(e)) throw (0, f.error)("Page 注冊(cè)錯(cuò)誤", "Options is not object: " + JSON.stringify(e) + " in " + __wxRoute + ".js"),
new a.AppServiceEngineKnownError("Options is not object: " + JSON.stringify(e) + " in " + __wxRoute + ".js"); (0, f.info)("Register Page: " + t),
void 0 !== n.usingComponents ? (__virtualDOM__.Page(e), M[t] = exparser.Component._list[t]) : M[t] = e
},
分析上述代碼,我們可以總結(jié) pageHolder 方法的處理流程如下:
小程序在每加載一個(gè)頁(yè)面前,會(huì)先設(shè)置 __wxRouteBegin = true,用于標(biāo)記防重;
判斷 __wxRouteBegin 是否為 false,如果是,則拋出多次調(diào)用 Page 注冊(cè)錯(cuò)誤;
設(shè)置 __wxRouteBegin 為 false,避免被后續(xù)代碼被重復(fù)執(zhí)行;
調(diào)用 A(...) 方法檢查當(dāng)前頁(yè)面是否在 app.json 中定義,如果沒(méi)有,則拋出錯(cuò)誤;
檢查外部入?yún)ⅲ≒ageOptions)是否為 Object 對(duì)象,如果不是,則拋出錯(cuò)誤;
判斷當(dāng)前頁(yè)面是否使用了自定義組件(對(duì)于使用了自定義組件的 Page 對(duì)象會(huì)采用不同的配置),然后緩存當(dāng)前 Page 的配置到 M 對(duì)象中。
此外,我們可以發(fā)現(xiàn),與 App() 不同的是,外部通過(guò) Page() 函數(shù)傳入的(生命周期)代碼并不會(huì)在這里被執(zhí)行,而是等待頁(yè)面 Ready 并進(jìn)入頁(yè)面進(jìn)行實(shí)例化后才執(zhí)行。
同樣地,根據(jù) page-frame.html 的加載順序,在 app.js 被加載并執(zhí)行后,小程序之后會(huì)先依次按順序加載所有的自定義組件代碼(如果有)并自動(dòng)注冊(cè)。自定義組件(Component)在小程序開(kāi)發(fā)中具有重要地位,它可以豐富小程序的基礎(chǔ)功能,擁有的能力比 Page 更強(qiáng)大,因此實(shí)現(xiàn)也更加復(fù)雜,篇幅有限,我們后續(xù)再單獨(dú)寫(xiě)文章進(jìn)行分析。
在加載執(zhí)行完自定義組件的代碼后,小程序緊接著會(huì)依次按順序加載每個(gè)頁(yè)面的代碼,并執(zhí)行 require(...) 進(jìn)行頁(yè)面注冊(cè),如下:
<script>__wxRoute = "pages/index/index";__wxRouteBegin = true</script>
<script>__wxAppCurrentFile__ = "pages/index/index.js"</script>
<script src="./pages/index/index.js"></script>
<script>require("pages/index/index.js")</script>
<script>
if(__wxRouteBegin) {
console.group("Tue Jun 26 2018 17:53:09 GMT+0800 (CST) page 編譯錯(cuò)誤")
console.error("pages/index/index.js 出現(xiàn)腳本錯(cuò)誤或者未正確調(diào)用 Page()")
console.groupEnd()
}
</script>
<!-- 加載注冊(cè)下一個(gè) Page -->
設(shè)置 __wxRoute 為當(dāng)前 Page 的路徑,設(shè)置 __wxRouteBegin 為 true;
設(shè)置 __wxAppCurrentFile__ 為當(dāng)前加載的文件路徑;
加載頁(yè)面代碼并執(zhí)行進(jìn)行注冊(cè)頁(yè)面(參考上述 pageHolder 的處理流程);
判斷 __wxRouteBegin 是否為 false,來(lái)判斷該頁(yè)面是否被成功注冊(cè)(因?yàn)樵?nbsp;pageHolder方法中,成功執(zhí)行是,會(huì)把 __wxRouteBegin 置為 false);
依次加載其他 Page;
等待頁(yè)面 Ready 和 Page 實(shí)例化,page Load 由 wx.onAppRoute 事件觸發(fā)。
在 page-frame.html 中,當(dāng) head 中的所有 JS 代碼都執(zhí)行完畢后,會(huì)在 body 中觸發(fā) DOCUMENT_READY 事件,如下:
<body>
<script>
if (document.readyState == 'complete') {
window.__global.alert('DOCUMENT_READY')
} else {
var fn = function(event) {
window.__global.alert('DOCUMENT_READY')
window.removeEventListener("load", fn)
}
window.addEventListener('load', fn)
}
</script>
</body>
在小程序框架 WAService.js 中,最終 DOCUMENT_READY 會(huì)轉(zhuǎn)化為 wx.onAppRoute 事件(邏輯待驗(yàn)證),最終在 wx.onAppRoute 事件中進(jìn)行頁(yè)面的實(shí)例化或者頁(yè)面切換。
PS:關(guān)于一個(gè)小程序頁(yè)面的完整初始化加載流程,我們將在下一篇文章中詳解。
本文簡(jiǎn)要地分析了 App() 、getApp() 和 Page() 、getCurrentPages() 等幾個(gè)函數(shù)的內(nèi)部實(shí)現(xiàn),希望能讓你更好地理解小程序?qū)嵗龑?duì)象和頁(yè)面的加載過(guò)程,給你實(shí)際開(kāi)發(fā)帶來(lái)幫助。