weixin_39539761
2021-01-09 03:28 阅读 5

在小程序中使用 React with Hooks

介绍一下 Remax

Remax 是一个跨多端小程序 React 开发方案,之所以称其为“方案”而非框架是因为这并非一个新的框架,其主要能力就是让 React 能够直接运行在 微信小程序/支付宝小程序/字节跳动小程序/H5(当然这个本来就支持) 等环境。

可能会有人要会问 “React 不是早就可以运行在小程序中了么“?本文会介绍一下现如今的一些小程序框架的解决方案,以及为什么我们认为把 React 直接搬进小程序是个更为合理的方案。

静态编译类框架

由于大多开发者都更熟悉 React 和 Vue 的 API 和语法,加上小程序本身的开发方式确实让人痛苦,于是便有了一些框架来将这些熟悉的语法编译到小程序的 WXML/WXSS/JS 上,其中比较具有代表性的例如 taro,其目标就是让开发者能够用 React 的开发方式编写小程序。

而这类框架的实现原理其实并非真的是一个 React 或者类 React 框架,而是把看起来像是 JSX 的模板通过静态编译的方式翻译成小程序自身的模板。

这样做的限制非常明显,那就是 JSX 是 JavaScript 的拓展语言(React Blog 写的是 is a syntax extension to JavaScript),而小程序所采用的 WXML 却是一个表达能力非常受限的模板语言,我们不可能完成从一个通用编程语言到模板语言的编译。

而静态编译类框架为了做到这一点,采取的方式就是限制开发者的写法,这也是为什么上面称之为看起来像是 JSX 的模板,这也是为什么 taro 对 JSX 的写法做出了诸多限制。

image image image image

这种方案大多声称这些限制并没有限制生产力,或者符合最佳实践等等。然而我们其实都知道这是由于小程序本身的坑造成的,静态编译方案编译的永远都只会是模板语言,而不是 JSX。

React Hooks

之所以我说这些限制并非基于最佳实践,是因为 React 本身对于 JSX 的定位就 并非模板

JSX is a syntax extension to JavaScript.

在最近 React 团队已经向我们介绍了 Hooks,期望可以 functional component 不仅仅可以是无状态组件,也可以是 useState 的。

js
import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onclick="{()"> setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

React 官方博客提到 Classes confuse both people and machines,我们也明显可以看到基于 function 的组件明显更为简洁,噪声更小,未来 React 社区的方向更是会逐渐从 class component 过渡到 functional component。

在这种趋势下,把 JSX 当做模板写,且未来永远也不可能支持 functional component 的方案绝非真的基于最佳实践的选择。

在 Remax 中,我们完全可以使用全新的 Hooks API 来开发组件

image

因为 Remax 中的 React 就是 React.js,而 JSX 就是 JavaScript 的超集。

上图中使用小程序的原生语法,classname 和 inline style 就只能写成

js
<view class="weui-navbar__item {{activeIndex == index ? 'weui-bar__item_on' : ''}}">
</view>
<view style="left: {{sliderLeft}}px; transform: translateX({{sliderOffset}}px); -webkit-transform: translateX({{sliderOffset}}px);"></view>

而使用 remax 后就可以写成正常的 react:

js
 const innerStyle = {
    left: `${sliderLeft}px`,
    transform: `translateX(${sliderOffset}px);`,
    '-webkit-transform': `translateX(${sliderOffset}px)`,
    width: sliderWidth,
  };
const itemClassName = classnames({
   'weui-navbar__item': true,
    'weui-bar__item_on': activeIndex === index,
});

return <view classname="{itemClassName}">
    <view style="{innerStyle}"></view>
    </view>

实现原理

核心部分

Remax 的实现原理和基于静态编译的方案有所不同,其核心其实是重新实现了 ReactDOM 的部分。

众所周知,React 本身的设计就是支持跨端渲染的,render 部分和 React 的核心逻辑是解耦的(甚至不在一个 npm 包里)。主要的 render 有 ReactDOM(浏览器),ReactDOMServer(服务器端)和 ReactNative。

Remax 要做的事情和 ReactNative 要做的事情非常类似,我们重新接管了 ReactDOM 的 render。

在原有的 React 页面中,React 在完成 Diff 发现需要修改界面时,又 ReactDOM 把改变 Patch 到页面上。

image

而在小程序中由于我们不能直接修改页面,则由 React 完成 DIFF 后由 Remax 把修改 Patch 到内存中的虚拟 DOM 上,然后再通过小程序自己的虚拟 DOM 最后把改变同步到页面上。

image

在这里我把这个过程说得非常简单,但实际上是有些坑要填的,主要也都是来自于小程序的限制,后续会有新的文章展开来讲。但是这种实现方式使得我们完全可以把 React 的代码放在小程序的环境中运行。

工程化

工程化很理所当然的用 Webpack 来实现, 除了我们常用的打包等功能外,Webpack 插件也使我们很容易构建一些我们需要的东西出来,例如我们需要在每个 js 入口除了放一个 js 外还需要添加一个 wxml 文件,就可以通过一个很简单的 Webpack 插件来实现。

js
function GeneraeWxmlWebpackPlugin() {
  const content = `<view>...</view>`;
  const apply = (compiler) => {
    const emit = (compilation, cb) => {
      const {
        chunks,
      } = compilation;
      chunks.forEach((item) => {
        compilation.assets[`${item.name}.wxml`] = {
          source: () => content,
          size: () => content.length,
        };
      });

      cb();
    };

    if (compiler.hooks) {
      const plugin = { name: 'GeneraeWxmlWebpackPlugin' };
      compiler.hooks.emit.tapAsync(plugin, emit);
    } else {
      compiler.plugin('emit', emit);
    }
  };

  return {
    apply,
  };
}

跨端

这种方案想实现同一套代码跨到 H5 端显然没有什么问题,至于支付宝小程序目前验证了一下可行性也是可行的。

miniapp1

1547346366247-2ff00034-c5fc-449a-bc5c-7c65302c96b6

项目结构

这个项目主要由几块组成

  • /core 核心部分,负责 React 组件的 render
  • /cli 顾名思义,CLI 工具,用于构建生成相应的小程序项目等工作
  • /components 底层 Component,包括诸如 View 等一些基础组件,用于抹平不同环境的差异
  • /ui 自带的基础组件库,这部分还待开发,目前只有一两个示例组件

由于目前整个项目才刚刚起步,暂时还不能用于生产环境,目前的几个主要开发者(和打算参与的)有

目前的 DEMO 可以扫码体验: image

或者在可以按照 https://github.com/CodeFalling/remax#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B 体验本地 DEMO。

如果有人想要参与进来一起开发可以联系我,开发相关的细节文档会陆续更新在 https://github.com/CodeFalling/remax/wiki/%E5%85%A5%E9%97%A8 。

推荐阅读

该提问来源于开源项目:remaxjs/remax

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

23条回答 默认 最新

  • weixin_39934063 weixin_39934063 2021-01-09 03:28

    在小程序内运行react的思路还是让人为之一振的,赞一个~ 只是有个疑问啊: react 是一颗组件树(一般只会在根组件调用render方法),而小程序内每个Page都会是一个webview。 目前看render方法是return了一个Page,感觉这会是在一个webview内运行的SPA?小程序的多页体验是否还能保留?或者是会再实现一个remax-router来支持多页体验?

    点赞 评论 复制链接分享
  • weixin_39623805 weixin_39623805 2021-01-09 03:28

    为 LZ 的 idea 点赞👍 !不过这样实现不会产生性能问题么?vdom tree 往往是一个比较大的json,而微信小程序的 setData 涉及到跨进程通信,对于 传递大 json 的行为则可能会导致很明显的卡顿。而小程序底层是模版,想只传递 diff 之后的结果也不是很方便,或者是用的 wxs 处理?

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    可以扫一下上面的小程序 DEMO 体验一下,体验仍然和小程序是一致的。

    确实如你所说,目前小程序的多页其实就是多个 webview,体验也比较好。

    所以我们就直接用小程序的 router(即写多个 Page)就好,每个 Webview 跑的是一个独立页面而不是 SPA,不强求一定要用类似 react-router 的方式在 JS 层面再实现一遍多页。

    ezgif-4-f8d58b86d07a

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    目前是靠传递整个 vdom 树实现的,只传递 diff 结果估计难以实现(因为 View 层的逻辑太弱了)。

    不过可以考虑分片的更新 data(因为小程序的 setData 是 update 而非 rewrite)来提高性能,当然这是个很浅的思路,还没有想得非常的清楚。

    其实这里有另外一个问题更让我头疼:我怎么实现一个小程序环境的 benchmark🤣

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    稍微思考了一下,把整个 tree 分成多块,然后更新时只 set 改变的块的 tree,应该能实现(虽然不能精细到完全只传递 diff 部分)

    不过在这之前仍然要解决量化小程序性能的问题,现在只能靠微信提供的调试工具中的 重渲染时间 实在是看不出什么花样,我甚至不知道在 传递 data => render tree => diff => patch 的过程中它的 重渲染时间 到底指的是哪一段。

    点赞 评论 复制链接分享
  • weixin_39623805 weixin_39623805 2021-01-09 03:28

    setData 不会做自动 merge,而且大数组之类的感觉还是不好处理。。微信小程序模版里支持 wxs 可能可以对 json 做一些操作,不过这样对于其他小程序就支持不太好了。。

    小程序的 benchmark 我觉得还好吧,和 App 一样直接上真机,然后运行时查询通过 setData 的 callback 计算渲染时间 log 到 vconsole 上, 感觉应该没问题。。

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    分片

    不是指 Object 自动 merge,而是数据 key 本身就不同的时候是可以单独 set 的,我们可以据此把单个 VDOM 分割成

    js
    {
      splitCount: 2,
      VDOM_SPLIT_1: {},
      VDOM_SPLIT_2: {},
    }
    

    然后在第二块改变时只

    js
    setData({
      VDOM_SPLIT_2: ...
    });
    

    不过这个思路仍然很粗,不能很精细的解决这个问题。

    benchmark

    benchmark 你的思路确实可行,之前一直在想电脑上怎么跑,确实还是真机跑才有意义,可以先把这些性能相关的信息都通过 vconsole 打出来让开发者有个感知。

    wxs

    感觉 WXS 确实是这个问题的最靠谱的解法,关于兼容性的问题一方面我相信其他家的小程序会跟进的;另外一方面其实这一层的实现对上层是没有影响的,加上小程序目前最大的 target 仍然是微信小程序,我觉得在不支持的场景降级是可以接受的。

    感谢你的思路,非常棒 👍

    点赞 评论 复制链接分享
  • weixin_39683368 weixin_39683368 2021-01-09 03:28

    我了解过taroanu等都是将JSX直接编译为wxml文件,所以支持的语法才有限。觉得作者的实现方案很赞,经我测试Remax是可以使用类似jsx-control-statements等基于babel的JSX插件的。

    跑了下demo,然后看了下demo/dist目录中的生成文件,里面有一个很大的base.wxml;然后每个页面的wxml都引用了它。我初步理解是不是在base.wxml中将运行时需要的各小程序组件的wxml场景都枚举了一遍,这样在运行时会不会影响性能呢?

    作者这样的实现方式貌似是给小程序的wxml赋予了一定的动态生成能力,想了解下这种方式是否有其他类似的成熟方案(比如mpvue等)在使用?

    点赞 评论 复制链接分享
  • weixin_39789327 weixin_39789327 2021-01-09 03:28

    自从看了Taro感觉还是鸡肋,一直在等一个能够真正兼容React的出来。楼主的这个idea给10086个👍。 这个目前来看实现简单的内容呈现类小程序可能问题不大,但是牵扯到交互比较多的组件时,React with Hooks 还是解决不了多个component状态共享问题,除非用context API. 考虑过要支持现有的redux么?

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    -sky 你理解的没错,我们需要 wxml 枚举把 vdom 显示成界面。据我所知 mpvue 仍然是采用的静态编译方案,应该没有采取这种方案。至于其对运行时性能的影响就取决于微信的实现了,目前从 demo 看没有什么问题,不过更复杂的场景确实需要验证。

    目前也在思考有没有更优雅的方案,但小程序似乎没有给我们开什么其他口子

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    理论上 redux(准确说是 react-redux)只和 react 相关而无关 react-dom。应该是可以直接支持的。有空我写个 demo 跑跑看有什么问题。

    点赞 评论 复制链接分享
  • weixin_39789327 weixin_39789327 2021-01-09 03:28

    , 原理上好像是这样的。又仔细看了一下代码以及整个项目结构。如果我理解正确的话remax 就是重写了react 的render ?还有就是把微信小程序对应的tag 通过特定的react component来展现? 用户可定义自己的component 么? 问的问题比较多,望海函,用react 有年头了,就是现在的各种小程序太多了,国外根本无此项目资源,脱节了都。。。。

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    /core 主要是重写 react 的 render,/components 是提供一些 react 组件可以渲染成小程序基础组件,/cli 是相关的构建(生成小程序对应的文件结构之类的)

    在这种结构下,用户的组件机制完全在 js 里(由 react 接管),不需要真的在小程序的 wxml 中真的体现成 custom tag。

    点赞 评论 复制链接分享
  • weixin_39730587 weixin_39730587 2021-01-09 03:28

    或许可以考虑一下怎么把现有的第三方小程序组件接入进来,提供包组件的方法之类的?

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    可以,就具体的 feature request 我们可以单开 issue 讨论

    点赞 评论 复制链接分享
  • weixin_39789327 weixin_39789327 2021-01-09 03:28

    那如果要支持其他平台的小程序,Components 有一些是不是要重写?webpack 配置那块是不是可以通过CLI不同命令来配置不同的template?

    点赞 评论 复制链接分享
  • weixin_39539761 weixin_39539761 2021-01-09 03:28

    是的 CLI 肯定是根据平台来生成不同的东西,不仅仅是 template,也包括 WXSS => ACSS 等。/components 就是为了这一点预留的抽象层。

    点赞 评论 复制链接分享
  • weixin_39988779 weixin_39988779 2021-01-09 03:28

    个人觉得没必要支持其他小程序平台,微信小程序和H5才是王道,专注才能做好。

    点赞 评论 复制链接分享
  • weixin_39988779 weixin_39988779 2021-01-09 03:28

    加油,急迫等待在生产环境使用。

    点赞 评论 复制链接分享
  • weixin_39646405 weixin_39646405 2021-01-09 03:28

    这里又跑了 react runtime 么,那性能数据呢,逻辑层运行时复杂度?

    点赞 评论 复制链接分享
  • weixin_39543773 weixin_39543773 2021-01-09 03:28

    群二维码已过期

    点赞 评论 复制链接分享
  • weixin_39795065 weixin_39795065 2021-01-09 03:28

    群二维码已过期

    点赞 评论 复制链接分享
  • weixin_39851974 weixin_39851974 2021-01-09 03:28

    #194

    点赞 评论 复制链接分享

相关推荐