Taro在微信小程序上的机制分析

1,439 阅读4分钟

背景

Taro是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv等框架来开发微信/京东/百度/支付宝/字节跳动/QQ/飞书小程序/H5/RN等应用。

关键特性:编写一套代码就能够适配到多端。

当前Taro已进入3.x时代,相较于Taro 1/2编译时架构,Taro3采用了重运行时的架构,让开发者可以获得完整的React/Vue等框架的开发体验。

微信小程序

在讨论Taro之前,必须先下介绍下微信小程序的架构

小程序的运行环境分成视图层(渲染层)和逻辑层,其中 WXML 模板和 WXSS 样式工作在视图层,JS 脚本工作在逻辑层。

小程序的视图层和逻辑层分别由2个线程管理:视图层的界面使用了WebView 进行渲染;逻辑层采用 JsCore 线程运行 JS 脚本。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程,这两个线程的通信会经由Native JS Bridge做中转。

<block>
    <view style="margin-top: 100px;">
        <view>{{count}}</view>
        <button bindtap="onClick">按钮</button>
    </view>
</block>


// index.js
Page({
    data: {
        count: 1
    },
    // 事件处理函数
    onClick() {
        this.setData({ count: this.data.count + 1 })
    },
})

Taro1/2

Taro1/2 的架构基于小程序的基础架构出发。将用户编写的基于Nerv框架(类React)的代码编译为类似于小程序的代码。

编译前代码:

import { Button, View } from '@tarojs/components';
import Taro from '@tarojs/taro';

export const Test: Taro.FC = () => {
  const [count, setCount] = Taro.useState(0);

	const handleClick = () => {
		setCount((v) => v + 1)
	}

  return (
    <View style={{marginTop: '100px'}}>
      <View>{count}</View>
      <Button onClick={handleClick}>按钮</Button>
    </View>
  );
};

export default Test;

编译后代码

<block wx:if="{{$taroCompReady}}">
    <view style="{{anonymousState__1}}">
        <view>{{count}}</view>
        <button bindtap="anonymousFunc1">按钮</button>
    </view>
</block>


/** 省略 默认编译代码 **/
var Test = exports.Test = (_temp2 = _class = function (_Taro$Component) {
  _inherits(Test, _Taro$Component);

  function Test() {
    var _ref;

    var _temp, _this, _ret;

    _classCallCheck(this, Test);

    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Test.__proto__ || Object.getPrototypeOf(Test)).call.apply(_ref, [this].concat(args))), _this), _this.$usedState = ["anonymousState__1", "count"], _this.customComponents = [], _temp), _possibleConstructorReturn(_this, _ret);
  }

  _createClass(Test, [{
    key: "_constructor",
    value: function _constructor(props) {
      _get(Test.prototype.__proto__ || Object.getPrototypeOf(Test.prototype), "_constructor", this).call(this, props);

      this.$$refs = new _index2.default.RefsArray();
    }
  }, {
    key: "_createData",
    value: function _createData() {
      this.__state = arguments[0] || this.state || {};
      this.__props = arguments[1] || this.props || {};
      var __isRunloopRef = arguments[2];
      var __prefix = this.$prefix;
      ;

      var _Taro$useState = _index2.default.useState(0),
          _Taro$useState2 = _slicedToArray(_Taro$useState, 2),
          count = _Taro$useState2[0],
          setCount = _Taro$useState2[1];

      var handleClick = function handleClick() {
        setCount(function (v) {
          return v + 1;
        });
      };
      var anonymousState__1 = (0, _index.internal_inline_style)({ marginTop: '100px' });
      **this.anonymousFunc1 = handleClick;**
      **Object.assign(this.__state, {
        anonymousState__1: anonymousState__1,
        count: count
      });**
      return this.__state;
    }
  }, {
    key: "anonymousFunc1",
    value: function anonymousFunc1(e) {
      ;
    }
  }]);

  return Test;
}(_index2.default.Component), _class.$$events = ["anonymousFunc1"], _class.$$componentPath = "pages/test/index", _temp2);
exports.default = Test;

**Component(require('../../npm/@tarojs/taro-weapp/index.js').default.createComponent(Test, true));**

从编译前和编译后的代码可以看出,Taro1/2尽力的将React语法的代码,还原成小程序的代码

render函数(jsx)代码,被编译成了小程序的wxml。

wxml用到的变量,全部在Page里的data里面注册。

Object.assign(this.__state, { anonymousState__1: anonymousState__1, count: count });

wxml用到的事件,全部在绑定在Page上面

this.anonymousFunc1 = handleClick;

其运行逻辑基本和小程序的逻辑保持一致。

总结**,Taro1/2架构的特点是:**

  • 重编译时,轻运行时
  • 编译后代码与 React 无关:Taro 只是在开发时遵循了 React 的语法。

同时在深度使用Taro1/2后,也发现了Taro1/2架构的特点即其缺点**:**

目前的Taro1/2的编译工具没办法做到把React代码转换成小程序代码的同时,运行逻辑也和React保持一致

这导致很多时候,开发者基于React经验开发的代码,编译出来的代码跑起来却不符合开发者的意图,在开发体验上是割裂的。同时因为编译后的代码和原代码差异很大,定位问题也很困难。

Taro3

也许是编译前React、编译后小程序代码之间的gap太大,团队没法使用静态编译解决Taro1/2中问题。Taro3整体架构上,而是决定向浏览器环境靠齐(社区也已有类似方案:Remax)。

Taro3虚拟了一套运行在小程序内部的DOM/BOM API,源码:

taro/packages/taro-runtime/src at next · NervJS/taro

通过虚拟化的document、window, taro-runtime解决了逻辑层的代码的运行问题。

理论上来说,有了runtime,React就可以运行在小程序中了。但react-dom中存在大量的浏览器兼容代码。包很大但实际上runtime并不需要这些兼容性的代码。因此Taro团队实现了一套更精简的taro-react,以取代react-dom。

以上解决了小程序逻辑层代码运行的问题。

逻辑层最终生成的结果,是由runtime生成的一套模拟的dom树。而dom树在小程序内的渲染,简单理解即runtime将dom树作为Page中的data,每次根据data来渲染该页面的template。

编译前代码:

import { FC, useEffect, useState } from 'react'
import { View, Button } from '@tarojs/components'
import { SuperButton } from '../../components/SuperButton'

const Index: FC = () => {
  const [count, setCount] = useState(1);

  const handleClick = () => {
      setCount(v => v + 1)
  }

  return (
    <View className='jsx-123'>
      <View>{count}</View>
      <Button onClick={handleClick}>按钮</Button>
    </View>
  )
}

export default Index

编译后代码:

<import src="../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />


"use strict";
(wx["webpackJsonp"] = wx["webpackJsonp"] || []).push([["pages/index/index"],{

/***/ "./node_modules/.pnpm/babel-loader@8.2.1_dkc6jw7azevnwlfo36kgx2me2a/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[5].use[0]!./src/pages/index/index.tsx":
/*!*************************************************************************************************************************************************************************!*\\
  !*** ./node_modules/.pnpm/babel-loader@8.2.1_dkc6jw7azevnwlfo36kgx2me2a/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[5].use[0]!./src/pages/index/index.tsx ***!
  \\*************************************************************************************************************************************************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

/* harmony import */ var _Users_michael_Projects_taroV3_node_modules_pnpm_babel_runtime_7_19_4_node_modules_babel_runtime_helpers_esm_slicedToArray_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/.pnpm/@babel+runtime@7.19.4/node_modules/@babel/runtime/helpers/esm/slicedToArray.js */ "./node_modules/.pnpm/@babel+runtime@7.19.4/node_modules/@babel/runtime/helpers/esm/slicedToArray.js");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "webpack/container/remote/react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _tarojs_components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @tarojs/components */ "./node_modules/.pnpm/@tarojs+plugin-platform-weapp@3.5.5_@types+react@17.0.50/node_modules/@tarojs/plugin-platform-weapp/dist/components-react.js");
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react/jsx-runtime */ "webpack/container/remote/react/jsx-runtime");
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__);

var Index = function Index() {
  var _useState = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(1),
    _useState2 = (0,_Users_michael_Projects_taroV3_node_modules_pnpm_babel_runtime_7_19_4_node_modules_babel_runtime_helpers_esm_slicedToArray_js__WEBPACK_IMPORTED_MODULE_2__["default"])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var handleClick = function handleClick() {
    setCount(function (v) {
      return v + 1;
    });
  };
  return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(_tarojs_components__WEBPACK_IMPORTED_MODULE_3__.View, {
    className: "jsx-123",
    children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_tarojs_components__WEBPACK_IMPORTED_MODULE_3__.View, {
      children: count
    }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_tarojs_components__WEBPACK_IMPORTED_MODULE_3__.Button, {
      onClick: handleClick,
      children: "\\u6309\\u94AE"
    })]
  });
};
/* harmony default export */ __webpack_exports__["default"] = (Index);

/***/ }),

/***/ "./src/pages/index/index.tsx":
/*!***********************************!*\\
  !*** ./src/pages/index/index.tsx ***!
  \\***********************************/
/***/ (function(__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) {

/* harmony import */ var _tarojs_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @tarojs/runtime */ "webpack/container/remote/@tarojs/runtime");
/* harmony import */ var _tarojs_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_tarojs_runtime__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _node_modules_pnpm_babel_loader_8_2_1_dkc6jw7azevnwlfo36kgx2me2a_node_modules_babel_loader_lib_index_js_ruleSet_1_rules_5_use_0_index_tsx__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../node_modules/.pnpm/babel-loader@8.2.1_dkc6jw7azevnwlfo36kgx2me2a/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[5].use[0]!./index.tsx */ "./node_modules/.pnpm/babel-loader@8.2.1_dkc6jw7azevnwlfo36kgx2me2a/node_modules/babel-loader/lib/index.js??ruleSet[1].rules[5].use[0]!./src/pages/index/index.tsx");

var config = {"navigationBarTitleText":"首页"};

**var inst = Page((0,_tarojs_runtime__WEBPACK_IMPORTED_MODULE_0__.createPageConfig)(_node_modules_pnpm_babel_loader_8_2_1_dkc6jw7azevnwlfo36kgx2me2a_node_modules_babel_loader_lib_index_js_ruleSet_1_rules_5_use_0_index_tsx__WEBPACK_IMPORTED_MODULE_1__["default"], 'pages/index/index', {root:{cn:[]}}, config || {}))**

/***/ }),
/** 省略代码 */

},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
/******/ __webpack_require__.O(0, ["taro","common"], function() { return __webpack_exec__("./src/pages/index/index.tsx"); });
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ }
]);
//# sourceMappingURL=index.js.map

从上面我们可以看到关键的三行代码:

root:{cn:[]}