- 微信 schema 跳转之非官方文档
微信“应该”是最近开放了 schema 跳转小程序 的能力,大大方便了短信、邮件、外部网页等唤起微信小程序。 schema 链接格式大体是这样:[代码]weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e[代码],但是这段文本在安卓端无法识别。小规模测试结果如下: [图片] 后来想到用一个正常能够识别的网页地址,内容是重定向到指定的 schema 链接。这就是擅长的领域了,query 参数上带上 schema 链接,location.href 一下不就行了。这里就不 show 代码了,能看到文章的你一定行。 但是,发现在部分安卓手机下(如小米)还是没反应,原来简单的 schema 跳转水这么深的,于是百度谷歌了一下,找到了下面两份关键材料: H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 看源码也不多,总结下来,因不同系统和浏览器对 schema 规范的理解不同,还有一些商业因素,不同环境下面需要用不同的方式进行跳转,甚至有的环境你根本就跳不了。 时间紧,任务重。简单处理吧,不同方式都来一遍,谁好使就用谁。所以简单总结了下,能用的几种方式: location 跳转 a 链接跳转 iframe 跳转 以上三种方式,逐一试用,最后实在不行就不行吧,简单处理,看有没有大神补充的。 相关代码如下: location [代码]location.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; [代码] a 链接跳转 [代码]var aLink = document.createElement("a"); aLink.className = 'call_up_a_link'; aLink.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; aLink.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(aLink); aLink.click(); [代码] iframe [代码]var iframe = document.createElement('iframe'); iframe.className = 'call_up_iframe'; iframe.src = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; iframe.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(iframe); [代码] 以上代码均可从参考资料中找到出处,感谢 是直接一进来就执行,还是事件触发,都可以。或者是一开始进来就执行,失败了显示几个可选跳转按钮让用户手动触发跳转。 但是关键问题还有一个,如何判断是可以成功唤起了呢?上述 github 代码里提到了一个根据页面 hidden 状态,但不够精准,如果用户没有选择跳转到微信呢?这是另一个需要深究的问题。 出于时间考虑,先以业务交付优先,如果有朋友知道的也可以一起讨论下。 另行文时间短,以技术交流为主,若有瑕疵,欢迎指出。 附上 vue 版本源码:微信 schema 跳转 参考链接: 微信官方文档:urlscheme.generate H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 安卓端,微信schema无法跳转微信小程序?
2021-01-04 - 从源码角度理解 wx.reLaunch 执行过程
正文稍显啰嗦,可直接拖到文末看总结的执行流程图以及在开发过程中需要注意的一些坑点。 相信老司机们看到这个标题就笑了,[代码]wx.reLaunch[代码]有啥好解释的,顾名思义就是:「重新启动小程序」。重启这个词,对于工程师最熟悉不过了,很多问题一出现,往往第一反应就是“重启”一下就能解决掉,能重启解决的问题,那都不是“大问题”。 wx.reLaunch === 重启? 那这样来理解[代码]wx.reLaunch[代码]的话,那就应该是“微信会 kill 掉当前这个小程序进程,然后像第一次加载小程序那样,重新打开小程序”。 如果是这样的话,那[代码]bug[代码]应该离你就不远了。事实并非如此。 微信文档怎么说 进一步,那不是这样的话,那我们查查微信文档怎么说的?wx.reLaunch(Object object) 关闭所有页面,打开到应用内的某个页面 看到这里,老司机们都拍了拍大腿,原来是这样:清空所有路由,并打开新的页面。我知道了,也很简单嘛。ok,理解了的话,那我再追问几个问题吧: 关闭所有页面会执行什么操作? 打开新页面的过程中,除了新页面 Page 的各生命周期外,全局 js 会重新执行吗(加载时即执行)? 如果 reLaunch 的目标页是分包内的页面呢?执行过程又是怎样的 写段代码测试下 事实胜于雄辩,依赖文档来编程是合理的,但是如果我们要继续深入来探讨代码的执行,还是远远不够的。 我们先来写段代码片段测试一下,链接:https://developers.weixin.qq.com/s/tis8tVmX7ShS(需要真机调试需要填写 appId)。 有 p0 - p4 共 5 个页面,我们的测试路径是这样的,打开小程序-p0-p1-p2,然后 reLaunch 到 p3,看控制台输出: [图片] 这里我们能看到,“关闭所有页面”,对应会依次会把当前页面栈的页面出栈,并触发相应的 [代码]unUnload[代码]回调。 就不耽误大家的时间,经过反复测试,最终得出在 [代码]wx.reLaunch[代码]的执行过程如下: [图片] 追溯源码 其实到了这一步,相信大家对 [代码]wx.reLaunch[代码]有了进一步的认识,而不是像开始那样觉得就是单纯的重启了。但是到这里就结束了吗?如果说有一样东西能够 100% 让人信服,单纯测试是不够的,总可能会有遗漏的测试用例,无法保证结论的正确性。so… Talk is cheap, show me your code. 我们下一步打算从源码层去看一看 [代码]wx.reLaunch[代码]到底做了些什么。 什么?你不知道源码怎么看… (嗯,网上其实有文章,介绍怎么去找到小程序框架层的源码) 我的方法是这样的:在控制台输入[代码]wx.reLaunch()[代码]并执行,我们看到会抛出一个错误,我们点开错误堆栈: [图片] WAService.js 就是小程序框架的源码了,cmd+f,点进去输入关键词[代码]reLaunch[代码]搜索,我们会找到关键代码,也就是搜索结果的 9/40 和 40/40,代码如下: 9/40 [代码]b = function(e) { var t; "active" === a.default.runningStatus || "ios" !== c.PLATFORM ? Object(u.beforeInvoke)("reLaunch", e, { url: "" }) && (!v("reLaunch", e) || (t = g("reLaunch", e, !1)) && (e.url = t, e.url = Object(i.encodeUrlQuery)(e.url), h("reLaunch", e.url, e) && (a.default.navigatorLock = !0, Object(u.invokeMethod)("reLaunch", e, { afterFail: function() { a.default.navigatorLock = !1 } }), p.emit({ type: "reLaunch", start: Date.now() })))) : Object(u.beforeInvokeFail)("reLaunch", e, "can not invoke reLaunch in background") } [代码] 40/40 [代码]"reLaunch" === r || "autoReLaunch" === r ? function(t, e, n, r, o, i) { __appServiceSDK__.traceBeginEvent("Framework", "onReLaunch"); var a = !1; for ("reLaunch" === o && (a = !0); 0 < lt.length;) { var s = !0, a = a && (s = !1); Pt(lt[lt.length - 1], t, !1, s) } Object.keys(st).forEach(function(e) { Pt(st[e], t, !1, !0) }), le(t), jt(t, e, n, r, { isMainTabBarPage: xt({ route: t }), initialRenderingCacheData: i }), __appServiceSDK__.traceEndEvent() } (e, C, t, n, r, i) [代码] 然后,我们开启漫长的 debug 的过程,因为我们对其内部实现并不熟悉,所以最开始我们找到入口,然后逐步调试。 我们第一步其实就有发现: [代码]"active" === a.default.runningStatus || "ios" !== c.PLATFORM ? ... : Object(u.beforeInvokeFail)("reLaunch", e, "can not invoke reLaunch in background") [代码] 这段代码其实一眼看上去就能猜测到,[代码]wx.reLaunch[代码]在非 iOS 设备下,如果小程序不在前台,那执行会报错。我们看社区里反馈了很多类似的问题,最常见的一个就是「在微信支付后为什么页面没有跳转」,其实都可以从这行代码找到问题的原因(https://developers.weixin.qq.com/community/develop/doc/000caaf5e9cf486e710aaf18751800)。 这里其实我们会想为什么微信会有这个限制,我猜测可能和 [代码]wx.reLaunch[代码]执行过程中,有一些操作在非 iOS 设备下是无法完成的。(在使用小程序过程中,我们会发现安卓下的小程序和微信进程是相互独立的,但是在 iOS 下,小程序进程和微信进程是同一个。参见小程序运行环境) 我们继续往下,又发现一个关键信息: [图片] 这就验证了如果 reLaunch 目标页是分包代码代码时, 会先加载分包代码的结论了。而且事实上继续往下单步执行,在控制台也会先执行 p4/index.js 中 Page 外面写的 console 语句。 接下来,我们先把我们之前发现的第 2 段关键代码打上断点,因为发现如果不断的话会直接执行掉,这里应该是被另外一层函数封装了,没进到里面的代码。我们接下来分析[代码]wx.reLaunch[代码]定义的核心代码会执行什么逻辑: 代码结构是非常清晰的,是一个 IIFE,函数主体被包裹在了 begin 和 end 中,赞一个。 [代码]"reLaunch" === r || "autoReLaunch" === r ? function(t, e, n, r, o, i) { __appServiceSDK__.traceBeginEvent("Framework", "onReLaunch"); // ... __appServiceSDK__.traceEndEvent() } (e, C, t, n, r, i) [代码] 入参的 6 个变量值分别是: [图片] 我们分为 5 句代码: 第一句: [代码]var a = !1; [代码] 这里定义了一个局部变量 a,值为 false 第二句: [代码]for ("reLaunch" === o && (a = !0); 0 < lt.length;) { var s = !0, a = a && (s = !1); Pt(lt[lt.length - 1], t, !1, s) } [代码] 看到这里,我们会看到有 3 个变量,我们没办法直接看出分别代表什么: s,局部变量,初始值为 true,如果 a 为 true 的话,s 会变成 false。s 作为第 4 个变量传入 Pt 函数中 lt,通过 watch,猜测 lt 即为页面栈 [图片] Pt,直接通过 watch,没办法看出来这个函数是什么,不过我们可以点击下面的 [[FunctionLocation]] 查看函数定义的位置。 [图片] [代码]function Pt(e, t, n, r) { __appServiceSDK__.traceBeginEvent("Framework", "unloadPage"), Ze(e.webviewId), e.page.__toRoute__ = t, e.page.__isBack__ = n, e.page.__notReportHide__ = r, e.page.__callPageLifeTime__("onUnload"), e.node && Y.destroy(e.page), Object(L.isDevTools)() && (delete __wxAppData[e.route], __appServiceSDK__.publishUpdateAppData()), delete st[e.webviewId], (lt = lt.slice(0, lt.length - 1)).length ? gt(lt[lt.length - 1].route) : gt(""), ze("pageUnload", e.page), ze("leavePage", e.page), __appServiceSDK__.traceEndEvent(), __appServiceSDK__.uploadUserLogOnHide(e.route) } [代码] 这里,我们就能知道了,Pt 函数做的事情就是对传入的页面执行其 [代码]onUnload[代码]生命周期函数。 这段代码是**对页面栈进行遍历,依次触发页面的[代码]onUnload[代码]**回调。 第三句: [代码]Object.keys(st).forEach(function(e) { Pt(st[e], t, !1, !0) }), [代码] 我们先看看 st 是什么? [图片] 发现 st 是以 webviewId 为 key,以 page 对象及一些其他带有 page 特性的标识字段组合 为 value 的结构。 所以这段代码仍然是遍历页面(这里我理解是无序的,因为 Object.keys 首先就不能保证有序,参考Object.keys(…)对象属性的顺序?),依次触发其 [代码]onUnload[代码]回调。 这里先抛出两个问题: 第 2 句代码执行后,这段代码还有什么作用? 重复触发 [代码]onUnload[代码],不出意外,框架内部会根据 webviewId 来判断能否执行。即 [代码]__callPageLifeTime__[代码]函数内部逻辑。 暂时先跳过,回到主流程。 第四句: [代码]le(t) [代码] 同样的方式,我们找到 le 函数的源码: [代码]// line 94280 var ue = !1; function le(e) { var t = __appServiceSDK__.isIsolatedSubpackage(e); // 是否独立分包 !ue && t ? ge() : ue && !t && he(), ue = t } function he(t) { t = t || pe, __appServiceSDK__.emitIsloatedAppShow(t), de.forEach(function(e) { e.preventOnShow || e.app.onShow(t), e.preventOnShow = !1 }), pe = t } function ge(t) { __appServiceSDK__.emitIsloatedAppHide(), de.forEach(function(e) { 7 === __wxConfig.appType ? e.app.onHide(t) : e.app.onHide() }) } [代码] 我们找到微信文档对于独立分包的定义:独立分包。从独立分包中页面进入小程序时,不需要下载主包,可以很大程度上提升分包页面的启动速度。 配置方式是在分包配置里加上[代码]independent[代码]字段。因为我们这里没考虑到这种情况,感觉这里不会影响主流程,先跳过。 第五句: [代码]jt(t, e, n, r, { isMainTabBarPage: xt({ route: t }), initialRenderingCacheData: i }) [代码] 通过前面,我们不难推测出,这段代码应该是“打开目标页面”。通过参数发现,目标页面对于 tab 页和非 tab 页有区别对待,继续找到 jt 函数: [代码]function jt(e, t, n, r, o) { var i = 4 < arguments.length && void 0 !== o ? o: {}; __appServiceSDK__.traceBeginEvent("Framework", "openNewPage"), se(te); var a = ot; ot = void 0; var s = null; Ye.call(ct, e) ? s = ct[e] : console.info('Page "' + e + '" has not been registered yet.'); // 检查是否注册 var c = bt(e); wt.newPageTime = Date.now(), gt(e); var u = c ? Y.create(n, e) : q.create(n, e, s || {}), // 初始化 page l = u.page, d = b(r); (nt = { // nt,新页面对象 page: l, webviewId: n, route: e, rawPath: t, lastRoute: nt ? nt.route: "", lastQuery: nt ? nt.page.options: {}, node: u.node }).isTabBarPage = xt(nt), // 是否 tabBar nt.isMainTabBarPage = i.isMainTabBarPage || !1, // 是否主 tabBar lt.push(nt), // 页面栈 l.__exitState__ = a; var f, p, h = u.node, g = {}, v = !1; Object.keys(d).forEach(function(e) { exparser.Component.hasProperty(h, e) && (g[e] = decodeURIComponent(d[e]), v = !0) }), v && h.setData(g), __virtualDOM__.attachView(n), // attachView /^__wx__\//.test(e) && (f = {}, /^__wx__\/open-api-redirecting-page/.test(e) ? f = me || {}: !/^__wx__\/functional-page/.test(e) || (p = _e()) && p.functionalPage && (f = Object.assign({}, p.functionalPage, { accountInfo: __wxConfig.accountInfo })), l.setData(f)), l.options = d, Lt(nt, n, wt.newPageTime, void 0, !1), // Lt 函数 Object(L.isDevTools)() && (__wxAppData[e] = l.data, __wxAppData[e].__webviewId__ = n, __appServiceSDK__.publishUpdateAppData()), i.initialRenderingCacheData && l.setData(i.initialRenderingCacheData); var _ = __appServiceSDK__._getOpenerEventChannel(); _ && (nt.eventChannel = _), l.__callPageLifeTime__("onLoad", r), // 1.触发 onLoad l.__callPageLifeTime__("onShow"), // 2.触发 onShow st[n] = { page: l, route: e, rawPath: t, webviewId: n, statesData: null, node: c ? u.node: void 0 }, // webviewId 为 key 的特殊结构 ze("pageLoad", l), ze("enterPage", l), vt("appRoute2newPage", wt.appRouteTime, wt.newPageTime), __appServiceSDK__.traceEndEvent() } [代码] 我们在里面发现了我们熟悉的 lt、st 变量,并且在函数内被初始化。其中有一个 Lt 函数,我们找到其函数定义。 [代码]var Lt = _(function(e, t, n, r, o) { __appServiceSDK__.traceBeginEvent("Framework", "publishInitData"), j("Update view with init data"); var i = e.page, a = {}; a.wechatLibVersion = ("undefined" != typeof __libVersionInfo__ ? __libVersionInfo__.version: "") || "", a.webviewId = t, a.enablePullUpRefresh = _t(i, "onReachBottom"), a.enablePageScroll = _t(i, "onPageScroll"), a.onReachBottomDistance = function(e) { try { if ("number" == typeof __wxConfig.page[e + ".html"].window.onReachBottomDistance) return __wxConfig.page[e + ".html"].window.onReachBottomDistance } catch(e) { return $.DEFAULT_ON_REACH_BOTTOM_DISTANCE } return $.DEFAULT_ON_REACH_BOTTOM_DISTANCE } (i.__route__), a.statesData = r, a.scene = rt, a.route = i.__route__, a.query = i.options, a.lastRoute = e.lastRoute, a.lastQuery = e.lastQuery, a.wxConfig = { accountInfo: __wxConfig && __wxConfig.accountInfo || {}, appContactInfo: __wxConfig && __wxConfig.appContactInfo || {}, appLaunchInfo: __wxConfig && __wxConfig.appLaunchInfo || {}, plugins: __wxConfig && __wxConfig.plugins || {} }, a.windowConfig = __wxConfig && __wxConfig.page && __wxConfig.page[i.__route__ + ".html"] && __wxConfig.page[i.__route__ + ".html"].window || {}, a.debug = __wxConfig && __wxConfig.debug, a.appId = __wxConfig && __wxConfig.accountInfo && __wxConfig.accountInfo.appId, a.appLaunchTime = wt.appLaunchTime, a.appFgTime = wt.appFgTime, a.isTabBarPage = e.isTabBarPage, a.isMainTabBarPage = e.isMainTabBarPage, a.navigationStyle = __wxConfig && __wxConfig.global && __wxConfig.global.window && __wxConfig.global.window.navigationStyle, a.packageType = function(e) { if (__wxConfig && __wxConfig.subPackages && __wxConfig.subPackages.length) { for (var t = 0; t < __wxConfig.subPackages.length; t++) { var n = __wxConfig.subPackages[t]; if (0 === e.indexOf(n.root)) return n.independent ? "independent": "normal" } return "main" } return "none" } (i.__route__), a.needGetSubjectInfo = !At.isInit, a.subPackages = __wxConfig.subPackages, a.perfData = (ie[ne] = Date.now(), ie[X] = ae ? 1 : 0, ie[oe] = __wxConfig.isSubContext ? 1 : 0, ie[Q] = __wxConfig.onReadyStart, ie[re] = __wxConfig.onReadyEnd || 0, ae = !1, ie), a.isReload = o, a.adInfo = { preloadVideoAdUnitIds: __appServiceSDK__.getPreloadVideoAdUnitIds() }, a.permissionBytes = __appServiceSDK__.getPermissionBytes(), a.fontFaceRecords = __appServiceSDK__.fontFaceRecords, a.fontSizeSetting = Object(L.getCachedSystemInfo)().fontSizeSetting; var s = { ext: a, options: { firstRender: !0, timestamp: n, path: i.__route__ } }; if (r) { var c = JSON.stringify(s), u = c.length; if (262144 < u) { for (var l = [], d = 0; d < u;) l.push(c.substr(d, 262144)), d += 262144; for (var f = ++$e, p = 0, h = l.length; p < h; p++) qe.emit({ isSplitData: !0, splitInfo: { id: f, index: p + 1, total: h, data: l[p] } }, t); return ze("pageReady", i), void __appServiceSDK__.traceEndEvent() } } qe.emit(s, t), // qe 函数,s 是组装 page 信息的对象 ze("pageReady", i), __appServiceSDK__.traceEndEvent() }) [代码] qe 函数: [代码]var qe = function() { function e() { Object(g. default)(this, e) } return Object(r. default)(e, null, [{ key: "emit", value: function(e, t, n) { __appServiceSDK__.invokeWebviewMethod("appDataChange", e, [t], n) } }]), e } (); [代码] 搜索 [代码]appDataChange[代码],找不到其他地方定义。我们找到 [代码]__appServiceSDK__.invokeWebviewMethod[代码] 的定义: [代码]function(e, t, n) { n.r(t), n.d(t, "invokeWebviewMethod", function() { return r }); var c = n(0), u = n(3), l = 0, d = [], r = function(e, t, n, r) { var o = 1 < arguments.length && void 0 !== t ? t: {}, i = 2 < arguments.length ? n: void 0, a = 3 < arguments.length ? r: void 0, s = l++; d[s] = a, Object(c.publish)("invokeWebviewMethod", { // 发布 name: e, args: o, callbackId: s }, void 0 === i ? [u. default.currentWebviewId]: i) }; Object(c.subscribe)("callbackWebviewMethod", // 订阅 function(e) { var t = e.res, n = e.callbackId, r = d[n]; delete d[n], r && r(t) // 执行 }) } [代码] 最终发现以下代码会被执行: [代码]__appServiceSDK__.onWebviewEvent(_(function(e) { __appServiceSDK__.traceBeginEvent("Framework", "onWebviewEvent"); var t = e.webviewId, n = e.eventName, r = e.data, o = function(e, t, n, r) { if (Ye.call(st, e)) { var o = st[e], i = o.page; if (n === $.DOM_READY_EVENT) return wt.pageReadyTime = Date.now(), j("Invoke event onReady in page: " + o.route), i.__callPageLifeTime__("onReady"), // 3.触发 onReady void vt("newPage2pageReady", wt.newPageTime, wt.pageReadyTime); if (r._requireActive) { var a = lt[lt.length - 1]; if (!a || a.webviewId !== e) return } if (r._relatedInfo && F.DisplayReporter.setEventRelatedInfo(r._relatedInfo), t) { var s = __virtualDOM__.getNodeById(t, e); if (!s) return; var c = exparser.Element.getMethodCaller(s); return j("Invoke event " + n + " in component: " + s.is), _t(c, n) ? tt(c, n, r) : void x("事件警告", "Do not have " + n + " handler in component: " + s.is + ". Please make sure that " + n + " handler has been defined in " + s.is + ".") } if (j("Invoke event " + n + " in page: " + o.route), _t(i, n)) return tt(i, n, r); x("事件警告", "Do not have " + n + " handler in current page: " + o.route + ". Please make sure that " + n + " handler has been defined in " + o.route + ", or " + o.route + " has been added into app.json") } } (t, e.nodeId, n, r); return __appServiceSDK__.traceEndEvent(), o }, "onWebviewEvent")) [代码] 说实话,这段代码没太看懂,对着 Page 的 生命周期 一起看理解起来会更清晰点,这里还是回到最开始,看传入的 [代码]isMainTabBarPage[代码]字段做了什么操作。 我们回到 jt 函数,发现关键语句: [代码]var u = c ? Y.create(n, e) : q.create(n, e, s || {}) // 初始化 page [代码] Y: [代码]K = ["onLoad", "onReady", "onShow", "onRouteEnd", "onHide", "onUnload", "onResize"], J = __appServiceSDK__.getLogManager(), Y = function() { function e() { Object(g. default)(this, e) } return Object(r. default)(e, null, [{ key: "create", value: function(d, e) { var f = __virtualDOM__.addView(d, e), p = exparser.Element.getMethodCaller(f), u = __virtualDOM__.getOwnerPluginAppId(p); if (p.__wxWebviewId__ = d, p.__route__ = e, p.route = e, p.__displayReporter = new F.DisplayReporter(e, 2), f.__customConstructor__ === __virtualDOM__.Page) { var t = f.getRootBehavior().methods, n = p.__freeData__; for (var r in t) p[r] = t[r].bind(p); for (var o in n) p[o] = b(n[o]) } var h = __appServiceSDK__.getSystemInfoSync().deviceOrientation; p.__callPageLifeTime__ = function(e) { var t = this[e] || I; Reporter.__route__ = this.__route__, Reporter.__method__ = e; for (var n, r, o, i, a, s = arguments.length, c = new Array(1 < s ? s - 1 : 0), u = 1; u < s; u++) c[u - 1] = arguments[u]; "onLoad" === e && (n = p.__displayReporter).setQuery.apply(n, c), "onShow" === e ? (p.__displayReporter.reportShowPage(), Object(F.checkWebviewAlive)(d)) : "onReady" === e ? p.__displayReporter.setReadyTime(Date.now()) : "onHide" === e || "onUnload" === e ? (r = this.__toRoute__, o = this.__isBack__, i = this.__notReportHide__, delete this.__toRoute__, delete this.__isBack__, delete this.__notReportHide__, i || p.__displayReporter.reportHidePage(r, o), Object(F.stopCheckWebviewAlive)(d)) : "onResize" === e && (a = c[0] || {}, h !== a.deviceOrientation && (h = a.deviceOrientation, p.__displayReporter.addOrientationChangeCount())), "onShow" === e ? f.triggerPageLifeTime("show", c) : "onHide" === e ? f.triggerPageLifeTime("hide", c) : "onResize" === e && f.triggerPageLifeTime("resize", c), j(this.__route__ + ": " + e + " have been invoked"), __appServiceSDK__.traceBeginEvent("LifeCycle", "Page." + e); var l = t.apply(this, c); return __appServiceSDK__.traceEndEvent(), Reporter.__route__ = Reporter.__method__ = "", l }, K.forEach(function(s) { var c = p[s]; p[s] = function() { var e, t = c || I; try { for (var n = Date.now(), r = arguments.length, o = new Array(r), i = 0; i < r; i++) o[i] = arguments[i]; e = t.apply(this, o); var a = Date.now() - n; 1e3 < a && Reporter.slowReport({ key: "pageInvoke", cost: a, extend: 'at "' + this.__route__ + '" page lifeCycleMethod ' + s + " function" }), J && J.logApiInvoke && J.log("page " + this.__route__ + " " + s + " have been invoked"), __appServiceSDK__.nativeConsole.info("component page " + this.__route__ + " " + s + " have been invoked") } catch(e) { Reporter.thirdErrorReport({ source: u, error: e, extend: 'at "' + this.__route__ + '" page lifeCycleMethod ' + s + " function" }) } return e }.bind(p) }); var i = "function" == typeof p.onShareAppMessage; i && __appServiceSDK__.showShareMenu(); var a = "function" == typeof p.onShareTimeline; return i && a && __appServiceSDK__.showShareTimelineMenu(), { page: p, node: f } } }, { key: "destroy", value: function(e) { __virtualDOM__.removeView(e.__wxWebviewId__) } }]), e } () [代码] q: [代码]var B = Object.assign, L = n(4), F = n(5), W = ["onLoad", "onReady", "onShow", "onRouteEnd", "onHide", "onUnload", "onResize"], U = function(e) { for (var t = 0; t < W.length; ++t) if (W[t] === e) return ! 0; return "data" === e }, V = ["__wxWebviewId__", "__route__"], G = ["route"], z = function(e) { return - 1 !== V.indexOf(e) }, H = __appServiceSDK__.getLogManager(), q = function() { function h() { var t = this, c = 0 < arguments.length && void 0 !== arguments[0] ? arguments[0] : {}, d = 1 < arguments.length ? arguments[1] : void 0, e = 2 < arguments.length ? arguments[2] : void 0; Object(g. default)(this, h); var n = { __wxWebviewId__: d, __route__: e }; V.forEach(function(e) { Object.defineProperty(t, e, { set: function() { x("关键字保护", "should not change the protected attribute " + e) }, get: function() { return n[e] } }) }); var r = __virtualDOM__.addView(d, e), o = exparser.Element.getMethodCaller(r), u = __virtualDOM__.getOwnerPluginAppId(o); this.__wxExparserNode__ = r, this.__wxComponentInst__ = o, exparser.Element.setMethodCaller(r, this), c.data = c.data || {}, T(c.data) || A("Page data error", "Page.data must be an object"); var i = JSON.stringify(c.data); this.data = JSON.parse(i), this.__viewData__ = JSON.parse(i), this.__displayReporter = new F.DisplayReporter(e, 1); var f = __appServiceSDK__.getSystemInfoSync().deviceOrientation; this.__callPageLifeTime__ = function(e) { var t = (this[e] || I).bind(this); Reporter.__route__ = this.__route__, Reporter.__method__ = e; for (var n, r, o, i, a, s = arguments.length, c = new Array(1 < s ? s - 1 : 0), u = 1; u < s; u++) c[u - 1] = arguments[u]; "onLoad" === e && (n = this.__displayReporter).setQuery.apply(n, c), "onShow" === e ? (this.__displayReporter.reportShowPage(), Object(F.checkWebviewAlive)(d)) : "onReady" === e ? this.__displayReporter.setReadyTime(Date.now()) : "onHide" === e || "onUnload" === e ? (r = this.__toRoute__, o = this.__isBack__, i = this.__notReportHide__, delete this.__toRoute__, delete this.__isBack__, delete this.__notReportHide__, i || this.__displayReporter.reportHidePage(r, o), Object(F.stopCheckWebviewAlive)(d)) : "onResize" === e && (a = c[0] || {}, f !== a.deviceOrientation && (f = a.deviceOrientation, this.__displayReporter.addOrientationChangeCount())), j(this.__route__ + ": " + e + " have been invoked"), __appServiceSDK__.traceBeginEvent("LifeCycle", "Page." + e); var l = t.apply(this, c); return __appServiceSDK__.traceEndEvent(), Reporter.__route__ = Reporter.__method__ = "", l }, W.forEach(function(s) { t[s] = function() { var e, t = (c[s] || I).bind(this); try { for (var n = Date.now(), r = arguments.length, o = new Array(r), i = 0; i < r; i++) o[i] = arguments[i]; e = t.apply(this, o); var a = Date.now() - n; 1e3 < a && Reporter.slowReport({ key: "pageInvoke", cost: a, extend: "at " + this.__route__ + " page lifeCycleMethod " + s + " function" }), H && H.logApiInvoke && H.log("page " + this.__route__ + " " + s + " have been invoked"), __appServiceSDK__.nativeConsole.info("page " + this.__route__ + " " + s + " have been invoked") } catch(e) { Reporter.thirdErrorReport({ source: u, error: e, extend: "at " + this.__route__ + " page lifeCycleMethod " + s + " function" }) } return e }.bind(t) }); for (var a in c) ! function(a) { z(a) ? x("关键字保护", "Page's " + a + " is write-protected") : U(a) || ("Function" === k(c[a]) ? t[a] = function() { var e; Reporter.__route__ = this.__route__, Reporter.__method__ = a, __appServiceSDK__.traceBeginEvent("User Script", "Page." + a); try { for (var t = Date.now(), n = arguments.length, r = new Array(n), o = 0; o < n; o++) r[o] = arguments[o]; e = c[a].apply(this, r); var i = Date.now() - t; 1e3 < i && Reporter.slowReport({ key: "pageInvoke", cost: i, extend: "at " + this.__route__ + " page " + a + " function" }) } catch(e) { Reporter.thirdErrorReport({ source: u, error: e, extend: "at " + this.__route__ + " page " + a + " function" }) } return __appServiceSDK__.traceEndEvent(), Reporter.__route__ = Reporter.__method__ = "", e }.bind(t) : t[a] = b(c[a])) } (a); var s = { route: e }; G.forEach(function(e) { Object.prototype.hasOwnProperty.call(t, e) || (t[e] = s[e]) }); var l = "function" == typeof c.onShareAppMessage; l && __appServiceSDK__.showShareMenu(); var p = "function" == typeof c.onShareTimeline; l && p && __appServiceSDK__.showShareTimelineMenu() } return Object(r. default)(h, null, [{ key: "create", value: function(e, t, n) { var r = new h(n, e, t), o = r.__wxExparserNode__; return delete r.__wxExparserNode__, { page: r, node: o } } }, { key: "destroy", value: function(e) { __virtualDOM__.removeView(e.__wxWebviewId__) } }]), Object(r. default)(h, [{ key: "setData", value: function(c, e) { var u = this; try { var t = k(c); if ("Object" !== t) return void A("类型错误", "setData accepts an Object rather than some " + t); Object.keys(c).forEach(function(e) { void 0 === c[e] && A("Page setData warning", 'Setting data field "' + e + '" to undefined is invalid.'); var t, n, r, o = M(e), i = R(u.data, o), a = i.obj, s = i.key; a && (a[s] = b(c[e])), void 0 !== c[e] && (n = (t = R(u.__viewData__, o)).obj, r = t.key, n && (n[r] = b(c[e]))) }), __appServiceSDK__.traceBeginEvent("Framework", "DataEmitter::emit"), this.__wxComponentInst__.setData(JSON.parse(JSON.stringify(c)), e), __appServiceSDK__.traceEndEvent() } catch(e) { v(e) } } }, { key: "pageScrollTo", value: function(e) { __appServiceSDK__.publishPageScrollTo(e, [this.__wxWebviewId__]) } }]), h } () [代码] 仍然没有发现 [代码]isMainTabBarPage[代码]关键字。 以上,第五步虽然没找到我们想找到的逻辑,但是发现其实 jt 函数其实和其他路由函数执行的是同一段逻辑,不影响我们对 [代码]wx.reLaunch[代码]执行过程的分析。 结论 最终,结合源码,我们得出[代码]wx.reLaunch[代码]的最终执行过程如下图所示: [图片] 关于 [代码]wx.reLaunch[代码]的执行过程,额外提出几个点需要注意: [代码]wx.reLaunch[代码]真正的逻辑是清空路由,再打开新页面,并不是传统意义上的“重启”; [代码]wx.reLaunch[代码]只会影响小程序各生命周期(回调)的执行,全局 js 代码在小程序加载时执行,分包中的全局 js 代码在分包加载时执行; [代码]wx.reLaunch[代码]的目标页在分包内,且分包未加载过时,会先加载分包代码,再执行后续逻辑(unUnload + openNewPage); [代码]wx.reLaunch[代码]在非 iOS 设备中,如果小程序不在前台时,执行会报错,导致无法跳转。 除此之外,再额外说几个在 debug 过程中的总结的小 tips: 1、如果在代码里不太好看一些变量或表达式的值,可以复制下来贴在 watch 里。 2、发现一些 function 执行了,但是不知道函数定义的位置位置,可以先将其放进 watch 里,等其有值的时候点开会有一个路径点击开就到了函数定义的位置了。 3、不要无脑 debug,带着疑问,先在脑子里假设出你推测或者认为的一些结论,用 debug 去验证,否则中间很难发现关键信息。其实本文的成因并非是我真的想去深入了解下 [代码]wx.reLaunch[代码]的执行过程,而是因为对他的理解有偏差,在排查线上 bug 时产生了一些自己无法理解的现象。
2020-06-12 - IOS scroll-view中的自定义组件fixed问题
这个是正常现象,因为 iOS 下加了 -webkit-overflow-scrolling: touch,这个会产生滚动惯性,体验更好,但会改变 fixed 的行为,建议不在 scroll-view 里有 fixed 元素
2020-04-23 - wx.onError、App.onError疑惑及如何捕获Promise异常?
1、官方文档上说 wx.onError 和 App.onError 的回调时机与参数一致(https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onError.html),是指两种方式收集到的异常信息完全一致吗?我们实践过程中发现,wx.onError 获取到的信息比 App.onError 要少。 在我们上一个版本的微信小程序中,我们是在 App.onError 中监控异常,并通过 ELK 收集、查询异常。收集到的部分异常信息截图如下: [图片] [图片] 在我们小程序的最新版本中,我们使用了针对小程序平台的 Sentry SDK(https://github.com/lizhiyao/sentry-miniapp,该 SDK 原理是使用 wx.onError、wx.onPageNotFound、wx.onMemoryWarning 监控异常信息)进行信息收集上报,基于公司私有化部署的 Sentry 服务接收、存储、展示异常信息。结果发现 Sentry 服务没有收到 wx.onError 上报的异常(1. 上线之前有做过测试,Sentry SDK 是可以正常上报代码执行异常的。 2. 可以收集到页面无法找到、内存警告异常,说明线上版本小程序中 Sentry SDK 已经成功初始化,可以进行信息上报): [图片] 但是官方的微信预警群是有推送异常信息的: [图片] [图片] [图片] 2. App.onError 收集到的信息和官方后台运维中心收集到的信息是一致的吗?我们发现 onError 捕获的信息,在小程序官方后台查不到。 比如: 通过 App.onError 在 8.12 收集到了这样一条异常信息: [图片] 在小程序官方后台是搜不到这个异常信息记录的: [图片] 3. 假设 wx.onError 和 App.onError 获取到的异常信息完全一致,且和官方后台收集记录的异常信息完全一致。如果小程序后台运维中心的预警推送频率设置为 1次/5min,那么 onError 获取到的信息和微信预警群推送的信息完全一致吗? 4. 关于 Promise 的异常,对于浏览器有 window.onunhandledrejection,对于 node 有 global.process.on('unhandledRejection', callback()),对于小程序平台,有什么推荐的方式可以获取到 Promise 的异常吗?官方后台运维中心有收集到小程序中 Promise 中的异常吗?目前实践来看,小程序的 App.onError、wx.onError 中是无法捕获 Promise 的异常的。示例代码可参考:https://github.com/lizhiyao/sentry-miniapp/blob/master/examples/weapp/app.js 。 5. 在小程序官方后台及官方预警群中,会发现偶尔会出现非线上版本的异常被收集和上报了。请问这种情况是正常的吗? 比如:截图中 小程序版本对应为 0 的就是我们未发布版本代码中出现的异常。判断的依据是 /pages/homepage-config/skilled-tag/index 是新版本新增的页面,异常上报时新版本并未发布。 [图片]
2019-08-15 - (19)文件系统能力
文件系统能力 文件系统能力可便于用户在客户端保存文件资源,并在下次启动客户端之后可以使用已保存的文件。 只要用户不主动删除小程序或小游戏,并保持一定的使用频率,文件都可以一直被保留。 合理的使用文件系统能力来缓存资源文件,可以给开发者更好的使用体验。 今天,我们来分享文件系统能力的小故事。 1 文件系统的演进历史 小程序在最早发布的版本中就已提供了最基础的文件存储和删除接口:wx.saveFile、 wx.removeSavedFile ; 对于绝大部分的小程序来说,这两个接口已经能够满足开发者的需求。但对于小游戏来说,需要更完整的能力来做支撑。 因此,发布小游戏的时候我们便提供了一套更完整的文件管理系统:FileSystemManager,其中主要包含了目录管理、文件内容读写等能力。 2 文件系统的设计背景 文件系统能力是应小游戏开发需求的迭代而逐步增强的。在小程序的场景下,很多时候只是需要把一个图片或视频资源缓存起来便可继续使用,文件内容与文件存储的目录结构都不是开发者所关心的。 但是在小游戏场景下情况则不同—— 一方面,小游戏除了有图片和视频文件、还有游戏引擎生成的配置文件,游戏需要能够去读取并理解配置文件的具体内容; 另一方面,游戏使用的资源文件会比普通小程序更多,若没有内容目录管理的功能,维护成本会变高。 除此之外,由于小游戏代码包大小限制只有4MB (加上分包最多8MB),对于一些偏重的游戏,资源甚至容易超100MB。 因此在此大背景下,我们给文件系统主要增加了目录管理、文件内容读写等两项接口—— 目录管理的需求场景是在使用游戏引擎时需要按目录来管理资源文件,文件内容读写的需求场景是在使用游戏引擎时需要读取配置文件;同时,我们对小游戏类目的本地存储容量的规范限制扩容到50MB。 开发者可能会疑惑,为什么在小程序的文件系统中会有一些功能相接近的接口?例如,想缓存一个文件,可以用 saveFile 或 copyFile ;再比如 removeSavedFile和 unlink 都可以用来删除一个文件。 上述情况的原因是我们在早期便提供了基础的文件存储接口 saveFile 和removeFile ,但不提供自定义目录相关的能力,开发者调用 saveFile 之后只能得到微信返回到的一个随机文件名。 小游戏应运而生的同时也增强了对文件系统能力扩展的需求,为了保证向后兼容,我们保留了这批基础接口,并在这个基础上增加了目录管理接口以及对应的文件操作接口。因此,便出现了上述一些相似接口的情况。 3 文件系统的优势—存储隔离 有不少开发者询问过关于文件存储的问题,他们担心文件内容被其他小程序读取到,也担心多个登录用户之间的文件内容会互相影响。为了保证用户的隐私安全,也为了保证小程序的数据安全,本地文件存储的一个重要规则便是保证隔离。 文件被存储到本地后,会以小程序账号和用户账号两个不同的维度来区分和隔离。即:同个微信用户使用不同小程序之间的文件存储会互相隔离;不同微信用户(在同一台手机中)使用同个小程序时,不同用户间的文件存储也会互相隔离。 [图片] 4 适当的存储容量 考虑到存储的问题,我们规范了小游戏文件存储的容量。普通小程序是10MB,小游戏则是50MB,当文件存储超出限制时,写入的文件会失败。 功能上线以后,我们曾收到过若干宝贵意见与反馈,希望能提高容量限制。但在经过反复论证与评估后,我们认为如果将文件存储的容量再往上提,就会有用户新增需要管理或清理手机存储空间的需求,小程序和小游戏将会变得不再“小”了。对于资源文件超过上述标准限制的小程序与小游戏,应该合理地管理本地文件,及时清理不常用的文件,这样在大多数情况下,手机存储空间便能保证顺畅。 更多关于小程序文件系统能力的信息,可查阅 接口文档 。
2018-08-21 - 浅谈小程序运行机制
摘要: 理解小程序原理… 原文:浅谈小程序运行机制 作者:小白 Fundebug经授权转载,版权归原作者所有。 写作背景 接触小程序有一段时间了,总得来说小程序开发门槛比较低,但其中基本的运行机制和原理还是要懂的。“比如我在面试的时候问到一个关于小程序的问题,问小程序有window对象吗?他说有吧”,但其实是没有的。感觉他并没有了解小程序底层的一些东西,归根结底来说应该只能算会使用这个工具,但并不明白其中的道理。 小程序与普通网页开发是有很大差别的,这就要从它的技术架构底层去剖析了。还有比如习惯Vue,react开发的开发者会吐槽小程序新建页面的繁琐,page必须由多个文件组成、组件化支持不完善、每次更改 data 里的数据都得setData、没有像Vue方便的watch监听、不能操作Dom,对于复杂性场景不太好,之前不支持npm,不支持sass,less预编译处理语言。 “有的人说小程序就像被阉割的Vue”,哈哈当然了,他们从设计的出发点就不同,咱也得理解小程序设计的初衷,通过它的使用场景,它为什么采用这种技术架构,这种技术架构有什么好处,相信在你了解完这些之后,就会理解了。下面我会从以下几个角度去分析小程序的运行机制和它的整体技术架构。 了解小程序的由来 在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,微信发布了一整套网页开发工具包,称之为 JS-SDK,给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。 但JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,比如受限于设备性能和网络速度,会出现白屏的可能。因此又设计了一个增强版JS-SDK,也就是“微信 Web 资源离线存储”,但在复杂的页面上依然会出现白屏的问题,原因表现在页面切换的生硬和点击的迟滞感。这个时候需要一个 JS-SDK 所处理不了的,使用户体验更好的一个系统,小程序应运而生。 快速的加载 更强大的能力 原生的体验 易用且安全的微信数据开放 高效和简单的开发 小程序与普通网页开发的区别 小程序的开发同普通的网页开发相比有很大的相似性,小程序的主要开发语言也是 JavaScript,但是二者还是有些差别的。 普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。 普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。 网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。 小程序的执行环境 [图片] 小程序架构 一、技术选型 一般来说,渲染界面的技术有三种: 用纯客户端原生技术来渲染 用纯 Web 技术来渲染 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染 通过以下几个方面分析,小程序采用哪种技术方案 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险 由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。 所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。 所以最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处: 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力 体验更好,同时也减轻 WebView 的渲染工作 绕过 setData、数据通信和重渲染流程,使渲染性能更好 用客户端原生渲染内置一些复杂组件,可以提供更好的性能 二、双线程模型 小程序的渲染层和逻辑层分别由 2 个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本。 [图片] [图片] 那么为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。 我们可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。 这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。 这就是小程序双线程模型的由来: 逻辑层:创建一个单独的线程去执行 JavaScript,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程 JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得小程序可通过API使用原生的功能,且部分组件为原生组件实现,从而有良好体验 三、双线程通信 把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。 那要怎么去实现动态更改界面呢? 如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。 这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。 Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。 如图所示: [图片] 1. 在渲染层把 WXML 转化成对应的 JS 对象。 2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。 3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。 我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。 而这样一个完整的框架,离不开小程序的基础库。 四、小程序的基础库 小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面: 在视图层,提供各类组件来组建界面的元素 在逻辑层,提供各类 API 来处理各种逻辑 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑 由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。 小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。 这样可以: 降低业务小程序的代码包大小 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包 五、Exparser 框架 Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。 Exparser的主要特点包括以下几点: 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。 小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。 内置组件 基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。 六、运行机制 小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。 小程序没有重启的概念 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁 [图片] 七、更新机制 小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。 八、性能优化 主要的优化策略可以归纳为三点: 精简代码,降低WXML结构和JS代码的复杂性; 合理使用setData调用,减少setData次数和数据量; 必要时使用分包优化。 1、setData 工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 2、常见的 setData 操作错误 频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时; 每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行 setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。 总结 大致从以上几个角度分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!
2019-06-14