来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

小程序学习笔记-双线程【网上澳门金莎娱乐】

时间: 2019-09-08阅读: 287标签: 原理小程序底层实现原理及一些思考

技术选型

目前来说,页面渲染的方式主要有三种

  1. Web 渲染。
  2. Native 原生渲染。
  3. Web 与 Native 两者掺杂,也即我们常说的 Hybrid 渲染。

前面也说过,小程序最终的呈现形式,是 WebView + 原生组件,Hybrid 方式。我们结合之前对小程序的期望来看:

  • 开发门槛:Web 门槛低,不过 Native 也有像 RN 这样的框架支持
  • 体验:Native 体验比 Web 不要好太多,Hybrid 在一定程度上比 Web 接近原生体验
  • 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布
  • 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险

由于小程序的宿主是微信,如果用纯客户端原生技术来编写小程序 ,那小程序代码需要与微信代码一起编包,跟随微信发版本,这种方式跟开发节奏必然都是不对的。所以方向应该是需要像 Web 技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。

如果用纯 Web 技术来渲染小程序,在一些有复杂交互的页面上可能会面临一些性能问题。这是因为在 Web 技术中,UI渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。

总地看来,小程序选择了 Hybrid 的渲染方式,可以用一种近似 Web 的方式来开发,并且还可以实现在线更新代码。同时,引入原生组件有以下好处:

  • 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力
  • 体验更好,同时也减轻 WebView 的渲染工作
  • 绕过 setData、数据通信和重渲染流程,使渲染性能更好

现在,我们还剩下一个很重要的问题:管控性和安全性。于是,双线程的设计被提出来了。

两月以后,看着电脑,我回想起接到通知说要开发小程序引擎的那个下午。当时的我以为,这个小程序和其他小程序都不一样,因为它是个假的,其实是个网页。两月之后,我才发现,“噢~原来大家都是这么做的啊”。

双线程的小程序

也不知道是哪位大佬,能想出双线程这样的模型,反正我是佩服得666的。

双线程是什么?我们先来看个官方的图:

网上澳门金莎娱乐 1image

小程序的渲染层和逻辑层分别由 2 个线程管理:渲染层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS 脚本。

为什么要这么设计呢?前面提到的管控和安全,为了解决这些问题,我们需要阻止开发者使用一些浏览器提供的,诸如跳转页面、操作 DOM、动态执行脚本的开放性接口。

我们可以使用客户端系统的 JavaScript 引擎,iOS下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。通过提供一个沙箱环境来运行开发者的 JavaScript 代码来解决。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。

这就是小程序双线程模型的由来:

  • 逻辑层:创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码
  • 渲染层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程

最近一直在做小程序的底层实现,过程中磕磕绊绊也多次进行架构方向上的转型,趁着周末抽空写一篇文章记录一下开发过程中遇到的问题和一些思考与决策。

双线程通信

把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。那要怎么去实现动态更改界面呢?

前面我们知道,逻辑层和渲染层的通信会由 Native 做中转,逻辑层发送网络请求也经由 Native 转发。这是不是意味着,我们可以把 DOM 的更新通过简单的数据通信来实现呢?

Virtual DOM 相信大家都已有了解,大概是这么个过程:用JS对象模拟DOM树 -> 比较两棵虚拟DOM树的差异 -> 把差异应用到真正的DOM树上

在这里我们可以用上,如图:

网上澳门金莎娱乐 2image

  1. 在渲染层把 WXML 转化成对应的 JS 对象。
  2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。
  3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。

我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。而这样完整的一套框架,基本上都是通过小程序的基础库来完成的。

本篇文章更多的是在描述架构与技术方向层面的思考和决策,不会过多介绍具体某个问题是如何解决的,因为细节实在太多。

小程序的基础库

小程序的基础库是 JavaScript 编写的,它可以被注入到渲染层和逻辑层运行。主要用于:

  • 在渲染层,提供各类组件来组建界面的元素
  • 在逻辑层,提供各类 API 来处理各种逻辑
  • 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。这样可以:

  • 降低业务小程序的代码包大小
  • 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包

Exparser 框架Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。Exparser 特点包括:

  1. 基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  2. 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  3. 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
  1. 单线程

当时的我将我们的小程序定位成一个SPA单页应用,因为我们的小程序的宿主环境是浏览器。

它只是看起来像小程序(因为这个窗口没有地址栏什么的),但其实包括UI渲染和事件交互在内的绝大部分功能都是基于Web技术,虽然会提供Native和OS的一些能力与API,但本质上其实是个网页。又考虑到目前很多人使用第三方工具用Vue或React写小程序,我就在思考:“反正本质上就是一个网页,那为什么不原生内置Vue让用户直接用Vue的语法写小程序呢?”。

所以当时定了一个基本方向:让开发者使用Vue开发我们的小程序,开发体验完全与Web保持一致。

虽然开发体验与Web保持一致,但是Web技术实在是太开放了,开发者可以为所欲为。这种情况在小程序中是不允许的,不允许使用iframe、不允许a直接外跳到其他在线网页、不允许开发者触碰DOM、不允许使用某些未知的危险API等。

所以遇到的第一个问题是如何禁止用户在Vue的模板中使用iframe或a或其他不允许使用的东西。

若想做到这一点就不得不对Vue的渲染层进行一个托管与改造。

1.1 改造Vue

对Vue进行改造通常有两种方案:

polyfill

第一个方案能力有限,有一些Vue内部的逻辑没有办法通过polyfill的形式更改。第二种方案的缺陷是如果我只想修改Vue中的某一块逻辑,其他我不修改的部分如果有Bug,Vue官方更新了版本我没有办法同步。

这两种方案都有缺陷和不足,所以我没有使用这两个其中的任何一个,我使用了另一个方案,我觉得应该是目前为止最好的一种方案。

我简单介绍一下这种方案:

node_modules

因为我需要对渲染层进行改造,所以我需要重设web这个别名,如下:

const path = require('path')module.exports = { 'vue$': path.resolve(__dirname, '../src/web/entry-runtime-with-compiler'), compiler: 'vue/src/compiler', core: 'vue/src/core', shared: 'vue/src/shared', web: path.resolve(__dirname, '../src/web'), weex: 'vue/src/platforms/weex', server: 'vue/src/server', sfc: 'vue/src/sfc'}

大致原理是:如果import了一个不需要改造的,那其实是import了node_modules里的原始Vue的文件,如果是import了需要改造的,那其实import的是我的目录,文件也是我修改后的文件。

对这方面技术有兴趣可以留言详细讨论,由于不是本文重点,不再展开。

1.2 为标签设置黑名单

Vue通过一系列计算之后最终产出的结果是一些指令,比如创建一个DOM元素,移除一个DOM元素,插入到某个位置等。

所以当时的做法就简单在创建DOM元素时,用tagName校验是否在黑名单中,如果在黑名单中就触发警告并怎么怎么样。

但其实这种做法只能是:防君子不防小人。

1.3 遇到的问题

项目做到这里遇到一个问题是不论怎样,都没有办法防止开发者做一些我们想禁用的功能。因为是一个网页,开发者可以执行JS,可以操作DOM,可以操作BOM,可以做一切事情。

所以我们开始考虑将用户的代码放到一个绝对安全的沙箱中去执行。

  1. 双线程

在Web技术下,可以将用户的代码放到Web Worker中去执行,也可以放到一个隐藏的iframe中去执行,或者宿主环境提供一个环境。无论怎样,目的都是相同的,就是把用户的代码放到一个绝对安全的沙箱中执行。

由于开发者是基于Vue.js开发,用户的代码是没有办法单独放到沙箱中去执行的,所以我把Vue也放到沙箱中去执行。

这个时候技术架构和技术方向被调整成了Master-Slave的双线程模式。

沙箱中的代码我称为Master,它通过一系列计算,最终输出一些指令:创建元素、修改元素、插入元素、路由跳转、事件绑定等一些基础指令。这些指令从沙箱中通过线程间的消息机制传递到网页中,这个网页有一个模块叫做Slave,它负责监听Master发过来的指令并根据指令做指定操作。

把Vue放到Web Worker中执行需要解决非常多的问题,比如:原本Vue直接对DOM的操作需要转换成向另一个线程发送指令,还有事件绑定问题,事件的Event对象问题,事件修饰符(event.preventDefault)问题,路由控制(双向的)问题,表单元素的双向绑定问题、ref问题等。因为线程间的消息传递只能传递字符串,所以很多东西就会变得非常麻烦。

不过这些具体的技术问题都是比较容易解决的,比较难的问题是两个:“性能”和“原生能力受限”。

2.1 性能

在这种架构下,当页面有大范围UI变化时(例如首次渲染),Logic线程需要往UI线程发送大量的指令,包括:创建DOM,插入DOM,绑定事件等,每条指令都是一个单独的跨线程的消息通信,当消息数量庞大时,性能问题就会暴露出来,而且非常明显。

如果去Chrome DevTools的Performance面板看,会发现UI线程其实很闲,但是渲染的就是很慢。因为消息传递的代价,而且每次encode与decode也都需要代价,我自己写DEMO时没发现问题,但是投放到生产环境下去渲染一个真实的组件时,就会发现性能问题非常明显。

这个架构下虽然有性能问题,但以我的能力想解决这个问题也并不是太困难。另一个问题(原生能力受限)是这个架构下永远都无法解决的一个问题。

2.2 原生能力受限

为了安全,需要把用户的代码放到沙箱中去执行,但因为想让用户使用Vue开发,所以Vue也得放到沙箱中,这就导致了一个无法解决的问题是:我们官方提供的原生组件,也会受限。

道理很简单,假设用户在模板中使用了一个官方原生提供的组件,那么这个组件一定是需要提前注册到Vue中的,那就代表,我们官方提供的组件,也得放在沙箱中,所以把我们自己也给限制住了。

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:小程序学习笔记-双线程【网上澳门金莎娱乐】

关键词: