嘿,各位作为小程序的开发者,有认真看过小程序的page-frame.html吗!想要告诉大家,很有必要的喔!通过这段html,管中窥豹,可以领悟到小程序代码的加载流程,还蛮有收获的,故在此记录一番~
在此之前,先强烈安利有赞团队的从源码看微信小程序启动过程 ,超良心超有料,我看了三遍都么有完全领悟得道(¯∀¯٥)…有想深入了解的同学可以研读一下这篇文章~
page-frame.html
其实我觉得从模块引用和页面展示来说,小程序开发颇像开发单页应用。虽然开发小程序时会分不同的页面来编写,但实际呈现出来的是一张大页面,也就是page-frame.html。
在开发者工具的控制台输入document,即可以清晰得到整段html。如下图:
整份html的各个部分在这里就不再赘言啦,基本是初始化全局变量>>加载框架WAService.js>>加载业务代码的过程。而我最感兴趣的是业务代码的加载。
显而易见,每一份js代码实际通过script的方式加载。那浮现在我脑中的第一个问题自然是,小程序是如何实现js模块的引用的呢?
小程序的js模块加载机制
以一个简单的demo为例,pages/home/index.js与pages/home/common.js,前者引入后者。
pages/home/index.js:
import boom from './common';
Page({});
复制代码
pages/home/common.js:
export default 'boom';
复制代码
经小程序处理后,结果如下>>>>
pages/home/index.js:
define("pages/home/index.js", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,fetch,XMLHttpRequest,WebSocket,webkit,WeixinJSCore,Reporter,print,URL,DOMParser,upload,preview,build,showDecryptedInfo,syncMessage,checkProxy,showSystemInfo,openVendor,openToolsLog,showRequestInfo,help,showDebugInfoTable,closeDebug,showDebugInfo,__global,WeixinJSBridge){ 'use strict';
var _common = require('./common');
var _common2 = _interopRequireDefault(_common);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Page({});
});
复制代码
pages/home/common.js:
define("pages/home/common.js", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,fetch,XMLHttpRequest,WebSocket,webkit,WeixinJSCore,Reporter,print,URL,DOMParser,upload,preview,build,showDecryptedInfo,syncMessage,checkProxy,showSystemInfo,openVendor,openToolsLog,showRequestInfo,help,showDebugInfoTable,closeDebug,showDebugInfo,__global,WeixinJSBridge){ 'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = 'boom';
});
复制代码
可见小程序对这份代码做了es6>es5的转换(当然前提是要在项目设置中打开允许),并将其处理成类amd的模式。那么小程序是如何处理define及require的呢?
因为代码在执行时全局变量define和局域变量require已存在,所以我们可以在代码中打印它们出来,并顺势找到这两段代码定义的位置。它们都位于WAService.js。
define:
代码表明,所有js都以路径-模块的键值对的方式存在了某全局变量c里。模块有两个属性,status和factory。其中status暂为1,代表模块尚未加载;factory即被闭包在函数中的模块内容。
require:
可以看到require可分为三步:
- 从全局变量c中读出define里定义的模块对象(即当前的t)。
- 如status为1,则初始化模块;否则直接返回t.exports。
- 加载模块
①将status的值置为2,表示该模块已被初始化。
②调用t.factory(即r(...)这一步)加载模块,并将结果赋给t.exports。
仔细看看r()这一步,有传入三个入参,分别对应define中的require, module及exports。第一个入参,i(e),它到底执行了什么呢?答案就在require的上面一个代码块中(line 37005)。它对模块的存在性做了校验,若读不到模块,会抛出cant find module xxx的异常。它的返回值就是require函数。
③当一段js模块加载完成后,其模块对象会变成
{
status: 2,
factory: t,
exports: t里导出的值
}
复制代码
所以,这其实就是典型的模块加载思路,我觉得也与webpack打包模块的处理方式十分相似,保证每份js模块只在被第一次引用时被初始化一次,并将结果缓存全局以供其他模块日后使用。
js引入流程
通用加载
了解完小程序的js模块加载机制,再将目光移回page-frame.html~
不难发现小程序通过script标签引入所有js,并按照其他js > 页面js >app.js的顺序。之后会立即执行require('app.js')和require('各页面.js')来注册app和页面。
不过,大家有好奇过这几块script标签是如何生成的吗?它们是一开始就存在吗?只要我们接着往下看,注意之后的script标签,可以留意到这样一段代码:
重要的部分我大概圈了出来~从中发现,其实上段代码中小程序引入的所有js script,都是通过这段终极script制造出来的喔。它的行动过程大概分为两步:
- 通过document.head.appendChild()动态append所有需要加载的js,如
<script src="pages/home/index"></script>
`(小小留意一下顺序问题,app.js最后才被add script) - 待所有需加载js的script添加完毕后,会在最后append进
require('app.js')和require('各页面.js')
的代码块来执行对应板块的代码!~
所以这就是为什么app.js和各page.js在引入完成后,就会自动执行的原因~
组件加载
组件是小程序晚些时候提供的能力,我们也可以将其视为一种特殊的页面。如下图,可见组件加载方式也是与页面一样一样的~(当然实现不一样)。
好的,目前为止,普通页面的加载过程应该已一目了然~那自然引入下一个问题,小程序是如何处理分包代码加载的呢,大概率也是在前往分包页面后,将分包代码append script的把?
分包加载
那么页面结构保持不变,不过在app.json中,将pages/pageA划入subPackages的分包范畴。再次刷新代码:
在home页时,可以看到document的结构中确实只有app.js和home.js,没有引入pageA.js:
然后navigateTo pageA页,再次检阅document结构,发现符合猜想:
原代码的基础上多了pageA的script,以及又一大坨代码段,粗略看起来是处理wxml的nodes外加与处理加载相关的其他代码…不过之前我以为也会显性通过appendrequire('pages/pagesA/index.js')
代码块的方式来执行pageA.js页面,现在看来并没有。
总结
①page-frame.html体现了小程序处理页面的显性流程。
②各js模块通过script标签引入,通过类AMD方式进行加载执行。
③app.js和page.js以及component.js会在引入后立即执行。
emmm…虽然知道了这些我很开心 但是貌似对开发也么有什么太大的帮助>.< 不过总比两眼一抹黑的状态要好啦,并且想做小程序打包时,知道这些会有些许裨益~!Happy wxa coding!