解析小程序原理

2022-07-29 08:34:04 浏览数 (1)

实际学习过程中,有些同学常常会对小程序和 Web 应用之间的差别产生疑惑,它们之间到底有什么不同, Web 应用不能作为小程序吗?本期文章将会带你比较小程序和 Web 应用之间的差异。

小程序实际的运行环境是在托管平台(也称为托管环境)。托管平台可以是本机应用程序(类似于 Web 浏览器),也可以是(嵌入的引擎)操作系统。 小程序通常需要经过托管平台审核才能上线,而** Web 应用只需要拥有一个 Web 服务器(以及可选的域名)即可上线。 其中,小程序包含一个全局的 [[[#manifest]]] 文件和零个或多个页面清单文件**。*清单文件通常采用 JSON 格式。如下所示,这是一个典型的小程序目录结构:*

代码语言:javascript复制
/
|___manifest.json
|___app.js
|___app.css
|___pages/
|       |___page1.js
|       |___page1.html
|       |___page1.css
|___common/
|       |___componentA.js
|       |___componentA.html
|       |___componentA.css
|       |___example.png
|___i18n/
|___zh-Hans.json
|___en-US.json

另外,小程序的运行环境分为视图层和逻辑层。视图层只处理渲染并监听发生的事件逻辑层只处理数据和逻辑。

Untitled-2022-06-03-1421.png

逻辑层通常用 JavaScript 编写。 它用于处理数据并将其发送到视图层,并接收来自视图层的反馈。虽然使用了 Web 技术,但 小程序 通常不运行在浏览器中,因此 Web 中的 JavaScript 的某些功能不可用,例如文档和窗口。

在 Web 应用中,渲染和脚本会相互阻塞,这就是为什么长时间运行脚本可能会导致页面变得无响应的原因,但在 小程序 中,两者是分开的,并且运行在不同的线程中。 开发人员可以使用浏览器公开的 DOM API 来进行 DOM 操作但是由于小程序的逻辑层和视图层是分开的,所以逻辑层(例如运行在 JavaScriptCore 中)不包含文档或窗口对象,并且不能使用某些 Web API。** 因此,像 jQuery 这样的一些库不能在小程序中运行 此外,由于 JavaScriptCore 环境与 Node.js 不同,一些 npm 包也无法在 小程序 中运行。**

在视图层中,托管平台会将布局语言(例如 WXML)转换为 JavaScript 对象。 当逻辑层数据发生变化时,通过宿主平台提供的方法将数据从逻辑层传递到视图层,然后生成前后DOM的diff。之后,差异将应用于原始 DOM 树并呈现更改后的 UI。

视图层

小程序页面的视图层通常有一种带有模板机制的标记语言(如WXML、swan、AXML、TTML等),类似于Web开发中的HTML。** 小程序 运行时将标记语言翻译成 HTML每个页面在不同的 WebView 中呈现,但所有 WebView 共享图像缓存**)或原生代码。

在为 小程序 页面设置样式时,通常会使用 CSS,有时还会使用响应式像素(也称为与密度无关的像素)等扩展。

视图层中使用的元素在 [[[#components]]] 部分中描述。例如下面这些例子:

代码语言:javascript复制
// example1
<!-- xxx.swan -->
<view>
  Hello My {{ name }}
</view>
// xxx.js
Page({
    data: {
    name: 'SWAN'
    }
});
// example2
<view>
    <view s-for="p in persons">
    {{p.name}}
    </view>
</view>
Page({
    data: {
    persons: [
        {name: 'superman'},
        {name: 'spiderman'}
    ]
    }
});
// example3
<view s-if="is4G">4G</view>
<view s-elif="isWifi">Wifi</view>
<view s-else>Other</view>
Page({
    data: {
    is4G: true,
    isWifi: false
    }
})
// example4
<view> Hello {{name}}! </view>
<button bindtap="changeName"> Click me! </button>
// This is our data.
var helloData = {
  name: 'WeChat'
}

// Register a MiniApp page.
Page({
  data: helloData,
  changeName: function(e) {
    // sent data change to view
    this.setData({
      name: 'MiniApp'
    })
  }
})

用户代理将数据中的名称与视图层中的名称绑定,所以当页面加载时,会显示“你好微信!”

当按钮被点击时,视图层会向逻辑层触发changeName事件,逻辑层会找到并执行相应的事件处理程序。

回调函数触发后,逻辑层将数据中的名称从微信改成小程序。因为数据和视图层已经绑定,所以视图层会自动变为“Hello MiniApp!”。

代码语言:javascript复制
<view>
  <view class="test" style="background:{{background}};color:{{color}};height:{{height}}">This can be changed.</view>
  <button bindtap="changeStyle">改变样式</button>
</view>

.test {
  height: 150rpx;
  line-height: 150rpx;
  text-align: center;
  border: 1px solid #89dcf8;
  margin-bottom: 112rpx;/* rpx 单位并非 CSS 标准的一部分,但这并不是本文重点*/
  margin: 13rpx;
}
Page({
  data: {},
  changeStyle: function() {
    this.setData({
      background: "#89dcf8",
      color:'#ffffff',
      height:"322rpx"
    })
  }
})

Pages

小程序中的页面代表小程序的一个页面,负责页面展示和交互每个页面通常对应小程序结构中的一个目录。

每个小程序页面通常包含一个用于逻辑层的 JavaScript 文件、一个用于展示视图的模板文件,以及用于页面样式和元数据的可选 CSS 和 JSON 文件。

对于小程序中的每个页面,开发者都需要在页面对应的 JavaScript 文件中进行注册,并在 Page 构造函数中指定页面的初始数据、生命周期回调、事件处理器等。

代码语言:javascript复制
// index.js
  Page({
    data: {
      text: "This is page data."
    },
    onLoad: function(options) {
      // Execute when the page is created
    },
    onShow: function() {
      // Execute when the page appears in the foreground
    },
    onReady: function() {
      // Execute when the page is rendered for the first time
    },
    onHide: function() {
      // Execute when the page changes from the foreground to the background
    },
    onUnload: function() {
      // Execute when the page is destroyed
    },
    onPullDownRefresh: function() {
      // Execute when a pull-down refresh is triggered
    },
    onReachBottom: function() {
      // Execute when the page is scrolled to the bottom
    },
    onShareAppMessage: function () {
      // Execute when the page is shared by the user
    },
    onPageScroll: function() {
      // Execute when the page is being scrolled
    },
    onResize: function() {
      // Execute when the page size changes
    },
    // Event handler
    viewTap: function() {
      this.setData({
        text: 'Set some data for updating view.'
      }, function() {
        // this is setData callback
      })
    },
    // Custom data
    customData: {
      hi: 'MiniApps'
    }
  })

Behavior(**类似于 Vue 中的 mixin **)可用于使多个页面具有相同的数据字段和方法。

代码语言:javascript复制
// my-behavior.js
  module.exports = Behavior({
    data: {
      sharedText: 'This is a piece of data shared between pages.'
    },
    methods: {
      sharedMethod: function() {
        this.data.sharedText === 'This is a piece of data shared between pages.'
      }
    }
  })


  // page-a.js
  var myBehavior = require('./my-behavior.js')
  Page({
    behaviors: [myBehavior],
    onLoad: function() {
      this.data.sharedText === 'This is a piece of data shared between pages.'
    }
  })

页面路由

小程序中所有页面的路由由托管平台管理。托管平台会以堆栈的形式维护所有当前页面,同时小程序托管平台通常会提供获取当前页面堆栈的功能。比如微信小程序(以及其他一些实现)中的getCurrentPages()函数返回一个数组,数组的每个元素都是一个代表一个页面的对象。

组件

因为应用商店审核机制的原因,小程序通常会禁止使用一些 HTML 元素,当然也允许添加一些新的组件(元素),例如滑动条、常用图标(警告、搜索、设置、加载等)、地图、富文本编辑器、广告等。你也可以自定义组件。

自定义组件

小程序开发人员还可以创建自定义组件。这是百度智能程序中带有自定义组件的示例目录结构:

代码语言:javascript复制
 ├── app.js
  ├── app.json
  ├── project.swan.json
  └── components
      └── custom
          ├── custom.swan
          ├── custom.css
          ├── custom.js
          └── custom.json

组件也有自己的。例如,在自定义组件的 JavaScript 文件中,开发人员可以:

代码语言:javascript复制
  Component({
      // ...
      pageLifetimes: {
          show: function() {
          // 当组件显示的时候触发
          },
          hide: function() {
          // 当组件隐藏的时候触发
          }
      }
      // ...
  });

Manifest

MiniApp 使用基于 JSON 的 manifest 文件,让开发者可以设置小程序的基本信息窗口样式、页面路径等信息。

代码语言:javascript复制
{
  "dir": "ltr",
  "lang": "en-US",
  "appID": "org.w3c.miniapp",
  "appName": "MiniApp Demo",
  "shortName": "MiniApp",
  "versionName": "1.0.0",
  "versionCode": ,
  "description": "A Simple MiniApp Demo",
  "icons": [
    {
      "src": "common/icons/icon.png",
      "sizes": "48x48"
    }
  ],
  "minPlatformVersion": "1.0.0",
  "pages": [
    "pages/index/index",
    "pages/detail/detail"
  ],
  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "Demo",
    "navigationBarBackgroundColor": "#f8f8f8",
    "backgroundColor": "#ffffff",
    "fullscreen": false
  },
  "widgets": [
    {
      "name": "widget",
      "path": "widgets/index/index",
      "minPlatformVersion": "1.0.0"
    }
  ],
  "reqPermissions": [
    {
      "name": "system.permission.LOCATION",
      "reason": "To show user's position on the map"
    },
    {
      "name": "system.permission.CAMERA",
      "reason": "To scan the QR code"
    }
  ]
}

下表主要对比了 MiniApp 和 Web App 中的 manifest 属性。表中的“-”字符表示相应清单中不存在此类属性。(参考:https://github.com/w3c/miniapp-manifest/blob/main/docs/explainer.md)

Xnip2022-06-03_14-42-49.jpg

这么做为希望部署同一应用到 Web 应用和 小程序的开发人员提供了方便,因为:

  1. 使用 Web App Manifest 作为基本格式,因为 Web App Manifest 中的所有属性都是可选的。指示哪些 Web App Manifest 属性是相关的,应用于值的一组限制(例如仅使用页面的本地 URL)。 当需要向后兼容时,请指出并推荐使用重复值
  2. 对于特定于 小程序 的属性,请考虑使用前缀或将它们添加到 小程序 属性以避免将来出现兼容性问题
  3. 对于权限,请考虑重新**使用 document.featurePolicy.features() **使用的相同功能,例如,“geolocation”、“gamepad”、“magnetometer”、“accelerometer”、“xr-spatial-tracking”、“picture-in” -picture”、“camera”、“payment”、“pointer-lock”等。

打包

小程序本身没有“域”,也不存在“跨域”,它下载到用户本地设备后会以包的形式运行。

开发者可以将小程序分成几个子包,用户代理可以在小程序使用时根据需要加载子包。根据业务特点对小程序进行合理的分包,可以提高小程序的加载速度,优化用户体验。 另外,开发者可以通过manifest文件或者小程序API,在用户访问小程序的某个页面时,预加载一些子包。

Tips: 这篇文章解释了小程序打包的规范。https://github.com/w3c/miniapp-packaging/blob/main/docs/explainer.md

运行时

多进程/线程架构

小程序的具体进程模型与操作系统有关小程序托管平台尽可能确保 小程序在单独的进程中运行。这样做的好处是更高的性能和更好的安全性。 在 iOS 下,使用线程级运行时模型,每个 小程序 都运行在自己的线程中。 每个小程序页面在逻辑层可以有一个或多个独立的线程(也称为worker)。 在视图层,通常只有一个线程,但一些小程序的实现会使用多个线程来预加载 WebView,以提高页面导航的性能。

但是,不同的小程序实现有不同的运行环境。例如下面几个类型的小程序:

微信小程序:

微信小程序运行在多个平台上:iOS、Android、Windows、macOS,以及微信开发者调试工具。 这些平台中用于渲染非原生组件的脚本执行环境和环境都是不同的。 **由于这些环境支持的 CSS 和 ECMAScript 特性不同,开发者需要使用特性检测。**微信小程序为某些功能提供了内置的 polyfill,以缓解互操作性问题。

ISO: 小程序**逻辑层中的 JavaScript 代码运行在 JavaScriptCore **(https://developer.apple.com/documentation/javascriptcore)中,视图层在 WKWebView 中渲染。

Andriod: 小程序**逻辑层的 JavaScript 代码运行在 V8 **(https://v8.dev/)中,视图层渲染在腾讯的 X5/XWeb 浏览器引擎(基于 Chromium)中。

开发者工具: 小程序逻辑层的 JavaScript 代码运行在 NW.js(基于 Chromium 和 Node.js,https://nwjs.io/),视图层使用 Chromium 渲染。

百度智能程序:

百度智能程序运行在三个平台上:iOS、Android,以及百度的开发者调试工具。这三个平台的脚本执行环境和渲染非原生组件的环境是不同的。 由于三种环境支持的 CSS 和 ECMAScript 特性不同,开发者需要使用特性检测。百度智能程序为某些功能提供了内置的 polyfill,以缓解互操作性问题。

IOS: 在旧版中,小程序的逻辑层和视图层都在 WebView 中运行并渲染。 在新版本中,小程序逻辑层的 JavaScript 代码运行在 JavaScriptCore 中,视图层渲染在 WebView 中。

Andriod: 在旧版中,小程序的逻辑层和视图层都在 WebView 中运行并渲染。 在新版本中,小程序逻辑层的JavaScript代码运行在V8中,视图层使用百度 T7 浏览器引擎在WebView中渲染。

开发者工具: 小程序逻辑层的 JavaScript 代码运行在 Electron 中,视图层由 Chromium 渲染。

360 小程序

360 小程序运行在 Windows 的360安全浏览器中。

生命周期

小程序提供了一些生命周期事件和流程来管理整个 小程序 和每个页面的生命周期。详细部分可以参阅 小程序 生命周期规范(https://w3c.github.io/miniapp-lifecycle/)。 小程序有两种打开方式:

  • 冷启动:用户第一次打开小程序时(或者用户退出小程序时间过长,或者打开的小程序太多,导致本小程序被销毁);
  • 热启动:用户在一定时间内重新打开小程序;

小程序冷启动时,托管平台会使用本地包加载小程序,同时会自动检测云端是否有新的包版本并异步下载下载完成后,下次用户触发冷启动时会使用新版本的包。

详细分析

在小程序中,应用程序、页面和组件都有自己的生命周期。本节主要将小程序应用/页面的生命周期状态与相关Web技术进行对比。

小程序页面通常使用 Page 构造函数注册到 JavaScript 文件中,并接受一个对象来指定初始数据、生命周期回调、事件处理程序等。

以下是一个基本的 Page 构造函数示例:

代码语言:javascript复制
 // pages/index/index.js
  Page({
    data: {
      title: "Alipay",
    },
    onLoad(query) {
      // Execute when the page is created
    },
    onShow() {
      // Execute when the page appears in the foreground
    },
    onReady() {
      // Execute when the page is rendered for the first time
    },
    onHide() {
      // Execute when the page changes from the foreground to the background
    },
    onUnload() {
      // Execute when the page is destroyed
    },
    onTitleClick() {
      // Execute when the title is clicked
    },
    onPullDownRefresh() {
      // Execute when a pull-down refresh is triggered
    },
    onReachBottom() {
      // Execute when the page is scrolled to the bottom
    },
    onShareAppMessage() {
     // Return to custom sharing information
    },
    // Event handler
    viewTap() {
      this.setData({
        text: 'Set data for update.',
      });
    },
    // Event handler
    go() {
      my.navigateTo({url:'/page/ui/index?type=mini'});
    },
    // Custom data
    customData: {
      name: 'alipay',
    },
  });

小程序由一个视图线程和一个或多个逻辑线程(也称为工作线程)管理。** 当用户第一次打开小程序时,视图线程和逻辑线程会同时启动初始化。**

逻辑线程初始化后,**运行小程序全局生命周期回调函数 app.onLaunch 和 app.onShow 创建小程序实例。“launched”状态表示小程序初始化完成,只触发一次。**触发该事件后,开发者可以获取小程序的基本信息,如URI等。 “Shown”是小程序在前台运行的生命周期状态。一旦小程序启动完成,或者小程序从后台切换到前台,就会触发它。

全局应用初始化后,逻辑线程运行小程序页面生命周期回调函数page.onLoad创建小程序页面实例。“Loaded”表示小程序页面初始化完成。通过该事件,开发者可以获得页面的基本信息,如页面的路径、查询等。

页面初始化后,逻辑线程等待视图线程初始化完成的通知。当视图线程初始化完成并通知逻辑线程后,逻辑线程将初始化数据发送给视图线程进行渲染。此时,视图线程开始第一次数据渲染。

第一次渲染完成后,视图线程进入“就绪”状态并通知逻辑线程。 逻辑线程调用 **page.onReady **函数,页面现在可用。

**页面准备好后,每次修改数据时,逻辑线程都会通知视图线程,视图线程会进行渲染。*当页面切换到后台时,逻辑线程调用* page.onHide **函数。当页面回到前台时,会调用 **page.onShow 函数。当小程序遇到脚本错误时,会调用 app.onError **函数。当页面被销毁时,在页面被销毁之前调用 **page.onUnload **函数。

在 Web 应用中,“页面”通常是指顶层浏览上下文的文档,并没有(严格的)“应用”概念,因此小程序中的一些生命周期状态在 Web 应用中没有对应状态。

例如,当小程序进入后台时,逻辑线程调用 app.onHide 函数,而在 Web 应用中,visibilityState 属性和 onvisibilitychange 事件处理函数仅在网页隐藏时才有用。 和大多数原生应用一样,一个小程序只能有一个实例,但是一个网页应用可以同时出现在多个浏览器标签页中,所以网页应用中的 app.onHide 是没有对应关系的。

导航

要在同一个托管平台上从一个小程序跳转到另一个小程序,通常会使用平台特定的 API 或组件。例如,微信小程序有一个 **wx.navigateToMiniProgram(Object object) **方法和一个导航器组件:

代码语言:javascript复制
wx.navigateToMiniProgram({
    appId: 'com.company.miniapp', // https://w3c.github.io/miniapp/specs/manifest/#appid
    path: 'page/index/index?id=123', // 空的时候,进入首页
    extraData: {
      foo: 'bar'
    },
    envVersion: 'develop', // 有效值: `develop`, `trial`, `release`.
    success(res) {
      // Success
    }
  })
<!--`target`可以是`self`,表示当前小程序,或`miniProgram`,表示另一个小程序。  -->
<navigator target="miniProgram" open-type="navigate" app-id="com.company.miniapp" path="" extra-data="" version="release">Open a MiniApp</navigator>

有的小程序也支持 HTML 中的 a 元素用于导航。 目前不支持在不同的托管平台上从一个小程序跳转到另一个小程序。 解决此问题可以参考 MiniApp URI Scheme 的建议。(https://github.com/w3c/miniapp/blob/gh-pages/specs/uri/docs/explainer.md)

离线体验

在 Web 应用中,Service Worker 为离线体验提供了技术基础。** 它是浏览器在后台运行的脚本,可以拦截和处理网络请求,包括以编程方式管理响应缓存。 在小程序中,离线体验是通过将小程序包下载到用户的设备上并在需要时进行更新来实现的。 与 Web 应用相比,小程序开发者可以更专注于业务逻辑,而不是缓存静态资源。 小程序包的缓存和更新机制由小程序托管平台自动管理,如果需要,开发者可以通过托管平台提供的 API 来修改这个过程。**

APIs

小程序平台通常会提供自己的 API,有的有对应的 Web API,有的没有。(可以在此处查看比较https://w3c.github.io/miniapp/white-paper/comparison.html)。

快照

一些小程序的实现,比如快应用,可以在云端的一些页面上进行预渲染,生成渲染中间格式,称为快照压缩后的快照格式非常小。例如,一个 50K 的 JavaScript 文件会生成一个约 3K 的快照。 由于快照文件非常小,可以作为小程序元数据的一部分进行分发。例如,当用户在应用商店搜索小程序时,已经加载了快照以及小程序的名称、包下载链接等信息。 小程序第一次加载启动时,会将包的下载地址和快照文件传递给小程序引擎。当引擎请求下载小程序包时,它会加载并解析快照并呈现它。 包下载完成后,标准渲染过程会在快照的基础上继续进行。

虚拟DOM

小程序的页面渲染经常使用虚拟 DOM 来保证页面更新时只更新变化的数据。在 Web 应用中,虚拟 DOM 通常使用 JavaScript 实现,消耗大量 CPU,效率不高。 一些小程序引擎使用 WebAssembly 来实现虚拟 DOM 的核心逻辑,并修改 JavaScript 引擎以提高 JavaScript 和 WebAssembly 之间的调用性能。

安全和隐私

由于小程序运行在托管平台,在原生应用中人们只关心应用程序本身是否存在漏洞,在小程序中人们还需要知道托管平台是否安全。

image.png

(小程序与用户代理的关系类似于原生应用与操作系统的关系,或者网络应用与浏览器的关系)

  1. 原生应用会直接调用系统API,所以很多漏洞都和操作系统版本有关,比如用户操作系统中的 WebView 版本。但是,由于一些小程序实现使用了自己的浏览器引擎,如果引擎缓解了漏洞,即使在低版本操作系统上也无需考虑该漏洞的影响。另一方面,如果小程序托管平台中存在特定于浏览器引擎的漏洞,它会影响小程序,但不会影响系统的其他部分。
  2. 由于小程序无法访问 DOM 和全局对象窗口(通过分离视图层和逻辑层执行环境),只能使用用户代理提供的 API 和组件,因此不可能(或很难)进行恶意攻击 代码跳转到随机网页或小程序,或更改 UI 上的内容。由于有小程序组件可以显示用户名、头像、性别、地理位置等敏感数据,如果开发者可以对 DOM 进行操作,就意味着可以随意获取用户的敏感信息。
  3. 由于大多数小程序实现不允许动态代码加载,如 eval 和 new Function,XSS 非常困难。
  4. 小程序通常有一个域名安全列表,只有当域名在安全列表中时,小程序中的脚本才能访问 URL 中的数据。
  5. 由于小程序限制了 cookie 的使用,因此小程序中的 CSRF 攻击比普通 Web 应用程序更难。

0 人点赞