React跨端动态化序章—从JS引擎到RN落地

10,812 阅读11分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

一 为什么跨端动态化迫在眉睫

目前很多互联网大厂的移动端开发都在朝着跨端动态化方向发展。由于快速迭代开发或者对原生包体积要求严格,及其对资源成本的把控,实现跨端动态化迫在眉睫。我们先来看看 Native 原生开发的一些不足之处:

  • 1 原生开发周期时间长,审核周期长,会影响到需求发布和迭代效率,有些场景下会更加棘手,比如修复线上紧急 bug ,或者是频繁迭代一些开发需求。

  • 2 目前移动端主要的平台就是 Android 和 iOS,如果一款前端应用想要同时运行在两个平台的话,采用 Native 就需要双端各自开发一遍,这样无疑浪费了资源和提高了维护成本。

  • 3 Native 开发代码要打包在客户端包中,这样增加了包的体积,用户下载的时候,会下载更多的资源,轻量级的包会提高运营效率,而且 Android 和 iOS 应用平台也对包体积严格把控。

说到跨端化方案,首先想到的就是 React Native,为什么这么说呢? 我们往下看。

二 首当其冲的为什么是 React Native

React 在跨端领域也有一席之地,功劳来源跨端方案 React Native,简称 RN ,RN 是目前主流的动态化方案之一,是 Facebook 在 2015 年开源的 JS 框架 React 在原生移动应用平台的跨平台技术,支持安卓和 iOS 平台。

RN 的受欢迎并不仅仅是支持安卓和 iOS 平台,还有一个重要的因素就是动态化,那么这种动态化相比于原生客户端有什么优点呢?

RN 对于原生开发有着明显的优点:

  • 1 RN 是采用运行 React 的 JS 作为开发平台,这样可以让 web 开发者也能够参与到 Native 开发中来,还有就是 RN 让一套代码可以运行在两端,大大减少了开发和维护成本。

  • 2 RN 是采用原生渲染的,性能和体验仅次于 Native 开发。

  • 3 还有一点也是最重要的,就是 RN 是动态化的方案,也就是 RN 打出来的应用包,并不是和 Native 包绑定在一起发布的,而是在运行 Native 的时候拉下 RN 的包,这样一是减少了 Native 包体积,二是 RN 包可以随时发布,提高了迭代效率,也让一些线上问题能够快速解决。

近两年,也有一些兴起的跨端技术方案,比如 Flutter,阿里巴巴开源的 Rax 等,相比这些动态化方法,RN 也有一定优势:

  • 1 生态成熟,技术社区活跃,采用 React 语法,学习成本低。

  • 2 目前业界已经出现了很多成熟方案,比如京东的 JDReact 和美团的 MRN 等。

RN 是基于 React 框架开发的原生应用,React 凭借着 JSX 语法让使用者结合多种设计模式使开发变得非常灵活,React 是 JavaScript (简称 JS ) 框架,那么如果想要运行 RN ,那么就需要运行 JS ,在我们的影响中,JS 作为脚本语言运行到浏览器端,或者运行在 Node.js 中,那么如今却能够作为跨端方案运行到 Native 应用中,这是为什么呢?

原来能够让 JS 运行到 Native 中的法宝就是 JS 引擎,最常见的 JS 引擎就是 v8 ,v8 使用在 Chrome 浏览器和 Node.js 中,构建了 JS 运行时,能够执行 JS 脚本。那么接下来我们就来看一下 RN 中的 JS 引擎。

三 JS 引擎让跨端动态化成为可能

V8 引擎简介

计算机本身并不能读懂编程语言,计算机只能读懂二机制文件,但是为了能够让编程语法能够让计算机读懂,就必须编译成二机制文件,这就是编译语言,比如 JAVA GO 等都是编译型的语言,编译型语法在编译成二机制文件后,会保存二机制文件,在运行时候,可以直接运行二机制文件,不需要重复编译。

还有一类语言,不需要编译成文件,而是需要通过解释器对语言进行动态解释和执行,这类语言就是解释型语言,比如 Python,JS 等。如下图就是两种类型的语言执行过程:

图片1

编译型语言启动需要编译成二进制文件,所以启动速度会比很慢,但是执行的时候是直接使用编译好的二进制文件,所以执行速度会快一些。

但是相比解释型语言,启动会很快,但是执行时候,需要通过解释器解析语法树,变成中间代码,执行字节码,这样就浪费了时间,使得执行速度会变慢。

由于 JS 是解释型语言,它的执行需要宿主环境提供,转成语法树 ,并且读懂语法树,转成字节码并执行的能力,v8 引擎的工作就需要有这些能力:

Parser:将 JS 源码转换成抽象语法树,什么是抽象语法?在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

Lgniton:interpreter 解释器,负责将 AST 转换成指令字节码,解释执行指令字节码(ByteCode),解释器执行的时候主要有四个模块:内存中的字节码,寄存器,栈和堆。

TurboFan:compiler 编译器,通过 Lgniton 收集的信息,将指令字节码转换成优化汇编代码。

Orinoco:garbage collector 简称 GC,垃圾回收模块,负责将程序不需要的内存空间回收,提升引擎性能。

如上还有一个问题就是如果每一次都通过 TurboFan 将指令字节码转换成汇编代码,那么这样十分浪费性能。在 v8 出现之前,所有的 JS 虚拟机所采用的都是解释执行的方式,这是 JS 执行速度过慢的主要原因之一。

而 v8 率先引入了即时编译(JIT)的双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,给 JS 的执行速度带来了极大的提升。

那么 JIT 就是取编译执行语言和解释执行语言的长处,利用解释器对代码进行处理,对于频率高的代码进行热区收集,在之后指令字节码编译成机器码的时候,储存高频率的二机制机器码,之后就可以复用并执行二机制代码,以减少解释器和编译器的压力。

v8 通过优化后的工作流程如下:

7-4-2.png

知道了 v8 JS 引擎的工作流程之后,那么 RN 应用中用什么 JS 引擎呢?

RN 在 0.60 版本之前使用 JSCore 作为默认的 JS 引擎, JSCore 全名 JavaScriptCore ,JSCore 是 WebKit 默认内嵌的 JS 引擎,JSCore作为一个系统级 Framework 被苹果提供给开发者,作为苹果的浏览器引擎 WebKit 中重要组成部分。

所以在 iOS 应用中默认为 JSCore 引擎,这使得 RN 也用 JSCore ,但是 JSCore 没有对 Android 机型做好适配,在性能,体积,和内存上和 v8 有着明显的差别。

基于这个背景,RN 团队提供了 JSI (JavaScript Interface)框架,JSI 并不是 RN 的一部分,JSI 可以视作一个兼容层,意在磨平不同 JS 引擎中的差异性。

JSI 实现了引擎切换,比如在 iOS 平台运行的 JSCore ,在 Andriod 中运行的是 v8 引擎。

JSI 同样提供了抽象的 API 接口,定义了与各个 JS 引擎交互的接口。

在 JS 中调用 C++ 注入到 JS 引擎中的方法,数据载体格式是通过 HostObject 接口规范化后的,摒弃了旧架构中以 JSON 作为数据载体的异步机制,让 JS 和 C++ 相互感知。

7-4-3.png

明白了 RN 内部运转的背景之后,我们开始正式进入 RN 的世界。

四 走进 React Native 的世界

React Native 将原生开发的最佳部分与 React 相结合, 致力于成为构建用户界面的顶尖 JavaScript 框架。

React Native 开发和传统的 web 端 React 应用开发类似,并且都是 js 语言,使得 web 开发者上手 RN 开发特别简单。

在 React web 应用中,打包,部署到上线的产物,是一个 html ,css,js 文件的集合体,最后把这些产物放在服务器上就可以了。

但是在 RN 中,最后打包产物是一个 js 文件,叫做 jsbundle ,在 Native 端运行 RN 项目,本质上是远程拉取了 jsbundle ,并通过上述的 js 引擎运行当前 jsbundle,每次运行一个 bundle 就需要外层容器提供一个 js 引擎。

在 React 构建的应用为单页面应用,如果存在多个页面,可以通过路由的方式实现页面的跳转,在 RN 中,也有一些解决方案,通常的手段是一个 jsbundle 对应一个页面,或者是一个 jsbundle 对应多个页面,如下图所示:

图四

如果采用,原生 + RN + H5 等融合的技术方案开发的话,单 jsbundle 对应单页面的方式比较适合,但是如果是 Native 作为外层容器,里面都页面都是 RN 的话,单页面多 bundle 也是一个不错的选择。

基础用法

知道了 RN 的本质之后,我们看一下 RN bundle 的注册,在 RN 中每一个应用都有一个入口文件,RN 中提供了注册根本应用的方法,那就是 AppRegistry,这一点和 React web 应用会有一些区别,web 应用中,主要依赖于 react-dom 中提供的 api ,但是在 RN 项目中,无需再下载 react-dom,取而代之的是 react-native 包。

我们先来试着注册一个 RN 应用:

import {AppRegistry} from 'react-native'
/* 根组件 */
import App from './app' 

AppRegistry.registerComponent('Root', () => <App />)

如上我们注册了 Root ,指向了组件 App。接下来就可以在 App 就可以正常开发了。

在浏览器端,可以用 DOM 标签或者组件,但是在 RN 中是没有 DOM 的,所以如果想要引入原生的视图组件,就必须从 react-native 中引入,下面我们就编写一下 App 组件:

import react from 'react'
import { View, Text } from 'react-native';

function App(){
    return <View>
       <Text> Hello,React Native! </Text>
    </View>
}

除了基础的视图容器组件之外,RN 还提供了一些移动端常用的组件,比如列表组件 ScrollView,SectionList 等。

事件

对于一些用户交互事件,RN 中也提供了对应事件组件载体,比如点击事件用的是 TouchableOpacity。 如下所示

function App(){
    /* 处理点击事件 */
    const handlePress = ()=>{}
    return <TouchableOpacity onPress={handlePress} >
        <Text> click </Text>
    </TouchableOpacity>
}

样式

在 RN 中,是没有 css 样式文件的,RN 中的样式就和 CSS IN JS 类似,都是通过 JS 来完成的,RN 提供了 StyleSheet 可以创建 style 对象,如下所示:

import { StyleSheet, View } from "react-native"

const styles = StyleSheet.create({
    container: {
        flex: 1,
        padding: 24,
        backgroundColor: "#eaeaea"
    }
})

function App(){
    return <View style={styles.container} >
        样式处理
    </View>
}

对于 RN 的基础使用,如果参考官方文档,学习成本不高,上手也很快。

弄明白 RN 的运行环境和基础使用之后,那么都知道 RN 最终的打包产物只是一个 js 文件,那么这个 js 文件是怎么运行到 native 应用中的,又是怎么和 native 应用进行交互的呢?

接下来文章中我们会对 RN 的原理进行探秘,探索一下 RN 内部运转的机制。

另一方面,现在的动态化方案已经不仅仅是 Android 和 iOS 双端,而是 Android ,iOS ,web ,小程序四端,我们通过 RN 进入到跨端动态化方案上来,研究一下以 React 做 dsl 四端动态化方案的现状与未来。

五 总结

本文从跨端发展现状,再到 RN 运转的本质 JS 引擎,再到 RN 的使用,讲述了移动端动态化的一个落地方案。 如果没有用过 RN 开发过 app 应用的同学,可以尝试跑一下基础 demo 。

感兴趣的同学可以关注一下我的新专栏——React跨端领域的新视界

这个新专栏会不断更新以 React 为跨端动态化方案为背景的技术硬文,从跨端的 React Native 到以 React 作为 dsl 的跨端开发工具,我们将以 React 为线索,探索 React 在跨端领域的新世界,向大前端方向进军。