关于小程序的一切,读这一篇就够了~

409次阅读  |  发布于3年以前

1. 小程序发展史

1.1 Native App

在智能机刚兴起的时代,网络还不是很发达,网页浏览速度也很慢,以文字为主。市面上的应用以 Native App 为主。

Native App 是基于 iOS 或者安卓的原生应用,特点是开发成本高,迭代慢,但是性能和体验很好,消息推送及时,比如 qq,微信等。

1.2 H5

2014 年 HTML5 完成标准定制,他的设计目的是为了在移动设备上支持多媒体,引入了 Video、Audio 等技术。

在网页上浏览视频变得很方便,特点是开发和发布成本低,打开方便,无需下载到本地,但是性能受浏览器的处理能力的限制,相较于原生 App 来说差了一些,消息推送不及时。

1.3 Hybrid App

Hybrid App 就是混合式的 App,也就是在移动端原生应用的基础上,通过 JSBrdige 等方法,访问原生应用的 API 进行 JS 的交互,并通过 WebView 等技术实现 HTML 与 CSS 的渲染。

WebView 可以理解为嵌套了一个浏览器内核(比如 webkit)的移动端组件。

这种技术实现的应用一般都是跨平台的,并且维护起来比较容易,性能介于 H5 和原生应用之间。

1.4 小程序

小程序是一种不需要下载安装即可使用的应用,它实现了应用 “触手可及” 的梦想,用户扫一扫或者搜一下即可打开应用。也体现了 “用完即走” 的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。—— 百度百科

自 2017 年 1 月微信小程序正式发布以来,目前互联网上已经有多种小程序:

微信小程序 百度智能小程序 支付宝小程序 QQ 小程序
2017.1 2018.7 2018.9 2019.6

基于小程序几乎相同的技术原理,以及小程序的方便快捷的特性,还衍生出了多款小程序,比如抖音小程序、快手小程序、京东小程序、美团小程序等,帮助各大厂商更好的为用户提供便捷的服务。

2018 年微信小程序 “跳一跳” 爆火,记得当年食堂排队打饭的时候很多同学都在玩,助力了微信小程序在用户中的扩张,也激发了其他厂商开发小程序的热潮。

2. 原理分析

2.1 双线程模型

无论是微信小程序还是支付宝小程序还是百度智能小程序等等,他们的总体架构都是基于双线程的。

其中用于处理业务逻辑的 JS 代码运行在单独的线程里,渲染层(template、css)则运行在另外一个单独的线程里。

以微信小程序为例:

双线程模型不同于单线程模型,逻辑层与渲染层的数据交互需要通过 JSBridge,二者是通过发布订阅,基于当前比较比较著名的 MVVM,来实现数据的双向绑定的,从而实现数据通信。

这样我们在微信小程序中通过在逻辑层中 setData 来改变 Model 层的数据就能够实现视图数据的异步更新。

以下是微信小程序的生命周期:

2.2 整体架构


注:以下所有内容均围绕微信开发者工具展开。

打开微信开发者工具的源代码,他是基于 NW.js 运行的,所以下图中的 package.nw 就是我们要重点钻研的对象:

这里面有很多代码,都是经过混淆与压缩的,将代码在 VSCode 中打开,并安装 Prettier - Code formatter 插件可以实现对源码的格式化。但此时代码中已经不具有语义化的变量了,只能通过 API 大致推断代码是干什么的。

源码中有一个 vendor 文件夹是值得注意的,通过它可以快速新建一个示例项目,同时里面有一个十分重要的 2.17.0.wxvpkg 包,它是微信小程序的基础库,包含了下文所提及的 WebService 与 WebView 等逻辑层与渲染层的处理。

2.2.1 WAWebview

小程序视图层基础库,提供视图层的基础能力:

var __wxLibrary = {
  fileName: 'WAWebview.js',
  envType: 'WebView',
  contextType: 'others',
  execStart: Date.now()
};
var __WAWebviewStartTime__ = Date.now();
var __libVersionInfo__ = {
  "updateTime": "2020.4.4 10:25:02",
  "version": "2.10.4"
};

/**
 * core-js 模块
 */
!function(n, o, Ye) {
  ...
  }, function(e, t, i) {
    var n = i(3),
      o = "__core-js_shared__",
      r = n[o] || (n[o] = {});
    e.exports = function(e) {
      return r[e] || (r[e] = {})
    }
  ...
}(1, 1);

var __wxConfig;
var __wxTest__ = false;

var wxRunOnDebug = function(e) {
  e()
};

/**
 * 基础模块
 */
var Foundation = function(i) {
  ...
}]).default;

var nativeTrans = function(e) {
  ...
}(this);

/**
 * 消息通信模块
 */
var WeixinJSBridge = function(e) {
  ...
}(this);

/**
 * 监听 nativeTrans 相关事件
 */
!function() {
  ...
}();

/**
 * 解析配置
 */
!function(r) {
  ...
  __wxConfig = _(__wxConfig), __wxConfig = v(__wxConfig), Foundation.onConfigReady(function() {
    m()
  }), n ? __wxConfig.__readyHandler = A : d ? Foundation.onBridgeReady(function() {
    WeixinJSBridge.on("onWxConfigReady", A)
  }) : Foundation.onLibraryReady(A)
}(this);

/**
 * 异常捕获(error、onunhandledrejection)
 */
!function(e) {
  function t(e) {
    Foundation.emit("unhandledRejection", e) || console.error("Uncaught (in promise)", e.reason)
  }
  "object" == typeof e && "function" == typeof e.addEventListener ? (e.addEventListener("unhandledrejection", function(e) {
    t({
      reason: e.reason,
      promise: e.promise
    }), e.preventDefault()
  }), e.addEventListener("error", function(e) {
    var t;
    t = e.error, Foundation.emit("error", t) || console.error("Uncaught", t), e.preventDefault()
  })) : void0 === e.onunhandledrejection && Object.defineProperty(e, "onunhandledrejection", {
    value: function(e) {
      t({
        reason: (e = e || {}).reason,
        promise: e.promise
      })
    }
  })
}(this);

/**
 * 原生缓冲区
 */
var NativeBuffer = function(e) {
  ...
}(this);
var WeixinNativeBuffer = NativeBuffer;
var NativeBuffer = null;

/**
 * 日志模块:wxConsole、wxPerfConsole、wxNativeConsole、__webviewConsole__
 */
var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
  return e[t] = function() {}, e
}, {});

var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
  return e[t] = function() {}, e
}, {});

var wxNativeConsole = function(i) {
  ...
}([function(e, t, i) {
  ...
}]).default;

var __webviewConsole__ = function(i) {
  ...
}([function(e, t, i) {
  ...
}]);

/**
 * 上报模块
 */
var Reporter = function(i) {
  ...
}([function(e, L, O) {
  ...
}]).default;

var Perf = function(i) {
  ...
}([function(e, t, i) {
  ...
}]).default;

/**
 * 视图层 API
 */
var __webViewSDK__ = function(i) {
  ...
}([function(e, L, O) {
  ...
}]).default;
var wx = __webViewSDK__.wx;

/**
 * 组件系统
 */
var exparser = function(i) {
  ...
}([function(e, t, i) {
  ...
}]);

/**
 * 框架粘合层
 * 
 * 使用 exparser.registerBehavior 和 exparser.registerElement 方法注册内置组件
 * 转发 window、wx 对象上到事件转发到 exparser
 */
!function(i) {
  ...
}([function(e, t) {
  ...
}, function(e, t) {}, , function(e, t) {}]);

/**
 * Virtual DOM 
 */
var __virtualDOMDataThread__ = false;
var __virtualDOM__ = function(i) {
  ...
}([function(e, t, i) {
  ...
}]);

/**
 * __webviewEngine__
 */
var __webviewEngine__ = function(i) {
  ...
}([function(e, t, i) {
  ...
}]);

/**
 * 注入默认样式到页面
 */
!function() {
  ...
  function e() {
     var e = i('...');
    __wxConfig.isReady ? void0 !== __wxConfig.theme && i(t, e.nextElementSibling) : __wxConfig.onReady(function() {
      void0 !== __wxConfig.theme && i(t, e.nextElementSibling)
    })
  }
  window.document && "complete" === window.document.readyState ? e() : window.onload = e
}();

var __WAWebviewEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;

WAWebview 主要由以下几个部分组件:

2.2.2 WAService

小程序逻辑层基础库,提供逻辑层基础能力:

var __wxLibrary = {
  fileName: 'WAService.js',
  envType: 'Service',
  contextType: 'App:Uncertain',
  execStart: Date.now()
};
var __WAServiceStartTime__ = Date.now();

(function(global) {
  var __exportGlobal__ = {};
  var __libVersionInfo__ = {
    "updateTime": "2020.4.4 10:25:02",
    "version": "2.10.4"
  };
  var __Function__ = global.Function;
  var Function = __Function__;

  /**
   * core-js 模块
   */
  !function(r, o, Ke) {
  }(1, 1);

  var __wxTest__ = false;
  var wxRunOnDebug = function(e) {
    e()
  };

  var __wxConfig;
  /**
   * 基础模块
   */
  var Foundation = function(n) {
    ...
  }([function(e, t, n) {
    ...
  }]).default;

  var nativeTrans = function(e) {
    ...
  }(this);

  /**
   * 消息通信模块
   */
  var WeixinJSBridge = function(e) {
    ...
  }(this);

  /**
   * 监听 nativeTrans 相关事件
   */
  !function() {
    ...
  }();

  /**
   * 解析配置
   */
  !function(i) {
    ...
  }(this);

  /**
   * 异常捕获(error、onunhandledrejection)
   */
  !function(e) {
    ...
  }(this);

  /**
   * 原生缓冲区
   */
  var NativeBuffer = function(e) {
    ...
  }(this);
  WeixinNativeBuffer = NativeBuffer;
  NativeBuffer = null;

  var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
      return e[t] = function() {}, e
    }, {});

  var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
    return e[t] = function() {}, e
  }, {});

  var wxNativeConsole = function(n) {
    ...
  }([function(e, t, n) {
    ...
  }]).default;

  /**
   * Worker 模块
   */
  var WeixinWorker = function(e) {
    ...
  }(this);

  /**
   * JSContext
   */
  var JSContext = function(n) {
    ...
  }([
    ...
  }]).default;

  var __appServiceConsole__ = function(n) {
    ...
  }([function(e, N, R) {
    ...
  }]).default;

  var Protect = function(n) {
    ...
  }([function(e, t, n) {
    ...
  }]);

  var Reporter = function(n) {
    ...
  }([function(e, N, R) {
    ...
  }]).default;

  var __subContextEngine__ = function(n) {
    ...
  }([function(e, t, n) {
    ...
  }]);

  var __waServiceInit__ = function() {
    ...
  }

  function __doWAServiceInit__() {
    var e;
    "undefined" != typeof wx && wx.version && (e = wx.version), __waServiceInit__(), e && "undefined" != typeof __exportGlobal__ && __exportGlobal__.wx && (__exportGlobal__.wx.version = e)
  }
  __subContextEngine__.isIsolateContext();
  __subContextEngine__.isIsolateContext() || __doWAServiceInit__();
  __subContextEngine__.initAppRelatedContexts(__exportGlobal__);
})(this);

var __WAServiceEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;

WAService 基本组成:

2.2.3 虚拟 DOM

微信小程序在 WAService 里面实现了小程序的 __virtualDOM__,通过 __virtualDOM__ 模块,可以实现 JS 对象到 DOM 对象的映射。

但是这个虚拟 DOM 通过 diff 和 patch 后并不是转换成原生的 DOM 元素,而是微信小程序里面自定义的 DOM 元素,这些 DOM 元素的操作通过 Exparser 模块来统一管理:

在 WAWebview 中包含了所有的 wx 自定义标签:

同时,__virtualDOM__ 模块提供了很多的基础 API,比如:

(更多的 API 定义可以在 WAService.js 里面去查询)

2.2.4 WeiXinJSBridge

WeixinJSBridge 提供了视图层 JS 与 Native、视图层与逻辑层之间消息通信的机制,提供了如下几个方法:

里面最重要的便是 on 和 invoke,通过 on 来注册事件,通过 invoke 来触发相应的事件。

2.3 微信开发者工具

微信开发者工具中的小程序是跑在 NW.js 中的,这里是他的官方 API 文档: https://nwjs.readthedocs.io/en/latest/

他是基于 ChromiumNode.js 的,因此我们编译后的虚拟 DOM 转换成真实 DOM 后,通过他来运行。

2.3.1 一些反编译技巧

我们可以通过开发者工具,在 Devtools 里输入 help 可以得到很多指令:

其中比较有用的是 openVendor。这个函数可以打开当前项目的源码,其实也就是包含了 wcc 和 wcsc 编译工具的一个文件夹:

有了这些文件之后,对我们之后的分析会很有帮助。

我们可以将这些文件拷贝到一个单独的目录,在 VSCode 中打开该项目,并安装以下插件:

这个插件可以将微信开发者工具中的所有以 .wxvpkg 结尾的文件进行解压缩。

同时通过他来将 quickstart 中的 miniProgramJs.wxvpkg 进行解压,得到我们在开发者工具中的源码文件。

2.3.2 编译原理

2.3.2.1 wcc 编译 wxml

微信小程序提供了 wcc 工具来编译 wxml 代码。通过上面得到的代码,我们可以实现对 wxml 的编译,以开发者工具创建的 Demo 项目中的首页为例:

<view class="container">
  <view class="userinfo">
    <block wx:if="{{canIUseOpenData}}">
      <view class="userinfo-avatar" bindtap="bindViewTap">
        <open-data type="userAvatarUrl"></open-data>
      </view>
      <open-data type="userNickName"></open-data>
    </block>
    <block wx:elif="{{!hasUserInfo}}">
      <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
      <button wx:elif="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button>
      <view wx:else> 请使用1.4.4及以上版本基础库 </view>
    </block>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
  </view>
  <view class="usermotto">
    <text class="user-motto">{{motto}}</text>
  </view>
</view>

经过以下指令编译:

./wcc ./quickstart/miniProgramJs.unpack/pages/index/index.wxml > index.js

会得到 JS 描述文件:

它会声明一个 $gwx 函数,通过它可以得到 Virtual DOM。接着我们在这个文件里添加几行代码去调用它,并通过 Node.js 或者 NW.js 执行这个文件:

var data = $gwx('./quickstart/miniProgramJs.unpack/pages/index/index.wxml')();
console.log(JSON.stringify(data, null, 2));

可以得到我们想要的最终的 Virtual DOM 结构:

{
  "tag": "wx-page",
  "children": [
    {
      "tag": "wx-view",
      "attr": {
        "class": "container"
      },
      "children": [
        {
          "tag": "wx-view",
          "attr": {
            "class": "userinfo"
          },
          "children": [
            {
              "tag": "wx-view",
              "attr": {},
              "children": [
                " 请使用1.4.4及以上版本基础库 "
              ],
              "raw": {},
              "generics": {}
            }
          ],
          "raw": {},
          "generics": {}
        },
        {
          "tag": "wx-view",
          "attr": {
            "class": "usermotto"
          },
          "children": [
            {
              "tag": "wx-text",
              "attr": {
                "class": "user-motto"
              },
              "children": [
                ""
              ],
              "raw": {},
              "generics": {}
            }
          ],
          "raw": {},
          "generics": {}
        }
      ],
      "raw": {},
      "generics": {}
    }
  ]
}

然后通过 window.exparser.registerElemtent 方法将这些 tag 转换成真实 DOM:

比如说,以上的 wx-text 就会被转换成类似于以下 DOM:

<span>
   <span style="display: none"></span>
   <span>{{这里是具体的文字内容}}</span>
</span>

2.3.2.2 wcsc 编译 wxss

同样的,以 Demo 项目里的 index.wxss 为例,运行一下指令:

./wcsc ./quickstart/miniProgramJs.unpack/pages/index/index.wxss -js -o ./css.js

可以将该文件从 wxss 格式的内容,转换成 JS 的内容:

这里面会生成 setCssToHead 方法,用于将相应的 css 转换后(如 rpx 转 px 等等),通过 style 标签插入到文档的 head 里面。

2.4 通信原理

小程序逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。

视图层组件

内置组件中有部分组件是利用到客户端原生提供的能力,既然需要客户端原生提供的能力,那就会涉及到视图层与客户端的交互通信。这层通信机制在 iOS 和安卓系统的实现方式并不一样,iOS 是利用了 WKWebView 的提供 messageHandlers 特性,而在安卓则是往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)这两种方法。

逻辑层接口

逻辑层与客户端原生通信机制与渲染层类似,不同在于,iOS 平台可以往 JavaScriptCore 框架注入一个全局的原生方法,而安卓方面则是跟渲染层一致的。

无论是视图层还是逻辑层,开发者都是间接地调用到与客户端原生通信的底层接口。一般微信小程序会对逻辑层接口做层封装后,才暴露给开发者,封装的细节可能是统一入参、做些参数校验、兼容各平台或版本问题等等。

2.5 启动机制

小程序有冷启动与热启动两种方式:

小程序没有重启的概念:

启动流程:

3. 总结

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8