跨端小程序框架 --Taro演进

hybrid 应用

Hybrid App(混合模式移动应用)是指介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。

客户端提供webview控件,能在App内使用H5技术开发页面,如果需要调用到系统能力的功能需要客户端实现,两者建立桥梁通信实现交互

优点

  • 具备完整webview功能
  • 可间接使用系统原生功能
  • 随时更新无需发版

缺点

  • 性能差距明显
  • 白屏时间长,用户体验差
  • 系统操作需要客户端原生支持提供方法
  • 有时候受限客户端原因,需要特殊兼容

微信小程序

简介

「触手可及,用完即走」

小程序提供了自己的视图层描述语言 WXMLWXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑。

框架的核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。

技术发展史

小程序并非凭空冒出来的一个概念。当微信中的 WebView 逐渐成为移动 Web 的一个重要入口时,微信就有相关的 JS API 了。

2015年初,微信发布了一整套网页开发工具包,称之为 JS-SDK,开放了拍摄、录音、语音识别、二维码、地图、支付、分享、卡券等几十个API。

JS-SDK是对之前的 WeixinJSBridge 的一个包装,以及新能力的释放,并且由对内开放转为了对所有开发者开放,在很短的时间内获得了极大的关注。从数据监控来看,绝大部分在微信内传播的移动网页都使用到了相关的接口。

JS-SDK 解决了移动网页能力不足的问题,通过暴露微信的接口使得 Web 开发者能够拥有更多的能力,然而在更多的能力之外,JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题

用户在访问网页的时候,在浏览器开始显示之前都会有一个白屏的过程,在移动端,受限于设备性能和网络速度,白屏会更加明显。因此设计了一个 JS-SDK 的增强版本,其中有一个重要的功能,称之为“微信 Web 资源离线存储”。

微信 Web 资源离线存储是面向 Web 开发者提供的基于微信内的 Web 加速方案。

通过使用微信离线存储,Web 开发者可借助微信提供的资源存储能力,直接从微信本地加载 Web 资源而不需要再从服务端拉取,从而减少网页加载时间,为微信用户提供更优质的网页浏览体验。每个公众号下所有 Web App 累计最多可缓存 5M 的资源。

在内部测试中,我们发现离线存储能够解决一些问题,但对于一些复杂的页面依然会有白屏问题,例如页面加载了大量的 CSS 或者是 JavaScript 文件。除了白屏,影响 Web 体验的问题还有缺少操作的反馈,主要表现在两个方面:页面切换的生硬和点击的迟滞感

微信面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK 所处理不了的,需要一个全新的系统来完成,它需要使得所有的开发者都能做到:

  • 快速的加载
  • 更强大的能力
  • 原生的体验
  • 易用且安全的微信数据开放
  • 高效和简单的开发
  • 无需安装

而缺点也很多:

  • 组件较少
  • webview限制很多,普通页面开发区别很大
  • 行为限制很多,包体积大小,资源使用,页面打开层级等等
  • 不同机型的兼容问题也是比较多

这就是小程序的由来。

小程序与普通网页开发的区别

网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。

网页开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。而如上文所述,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。这一区别导致了前端开发非常熟悉的一些库,例如 jQueryZepto 等,在小程序中是无法运行的。

同时 JSCore 的环境同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的。

开发端 开发场景
网页开发者 各式各样的浏览器
PC 端 IE、Chrome、QQ浏览器等
移动端 Safari、Chrome以及 iOS、Android 系统中的各式 WebView
小程序 两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具

小程序中三大运行环境也是有所区别的

运行环境 逻辑层 渲染层
iOS JavaScriptCore WKWebView
安卓 V8 chromium定制内核
小程序开发者工具 NWJS Chrome WebView

网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。

渲染层和逻辑层

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

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。

特别需要注意的是setData调用方式和时机

  • setData接口的调用涉及逻辑层与渲染层间的线程通信,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用。
  • 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间
  • setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗。

开发环境

小程序提供了一个基于nwjs实现的IDE开发工具,可以模拟代码预览效果,但是实际上并不是真机的webview环境,所以很多时候会发现开发环境跟真机预览的差别存在很多细节问题.并且因为小程序本身的包体大小限制,很多时候无法直接唤起真机调试,需要特殊处理将体积压到2M以下才行

加载过程

微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。

紧接着通过 app.jsonpages 字段就可以知道你当前小程序的所有页面路径:

{
  "pages":[
    "pages/logs/logs"
  ]
}

于是微信客户端就把首页的代码装载进来,通过小程序底层的一些机制,就可以渲染出这个首页。

小程序启动之后,在 app.js 定义的 App 实例的 onLaunch 回调会被执行:

App({
  onLaunch: function () {
    // 小程序启动之后 触发
  }
})

整个小程序只有一个 App 实例,是全部页面共享的

你可以观察到 pages/logs/logs 下其实是包括了4种文件的,微信客户端会先根据 logs.json 配置生成一个界面,顶部的颜色和文字你都可以在这个 json 文件里边定义好。紧接着客户端就会装载这个页面的 WXML 结构和 WXSS 样式。最后客户端会装载 logs.js,你可以看到 logs.js 的大体内容就是:

Page({
  data: { // 参与页面渲染的数据
    logs: []
  },
  onLoad: function () {
    // 页面渲染后 执行
  }
})

Page 是一个页面构造器,这个构造器就生成了一个页面。在生成页面的时候,小程序框架会把 data 数据和 index.wxml 一起渲染出最终的结构,于是就得到了你看到的小程序的样子。

在渲染完界面之后,页面实例就会收到一个 onLoad 的回调,你可以在这个回调处理你的逻辑。

开发痛点

依赖管理混乱、工程化流程落后、ES Next 支持不完善、命名规范不统一等。这些问题在现在看来都已经有了各种官方或非官方的解决办法.

最常见的开发模式就是使用某一套完善的开发框架,他们最主要的区别就是DSL,类Vue或者类React语法为主.

在微信小程序之后,各大厂商纷纷发布了自己的小程序平台,多端适配型框架的需求也就应运而生了,(Taro, uni-app等)

所以开发技术选型的主要考虑因素就是: DSL 以及 多端适配

Taro1.x

市面上第一款遵循React语法的多端小程序框架,自研出Nervjs.同时也支持使用 React/Vue/Nerv 等框架来开发

Write once Run anywhere

目前(Taro3.x)官方支持转换的平台如下:

  • H5
  • ReactNative
  • 微信小程序
  • 京东小程序
  • 百度小程序
  • 支付宝小程序
  • 字节跳动小程序
  • QQ 小程序
  • 钉钉小程序
  • 企业微信小程序
  • 支付宝 IOT 小程序
  • 飞书小程序

跨端小程序框架 --Taro演进_第1张图片

设计思路

必须满足下述要求:

  • 代码多端复用,不仅能运行在时下最热门的 H5、微信小程序、React Native,对其他可能会流行的端也留有余地和可能性。
  • 完善和强大的组件化机制,这是开发复杂应用的基石。
  • 与目前团队技术栈有机结合,有效提高效率。
  • 学习成本足够低
  • 背后的生态强大
在一个优秀且严格的规范限制下,从更高抽象的视角(语法树)来看,每个人写的代码都差不多。

也就是说,对于微信小程序这样不开放不开源的端,我们可以先把 React 代码分析成一颗抽象语法树,根据这颗树生成小程序支持的模板代码,再做一个小程序运行时框架处理事件和生命周期与小程序框架兼容,然后把业务代码跑在运行时框架就完成了小程序端的适配。

对于 React 已经支持的端,例如 Web、React Native 甚至未来的 React VR,我们只要包一层组件库再做些许样式支持即可。鉴于时下小程序的热度和我们团队本身的业务侧重程度,组件库的 API 是以小程序为标准,其他端的组件库的 API 都会和小程序端的组件保持一致。

跨端小程序框架 --Taro演进_第2张图片

架构

Taro 当前的架构主要分为:编译时运行时

跨端小程序框架 --Taro演进_第3张图片

Taro 编译时

使用 babel-parser 将 Taro 代码解析成抽象语法树,然后通过 babel-types 对抽象语法树进行一系列修改、转换操作,最后再通过 babel-generate 生成对应的目标代码。

跨端小程序框架 --Taro演进_第4张图片

Babel 的编译过程亦是如此,主要包含三个阶段

  1. 解析过程,在这个过程中进行词法、语法分析,以及语义分析,生成符合 ESTree 标准 虚拟语法树(AST)
  2. 转换过程,针对 AST 做出已定义好的操作,babel 的配置文件 .babelrc 中定义的 preset 、 plugin 就是在这一步中执行并改变 AST 的
  3. 生成过程,将前一步转换好的 AST 生成目标代码的字符串

如果对AST是什么不了解的话可以使用这个网站尝试一下https://astexplorer.net/

跨端小程序框架 --Taro演进_第5张图片
跨端小程序框架 --Taro演进_第6张图片

省去整个编译过程得到的结果代码如下


  
  
  
  {this.props.counter.num}
  
  
  


  
    
    
    
    {{counter.num}}
    
    
    
  

因为JSX的丰富自由度不是字符串模板可以比拟的,所以当时只能支持大概80%的写法转换,剩余不支持的写法转由eslint插件提醒用户修改。

在开源的过程中,Taro 支持的 JSX 写法一直在不断完善,力求让开发体验更加接近于 React,主要包括以下语法支持:

  • 支持 Ref,提供了更加方便的组件和元素定位方式
  • 支持 this.props.children 写法,方便进行自定义组件传入子元素
  • 在循环体内执行函数和表达式
  • 定义 JSX 作为变量使用
  • 支持复杂的 if-else 语句
  • 在 JSX 属性中使用复杂表达式
  • 在 style 属性中使用对象
  • 只有使用到的变量才会作为 state 加入到小程序 data,从而精简小程序数据

Taro 运行时

我们可以对比一下编译后的代码,可以发现,编译后的代码中,React 的核心 render 方法没有了。同时代码里增加了 BaseComponentcreateComponent ,它们是 Taro 运行时的核心。

// 编译前
import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import './index.scss'

export default class Index extends Component {

  config = {
    navigationBarTitleText: '首页'
  }

  componentDidMount () { }

  render () {
    return (
      
        Hello world!
      
    )
  }
}
// 编译后
import {BaseComponent, createComponent} from '@tarojs/taro-weapp'

class Index extends BaseComponent {

// ... 

  _createDate(){
    //process state and props
  }
}

export default createComponent(Index)

BaseComponent 大概的 UML (统一建模语言)图如下,主要是对 React 的一些核心方法:setStateforceUpdate 等进行了替换和重写,结合前面编译后 render 方法被替换,大家不难猜出:Taro 当前架构只是在开发时遵循了 React 的语法,在代码编译之后实际运行时,和 React 并没有关系

跨端小程序框架 --Taro演进_第7张图片

createComponent 主要作用是调用 Component() 构建页面;对接事件、生命周期等;进行 Diff Data 并调用 setData 方法更新数据。

跨端兼容

仅仅将代码按照语法规范转换之后还远远不够,因为不同端的特有原生能力或组件等限制,所以决定了跨端开发必然有部分代码需要开发中实现兼容,而为了最大程度弥补不同端的差异,Taro制定了一个统一标准,在不同端依靠它们的语法与能力去实现组件库与API,同时还要为不同端编写相应的运行时框架,初始化等等。因为最初的设计来源就是小程序,所以决定直接采用小程序的标准,让其他端向小程序靠齐。

因为我们有编译的操作,在书写代码的时候,只需要引入标准组件库 @tarojs/components 与运行时框架 @tarojs/taro ,代码经过编译之后,会变成对应端所需要的库。

跨端小程序框架 --Taro演进_第8张图片

例如,为了提升开发便利性,我们为 Taro 加入了 Redux 支持,我们的做法就是,在小程序端,我们实现了 @tarojs/redux 这个库来作为小程序的 Redux 辅助库,并且以他作为基准库,它具有和 react-redux 一致的 API,在书写代码的时候,引用的都是 @tarojs/redux ,经过编译后,在 H5 端会替换成 nerv-reduxNervRedux 辅助库),在 RN 端会替换成 react-redux。这样就实现了 Redux 在 Taro 中的多端支持。

小程序组件化

开源之初,由于种种原因,Taro 的微信小程序端组件化采用的是小程序