深入小程序系列之一: 小程序原理及模拟

Posted CNHK19

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入小程序系列之一: 小程序原理及模拟相关的知识,希望对你有一定的参考价值。

本文将介绍小程序的核心视图层逻辑层分离架构,并通过 iOS 的代码来模拟这种双线程模型。

什么是小程序

小程序是一种新的移动应用程序格式,是一种依赖 Web 技术,但也集成了原生应用程序功能的混合解决方案。

目前市面上小程序平台微信、支付宝、百度、头条、京东、凡泰等;小程序一些特性有助于填补 Web 和原生平台之间的鸿沟,因此小程序受到了一些超级应用程序的欢迎。

  • 它不需要安装,支持热更新。
  • 具备多个 Web 视图以提高性能。
  • 它提供了一些通过原生路径访问操作系统功能(原生接口)或数据的机制。
  • 它的内容通常更值得信赖,因为应用程序需要由平台验证。
  • 小程序可以分发到多个小程序平台(Web、原生应用,甚至是 OS)。这些平台还为小程序提供了入口,帮助用户轻松找到所需的应用。

小程序核心功能

分离视图层与逻辑层

在小程序中,视图层通常与逻辑层分离。

  • 视图层 View 负责渲染小程序页面,包括 Web 组件和原生组件渲染,可以将其视为混合渲染。例如,Web 组件渲染可以由 WebView 处理,但 WebView 不支持某些 Web 组件渲染,或者是性能受限;小程序还依赖于某些原生组件,例如地图、视频等。
  • 逻辑层 Service 是用主要用于执行小程序的 JS 逻辑。主要负责小程序的事件处理、API 调用和生命周期管理。扩展的原生功能通常来自宿主原生应用程序或操作系统,这些功能包括拍照、位置、蓝牙、网络状态、文件处理、扫描、电话等。它们通过某些 API 调用。当小程序调用原生 API 时,它会将 API 调用传递给扩展的原生功能,以便通过 JSBridge 进一步处理,并通过 JSBridge 从扩展的原生功能获取结果。Service 为每个 Render 建立连接,传输需要渲染的数据以进一步处理。
  • 如果事件由小程序页面中的组件触发,则此页面将向 Service 发送事件以进一步处理。同时,页面将等待 Service 发送的数据来重新渲染小程序页面。
  • 渲染过程可被视为无状态,并且所有状态都将存储在 Service 中。

视图层和逻辑层分离有很多好处:

  • 方便多个小程序页面之间的数据共享和交互。
  • 在小程序的生命周期中具有相同的上下文可以为具备原生应用程序开发背景的开发人员提供熟悉的编码体验。
  • Service 和 View 的分离和并行实现可以防止 JS 执行影响或减慢页面渲染,这有助于提高渲染性能。
  • 因为 JS 在 Service 层执行,所以 JS 里面操作的 DOM 将不会对 View 层产生影响,所以小程序是不能操作 DOM 结构的,这也就使得小程序的性能比传统的 H5 更好。

 

 

小程序双线程模型模拟

~~先看一下运行结果

 

 

 

接下来我们将用 iOS 代码来模拟上述的双线程模型。首先我们来实现视图层与逻辑层的数据通讯

 

 

如上图所示,视图层与逻辑层都分别通过 JS Bridge 的 publish 和 subscribe 来实现数据的收发。

模拟实现

1、视图层调用JSBridge.publish把事件传递给原生;参数: eventName: \'\', data:

//点击按钮,通知JS执行业务逻辑
function onTest() 
  console.log(\'aaa\')
  FinChatJSBridge.subscribe(\'PAGE_EVENT\', function (params) 
                            document.getElementById(\'testId\').innerHTML = params.data.title                                )
  FinChatJSBridge.publish(\'PAGE_EVENT\', 
    eventName: \'onTest\',data: 
  )


2、原生 view 层收到 page 的事件,把事件传递转发给 service 层处理

if ([message.name isEqualToString:@"publishHandler"]) 
        NSString *e = message.body[@"event"];
        [self.service callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]];
    

3、原生 service 层收到原生 view 层的事件,通过 jsbridge 把事件及参数传递给视图 ervice 层执行 js 逻辑

NSString *js = [NSString stringWithFormat:@"ServiceJSBridge.subscribeHandler(\'%@\',%@)",eventName,jsonParam];   
[self evaluateJavaScript:js completionHandler:nil];   

4、视图 service,收到事件后,执行 JS 业务代码

var Page = 
  setData: function(data) 
    //向原生视图层发送更新数据信息
    ServiceJSBridge.publish(\'PAGE_EVENT\', 
      eventName: \'onPageDataChange\',
      data: data
    )
  ,
  methods: 
    onTest: function() 
      // 执行JS方法,模拟小程序的setData,把数据更新到视图层
      Page.setData(
        title: \'我来自JS代码更新\'
      )
      console.log(\'my on Test\')
    
  

var onWebviewEvent = function(fn) 
  ServiceJSBridge.subscribe(\'PAGE_EVENT\', function(params) 
    console.log(\'FinChatJSBridge.subscribe\')
    var data = params.data,
      eventName = params.eventName
    fn(
      data: data,
      eventName: eventName
    )
  )

var doWebviewEvent = function(pEvent, params) 
  // do dom ready

  if (Page.methods.hasOwnProperty(pEvent)) 
    // 收到视图层的事件,执行JS对应的方法
    Page.methods[pEvent].call(params)
  

5、执行业务 JS 代码后,把数据更新传递给视图层去更新 UI 界面展示数据

ServiceJSBridge.publish(\'PAGE_EVENT\', 
  eventName: \'onPageDataChange\',
  data: data
)

6、原生 service 层收到视图 service 层的事件,把事件传递给原生视图层

if ([message.name isEqualToString:@"publishHandler"]) 
    NSString *e = message.body[@"event"];
    [self.controller callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]];    

7、原生视图层把收到的事件,传递给视图 view 层

NSString *js = [NSString stringWithFormat:@"FinChatJSBridge.subscribeHandler(\'%@\',%@)",eventName,jsonParam];
[self evaluateJavaScript:js completionHandler:nil];

8、视图 view 层,收到事件后,更新界面

FinChatJSBridge.subscribe(\'PAGE_EVENT\', function(params) 
  document.getElementById(\'testId\').innerHTML = params.data.title
)

订阅数据回调

// 首先订阅数据回调
JSBridge.subscribe(\'PAGE_EVENT\', function(params) 
  // ... 这里对返回的数据进行处理
)
// 向JS Bridge发布数据
// eventName: 用于标识事件名
// data: 为传递的数据
JSBridge.publish(\'PAGE_EVENT\',  eventName: \'onTest\', data:  )

WKWebView 初始化,

WKUserContentController *userContentController = [WKUserContentController new];
    NSString *souce = @"window.__fcjs_environment=\'miniprogram\'";
    WKUserScript *script = [[WKUserScript alloc] initWithSource:souce injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:true];
    [userContentController addUserScript:script];
    [userContentController addScriptMessageHandler:self name:@"publishHandler"];

    WKWebViewConfiguration *wkWebViewConfiguration = [WKWebViewConfiguration new];
    wkWebViewConfiguration.allowsInlineMediaPlayback = YES;
    wkWebViewConfiguration.userContentController = userContentController;

    if (@available(iOS 9.0, *)) 
        [wkWebViewConfiguration.preferences setValue:@(true) forKey:@"allowFileAccessFromFileURLs"];
    
    WKPreferences *preferences = [WKPreferences new];
    preferences.javaScriptCanOpenWindowsAutomatically = YES;
    wkWebViewConfiguration.preferences = preferences;

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:wkWebViewConfiguration];
    self.webView.clipsToBounds = YES;
    self.webView.allowsBackForwardNavigationGestures = YES;

    [self.view addSubview:self.webView];
    NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"view.html" ofType:nil];
    NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
    [self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL];

WKWebView 事件回调处理

// 执行视图层事件回调
- (void)callSubscribeHandlerWithEvent:(NSString *)eventName param:(NSString *)jsonParam

    NSString *js = [NSString stringWithFormat:@"FinChatJSBridge.subscribeHandler(\'%@\',%@)",eventName,jsonParam];
    [self evaluateJavaScript:js completionHandler:nil];




- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^)(id result,NSError *error))completionHandler


    [self.webView evaluateJavaScript:javaScriptString completionHandler:completionHandler];


#pragma mark - WKScriptMessageHandler
// 视图层JSBridge请求接收处理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message

    if ([message.name isEqualToString:@"publishHandler"]) 
        NSString *e = message.body[@"event"];
        [self.service callSubscribeHandlerWithEvent:e param:message.body[@"paramsString"]];
    

视图层代码

function onTest() 
  console.log(\'aaa\')
  FinChatJSBridge.subscribe(\'PAGE_EVENT\', function(params) 
    document.getElementById(\'testId\').innerHTML = params.data.title
  )
  FinChatJSBridge.publish(\'PAGE_EVENT\', 
    eventName: \'onTest\',
    data: 
  )

<div id="testId">我来自视图层!</div>
<input type="button" value="调用JS逻辑层setData" style="border-radius:15px;background:#ed0c50;border: #EDD70C;color: white;font-size: 14px; width: 80%;" onclick="onTest();" />

逻辑层代码

// page 对像模拟
var Page = 
  setData: function(data) 
    ServiceJSBridge.publish(\'PAGE_EVENT\', 
      eventName: \'onPageDataChange\',
      data: data
    )
  ,
  methods: 
    onTest: function() 
      Page.setData(
        title: \'我来自JS代码更新\'
      )
      console.log(\'my on Test\')
    
  

var onWebviewEvent = function(fn) 
  ServiceJSBridge.subscribe(\'PAGE_EVENT\', function(params) 
    var data = params.data,
      eventName = params.eventName
    fn(
      data: data,
      eventName: eventName
    )
  )

var doWebviewEvent = function(pEvent, params) 
  // do dom ready
  if (Page.methods.hasOwnProperty(pEvent)) 
    Page.methods[pEvent].call(params)
  

onWebviewEvent(function(params) 
  var eventName = params.eventName
  var data = params.data
  return doWebviewEvent(eventName, data)
)

文档中心: Document

本文示例代码: https://github.com/finogeeks/fino-applet

小程序框架原理之渲染流程及通信流程

MINA

MINA 是在微信中开发小程序的框架。其目标是通过尽可能简单、高效的方式让开发者可以在微信中开发具有原生 APP 体验的服务。

MINA 提供了自己的视图层描述语言 WXMLWXSS,以及基于 JavaScript 的逻辑层框架,核心是一个响应的数据绑定系统。整个系统分为视图层(View)和逻辑层(App Service),并在视图层与逻辑层间提供了数据传输和事件系统,可以让开发者可以方便的聚焦于数据与逻辑上。

MINA 让数据与视图保持同步非常简单。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。MINA 是腾讯给微信小程序命名的框架,实际上应用的是目前业界最著名的 MVVM 模式。

wxml的真实面目

我们都知道小程序提供了很多方便快捷的自定义组件(标签),但你知道小程序的这些组件编译过后会渲染成什么吗?先说答案,其实 wxml 经过编译后会渲染成 html 。很简单的一点,你发现在小程序内编写 html 标签,最终也可以运行。

探寻

光说可能体会不到,下面开始探寻小程序真实渲染的样子。先看下开发者工具内 wxml 的内容,待会和真实渲染的内容做对比。

接下来一步步找到小程序 wxml 渲染完成的真实样子,工具菜单栏点击微信开发者工具,选择调试微信开发者工具。打开的控制台可以调试整个微信开发者工具,用调试箭头指向小程序内容区域,这时可以看到小程序视图层是被嵌套在 webviewiframe 内。

但是当我们点开 iframe 是无法查看到里面内容的。如果想要查看调试 webview,只需选中 webview 打开它的调试工具即可,在控制台输入以下代码:

$$(\'webview\')[0].showDevTools(true)

可以看到又打开了一个调试窗口,这里面就是小程序视图层渲染的真实样子:

可以看到结构和 wxml 里的内容几乎一模一样,只是 topbar 变成了 wx-topbarview 变成了 wx-view等。这些都是内部实现的一套对应小程序标签的 webComponent 组件,而 webComponent 实际渲染出来还是 html 标签。

转换过程

转换过程是微信开发者工具内部通过一个可执行编译工具实现对小程序文件转换。在微信开发者工具控制台输入 openVendor() 会打开一个文件夹,里面存放着微信的基础库及工具,在里面可以找到 wcc.exewcsc.exe 执行文件,分别对应 wxmlwxss 的文件转换。

该工具可以单独对小程序文件进行转换,使用方法 ./wcc -d wxml文件路径 >> 输出路径。例如,将工具复制到一个文件夹内,再将一个 wxml 放入该文件夹内,命令行输入 :

./wcc -d index.wxml >> index.js

可能有人很好奇为什么是生成 js 文件,而不是 html 文件。原因很简单,因为需要处理 wxml 的动态绑定数据。看看这个 js 文件生成的是什么:

因为这些都是混淆压缩过的代码,基本没有可读性。这里只需要注意一个函数就好,那就是 $gwx。这是个很关键的函数,它的作用是生成虚拟dom树,用于渲染真实节点。

接下来回到 webview 调试窗口,在 head 内找到这段插入的 script 标签代码:

有没有很熟悉,没错,就是和上面转换后的代码是同一个东西。也就是说,我们的 wxml 文件通过编译,最终在视图层中执行的就是这段 js 代码(这里只是可以大概这么理解,实际需要向逻辑层获取数据才能渲染页面)。控制台输入 $gwx 发现这个函数存在,那么这个函数如何生成虚拟dom呢?$gwx 函数的第一个参数接收一个路径参数,这个路径就是 wxml 文件路径,此时在控制台输入:

let generateFunc = $gwx(\'./pages/index/index.wxml\')
generateFunc()

这时页面虚拟dom就生成出来了:

单纯调用 generateFunc 生成出来的虚拟dom是没有动态绑定数据的,如果想要动态的绑定数据,在调用 generateFunc 时传入一个数据对象。但是数据全在逻辑层里,这时就需要进行通信了。

数据通信

首先要知道小程序时运行在基础库之上的,但它们都是压缩打包好的,后面找到反编译出来的基础库代码,其中最重要的就是 WAService.jsWAWebview.js,它们分别是视图层和逻辑层的核心实现。

它们之间需要一个桥梁来进行通信,那就是 JS BridgeJS Bridge 提供调用原生功能的接口(摄像头,定位等),它的核心是构建原生和非原生间消息通信的通道,而且这个通信的通道是双向的。通过 JS Bridge 的发布订阅方法,视图层和逻辑层进行数据通信。

通信流程

接下来看看视图层和逻辑层的交互流程:

  1. wxml 转换成对应的 js 文件,等待生成虚拟dom函数 $gwx 准备完成,使用 dispatchEvent 通知 WAWebview

  1. WAWebview 监听到 generateFuncReady 事件触发,使用 WeixinJSBridge.publish 向逻辑层通信。

  1. 逻辑层处理逻辑,也就是我们平常写的小程序 js 文件里的东西,然后通过 JS Bridge 通知并返回数据给视图层。

  2. 视图层接收到数据,将数据传入生成虚拟dom的函数内,渲染页面,当然小程序也有相应的diff算法。

例如在 wxml 中绑定一个动态数据 title,视图层接收到数据后,重新生成虚拟dom

generateFunc({
  title: \'标题\'
})
  1. 初始化完成后,就会走对应的其他生命周期,或者用户触发事件,数据都会在逻辑层处理完成后通过 JS Bridge 通知到视图层,视图层再次调用生成虚拟dom的函数,更新页面。

wxss如何工作

wxss 工作原理和 wxml 差不多,都是通过工具转换为 js。为什么又是转换成 js,因为有 rpx 单位,需要根据手机尺寸进行设置 px

wcsc.exe 转换命令如下:

./wcsc -js index.wxss >> index.js

可以看到文件开头就是对 rpx 的转换

之后创建 style 标签,动态添加到视图层中

最后

附上 WAService.jsWAWebview.js 的代码作为学习参考。

以上是关于深入小程序系列之一: 小程序原理及模拟的主要内容,如果未能解决你的问题,请参考以下文章

深入小程序系列 ReactNative和小程序混编

深入小程序系列之Flutter 和小程序混编

深入小程序系列之三 ReactNative和小程序混编

深入小程序系列之二Flutter 和小程序混编

微信小程序 + mock.js 实现后台模拟及调试

程序员小助手Emacs,最强编辑器,没有之一