尽情摆徊 發表於 2019-7-22 09:56:00

从微信小程序开发者工具源码看实现原理(二)- - 小程序技术实现

<ul>
<li>wxml与wxss的转换
<ul>
<li>1、wxml使用wcc转换</li>
<li>2、wxss使用wcsc转换</li>
</ul>
</li>
<li>开发者工具主入口</li>
<li>视图层页面的实现</li>
<li>视图层页面实现技术细节
<ul>
<li>视图层快速打开原理</li>
<li>视图层新打开页面流程</li>
</ul>
</li>
<li>业务逻辑层页面的实现</li>
</ul>
<h2 id="wxml与wxss的转换">wxml与wxss的转换</h2>
<p>打开小程序开发者工具,在调试控制台输入<code>openVendor</code>就会打开小程序的WeappVendor目录,该目录包括以下几个主要内容:</p>
<ul>
<li><strong>wcc</strong>可执行程序,用于将wxml内容转换为js内容,执行方式:<code>wcc xxx.wxml</code></li>
<li><strong>wcsc</strong>可执行程序,用于将wxss内容转换为视图可使用css内容,执行方式 <code>wcsc xxx.wxss</code></li>
<li><strong>不同版本小程序基础库x.x.x.wxvpkg</strong>, 里面包含小程序基础库WAService和WAWebview</li>
</ul>
<h3 id="1wxml使用wcc转换">1、wxml使用wcc转换</h3>
<p>正如上面分析的,通过调用小程序内置的可执行程序执行<code>wcc xxx.wxml</code>,将指定的wxml转换为js脚本内容。其具体用法可以--help查看,如下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713141503851-217214552.png"></p>
<p>小程序开发者工具底层会将小程序项目中所有wxml转为js内容,可以理解为为每个页面wxml进行了注册。例如我们小程序demo有两个页面index.wxml和logs.wxml,其中index.wxml内容如下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713133204903-665720961.png"><br>
通过wcc可执行程序生成的相关页面注册的代码如下图所示:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713132539450-764878569.png"></p>
<p>从页面转换的js内容来看,主要记录标签的属性及其值等。另外,转化的js脚本提供最核心的方法是<code>$gwx</code>方法,可以在开发者工具开发控制台访问到,其方法签名如下:</p>
<pre><code class="language-js">$gwx = function(path, global) {
    ...
    return function(env,dd,global){
      ...
    }
}
</code></pre>
<p>该方法根据传入具体的页面wxml路径,找到对应的页面,然后返回一个函数,向该函数传入页面渲染需要的数据(即Page中data对象)就能得到该页面wxml对应的js对象形式表示的dom树。其实每个小程序页面在页面准备初始化渲染时会调用这个<code>$gwx</code>方法,调用如下图所示:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713142837257-175093636.png"></p>
<p>另外,我们直接在开发者工具的控制台直接调用,输入如下语句,可以得到的js对象表示如下图:</p>
<pre><code class="language-js"> $gwx('./pages/index/index.wxml')({show: true});
</code></pre>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713135812765-1798878148.png">
<h3 id="2wxss使用wcsc转换">2、wxss使用wcsc转换</h3>
<p><code>wcsc</code>可执行程序用于处理wxss,小程序底层使用该可执行程序转换为js内容来处理页面css的引用。首先我们来看下<code>wxss</code>提供功能,如下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713144947447-859433146.png"></p>
<p>小程序底层使用<code>wcsc -db -pc</code>来转换对应wxss文件的,其生成的js内容如下图eval函数中的字符串所示:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713150021286-1707906585.png"></p>
<p>生成是js主要作用:</p>
<ul>
<li><strong>添加尺寸单位rpx转换,可根据屏幕宽度自适应</strong></li>
<li><strong>提供<code>setCssToHead</code>方法将转换后的css内容添加到header</strong></li>
</ul>
<h2 id="开发者工具主入口">开发者工具主入口</h2>
<p>小程序开发者工具的主入口也是小程序的启动入口,是整个小程序开发者工具的控制层,例如创建或者销毁webview等。它主要包括小程序的视图层的webview,业务逻辑层webview,调试器的webview和编辑区的webview几大块;我们只需关心视图层和业务逻辑层的webview。启动入口对应这一个index.html页面,里面引入主入口js,如下:</p>
<pre><code class="language-html">&lt;div id=container class=container&gt;&lt;/div&gt;
&lt;script src=../js/core/index.js&gt; &lt;/script&gt;
</code></pre>
<p>最终初次进入小程序主页后,主入口index.html的渲染html中有关视图层和业务逻辑层结果如下图所示:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713152547322-1084018062.png"></p>
<p>由此可以证明,小程序开发者工具业务逻辑层是在webview中执行的,该webview虽然提供浏览器相关接口,但是小程序只是在其中单纯的执行js代码。</p>
<p>在我们小程序demo中有index首页navigateTo到logs日志页时,可以看主入口dom的变化,见下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190713153148959-560849610.png"></p>
<p>从dom变化可以看出,调用navigateTo相当于新打开一个webview加载另一个页面视图,随着打开的页面越来越多,内存就比较吃紧。这也是为什么小程序对打开页面数量有限制的原因。从图中可能也看出了,为啥多加载了一个<code>pageframe.html</code>的webview,这个是干什么用的?后面会说到它的作用。</p>
<h2 id="视图层页面的实现">视图层页面的实现</h2>
<p>我们在写小程序页面视图时,貌似并不关心webview中的html结构,这些都是小程序底层帮我们实现, 我们只需要写页面ui和业务逻辑即可。下面我们来看看view视图层小程序帮我们做了什么。先来看一下视图层pageframe.html的模板:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715211153890-265288152.png"><br>
其中,模板中的注释占位符经过后台服务处理会注入不同js脚本,主要js内容:</p>
<ul>
<li><strong><code>&lt;!-- deviceinfo --&gt;</code></strong>: 暂时无用的占位符,会被空字符串""替换</li>
<li><strong><code>&lt;!-- jsdebug --&gt;</code></strong>:提供视图层的WeixinJSBridge模拟实现以及一些事件的处理如enablePullDownRefresh,其对应的js内容为extensions/pageframe/index.js.</li>
<li><strong><code>&lt;!-- plugincode --&gt;</code></strong>: 小程序插件相关的代码,若小程序使用插件则会注入</li>
<li><strong><code>&lt;!-- wxmlcode --&gt;</code></strong>: 调用<code>wcc</code>可执行命令生成的小程序注册所有页面wxml对应的js脚本内容</li>
<li><strong><code>&lt;!-- wxsscode --&gt;</code></strong>: 调用<code>wcss</code>可执行命令生成的js脚本内容,提供注入css到页面的js方法;该内容会提前注入全局的css。</li>
<li><strong><code>&lt;!-- wxappcode --&gt;</code></strong>: 小程序当前视图页面相关的配置json内容以及wxml和wxss转换为js的内容,可在控制台输入__wxAppCode__看相关信息<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190716144411241-240941513.png"></li>
<li><strong><code>&lt;!-- vendorlist --&gt;</code></strong>: 小程序为视图层注入的基础库功能,包括WAWebview.js、WARemoteDebug.js和hls.js</li>
</ul>
<h2 id="视图层页面实现技术细节">视图层页面实现技术细节</h2>
<p>本节来详细介绍下小程序视图层实现的一些技术细节</p>
<h3 id="视图层快速打开原理">视图层快速打开原理</h3>
<p>首先看一下小程序官网页面层级准备小节描述的一段内容:</p>
<blockquote>
<p>wx.navigateTo会创建一个新的页面层级,对于每一个新的页面层级,视图层都需要进行一些额外的准备工作。在小程序启动前,微信会提前准备好一个页面层级用于展示小程序的首页。除此以外,每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级,使得每次调用wx.navigateTo都能够尽快展示一个新的页面。</p>
</blockquote>
<p>正如上文提到的,我们在打开pages/logs/logs视图页面时,发现dom中多加载了一个__pageframe__/pageframe.html的视图层,其模板内容正如上一节描述的。这个视图层的作用正是为了小程序提前为一个新的页面层准备的。</p>
<p>小程序每个视图层页面内容都是通过pageframe.html模板来生成的,包括小程序启动的首页;下面来看看小程序为快速打开小程序页面做的技术优化:</p>
<ul>
<li>首页启动时,即第一次通过pageframe.html生成内容后,<strong>后台服务会缓存pageframe.html模板首次生成的html内容</strong>。</li>
<li>非首次新打开页面时,页面请求的pageframe.html内容直接走后台缓存<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190719170607010-437214346.png">。</li>
<li>非首次新打开页面时,pageframe.html页面引入的外链js资源(如上图所示)走本地缓存</li>
</ul>
<p>这样在后续新打开页面时,都会走缓存的pageframe的内容,避免重复生成,快速打开一个新页面。</p>
<h3 id="视图层新打开页面流程">视图层新打开页面流程</h3>
<p>其实在小程序开发者工具实现中,在创建每个视图层页的webview时,都会为其绑定了<code>onLoadCommit</code>事件(它会在页面加载完成后触发,包含当前文档的导航和副框架的文档加载)。初始时webview的src会被指定为空页面地址<code>http://127.0.0.1:${global.proxyPort}/aboutblank?${c}</code>,其中c为对应webview的id。webview从空页面到具体页面视图的过程如下:</p>
<ul>
<li>空页面地址webview加载完毕后执行事件中的reload方法,即设置webview的src为pageframe地址<code>http://127.0.0.1:${global.proxyPort}/__pageframe__/pageframe.html</code>。<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190720150930401-361672568.png"></li>
</ul>
<p>加载完成后,设置其src为pageframe.html:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190720151041270-431102173.png"></p>
<ul>
<li>
<p>新的src内容加载完成后再次触发onLoadCommit事件但根据条件不会执行reload方法。</p>
</li>
<li>
<p>pageframe.html页面在dom ready之后触发注入并执行具体页面相关的代码,此时通过history.pushState方法修改webview的src但是webview并不会发送页面请求。<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190720151249682-1527676372.png"></p>
</li>
</ul>
<p>pageframe.html模板生成的内容除小程序基础库视图层的底层功能之外,还包括小程序所有页面的模板信息、配置信息以及样式内容,这些都可以在生成pageframe.html的dom结构中窥探一二。</p>
<p><img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190719171123437-412391137.png"></p>
<p>那么,既然每个视图层页面由pageframe模板生成,那么小程序每个页面独有的页面内容如dom和样式等如何生成呢,这主要是利用nw.js的<strong>executeScript</strong>方法来执行一段js脚本来注入只与当前页面相关的代码,包括当前页面的配置,注入当前页的css以及当前页面的virtual dom的生成,注入的代码如下:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190719173335884-1727938578.png"></p>
<p>最终生成的js代码(拿pages/index/index为例)如下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190716150339438-243452456.png"></p>
<p>其中:</p>
<pre><code class="language-js">history.pushState('','', 'http://127.0.0.1:59524/__pageframe__/pages/index/index')
</code></pre>
<p>这句代码的作用修改当前webview的src,因为视图层的webview的src为pageframe.html,通过这句代码将其变更为具体的页面地址。</p>
<p>另外,需要注意的是nw.js的<strong>executeScript</strong>方法注入的代码是需要时机的,需要等到视图层的初始化工作准备ready之后才行,那么这个时机如何知道呢?细心的读者可能发现,在pageframe模板的最后一个script的内容:</p>
<pre><code class="language-html">&lt;script&gt;alert("DOCUMENT_READY")&lt;/script&gt;
</code></pre>
<p>这个从字面意思可以看出此时应该是页面dom ready的一个时机,通过alert来进行通知。<br>
alert能通知消息?当然可以的,在nw.js的webview中alert、prompt对应的弹框是被会阻止的,那么通过为webview绑定<strong>dialog</strong>事件来知道是那种弹框类型,以及提示内容,具体可以查看这篇文章。例如小程序开发者工具绑定事件部分代码(便于查看有修改):</p>
<pre><code class="language-js">this.webview.on('dialog', (a) =&gt; {
   a.preventDefault();
   const b = a.messageType || '',
            c = a.messageText,
            d = a.dialog;
    if ('alert' === b) {
      c === 'DOCUMENT_READY' &amp;&amp; (this.documentReady = !0, this.loadPage())
    }
    ...
})
</code></pre>
<p>这样方法<code>loadPage</code>就会触发nw注入并执行页面相关的代码。最终生成的页面视图对应dom结构如下图:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190720224119426-990751594.png"></p>
<p>可以看出,视图页面生成的dom结构中,document.body已无pageframe.html模板中对应body中的script内容,这是因为视图层的WAWebview.js在通过virtual dom生成真实dom过程中,它会挂载到页面的document.body上,覆盖掉pageframe.html模板中对应document.body的内容。</p>
<h2 id="业务逻辑层页面的实现">业务逻辑层页面的实现</h2>
<p>小程序将所有业务代码置于同一个线程中运行,小程序开发者工具是在一个webview中执行;webview中的appservice.html引入业务代码js之外,还有后台服务内嵌的一些基础功能代码,appservice.html对应的模板内容如下:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715153127429-1197807281.png"><br>
经过后台服务的处理,模板中的各种占位符就被对应的js内容注入,下面就来简单说几个重要的注入内容:</p>
<ul>
<li><strong><code>&lt;!-- wxconfig --&gt;</code></strong>: 小程序的配置项,包括用户自定义与系统默认的整合结果。在控制台输入<code>__wxConfig</code>可以看出打印结果<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715155404432-206162450.png"></li>
<li><strong><code>&lt;!--devtoolsconfig--&gt;</code></strong>:小程序开发者配置,包括开发者工具版本,设置的请求域名、默认开发者工具的设置以及访问Native方法需要permission的方法。控制台输入<code>__devtoolsconfig</code>可以看到其对应的信息<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715200900319-1147746578.png"></li>
<li><strong><code>&lt;!-- wxmlxcjs --&gt;</code></strong>: 调用<code>wcc</code>可执行命令生成的小程序注册所有页面wxml对应的js脚本内容,js脚本提供<strong>$gwx</strong>方法。</li>
<li><strong><code>&lt;!-- asdebug --&gt;</code></strong>:提供业务逻辑层的WeixinJSBridge模拟以及一些针对开发者工具的接口,如在控制台输入help可以看到提供的接口。其内容为extensions/appservice/index.js.</li>
<li><strong><code>&lt;!-- vendorlist --&gt;</code></strong>: 为业务逻辑层注入WAService.js,为业务逻辑层提供小程序底层基础库的功能</li>
</ul>
<p>此外,开发者工具服务还在appservice.html的body注入一段脚本,脚本的作用是将业务逻辑代码通过script动态注入到head中执行,这段代码如下:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715203916856-838846636.png"></p>
<p>最终生成的appservice.html中的head情况如下图所示:<br>
<img src="https://img2018.cnblogs.com/blog/408483/201907/408483-20190715204458918-706052605.png"></p>
<p>通过上图可以看出,我们写的页面逻辑都引入到页面中,并且分别从app.js开始一一执行;小程序代码调用Page构造器的时候,小程序基础库会记录页面的基础信息,如初始数据(data)、方法等。需要注意的是,如果一个页面被多次创建,并不会使得这个页面所在的JS文件被执行多次,而仅仅是根据初始数据多生成了一个页面实例(this),在页面JS文件中直接定义的变量,在所有这个页面的实例间是共享的。</p>
<h2 id="参考">参考</h2>
<ul>
<li>浅看小程序</li>
<li>微信小程序架构分析 (中)</li>
</ul><br><br>
来源:https://www.cnblogs.com/wonyun/p/11176762.html
頁: [1]
查看完整版本: 从微信小程序开发者工具源码看实现原理(二)- - 小程序技术实现