当前位置: 首页 > 科技观察

探索小程序底层架构原理

时间:2023-03-17 14:54:38 科技观察

双线程架构在此之前,我们先思考一个问题,为什么小程序会选择双线程架构?为什么是双线程?小程序的加载和渲染性能在设计之初就要求快,这里的快指的是加载和渲染。目前主流的渲染方式有3种:Web技术渲染Native技术渲染混合技术渲染(同时使用webview和native渲染)就小程序的定位来说,不可能使用纯原生技术进行开发,因为那么它的编译和发布一定要跟着微信,所以它需要有一个可以随时更新的资源包,像Web技术一样,远程放置。下载到本地后,动态执行后即可渲染界面。但是如果使用纯web技术来开发,会有一个非常致命的缺点,就是在web技术中,UI渲染和JavaScript脚本执行是在单线程中执行的,这很容易导致一些逻辑任务抢占UI渲染。资源,这与设计之初的要求相悖。因此,微信小程序选择了Hybrid技术。界面以成熟的web技术渲染为主,辅以大量界面,提供丰富的客户端原生能力。同时,每个小程序页面渲染不同的WebView,可以提供更好的交互体验,更贴近原生体验,避免单一WebView的繁重任务。微信小程序是一种以webview渲染为主,原生渲染为辅的混合渲染方式。控制与安全由于Web技术的灵活和开放性,如果小程序基于纯Web技术进行渲染,难免会存在一些不可控因素和安全隐患。为了解决安全控制问题,小程序的设计阻止了开发者使用一些浏览器提供的开放API,例如跳转页面、操作DOM等。如果把这些东西一个一个的加入黑名单,势必会陷入一个很不好的循环,因为浏览器的界面也很丰富,那么很容易漏掉一些危险的界面,即使全部都是禁用接口,不能阻止浏览器内核的下一次更新。所以要彻底解决这个问题,必须提供一个沙箱环??境来运行开发者的JavaScript代码。该沙箱环境仅提供纯JavaScript解释执行环境,不带任何浏览器相关接口。那么像HTML5中的ServiceWorker、WebWorker这样的特性就满足了这样的条件,它们都可以让另一个线程去执行javaScript。这就是小程序双线程模型的由来:渲染层:所有与界面渲染相关的任务都在WebView线程中执行,逻辑层代码用于控制渲染哪些界面。一个小程序有多个界面,所以渲染层有多个WebView。逻辑层:创建一个单独的线程来执行JavaScript,在这个环境中,执行所有与小程序业务逻辑相关的代码。双线程模型小程序的架构模型不同于传统的web单线程架构,小程序是双线程架构。微信小程序的渲染层和逻辑层分别由两个线程管理。渲染层的界面使用webview渲染;逻辑层使用JSCore来运行JavaScript代码。webview渲染线程如何找到渲染层?1.我们可以调试微信开发者工具:微信开发者工具->调试->调试微信开发者工具2.然后我们会看到一个调试界面,和我们平时使用的浏览器调试界面几乎一样,但是这不是小程序的渲染层,而是开发者工具的结构。但是我们可以在里面找到一些webview标签。第一个webview的src属性是不是很眼熟?如果没猜错的话,就是我们当前小程序打开页面的路径。所以这个webview才是小程序真正的渲染层。到这里你会发现里面不止一个webview,实际上它包含了视图层的webview,业务逻辑层的webview,以及开发者工具的webview。开发者工具的逻辑层运行在webview中主要是为了模拟真机上的双线程。3、打开渲染层一探究竟。使用showdevTools方法打开用于调试webview界面的调试器document.querySelectorAll('webview')[0].showDevTools(true)这里看到的真的是小程序的渲染层,也就是编译后的代码小程序。我们会发现这里的标签和我们开发的时候写的不一样,都是加了wx-前缀的。了解过webComponent的同学相信一看就很相似,但是小程序并没有直接使用webComponent,而是自己搭建了一套组件系统Exparser。Exparser的组件模型与WebComponents标准中的ShadowDOM高度相似。Exparser会维护整个页面的节点树的信息,包括节点属性、事件绑定等,相当于ShadowDOM实现的简化版。为什么不直接使用webComponent,而是选择自己搭建一个组件系统呢?点击查看:控制与安全:Web技术可以通过脚本获取和修改页面的敏感内容或随意跳转到其他页面。能力有限:会限制小程序的表达形式多:增加理解JSCore逻辑线程逻辑层的成本我们在小程序开发者工具的调试器中直接进入document就可以看到。小程序在同一个线程中运行所有业务代码。在小程序开发者工具中,逻辑线程也是运行在一个webview中;webview中的appservice.html除了引入业务代码js外,还有一个后台服务Embedded,里面嵌入了一些基本的功能代码。编译原理了解了小程序的双线程架构之后,我们来看看小程序的代码是如何编译运行的。微信开发者工具模拟器运行的代码是在本地预处理和编译的,而微信客户端运行的代码是附加服务端编译的。这里我们仍然以微信开发者工具为例进行探讨。在开发者工具中输入openVendor(),会帮我们打开微信开发者工具的WeappVendor文件夹。这里我们会看到一些wxvpkg文件,这些文件是小程序各个版本的基础库文件。另外还有两个值得我们注意的文件:wcc、wcsc,这两个文件是小程序的编译器,分别用来编译wxml和wxss文件。编译wxml这里我们可以复制一份开发者工具中的wcc编译器,尝试用它来编译wxml文件,看看最终的成品是什么?让我们在终端中执行以下命令。/wcc-b索引。wxml>>wxml_output.js然后会在当前目录生成一个wxml_output.js文件。文件中有一个很重要的方法$gwx,会返回一个函数。对于这个函数的具体作用,我们可以尝试执行一下看看结果。我们打开渲染层webview搜索这个方法(为了方便查看,这里用一个小工程来演示)。从这里我们可以看出,这个方法会传入一个小程序页面的路径,返回的还是一个函数vardecodeName=decodeURI("./index/index.wxml")vargenerateFunc=$gwx(decodeName)让我们尝试按照这里的流程执行$gwx返回的函数,看看返回了什么?wxml编译{{name}}constfunc=$gwx(decodeURI('index.wxml'))console.log(func())是的,这个函数是用来生成VirtualDOM的。编译wxss我们也可以使用微信开发者工具中的wcsc来编译wxss文件。(你觉得这里应该生成css文件还是js文件?)我们在终端执行如下命令编译wxss文件/wcsc-jsindex.wxss>>wxss_output.js与之前wcc编译的wxml文件比较,这次编译比较简单,主要完成以下内容:rpx单位的转换,转换为px提供setCssToHead方法将转换后的css添加到头部rpx动态适配小程序提供rpx单位适配各种尺寸的设备。例如:/*index.wxss*/.qd_container{width:100rpx;背景:天蓝色;边框:1rpx实心鲑鱼;}.qd_reader{字体大小:20rpx;颜色:#191919;font-weight:400;}compiled然后会生成setCssToHead方法并执行setCssToHead([".",[1],"qd_container{width:",[0,100],";background:skyblue;border:",[0,1],"实心鲑鱼;}\n.",[1],"qd_reader{字体大小:",[0,20],";颜色:#191919;字体粗细:400;}\n",])(typeof__wxAppSuffixCode__=="undefined"?undefined:__wxAppSuffixCode__);这将调用transformRPX方法将rpx转换为pxvartransformRPX=window.__transformRpx__||function(number,newDeviceWidth){if(number===0)返回0;number=number/BASE_DEVICE_WIDTH*(newDeviceWidth||deviceWidth);number=Math.floor(number+eps);if(number===0){if(deviceDPR===1||!isIOS){return1;}else{return0.5;}}returnnumber;}//主公式number=number/BASE_DEVICE_WIDTH*(newDeviceWidth||deviceWidth);number=Math.floor(number+eps);//对于精度//rpx值/基础设备宽度750*真实设备宽度上面的渲染过程了解了wxml和wxss的编译过程之后,我们再整体来看一下页面的渲染过程。首先,让我们了解一下渲染层模板。从上面的渲染层webview中,我们可以找到两个webview的第一个index/index。上面我们说了对应我们小程序的渲染层才是真正的小程序页面。那么下面的instanceframe.html是什么呢?这个webview其实就是一个小程序渲染模板。打开它并检查它。它其实是预先注入了一些页面需要的公共文件,以及红框中的一些页面无关的文件占位符,这些占位符会在小程序对应的页面文件编译完成后被注入。如何保证渲染层webview初始化后执行代码注入?在呈现的模板webview下面有这样一段脚本:('load',fn)}window.addEventListener('load',fn)}显然,页面初始化完成后,会使用alert来通知。这个时候native/nw.js会拦截这个alert,从而知道这个时候webview已经初始化好了。整体渲染流程了解了上面的重要流程之后,我们就可以将整个流程串联起来了。1、打开小程序,创建视图层页面的webview。这个时候会初始化渲染层webview,设置webview地址为instanceframe.html,也就是我们的渲染层模板。2.然后进入页面/index/index,等待instanceframewebview初始化完成,页面index/index的编译代码会被注入并执行。//将webviewsrc路径改为页面路径history.pushState('','','http://127.0.0.1:26444/__pageframe__/index/index')/*...这里有一些wx配置和wxss编译代码*///这里是vardecodeName=decodeURI("./index/index.wxml")vargenerateFunc=$gwx(decodeName)if(decodeName==='./__wx__/functional-page.wxml'){generateFunc=function(){return{tag:'wx-page',children:[],}}}if(generateFunc){varCE=(typeof__global==='object')?(window.CustomEvent||__global.CustomEvent):window.CustomEvent;document.dispatchEvent(newCE("generateFuncReady",{detail:{generateFunc:generateFunc}}))__global.timing.addPoint('PAGEFRAME_GENERATE_FUNC_READY',Date.now())}else{document.body.innerText=decodeName+"notfound"console.error(decodeName+"notfound")}3.此时通过history.pushState方法修改webview的src,但是webview不会发送页面请求,会调用$gwx来生成一个generateFun方法,前面我们了解到这个方法是用来生成虚拟dom的。4.然后判断方法存在时,通过document.dispatchEvent派发自定义事件generateFuncReady,将generateFunc作为参数传递给底层渲染库。5、然后在底层渲染库WAWebview.js中会监听自定义事件generateFuncReady,然后通过WeixinJSBridge通知JS逻辑层视图视图就绪()。6、最后JS逻辑层将数据发送给Webview渲染层。当WAWebview.js通过虚拟dom生成真实dom时,会挂载到页面的document.body上,一个页面的渲染过程就结束了。数据更新小程序的视图层目前使用WebView作为渲染载体,而逻辑层则使用独立的JavascriptCore作为运行环境。在架构上,WebView和JSCore是独立的模块,没有直接数据共享的通道。所以在更新数据时,必须调用setData通知渲染层更新。setData在逻辑层遍历更新虚拟DOM树,触发组件生命周期和观察者等;将数据从逻辑层传输到视图层;在视图层更新虚拟DOM树和真实DOM元素,并触发页面渲染更新。这里的第二步,由于WebView和JSCore是独立的模块,数据传输是通过evaluateJavascript实现的,会额外有耗时的JS脚本解析和执行,所以数据是异步到达渲染层的。所以记住:不要频繁去setData,不要每次setData都传很多新数据(单次stringify后不超过256kb)不要在后台页面setData,会抢占资源frontpagethatisbeingexecutedvue)整体上,小程序或多或少有Vue的影子……(模板文件、数据、指令、虚拟dom、生命周期等)但是在数据更新上,小程序和Vue完全不一样。不同的。1、页面更新DOM是同步的还是异步的?2、既然更新DOM是一个同步过程,为什么Vue中会有nextTick钩子?mounted(){this.name='前端南九'console.log('sync',this.$refs.title.innerText)//旧文本//新文本Promise.resolve().then(()=>{console.log('microtask',this.$refs.title.innerText)})setTimeout(()=>{console.log('macrotask',this.$refs.title.innerText)},0)这个。$nextTick(()=>{console.log('nextTick',this.$refs.title.innerText)})}