native放在一行 react

2018-04-12 10:41:44 valada 阅读数 3027
  • 组件基础2

    掌握Vue.js基础知识; 掌握Vue.js核心功能; 掌握Vue.js实战技能;

    25641课时 0分钟 0人学习 刘宏强
    免费试看

课程介绍

React Native 充分利用了 Facebook 的现有轮子,是一个很优秀的集成作品,使用 RN 即可做到无需编译就能远程热更新 App,再加上友好的开发方式、快到爆炸的开发效率、RN 已经完爆了其他的 App 开发方式,即使是一个初入前端的开发者也能迅速开发一套 iOS、Android 双平台的 App。

文章中将分三个部分讲解 RN 的开发。通过开发一个比较完整的电商 App 来逐步带领读者走入 React Native 的世界。读者可以从中汲取到完整的项目经验,从菜鸟到精通也只需要学完这个达人课即可。

第一部分主要讲述封装基本组件、简单页面的开发:这个阶段主要解决开发中遇到的兼容问题,在文中会提出解决方案、避免无用的代码。

第二个部分将讲述复杂的业务逻辑:让第一次做开发的读者也能非常快速的适应业务形态,让开发有底气、不再受制于产品的约束。

第三部分将讲述性能的极致优化、热更新、统计等这些 App 必须的东西,让开发的 App 能够真正的比得上原生 App 和混合 App。

作者介绍

郭方超,技术总监、架构师、产品、运营。现担任泽旭商贸技术总监、达令前端架构师职务,有过多年的前端、后端开发经验,擅长 Node.js、.Net、Java 等开发技术。

开发(参与)过的项目如下: 泽旭商贸 PC、移动、微信、App 项目,达令家 App 的开发,心燃灵动前端库、微信端电商框架的开发,来吧旅行前端项目。开源了以下项目:React-Native 的多个组件、React-Native 的监控系统、Node 爬虫框架、模块化前端脚手架、微信小游戏引擎。

课程内容

导读:为什么选择 React Native

在所有的技术选型之前都有一个为什么。

为什么我选择了 React-Native?选择这个技术到底给我带来了什么样的技术福利?如果你正在考虑 React-Native,或者还在研究选择什么样的技术实现自家的 App,不妨看看这个课程,说不定你就有了不一样的感受。

注:React-Native 以下简称 RN。

优点

选择 RN 之前我也看过了其他的几种技术,之前也使用过其他的技术做 App,比如 HBuilder 的框架,非常的方便、非常的好用、什么都不需要管,但是开发完之后发现性能很差,做一个稍微复杂点的项目就非常上火,后来切了原生,但是使用 Java 做开发也有非常多的问题,开发效率很低。

后来我选择了 RN,它使用 JS 更新虚拟 DOM,通过一个桥接器将需要更新的结果通知到 UI 层,让 Native 执行 UI 的改变。简单来说,就是用 JS 做驱动开发一个类原生的 App,所有的渲染都是和原生一样的,一下子就将原生开发和 JS 开发连接起来。通过这么一个模式,将传统的 Java 者 OC 开发转变成了简单易懂的前端 JS 开发,这是移动开发的一大进步,避免了一个 App 多个平台多套代码的尴尬,同时提升了开发效率,将移动开发带入了一个新的层面。

enter image description here

性能相似

很多人说 RN 的性能比不上原生的 App,这个说法是要看具体的场景。

在一般的应用场景下 RN 的表现和原生 App 是没有太大的差别的,一个 App 也不会到处都是复杂的交互效果。一些简单的点缀动画再加上列表图片等才是一个 App 最常见的内容,这种情况下它们之间的表现是一样的。

RN 本身只是使用 JS 处理了 UI 渲染之前的一些逻辑,在最终的渲染上其实使用的还是原生的逻辑,尤其是渲染完成之后更是和原生的没有半点区别。

我们的案例是一个电商项目,主要渲染逻辑是首页的自定义模板、无限加载的列表等。目前最大的性能瓶颈其实是在事件的优化上,优化之后用户已经感知不到和原生的区别了,我们会在后面的部分提到性能优化,将一个粗糙的 App 通过简单的方法提高 10 倍性能,再通过另外一个稍微复杂的方法减低内存占用。

开发效率高

通常情况开发一款 App 需要发布在 Android 和 iOS 两个平台,导致的结果就是一个 App 有两个团队、两套代码,界面几乎一样,为什么不能使用一套代码呢?之前也有大神使用各种手段达成这个目标,但是并不是很理想。

由于使用熟悉的 React 和 JSX 的模式,开发者只需要有前端知识就可以很迅速的上手一个 RN 项目,如果再学一些实战的例子,稍微复杂一些的项目也难不倒各位前端开发者。

Debug 超级方便,一边开发一边看效果再也不是梦。

快速热更新

RN 生成的 JS 文件,只要不涉及原生功能的增减,已经发布的 App 完全不需要重新安装即可完成新版功能的上线,用户只需打开 App 就能体验到最新的 App,省去了下载重装的各种麻烦,把 App 的更新做到了和网页更新一样的方便快捷。

使用 RN 就能达到既有原生的所有能力,又有类似浏览器上的快速更新能力,同时还可以接入各种定制好的网页,将 App 的自由度提高到一个非常高的地步。

大公司背书

RN 的开发者是 Facebook,背靠大树好乘凉,社区更疯狂,Facebook 本身也在尝试使用 RN 技术开发自己的 App,RN 一定会越来越完善,截止写这篇文章的时候 RN 已经更新到了 0.53.0。

RN 本身也是开源的,所有的源代码都是可以看到的,社区的讨论也是比较热烈的,现在可能中文文档还比较少,未来随着开发者的努力,这些坑都会填起来的。

其实最开始的时候也没有想很多,仅仅是冲着 RN 可以快速开发,上线快体验好,经过了这么长时间的开发,我更加喜欢 RN 的这种开发方式,项目中也填了各种各样的坑,后面就用一个实际的例子来展示 RN 是怎样开发的吧。

RN 的缺点

升级快

RN 本身其实还处于测试版,开发组经常会升级 RN,解决一些遗留或者隐藏的 bug,在这个过程中就导致了 RN 本身升级非常快,开发者在使用 RN 开发 App 的过程中应该尽量提高自己的版本,不需要一直是最新的,只要能够跟的上 Facebook 的节奏即可。

自己搞定的问题也是可以合并入 RN 的源代码里的,不要一味的等 RN 更新,有些问题自己解决更方便,建议会 Android 或者 iOS 同学自己动手。

动画难

这里的难指的是复杂的动画在开发中很难去优化,尤其是开发者懂前端但是不懂原生的情况下,好在常见的 App 也不需要多么复杂的动画,一般使用位移变换就足够了,太复杂的动画建议使用 RN 的 SVG 组件来做。

WebView 难用

RN 自带的 WebView 跟浏览器有一定的差别,App 经常要打开一些网页,可能在开发的时候一切正常,但是到了 RN 里面就会有一些奇怪的问题,主要还是受到系统浏览器的影响,会有一些兼容方面的问题,这种情况下不如微信使用自己研发的浏览器,可以畅快的使用 ES 6 之类的新技术。

需要原生支持

简单的东西和界面的展现已经完全放手给了开发者,但是还是有一些功能只能原生去实现,如果原生部分的开发者对 RN 不太了解可能会给 App 带来不可预知的 bug,好在大多数开源看只需要执行 Link 命令就可以把原生部分也安装好。

技术都会有优点和缺点,选择合适的技术才能给项目带来长久的生命力。

其他技术

Weex

核心思想上,这两家其实并没有什么区别,Weex 也可以算是站在 RN 的肩膀上起步的,目前活跃度不高,大多数是在观望中。

开发框架

Weex 使用 Vue,熟悉 Vue 的开发者可能会更熟悉。

RN 使用 React,都是 Facebook 出品,框架融合上会更方便一些,它们都是组件化开发,都属数据绑定,都有虚拟 DOM,社区同样活跃,使用人数也都非常多。

学习成本

React 的 JSX 初期会比较难上手,CSS 的写法也跟前端的样式写法不一样,Weex 使用模板的形式,直接 html+css 开发,上手会稍微简单一些。

异步

Weex 只支持 callback 的形式,RN 支持 promise 的形式,这些都是可以解决的,不是什么问题。

社区

RN 开源早,有 Facebook 支持,社区的组件库已经比较丰富,社区活跃度比较高。Weex 开源晚,社区活跃不高,以阿里系比较多。

Flutter

Flutter 是 Google 推出的一个跨平台(Android 和 iOS)移动开发框架,使用的是 Dart 语言。

Flutter 的目标是用来创建高性能、高稳定性、高帧率、低延迟的 Android 和 iOS 应用,并且开发出来的应用在不同的平台用起来跟原生应用具有一样的体验。不同的平台的原生体验应该得到保留,让该应用看起来同整个系统更加协调;不同平台的滚动操作、字体、图标 等特殊的特性应该和该平台上的其他应用保持一致,让用户感觉就像操作原生应用一样。比如,返回图标 Android 和 iOS 是不一样的;滚动内容滚动到底的反馈也是不一样的。

兼容

Flutter 不使用系统提供的组件,自己实现了一套渲染机制,所以在性能优化、跨平台方面表现优秀。实际体验上,性能比 RN 要高不少。

enter image description here

RN 最终调用的还是系统的组件,虽然 Facebook 已经很努力了,但是在某些时候还是会有兼容性需要处理。

组件

Flutter 内置了对 Material Design 的支持,给开发者提供了丰富的 UI 控件库选择,同时所有的组件都有扩展,保持了很高的灵活性。

RN 通过 React 也做到了组件式开发,跟 Flutter 相比,多了一个桥接器的转换,性能上肯定不如 Flutter。

开发语言

Flutter 使用 Dart 实现,Dart 号称要完全取代 JS,不过目前离这个目标还非常远,初期上手还是有一些难度的。

RN 使用 JS 开发,做过前端的都非常熟悉,上手很容易。

enter image description here

Flutter 现在还在实验阶段,不排除 Google 使用别的框架替换它的可能性,Dart 语言也处于成长阶段,只有 Google 的浏览器在支持,或许在 Flutter 持续发展到一个阶段之后,才会有很多支持者。

在写文章的时候 Google 放出了第一个测试版,感兴趣的可以下载下来玩玩儿。

相比于其他几种技术,RN 是目前社区最活跃,开发效率最高的一种选择,选择 RN 也是需要在一个比较短的时间内能够完成 App 的开发。尤其现在前端开发者可以非常容易的从网页开发转到 App 开发上。对于我们包含 App、微信、小程序这样的三个平台更是需要 RN 这样的技术,一个团队就可以维护项目的持续增长。

如果需要 RN 来开发自己的项目,那就继续往下看吧,我们将从简单的界面开发、数据更新等开始逐步深入,后面涉及到性能优化、自定义原生部分等。

第01课:项目初始化

知识点:

  • 快速创建 RN 项目
  • 添加使用路由功能
  • 不要在 RN 中使用的功能

从这里开始,我们将一步一步的创建一个可以真正使用的 App:第一部分讲述开发一个 App 的大致过程,第二部分将开始优化性能、开发效率等,第三部分介绍添加热更新、支付、分享这些功能。大多数第三方组件可以很方便的 link 到项目里,部分需要手动导入甚至主动开发一些东西,这里也会在用到的时候讲出来。

创建项目的前提条件

使用 React-Native 创建一个新的项目请确保电脑上已经满足下面的这些条件。

  • nodejs:RN 的所有库都是从 NPM 上安装的,请确保电脑上已经安装了 NPM,可以使用npm -v来查看当前是否已安装,当前项目使用的 Node 版本是 8.9.3,NPM 的版本是 5.5.3。
  • react-native-cli:这个通常用作 RN 的初始化和启动模拟器等,使用 NPM 可以安装到电脑上。
  • Python:RN 里面有些脚本是使用 Python 写的,请确保电脑上已经安装了 Python 2.7 以上的版本。
  • JDK 1.8:安卓项目需要使用 JDK 1.8,请在电脑上安装好,安装的教程可以搜索其他资料。
  • Android Studio:调试以及编译安卓代码需要使用到,请在安装 Android Studio 之后安装好 Android SDK 以及模拟器,模拟器可以使用市面上的安卓模拟器,它们普遍比自带的模拟器要快,闲麻烦的读者可以直接连接手机调试。在安装好 IED 之后请下载好需要使用到的 Android SDK,下载 SDK 需要科学上网或者把下载地址替换成国内的几个下载源。
  • Git:后面的项目会加入到 Git 中,使用 Git 做版本管理的好处不言而喻。
  • Xcode(仅 iOS 项目中):安装了 Xcode 才能使用 iOS 模拟器,iOS 模拟器下的开发速度要明显优于安卓下,后面的开发过程大多数也是在 iOS 模拟器中进行的。在模拟器中的表现都差不多,有些不一样的效果可以在看到的时候单独调试。
  • Watchman(仅 Mac 系统用到):Watchman 用来监听文件变动等,Mac 下必须安装 Watchman。
  • VSCode:文中演示项目使用的编辑器是 VSCode,这里也推荐使用 VSCode 开发前端项目。

创建项目

我们先在本地创建一个可运行的项目,同时这个项目会加入到 Git 的版本管理中。

(1)执行react-native init anxintao --version 0.53.0

enter image description here

  • 图中1代表之前安装的 react-native-cli 的命令。
  • 图中2代表初始化命令。
  • 图中3代表项目的名称,这里是anxintao
  • 图中4代表指定 RN 的版本号,这个参数不传默认使用最新版。
  • 图中5代表 RN 具体使用的版本号。

(2)使用 VSCode 打开项目,在项目根目录下执行命令启动初始化之后的项目,Mac 下推荐react-native run-ios,Window 下推荐react-native run-android启动默认的项目。如果能启动说明项目初始化完成,否则说明项目的某些东西没有安装好。

enter image description here

这里推荐把启动的命令写入到 package.json 文件中,比如,输入npm run ios即可代替原来的react-native run-ios,输入命令的速度快了不少,也可以给 VSCode 安装一个启动 RN 的插件,不过效果跟输命令差不多,具体要看个人习惯了。

(3)到这里就说明项目创建成功了,这个项目现在还很简单,它的原生部分只有一个简单的空壳,这个空壳仅仅是初始化了一个 RN 的 Activity,所有的 JS 都是运行在根视图上的。

(4)这里注意一下,新建的项目提示了按键可以刷新页面或者调出菜单,这显示的是 iOS 模拟器,按键为command R刷新和command D调出菜单。

添加路由

一个完整的项目不能没有路由,这里使用 React Navigation

在写文章的时候已经有一个路由组件react-native-navigation热度超过了 react-navigation,它更多的使用的是原生的路由切换,效果更好,想用的读者可以去尝试一下。

  • 安装路由,在根目录下执行npm install --save react-navigation

  • 在根目录下新建src目录,所有页面放入这个文件夹下。

  • 新建一个首页,给后面的路由调用,页面路径为根目录/src/home/index.js

    'use strict';import React from 'react';import {    StyleSheet,    View,    Text} from 'react-native';export default class extends React.Component {    render() {        return <View style={{ marginTop: 200 }}>            <Text>这是首页</Text>        </View>    }}

修改根目录下的index.js,添加整个项目的路由。

import { AppRegistry } from 'react-native';import Pages from './src';//启动AppRegistry.registerComponent('anxintao', () => Pages);

在 src 目录下新建index.js文件,在这个文件里添加路由,这里从简单的一个页面开始。

'use strict';import React from 'react';import {    StyleSheet} from 'react-native';//添加路由组件import Navigation from 'react-navigation';//添加展示用的首页import Home from './home/index'//创建路由const Pages = Navigation.StackNavigator({    'Home': {        screen: Home    }}, {    //这里做了一个页面跳转的动画    transitionConfig: () => ({        screenInterpolator: sceneProps => {            const { layout, position, scene } = sceneProps;            const { index } = scene;            //设置页面跳转的动画            const translateX = position.interpolate({                inputRange: [index - 1, index, index + 1],                outputRange: [layout.initWidth, 0, 0]            });            const opacity = position.interpolate({                inputRange: [index - 1, index - 0.99, index, index + 0.99, index + 1],                outputRange: [0, 1, 1, 0.3, 0]            });            return { opacity, transform: [{ translateX }] };        }    }),    navigationOptions: {        header: null    }});//创建一个自己的容器,方便以后对路由做一些处理export default class extends React.Component{    constructor(props) {        super(props);    }    render() {        return <Pages onNavigationStateChange={this.listenChange.bind(this)}></Pages>;    }    //监听路由的跳转    listenChange(state1, state2, action) {    }}

添加完成之后删除掉初始化项目之后的 App.js,这个时候在模拟器中使用快捷键command+R即可刷新刷新页面。

479e89e0-1876-11e8-8087-9b6b3b447adf

至此就完成了简单的路由设置,之后只需要添加页面并在路由中注册即可使用。

路由升级版

简单的路由并不能起到很好的作用,我们还是创建一个更实用的路由吧,比如带 3 个 tab 切换的首页,这也是大多数 App 使用套路。

添加 4 个 tab 切换页,我们假定未来需要 4 个切换页,分别是首页、分类页、购物车、个人中心,在 home 下分别创建他们。

084841b0-187a-11e8-8087-9b6b3b447adf

修改路由所在的 index 文件,引入下面要用到的几个组件和页面。

添加新加入的页面:

import React from 'react';import {    StyleSheet,    Image} from 'react-native';//添加路由组件import Navigation from 'react-navigation';//添加展示用的首页import Home from './home/index'import Products from './home/products'import Shop_Cart from './home/shop_cart'import My from './home/my'

创建底部的样式:

//创建tab页的顶部样式const styles = StyleSheet.create({    tab: {        height: 40,        backgroundColor: '#fbfafc',        borderTopColor: '#efefef'    },    tabIcon: {        width: 20,        height: 20    },    tabLabel: {        marginBottom: 4    }})

创建一个 tab 路由,为了简单这里只展示 2 个页面的,具体的代码可以去 Git 仓库查看。

//创建首页的tab页const Tabs = Navigation.TabNavigator({    'Home': {        screen: Home,        navigationOptions: ({ navigation, screenProps }) => {            return {                tabBarLabel: '首页',                tabBarIcon: (opt) => {                    if (opt.focused) return <Image source={{ uri: require('./images/tab-home-active') }} style={styles.tabIcon}></Image>;                    return <Image source={{ uri: require('./images/tab-home') }} style={styles.tabIcon}></Image>;                }            }        }    },    'Products': {        screen: Products,        navigationOptions: ({ navigation, screenProps }) => {            return {                tabBarLabel: '产品分类',                tabBarIcon: (opt) => {                    if (opt.focused) return <Image source={{ uri: require('./images/tab-products-active') }} style={styles.tabIcon}></Image>;                    return <Image source={{ uri: require('./images/tab-products') }} style={styles.tabIcon}></Image>;                }            }        }    },}, {    //设置tab使用的组件    tabBarComponent: Navigation.TabBarBottom,    //点击哪个才加载哪个tab里的页面    lazy: true,    //设置tab放在界面的底部    tabBarPosition: 'bottom',    //设置tab里面的样式    tabBarOptions: {        style: styles.tab,        labelStyle: styles.tabLabel,        activeTintColor: '#d0648f'    }});

替换 Pages 里的第一个页面为刚才创建的 tab 路由。由于默认加载第一个,所以需要将第一个设置成 tab 页。

'Tabs': {        screen: Tabs }

现在再刷新模拟器,就会发现底部的 tab 切换已经好了,点击可以切换不同的页面。

da200e80-187e-11e8-8087-9b6b3b447adf

这里将图片转化成 base 64 的方式再引入到图片组件中,好处是打包之后会变成一个整体,坏处是打包之后的 bundle 文件会变大,做增量更新也比较麻烦。

不推荐使用的东西

  • 投影:安卓不支持投影,在开发的时候如果没有必要就使用别的方式代替吧,比如使用图片代替投影。
  • 边框色:在长列表中尽量不要使用边框色,在某些安卓手机下会闪退。
  • 使用了圆角的情况再使用背景色:iOS 手机会出现边框颜色异常或者异常色块,去掉背景即可。
  • 过于深层次的结构。
  • 过于频繁的刷新 state。
第02课:封装自己的库

知识点:

  • 封装一个自己的日志,代替原生的 console
  • 封装一个自己的请求库,代替原生的 fetch
  • 自适应方法,兼容各种手机屏幕
  • 封装一个本地存储
  • 集成 Toast 弹出提示
  • 让 iOS 支持 HTTP 地址
  • 安卓需要证书才能编译

在正式开始之前,我们先封装几个要用到的库。

自定义本地日志

自定义日志的一个好处就是省的每次都要手动注释 console,而且还可以同时将日志存在本地,或者发到日志服务器,一个方法一举多得了。

在 src 目录下新建一个 utils 文件夹,封装一个日志输出类,开发模式下使用console.log命令,正式情况下记录在变量中,方便在手机上查看日志。

新建一个 log.js 文件,路径为根目录/src/utils/log.js

80223590-1886-11e8-8087-9b6b3b447adf

新建一个数组变量logs,用来临时存放日志信息。

98604390-1886-11e8-8087-9b6b3b447adf

将日志分成信息、警告和错误 3 种,分别给出 3 个可调用的方法,同时给第一个参数加一个好看的颜色。

cc840ad0-1886-11e8-8087-9b6b3b447adf

在 index 中引入日志组件,写几个方法来看看调用的结果。

enter image description here

这里稍微定义一下日志的要求,参数 0,字符串;参数 1,对象;参数 2,字符串。

enter image description here

自定义请求

RN 默认提供了 fetch 方法去请求远程数据,我们再封装一次,这个方法将会针对现有的项目做封装,在使用请求的时候能够更适合、更方便。这里使用 header 保存了一些临时的变量,算是一个小小的全局缓存吧。

创建 request.js 文件,目录:根目录/src/utils/request.js

80223590-1886-11e8-8087-9b6b3b447adf

将请求 header 里的信息单独出来,每次请求都需要带上这个共享 header 数据。

29975590-1889-11e8-8087-9b6b3b447adf

创建一个 Request 类,并将这个类对外公开,这里将请求初始化一次,以后用到别的请求的时候也可以单独实例化一次。

/** * 请求库 */class Request {}export default new Request();

每次请求都将 header 中的内容带入请求中,单独检测 httpcode 和后端返回的 code 值,这里可以直接做权限检测,在需要的时候跳转到登录页。

/** * 请求库 */class Request {    /**    * 检测返回状态码    * @param {*} status     * @param {*} res     */    async _checkStatus(status, res, url) {        if (status !== 200) {            logWarm('请求失败参数', await res.text(), url, headers);            throw new Error('网络连接失败,请检查网络');        }    }    /**    * 检查后端返回的状态码    * @param {*} status     */    _checkAppStatus(json, url) {        if (json.status != 0) {            logWarm('返回状态报错', json, url);            throw new Error(`${json.errorMsg}`);        }    }    /**     * 内部实现网络请求     * @param {*} url      * @param {*} options      */    async _request(url, options, type) {        url = url.indexOf('http') == 0 ? url : url.indexOf('/api') == 0 ? domain + url : baseUrl + url;        let res = await fetch(url, options);        this._checkStatus(res.status, res, url)        if (type === 'json') return await this._jsonFactory(res, url, options)        return await this._jsonFactory(res, url, options)    }    /**         * 处理json数据         * @param {*} res          * @param {*} url          */    async _jsonFactory(res, url, options) {        let json;        let txt = '';        try {            txt = await res.text();        } catch (e) {            log('未拿到返回字符串', { url: url, txt: txt });            throw new Error('数据格式错误');        }        try {            json = JSON.parse(txt);        } catch (e) {            logErr('返回数据格式错误', { url: url, txt: txt });            throw new Error('数据格式错误');        }        this._checkAppStatus(json, url)        log("请求返回", json, url, options);        return json.data;    }    /**     * get请求     * @param {*} url      */    async get(url, data) {        if (data) data = urlEncoded(data);        if (url.indexOf('?') < 0 && data) url += '?' + data;        return this._request(url, {            method: 'GET',            headers: headers,            timeout: 10000        }, 'json')    }    /**     * post请求     * @param {*} url      * @param {*} data      */    async post(url, data) {        return this._request(url, {            method: 'POST',            headers: Object.assign(headers, { 'Content-Type': 'application/x-www-form-urlencoded' }),            timeout: 10000,            body: urlEncoded(data)        }, 'json')    }}

调用一次远程端口并查看日志输出,这里调用的也是案例中要使用到的获取 banner 的接口,该接口不需要用户权限,后面还会遇到需要用户权限的接口。

enter image description here

自适应方法

这里推荐一种自适应的方法,同时也是前端在开发移动端页面的时候常用的方法,将手机屏幕宽度默认为 750 像素,然后将所有的宽高按照这个比例去缩放,这要求设计出的设计稿宽度也是 750 像素。

在 utils 下新建一个px.js文件,按照出入的大小根据当前屏幕的宽度获取到缩放的比例并返回结果。

ab16ed00-188a-11e8-8087-9b6b3b447adf

在首页引入 px 方法,查看使用 px 之后的效果。

d4992580-188a-11e8-8087-9b6b3b447adf

d97950c0-188a-11e8-8087-9b6b3b447adf

可以看到使用 px 将 500 像素缩放之后的效果和最开始设置的纯数字 200 效果是一致的,这里使用的是 iOS 模拟器,真实的屏幕宽是 375,按照 750 宽去算的话会把传入的参数统统除以 2。

封装本地存储

RN 提供的 AsyncStorage 可以根据 key 存储相应的字符串,我们这里改进一下,让它可以存储所有类型的字段,利用的是将传入的参数改造成对象,然后使用 JSON 的方法将对象转化成一个可以存储的字符串。

在 utils 下新建一个Storage.js

3ddb9950-188c-11e8-8087-9b6b3b447adf

将传入的对象转化为字符串并存入 AsyncStorage。

'use strict';import { AsyncStorage } from 'react-native';/** * 获取存储的数据 * @param {*} key  */exports.getItem = async (key) => {    let item = await AsyncStorage.getItem(key);    if (!item) {        return null;    }    return JSON.parse(item).v || null;}/** * 存入数据 * @param {*} key  * @param {*} value  */exports.setItem = (key, value) => AsyncStorage.setItem(key, JSON.stringify({    v: value}));/** * 删除已经存在的数据 * @param {*} key  */exports.removeItem = (key) => AsyncStorage.removeItem(key);/** * 清除所有 */exports.clear = () => AsyncStorage.clear();/** * 获取所有的key */exports.getAllKeys = () => AsyncStorage.getAllKeys();

在首页使用 setItem 存入数据,然后第二次进入页面再使用 getItem 获取数据。

e9e272a0-188c-11e8-8087-9b6b3b447adf

这里用到了 componentDidMount 这个方法,该方法是在组件生命周期中的初始化完成之后执行的。

enter image description here

添加弹出 Toast

之前公司使用的是自己开发的提示方法,该方法需要改变原生代码,非常的不方便,这里推荐使用第三方的开源组件react-native-root-toast,只需要安装一下就好了。

执行命令,安装 Toast:npm i --save react-native-root-toast

在 utils 目录下新建 toast.js 文件,添加 Toast 的默认方法并填入默认参数,这里设置显示时间为 1000 毫秒,背景颜色是一个半透明的黑色,其目的也是为了方便调用,如果需要多种效果的就定义多个即可。

enter image description here

在首页引入 Toast 并看看实际的效果。

41c80400-18b3-11e8-8087-9b6b3b447adf

让 iOS 支持 Http 协议

苹果之前推荐使用 Https 协议,现在默认是不支持 Http 的,如果需要支持 Http 则要单独设置,案例中的项目也用到了 Http,所以需要修改 info.plist 文件,让 iOS 可以访问 Http 的地址。

使用 Xcode 选择打开其他项目。

0da257a0-1919-11e8-8087-9b6b3b447adf

打开项目下的 iOS 文件夹,选择项目文件并打开。

4d6ce8a0-1919-11e8-8087-9b6b3b447adf

选择 info.plist 文件,在右边选择第一行并点击 + 号添加一项。

enter image description here

选择 App Transport Security Setting 这一项,会弹出提示,单击“确定”按钮即可自动刷新。

01e614a0-191a-11e8-8087-9b6b3b447adf

在上面添加的新配置中添加一个新的配置 Allow Arbitrary Loads,同时设置为 YES。

enter image description here

改完配置还需要编译一次,单击左上角的三角形或者菜单中 product 下的 build 选项。

enter image description here

这里我使用的是 Xcode 修改配置文件,如果你发现配置文件没有变化,也可以自己改 info.plist 文件的内容。

编译安卓客户端

使用 Android Studio 打开根项目下的 Android 目录,打开 build.gradle 文件,这个就是项目的 Gradle 配置文件,通常使用这个文件对整个项目进行描述。

3d6d4f10-204b-11e8-94c8-43a1e6ab60e0

经过一定时间的等待,IDE 就会初始化整个项目,如果有一些需要下载的文件也会在这个时间通知下载。

单击菜单build/gennerate signed apk,这个就是编译一个可以安装在安卓手机上的安装包,也可以通过单击make project来看看项目是否可以编译通过。

enter image description here

单击 Next 按钮,IDE 会提示需要选择一个证书,这里可以选择一个已有的并输入密码,也可以通过单击create new来创建一个,后面一直单击 Next 按钮就可以了,IDE 会在生成 APK 之后弹出通知。

021b3160-204c-11e8-8063-17489f4fcf26

选择创建一个新的证书,根据提示填入相应的内容,之后单击 OK 按钮即可生成,记得选择 remember password,下次直接填入密码。

8d33ddb0-204c-11e8-a1dd-77d9e3f43337

经过一整机器躁动之后,IDE 弹出编译结果,点击蓝色字可以快速打开 APK 的地址。

enter image description here

将 APK 文件发送到手机上安装即可,一个自己开发的 App 就安装好了。

14e47fd0-204d-11e8-94c8-43a1e6ab60e0

这里要注意,在打包之前要把 RN 打包生成的 bundle 文件放入 Android 文件下的 assets 目录中,否则 Android 会因为找不到启动文件而报错。

第03课:首页模板和商品列表
第04课:组件和商品详情
第05课:使用 WebView 和运营页通讯
第06课:个人中心设置及 Debug 日志
第07课:添加自定义组件
第08课:注册登录
第09课:全局状态管理购物车
第10课:实现购买(1)
第11课:实现购买(2)
第12课:订单管理
第13课:添加优化动画
第14课:优化 App,提升10倍效率
第15课:使用 ESlint 规范化代码
第16课:使用 TypeScript 编写代码
第17课:集成极光
第18课:热更新
第19课:总结

阅读全文: http://gitbook.cn/gitchat/column/5aa8a68b0bb9e857450e2308

2017-01-17 22:25:44 xiangzhihong8 阅读数 8879
  • 组件基础2

    掌握Vue.js基础知识; 掌握Vue.js核心功能; 掌握Vue.js实战技能;

    25641课时 0分钟 0人学习 刘宏强
    免费试看

前言

之前我们讲了很多react-native的基础控件,为了方便大家的理解,我们来对react-native的布局做一个总结,观看本节知识,你将看到。

  • 宽度单位和像素密度
  • flex的布局
  • 图片布局
  • 绝对定位和相对定位
  • padding和margin的区别和应用场合
  • 文本元素

宽度单位和像素密度

我们知道在Android中是用设备像素来作为单位的(后面又出现了百分比这么 一个概念),ios中后面也有了Auto Layout和1倍图,二倍图等概念(xib+storyboard)。然而react的宽度不支持百分比,那么React怎么提供尺寸的呢?PixelRatio,PixelRatio及像素密度,可以看看官方的介绍。

 var image = getImage({
   width: 200 * PixelRatio.get(),
   height: 100 * PixelRatio.get()
 });
 <Image source={image} style={{width: 200, height: 100}} />

flex的布局

我们知道一个div如果不设置宽度,默认的会占用100%的宽度, 为了验证100%这个问题, 做三个实验:

  1. 根节点上方一个View, 不设置宽度
  2. 固定宽度的元素上设置一个View, 不设置宽度
  3. flex的元素上放一个View宽度, 不设置宽度
 <Text style={[styles.text, styles.header]}>
      根节点上放一个元素,不设置宽度
  </Text>        

  <View style={{height: 20, backgroundColor: '#333333'}} />

  <Text style={[styles.text, styles.header]}>
      固定宽度的元素上放一个View,不设置宽度
  </Text> 

  <View style={{width: 100}}>
    <View style={{height: 20, backgroundColor: '#333333'}} />
  </View>

  <Text style={[styles.text, styles.header]}>
      flex的元素上放一个View,不设置宽度
  </Text> 

  <View style={{flexDirection: 'row'}}>
    <View style={{flex: 1}}>
      <View style={{height: 20, backgroundColor: '#333333'}} />
    </View>
    <View style={{flex: 1}}/>
  </View>

来看一下运行的结果:
这里写图片描述

水平垂直居中

css 里边经常会将一个文本或者图片水平垂直居中,如果使用过css 的flexbox当然知道使用alignItems 和 justifyContent ,那如果用React Native如何实现呢?

<Text style={[styles.text, styles.header]}>
        水平居中
    </Text>

    <View style={{height: 100, backgroundColor: '#333333', alignItems: 'center'}}>
      <View style={{backgroundColor: '#fefefe', width: 30, height: 30, borderRadius: 15}}/>
    </View>

     <Text style={[styles.text, styles.header]}>
        垂直居中
    </Text>
    <View style={{height: 100, backgroundColor: '#333333', justifyContent: 'center'}}>
      <View style={{backgroundColor: '#fefefe', width: 30, height: 30, borderRadius: 15}}/>
    </View>

    <Text style={[styles.text, styles.header]}>
        水平垂直居中
    </Text>
    <View style={{height: 100, backgroundColor: '#333333', alignItems: 'center', justifyContent: 'center'}}>
      <View style={{backgroundColor: '#fefefe', width: 30, height: 30, borderRadius: 15}}/>
    </View>

这里写图片描述

网格布局

通常页面不是很复杂的时候,我们可以使用flex布局等分做到网格,复杂的那么就要用ListView实现,或者第三方控件。

等分的网格

<View style={styles.flexContainer}>
      <View style={styles.cell}>
        <Text style={styles.welcome}>
          cell1
        </Text>
      </View>
      <View style={styles.cell}>
        <Text style={styles.welcome}>
          cell2
        </Text>
      </View>
      <View style={styles.cell}>
        <Text style={styles.welcome}>
          cell3
        </Text>
      </View>
    </View>

    styles = {
        flexContainer: {
            // 容器需要添加direction才能变成让子元素flex
            flexDirection: 'row'
        },
        cell: {
            flex: 1,
            height: 50,
            backgroundColor: '#aaaaaa'
        },
        welcome: {
            fontSize: 20,
            textAlign: 'center',
            margin: 10
        },
    }

这里写图片描述
另一种方式可以参照我之前的实现: React Native实现九宫格效果

图片布局

<Text style={styles.welcome}> 100px height </Text>
  <Image style={{height: 100}} source={{uri: 'http://gtms03.alicdn.com/tps/i3/TB1Kcs5GXXXXXbMXVXXutsrNFXX-608-370.png'}} />

100px 高度, 可以看到图片适应100高度和全屏宽度,背景居中适应未拉伸但是被截断也就是cover。

  <Text style={styles.welcome}> 100px height with resizeMode contain </Text>
  <View style={[{flex: 1, backgroundColor: '#fe0000'}]}>
      <Image style={{flex: 1, height: 100, resizeMode: Image.resizeMode.contain}} source={{uri: 'http://gtms03.alicdn.com/tps/i3/TB1Kcs5GXXXXXbMXVXXutsrNFXX-608-370.png'}} />
  </View>

contain 模式容器完全容纳图片,图片自适应宽高。

  <Text style={styles.welcome}> 100px height with resizeMode cover </Text>
  <View style={[{flex: 1, backgroundColor: '#fe0000'}]}>
      <Image style={{flex: 1, height: 100, resizeMode: Image.resizeMode.cover}} source={{uri: 'http://gtms03.alicdn.com/tps/i3/TB1Kcs5GXXXXXbMXVXXutsrNFXX-608-370.png'}} />
  </View>

stretch模式图片被拉伸适应屏幕

 <Text style={styles.welcome}> set height to image container </Text>
  <View style={[{flex: 1, backgroundColor: '#fe0000', height: 100}]}>
      <Image style={{flex: 1}} source={{uri: 'http://gtms03.alicdn.com/tps/i3/TB1Kcs5GXXXXXbMXVXXutsrNFXX-608-370.png'}} />
  </View>

绝对定位和相对定位

<View style={{flex: 1, height: 100, backgroundColor: '#333333'}}>
    <View style={[styles.circle, {position: 'absolute', top: 50, left: 180}]}>
    </View>
  </View>
  styles = {
    circle: {
    backgroundColor: '#fe0000',
    borderRadius: 10,
    width: 20,
    height: 20
    }
  }

这里写图片描述
和css的标准不同的是, 元素容器不用设置position:’absolute|relative’ 。

<View style={{flex: 1, height: 100, backgroundColor: '#333333'}}>
    <View style={[styles.circle, {position: 'relative', top: 50, left: 50, marginLeft: 50}]}>
    </View>
  </View>

padding和margin

我们知道在css中区分inline元素和block元素,既然react-native实现了一个超级小的css subset。那我们就来实验一下padding和margin在inline和非inline元素上的padding和margin的使用情况。
padding

 <Text style={[styles.text, styles.header]}>
    在正常的View上设置padding 
  </Text>

  <View style={{padding: 30, backgroundColor: '#333333'}}>
    <Text style={[styles.text, {color: '#fefefe'}]}> Text Element</Text>
  </View>

  <Text style={[styles.text, styles.header]}>
    在文本元素上设置padding
  </Text>
  <View style={{padding: 0, backgroundColor: '#333333'}}>
    <Text style={[styles.text, {backgroundColor: '#fe0000', padding: 30}]}>
      text 元素上设置paddinga
    </Text>
  </View>

这里写图片描述

margin

 <Text style={[styles.text, styles.header]}>
    在正常的View上设置margin 
  </Text>

  <View style={{backgroundColor: '#333333'}}>
    <View style={{backgroundColor: '#fefefe', width: 30, height: 30, margin: 30}}/>
  </View>

  <Text style={[styles.text, styles.header]}>
    在文本元素上设置margin
  </Text>
  <View style={{backgroundColor: '#333333'}}>
    <Text style={[styles.text, {backgroundColor: '#fe0000', margin: 30}]}>
      text 元素上设置margin
    </Text>
    <Text style={[styles.text, {backgroundColor: '#fe0000', margin: 30}]}>
      text 元素上设置margin
    </Text>
  </View>

这里写图片描述

文本元素

先看看文字有哪些支持的style属性:

 Attributes.style = {
    color string
    containerBackgroundColor string
    fontFamily string
    fontSize number
    fontStyle enum('normal', 'italic')
    fontWeight enum("normal", 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900')
    lineHeight number
    textAlign enum("auto", 'left', 'right', 'center')
    writingDirection enum("auto", 'ltr', 'rtl')
  }

Text的样式继承

实际上React-native里边是没有样式继承这种说法的, 但是对于Text元素里边的Text元素是可以继承的。到底是继承的最外层的Text的值呢,还是继承父亲Text的值呢?肯定是继承父亲Text的值。

 <Text style={[styles.text, styles.header]}>
      文本样式继承
  </Text>

  <View style={{backgroundColor: '#333333', padding: 10}}>
    <Text style={{color: 'white'}}>
      <Text style={{color: 'red'}} onPress={this.onPressTitle}>
         文本元素{'\n'}
        <Text>我是white还是red呢?{'\n'} </Text>
      </Text>
      <Text>我应该是white的</Text>
    </Text>
  </View>

这里写图片描述

总结

针对上面的实例,我们做一个总结。

  • react 宽度基于pt为单位, 可以通过Dimensions 来获取宽高,PixelRatio 获取密度。
  • 基于flex的布局:
    view默认宽度为100%
    水平居中用alignItems, 垂直居中用justifyContent
    基于flex能够实现现有的网格系统需求,且网格能够各种嵌套无bug
  • 图片布局
    通过Image.resizeMode来适配图片布局,包括contain, cover, stretch 三种模式
    默认不设置模式等于cover模式
    contain模式自适应宽高,给出高度值即可
    cover铺满容器,但是会做截取
    stretch铺满容器,拉伸
  • 绝对定位和相对定位
    定位相对于父元素,父元素不用设置position也行
    padding 设置在Text元素上的时候会存在bug。所有padding变成了marginBottom

  • 文本元素
    文字必须放在Text元素里边
    Text元素可以相互嵌套,且存在样式继承关系
    numberOfLines 需要放在最外层的Text元素上,且虽然截取了文字但是还是会占用空间

2019-02-11 11:24:56 denghuizi 阅读数 1575
  • 组件基础2

    掌握Vue.js基础知识; 掌握Vue.js核心功能; 掌握Vue.js实战技能;

    25641课时 0分钟 0人学习 刘宏强
    免费试看

创建一个React Native 项目

在做混合开发之前我们首先需要创建一个没有Android和iOS模块的React Native项目。我们可以通过两种方式来创建一个这样的React Native项目:

  1. 通过npm安装react-native的方式添加一个React Native项目;
  2. 通过react-native init来初始化一个React Native项目;

1. 通过npm安装react-native的方式添加一个React Native项目

第一步:创建一个名为RNHybridApp的目录,然后在该目录下添加一个包含如下信息的package.json:

{
  "name": "RNHybrid",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

第二步:在为package.json添加react-native
在该目录下执行:

npm install --save react-native

执行完上述命令之后,你会看到如下警告:
在这里插入图片描述
其中,有一条警告npm WARN react-native@0.55.4 requires a peer of react@16.3.1 but none is installed告诉我们需要安装react@16.3.1:

npm install --save react@16.3.1

至此,一个不含Android和iOS模块的React Native项目便创建好了。

2. 通过react-native init来初始化一个React Native项目

除了上述方式之外,我们也可以通过react-native init命令来初始化一个React Native项目。

react-native init RNHybrid

上述命令会初始化一个完成的名为RNHybrid的React Native项目,然后我们将里面的android和ios目录删除,替换成已存在Android和iOS项目。

二、添加React Native所需要的依赖

在上文中我们已经创建了个一个React Native项目,接下来我们来看一下如何将这个React Native项目和我们已经存在的Native项目进行融合。

在进行融合之前我们需要将已经存在的Native项目放到我们创建的RNHybrid下,比如:我有一个名为RNHybridAndroid的Android项目,将其放到RNHybrid目录下:

RNHybrid
├── RNHybridAndroid
├── package.json
├── node_modules
└── .gitignore

第一步:配置maven

接下来我们需要为已经存在的RNHybridAndroid项目添加 React Native依赖,在RNHybrid/RNHybridAndroid/app/build.gradle文件中添加如下代码:

dependencies {
    compile 'com.android.support:appcompat-v7:23.0.1'
    ...
    compile "com.facebook.react:react-native:+" // From node_modules
}

在这里插入图片描述
然后,我们为RNHybridAndroid项目配置使用的本地React Native maven目录,在RNHybrid/RNHybridAndroid/build.gradle文件中添加如下代码:

allprojects {
    repositories {
        mavenLocal()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
        ...
    }
    ...
}

在这里插入图片描述
提示:为确保你配置的目录正确,可以通过在Android Studio中运行Gradle sync 看是否有 “Failed to resolve: com.facebook.react:react-native:0.x.x” 的错误出现,没有错误则说明配置正确,否则说明配置路由有问题。

第二步:配置权限

接下来我们为APP运行配置所需要的权限:检查你项目中的AndroidManifest.xml文件中看是否有如下权限:

<uses-permission android:name="android.permission.INTERNET" />

如果没有,则需要将上述权限添加到AndroidManifest.xml中。

另外,如果你需要用到RN的Dev Settings功能:则需要在AndroidManifest.xml文件中添加如下代码:

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

第三步: 指定要ndk需要兼容的架构(重要)

Android不能同时加载多种架构的so库,现在很多Android第三方sdks对abi的支持比较全,可能会包含armeabi, armeabi-v7a,x86, arm64-v8a,x86_64五种abi,如果不加限制直接引用会自动编译出支持5种abi的APK,而Android设备会从这些abi进行中优先选择某一个,比如:arm64-v8a,但如果其他sdk不支持这个架构的abi的话就会出现crash。如下图:
在这里插入图片描述
怎么解决呢:

defaultConfig {
....
    ndk {
        abiFilters "armeabi-v7a", "x86"
    }
}

上述代码的意思是,限制打包的so库只包含armeabi-v7a与x86。

三、创建index.js并添加你的React Native代码

通过上述两步,我们已经为RNHybridAndroid项目添加了React Native依赖,接下来我们来开发一些JS代码。
在RNHybrid目录下创建一个index.js文件并添加如下代码:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('App1', () => App);

上述代码,AppRegistry.registerComponent(‘App1’, () => App);目的是向React Native注册一个名为App1的组件,然后我会在第四步给大家介绍如何在Android中加载并显示出这个组件。

另外,在上述代码中我们引用了一个App.js文件:

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          this is App
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
 });

这个App.js文件代表了我们React Native的一个页面,在这个页面中显示了this is App的文本内容。

以上就是为本次演示所添加的React Native代码,你也可以根据需要添加更多的React Native代码以及组件出来。

四、为React Native创建一个Activity来作为容器

经过上述3、4步,我们已经为RNHybridAndroid项目添加了React Native依赖,并且创建一些React Native代码和注册了一个名为App1的组件,接下来我们来学习下如何在RNHybridAndroid项目中使用这个App1组件。

  • 1 通过ReactInstanceManager的方式:灵活,可定制性强;
  • 2 通过继承ReactActivity的方式:简单,可定制性差;

创建RNPageActivity

首先我们需要创建一个Activity来作为React Native的容器,

public class RNPageActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(getApplication())
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(new MainReactPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build();
        // 这个"App1"名字一定要和我们在index.js中注册的名字保持一致AppRegistry.registerComponent()
        mReactRootView.startReactApplication(mReactInstanceManager, "App1", null);

        setContentView(mReactRootView);
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }
}

参数说明

  • setBundleAssetName:打包时放在assets目录下的JS bundle包的名字,App release之后会从该目录下加载JS bundle;
  • setJSMainModulePath:JS bundle中主入口的文件名,也就是我们上文中创建的那个index.js文件;
  • addPackage:向RN添加Native Moudle,在上述代码中我们添加了new MainReactPackage()这个是必须的,另外,如果我们创建一些其他的Native Moudle也需要通过addPackage的方式将其注册到RN中。需要指出的是RN除了这个方法外,也提供了一个addPackages方法用于批量向RN添加Native Moudle;
  • setUseDeveloperSupport:设置RN是否开启开发者模式(debugging,reload,dev memu),比如我们常用开发者弹框;
  • setInitialLifecycleState:通过这个方法来设置RN初始化时所处的生命周期状态,一般设置成LifecycleState.RESUMED就行,和下文讲的Activity容器的生命周期状态关联;
  • mReactRootView.startReactApplication:它的第一个参数是mReactInstanceManager,第二个参数是我们在index.js中注册的组件的名字,第三个参数接受一个Bundle来作为RN初始化时传递给JS的初始化数据,它的具体用法我会在React Android 混合开发讲解的视频教程中再具体的讲解;

在中AndroidManifest.xml注册一个RNPageActivity

Android系统要求,每一个要打开的Activity都要在AndroidManifest.xml中进行注册:

<activity
    android:name=".RNPageActivity"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="adjustResize"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

上述代码中我们为RNPageActivity添加了一个@style/Theme.AppCompat.Light.NoActionBar类型的theme,这也是React Native UI组件所要求的主题。

为ReactInstanceManager添加Activity的生命周期回调

一个 ReactInstanceManager可以被多个activities或fragments共享,所以我们需要在Activity的生命周期中回调ReactInstanceManager的对于的方法。

@Override
protected void onPause() {
    super.onPause();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostPause(this);
    }
}

@Override
protected void onResume() {
    super.onResume();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostResume(this, this);
    }
}

@Override
public void onBackPressed() {
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onBackPressed();
    } else {
        super.onBackPressed();
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostDestroy(this);
    }
    if (mReactRootView != null) {
        mReactRootView.unmountReactApplication();
    }
}

从上述代码中你会发现有个不属于Activity生命周期中的方法onBackPressed,添加它的目的主要是为了当用户单击手机的返回键之后将事件传递给JS,如果JS消费了这个事件,Native就不再消费了,如果JS没有消费这个事件那么RN会回调invokeDefaultOnBackPressed代码。

@Override
public void invokeDefaultOnBackPressed() {
    super.onBackPressed();
}

添加开发者菜单

在RN中有个很好用的工具开发者菜单,我们平时调试RN应用时对它的使用频率很高,接下来我们来为RNHybridAndroid添加开着菜单。

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getUseDeveloperSupport()) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {//Ctrl + M 打开RN开发者菜单
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
	}
    return super.onKeyUp(keyCode, event);
}

通过上代码即可监听Ctrl + M来打开RN开发者菜单。
另外,RN也提供了双击R来快速加载JS的功能,通过如下代码即可打开该功能:

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getUseDeveloperSupport()) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {//Ctrl + M 打开RN开发者菜单
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, getCurrentFocus());
        if (didDoubleTapR) {//双击R 重新加载JS
            mReactInstanceManager.getDevSupportManager().handleReloadJS();
            return true;
        }
    }
    return super.onKeyUp(keyCode, event);
}

使用ReactActivity来作为RN容器

在上述的代码中我们都是通过ReactInstanceManager来创建和加载JS的,然后重写了Activity的生命周期来对ReactInstanceManager进行回调,另外,重写了onKeyUp来启用开发者菜单等功能。

另外,查看RN的源码你会发现在RN sdk中有个叫ReactActivity的Activity,该Activity是RN官方封装的一个RN容器。另外,在通过react-native init命令初始化的一个项目中你会发现有个MainActivity是继承ReactActivity的,接下来我们就来继承ReactActivity来封装一个RN容器。

public class ReactPageActivity extends ReactActivity implements IJSBridge{
    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "App1";
    }
}

另外,我们需要实现一个MainApplication并添加如下代码:

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage()
      );
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}

上述代码的主要作用是为ReactActivity提供ReactNativeHost,查看源码你会发现在ReactActivity中使用了ReactActivityDelegate,在ReactActivityDelegate中会用到MainApplication中提供的ReactNativeHost:

 protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}

另外实现了MainApplication之后需要在AndroidManifest.xml中添加MainApplication:

 <application
        android:name=".MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...

以上就是通过继承ReactActivity的方式来作为RN容器的。

那么这两种方式各有什么特点:

  • 通过ReactInstanceManager的方式:灵活,可定制性强;
  • 通过继承ReactActivity的方式:简单,可定制性差;

五. 运行React Native

经过上述的步骤,我们已经完成了对一个现有Android项目RNHybridAndroid添加了RN,并且通过两种方式分别创建了一个RNPageActivity与ReactPageActivity的Activity来加载我们在JS中注册的名为App1的RN 组件。

接下来我们来启动RN服务器,运行RNHybridAndroid项目打开RNPageActivity或ReactPageActivity来查看效果:

npm start

在RNHybrid的根目录运行上述命令,来启动一个RN本地服务:
然后我们打开AndroidStudio,点击运行按钮或者通过快捷键Ctrl+R来将RNHybridAndroid安装到模拟器上:

六. 添加更多React Native的组件

我们可以根据需要添加更多的React Native的组件:

import { AppRegistry } from 'react-native';
import App from './App';
import App2 from './App2';

AppRegistry.registerComponent('App1', () => App);
AppRegistry.registerComponent('App2', () => App);

然后,在Native中根据需要加载指定名字的RN组件即可。

七. 调试、打包、发布应用

打包

虽然,通过上述步骤,我们将RN和我们的RNHybridAndroid项目做了融合,但打包RNHybridAndroid你会发现里面并不包含JS部分的代码,如果要将JS代码打包进Android Apk包中,可以通过如下命令:

react-native bundle --platform android --dev false --entry-file index.js --bundle-output RNHybridAndroid/app/src/main/assets/index.android.bundle --assets-dest RNHybridAndroid/app/src/main/res/

参数说明

  • –platform android:代表打包导出的平台为Android;
  • –dev false:代表关闭JS的开发者模式;
  • -entry-file index.js:代表js的入口文件为index.js;
  • –bundle-output:后面跟的是打包后将JS bundle包导出到的位置;
  • –assets-dest:后面跟的是打包后的一些资源文件导出到的位置;

提示:JS bundle一定要正确放到你的Android言语的assets目录下这个和我们上文中配置的setBundleAssetName(“index.android.bundle”)进行对应。

发布应用

通过上述步骤我们完成了将RN代码打包并生成JS bundle,并放到了assets目录下,接下来我们就可以来通过Android Studio或者命令的方式来release我们的RN混合Android应用了。

转自:
http://www.devio.org/2018/08/26/React-Native-Hybrid-Android/

2018-08-28 23:01:18 fengyuzhengfan 阅读数 1049
  • 组件基础2

    掌握Vue.js基础知识; 掌握Vue.js核心功能; 掌握Vue.js实战技能;

    25641课时 0分钟 0人学习 刘宏强
    免费试看

期待已久的新课上线啦!解锁React Native开发新姿势,一网打尽React Native最新与最热技术,点我Get!!!

在React Native的应用场景中,有时候一个APP只有部分页面是由React Native实现的,比如:我们常用的携程App,它的首页下的很多模块都是由React Native实现的,这种开发模式被称为混合开发。

混合开发的一些其他应用场景:

在原有项目中加入RN页面,在RN项目中加入原生页面

RNHybrid

原生页面中嵌入RN模块

Native-RN-page

RN页面中嵌入原生模块

RN-Native-page

以上这些都属于React Native混合开发的范畴,那么如何进行React Native混合开发呢?

在这篇文章中我将向大家介绍React Native混合开发的流程,需要掌握的技术,以及一些经验技巧,与该文章配套的还有React Native与Android 混合开发讲解的视频教程

React Native混合开发的教程我们分为上下两篇,上篇主要介绍**如何在现有的Android应用上进行React Native混合开发,下篇主要介绍如何在现有的iOS应用上进行React Native混合开发**。

将React Native集成到现有的Android应用中需要如下几个主要步骤:

  • 首先,你需要有一个React Native项目;
  • 为已存在的Android应用添加React Native所需要的依赖;
  • 创建index.js并添加你的React Native代码;
  • 创建一个Activity来承载React Native,在这个Activity中创建一个ReactRootView来作为React Native服务的容器;
  • 启动React Native的Packager服务,运行应用;
  • (可选)根据需要添加更多React Native的组件;
  • 运行、调试、打包、发布应用;
  • 升职加薪、迎娶白富美,走向人生巅峰!;

1. 创建一个React Native项目

在做混合开发之前我们首先需要创建一个没有Android和iOS模块的React Native项目。我们可以通过两种方式来创建一个这样的React Native项目:

  • 通过npm安装react-native的方式添加一个React Native项目;
  • 通过react-native init来初始化一个React Native项目;

通过npm安装react-native的方式添加一个React Native项目

第一步:创建一个名为RNHybridApp的目录,然后在该目录下添加一个包含如下信息的package.json

{
  "name": "RNHybrid",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  }
}

第二步:在为package.json添加react-native

在该目录下执行:

npm install --save react-native

执行完上述命令之后,你会看到如下警告:

npm-install--save-react-native.png

其中,有一条警告npm WARN react-native@0.55.4 requires a peer of react@16.3.1 but none is installed告诉我们需要安装react@16.3.1

npm install --save react@16.3.1

至此,一个不含Android和iOS模块的React Native项目便创建好了。此过程所遇到的更多问题可查阅:React Native与Android 混合开发讲解的视频教程

提示:npm 会在你的目录下创建一个node_modulesnode_modules体积很大且是动态生成了,建议将其添加到.gitignore文件中;

通过react-native init来初始化一个React Native项目

除了上述方式之外,我们也可以通过react-native init命令来初始化一个React Native项目。

react-native init RNHybrid

上述命令会初始化一个完成的名为RNHybrid的React Native项目,然后我们将里面的androidios目录删除,替换成已存在Android和iOS项目。

2. 添加React Native所需要的依赖

在上文中我们已经创建了个一个React Native项目,接下来我们来看一下如何将这个React Native项目和我们已经存在的Native项目进行融合。

在进行融合之前我们需要将已经存在的Native项目放到我们创建的RNHybrid下,比如:我有一个名为RNHybridAndroid的Android项目,将其放到RNHybrid目录下:

RNHybrid
├── RNHybridAndroid
├── package.json
├── node_modules
└── .gitignore

第一步:配置maven

接下来我们需要为已经存在的RNHybridAndroid项目添加 React Native依赖,在RNHybrid/RNHybridAndroid/app/build.gradle文件中添加如下代码:

dependencies {
    compile 'com.android.support:appcompat-v7:23.0.1'
    ...
    compile "com.facebook.react:react-native:+" // From node_modules
}

app-build.gradle

然后,我们为RNHybridAndroid项目配置使用的本地React Native maven目录,在RNHybrid/RNHybridAndroid/build.gradle文件中添加如下代码:

allprojects {
    repositories {
        mavenLocal()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
        ...
    }
    ...
}

maven-directory-to-build.gradle

提示:为确保你配置的目录正确,可以通过在Android Studio中运行Gradle sync 看是否有 “Failed to resolve: com.facebook.react:react-native:0.x.x" 的错误出现,没有错误则说明配置正确,否则说明配置路由有问题。
此过程所遇到的更多问题可查阅:React Native与Android 混合开发讲解的视频教程

第二步:配置权限

接下来我们为APP运行配置所需要的权限:检查你项目中的AndroidManifest.xml文件中看是否有如下权限:

<uses-permission android:name="android.permission.INTERNET" />

如果没有,则需要将上述权限添加到AndroidManifest.xml中。

另外,如果你需要用到RN的Dev Settings功能:

DevSettingsActivity

则需要在AndroidManifest.xml文件中添加如下代码:

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

提示:上述图片就是RN 开发调试弹框中的Dev Settings功能,打开该功能会弹出上图的一个界面,这个界面就是DevSettingsActivity。

第三步:指定要ndk需要兼容的架构(重要)

Android不能同时加载多种架构的so库,现在很多Android第三方sdks对abi的支持比较全,可能会包含armeabi, armeabi-v7a,x86, arm64-v8a,x86_64五种abi,如果不加限制直接引用会自动编译出支持5种abi的APK,而Android设备会从这些abi进行中优先选择某一个,比如:arm64-v8a,但如果其他sdk不支持这个架构的abi的话就会出现crash。如下图:

libgnustl_shared.so"-is-32-bit-instead-of-64-bit

怎么解决呢:

app/gradle 文件中添加如下代码:

defaultConfig {
....
    ndk {
        abiFilters "armeabi-v7a", "x86"
    }
}

上述代码的意思是,限制打包的so库只包含armeabi-v7ax86此过程所遇到的更多问题可查阅:React Native与Android 混合开发讲解的视频教程

可参考:libgnustl_shared.so" is 32-bit instead of 64-bit

3.创建index.js并添加你的React Native代码

通过上述两步,我们已经为RNHybridAndroid项目添加了React Native依赖,接下来我们来开发一些JS代码。

在RNHybrid目录下创建一个index.js文件并添加如下代码:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('App1', () => App);

上述代码,AppRegistry.registerComponent('App1', () => App);目的是向React Native注册一个名为App1的组件,然后我会在第四步给大家介绍如何在Android中加载并显示出这个组件。

另外,在上述代码中我们引用了一个App.js文件:

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';

type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          this is App
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
 });

这个App.js文件代表了我们React Native的一个页面,在这个页面中显示了this is App的文本内容。

以上就是为本次演示所添加的React Native代码,你也可以根据需要添加更多的React Native代码以及组件出来。

4. 为React Native创建一个Activity来作为容器

经过上述3、4步,我们已经为RNHybridAndroid项目添加了React Native依赖,并且创建一些React Native代码和注册了一个名为App1的组件,接下来我们来学习下如何在RNHybridAndroid项目中使用这个App1组件。

创建RNPageActivity

首先我们需要创建一个Activity来作为React Native的容器,

public class RNPageActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(getApplication())
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(new MainReactPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build();
        // 这个"App1"名字一定要和我们在index.js中注册的名字保持一致AppRegistry.registerComponent()
        mReactRootView.startReactApplication(mReactInstanceManager, "App1", null);

        setContentView(mReactRootView);
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }
}

参数说明

  • setBundleAssetName:打包时放在assets目录下的JS bundle包的名字,App release之后会从该目录下加载JS bundle;
  • setJSMainModulePath:JS bundle中主入口的文件名,也就是我们上文中创建的那个index.js文件;
  • addPackage:向RN添加Native Moudle,在上述代码中我们添加了new MainReactPackage()这个是必须的,另外,如果我们创建一些其他的Native Moudle也需要通过addPackage的方式将其注册到RN中。需要指出的是RN除了这个方法外,也提供了一个addPackages方法用于批量向RN添加Native Moudle;
  • setUseDeveloperSupport:设置RN是否开启开发者模式(debugging,reload,dev memu),比如我们常用开发者弹框;
  • setInitialLifecycleState:通过这个方法来设置RN初始化时所处的生命周期状态,一般设置成LifecycleState.RESUMED就行,和下文讲的Activity容器的生命周期状态关联;
  • mReactRootView.startReactApplication:它的第一个参数是mReactInstanceManager,第二个参数是我们在index.js中注册的组件的名字,第三个参数接受一个Bundle来作为RN初始化时传递给JS的初始化数据,它的具体用法我会在**React Android 混合开发讲解的视频教程**中再具体的讲解;

在中AndroidManifest.xml注册一个RNPageActivity

Android系统要求,每一个要打开的Activity都要在AndroidManifest.xml中进行注册:

<activity
    android:name=".RNPageActivity"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="adjustResize"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

上述代码中我们为RNPageActivity添加了一个@style/Theme.AppCompat.Light.NoActionBar类型的theme,这也是React Native UI组件所要求的主题。

为ReactInstanceManager添加Activity的生命周期回调

一个 ReactInstanceManager可以被多个activities或fragments共享,所以我们需要在Activity的生命周期中回调ReactInstanceManager的对于的方法。

 @Override
protected void onPause() {
    super.onPause();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostPause(this);
    }
}

@Override
protected void onResume() {
    super.onResume();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostResume(this, this);
    }
}

@Override
public void onBackPressed() {
    if (mReactInstanceManager != null) {
        mReactInstanceManager.onBackPressed();
    } else {
        super.onBackPressed();
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onHostDestroy(this);
    }
    if (mReactRootView != null) {
        mReactRootView.unmountReactApplication();
    }
}

从上述代码中你会发现有个不属于Activity生命周期中的方法onBackPressed,添加它的目的主要是为了当用户单击手机的返回键之后将事件传递给JS,如果JS消费了这个事件,Native就不再消费了,如果JS没有消费这个事件那么RN会回调invokeDefaultOnBackPressed代码。

@Override
public void invokeDefaultOnBackPressed() {
    super.onBackPressed();
}

此过程更细致的讲解可查阅:React Native与Android 混合开发讲解的视频教程

添加开发者菜单

在RN中有个很好用的工具开发者菜单,我们平时调试RN应用时对它的使用频率很高,接下来我们来为RNHybridAndroid添加开着菜单。

 public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getUseDeveloperSupport()) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {//Ctrl + M 打开RN开发者菜单
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
	}
    return super.onKeyUp(keyCode, event);
}

通过上代码即可监听Ctrl + M来打开RN开发者菜单。

ctrl+m-android

另外,RN也提供了双击R来快速加载JS的功能,通过如下代码即可打开该功能:

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getUseDeveloperSupport()) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {//Ctrl + M 打开RN开发者菜单
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer).didDoubleTapR(keyCode, getCurrentFocus());
        if (didDoubleTapR) {//双击R 重新加载JS
            mReactInstanceManager.getDevSupportManager().handleReloadJS();
            return true;
        }
    }
    return super.onKeyUp(keyCode, event);
}

此过程更细致的讲解可查阅:React Native与Android 混合开发讲解的视频教程

使用ReactActivity来作为RN容器

在上述的代码中我们都是通过ReactInstanceManager来创建和加载JS的,然后重写了Activity的生命周期来对ReactInstanceManager进行回调,另外,重写了onKeyUp来启用开发者菜单等功能。

另外,查看RN的源码你会发现在RN sdk中有个叫ReactActivity的Activity,该Activity是RN官方封装的一个RN容器。另外,在通过react-native init命令初始化的一个项目中你会发现有个MainActivity是继承ReactActivity的,接下来我们就来继承ReactActivity来封装一个RN容器。

public class ReactPageActivity extends ReactActivity implements IJSBridge{
    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "App1";
    }
}

另外,我们需要实现一个MainApplication并添加如下代码:

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage()
      );
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}

上述代码的主要作用是为ReactActivity提供ReactNativeHost,查看源码你会发现在ReactActivity中使用了ReactActivityDelegate,在ReactActivityDelegate中会用到MainApplication中提供的ReactNativeHost

 protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}

另外实现了MainApplication之后需要在AndroidManifest.xml中添加MainApplication

 <application
        android:name=".MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...

以上就是通过继承ReactActivity的方式来作为RN容器的。

那么这两种方式各有什么特点:

  • 通过ReactInstanceManager的方式:灵活,可定制性强;
  • 通过继承ReactActivity的方式:简单,可定制性差;

此过程更细致的讲解可查阅:React Native与Android 混合开发讲解的视频教程

5. 运行React Native

经过上述的步骤,我们已经完成了对一个现有Android项目RNHybridAndroid添加了RN,并且通过两种方式分别创建了一个RNPageActivityReactPageActivity的Activity来加载我们在JS中注册的名为App1的RN 组件。

接下来我们来启动RN服务器,运行RNHybridAndroid项目打开RNPageActivityReactPageActivity来查看效果:

npm start

RNHybrid的根目录运行上述命令,来启动一个RN本地服务:

npm-start

然后我们打开AndroidStudio,点击运行按钮或者通过快捷键Ctrl+R来将RNHybridAndroid安装到模拟器上:

this-is-app-android

6. 添加更多React Native的组件

我们可以根据需要添加更多的React Native的组件:

import { AppRegistry } from 'react-native';
import App from './App';
import App2 from './App2';

AppRegistry.registerComponent('App1', () => App);
AppRegistry.registerComponent('App2', () => App);

然后,在Native中根据需要加载指定名字的RN组件即可。

7. 调试、打包、发布应用

调试

调试这种混合的RN应用和调试一个纯RN应用时一样的,都是通过上文中说讲到的RN 开发者菜单,另外搭建也可以通过学习React Native技术精讲与高质量上线APP开发课程来掌握更多RN调试的技巧。

打包

虽让,通过上述步骤,我们将RN和我们的RNHybridAndroid项目做了融合,但打包RNHybridAndroid你会发现里面并不包含JS部分的代码,如果要将JS代码打包进Android Apk包中,可以通过如下命令:

react-native bundle --platform android --dev false --entry-file index.js --bundle-output RNHybridAndroid/app/src/main/assets/index.android.bundle --assets-dest RNHybridAndroid/app/src/main/res/

参数说明

  • --platform android:代表打包导出的平台为Android;
  • --dev false:代表关闭JS的开发者模式;
  • -entry-file index.js:代表js的入口文件为index.js
  • --bundle-output:后面跟的是打包后将JS bundle包导出到的位置;
  • --assets-dest:后面跟的是打包后的一些资源文件导出到的位置;

提示:JS bundle一定要正确放到你的Android言语的assets目录下这个和我们上文中配置的setBundleAssetName("index.android.bundle")进行对应。

发布应用

通过上述步骤我们完成了将RN代码打包并生成JS bundle,并放到了assets目录下,接下来我们就可以来通过Android Studio或者命令的方式来release我们的RN混合Android应用了。

我在之前发表过React Native发布APP之签名打包APK的博文,
需要的同学可以去看一下,在这篇文章中就不在重复了。

更多React Native混合开发的实用技巧,可学习与此文章配套的视频课程:《React Native与Android 混合开发讲解》

参考

2017-08-14 15:47:03 u011272795 阅读数 18396
  • 组件基础2

    掌握Vue.js基础知识; 掌握Vue.js核心功能; 掌握Vue.js实战技能;

    25641课时 0分钟 0人学习 刘宏强
    免费试看

 

前言:

完成项目时,我们需要将项目打包成一个apk,方便测试以及发布版本.

这时,需要把js代码和图片资源都放进apk中, 并且发布版本还需要签名,今天把这一系列操作记录下来.

 

一.生成离线bundle包

离线包就是把 ReactNative 和你写的 js文件、图片等资源都打包放入 App ,不需要走网络下载。

首先看一下官方给的参数(中文版):

react-native bundle [参数]
  构建 js 离线包 


参数:


    -h, --help                   输出如何使用的信息
    --entry-file <path>          RN入口文件的路径, 绝对路径或相对路径
    --platform [string]          ios 或 andorid
    --transformer [string]       Specify a custom transformer to be used
    --dev [boolean]              如果为false, 警告会不显示并且打出的包的大小会变小
    --prepack                    当通过时, 打包输出将使用Prepack格式化
    --bridge-config [string]     使用Prepack的一个json格式的文件__fbBatchedBridgeConfig 例如: ./bridgeconfig.json
    --bundle-output <string>     打包后的文件输出目录, 例: /tmp/groups.bundle
    --bundle-encoding [string]   打离线包的格式 可参考链接https://nodejs.org/api/buffer.html#buffer_buffer.
    --sourcemap-output [string]  生成Source Map,但0.14之后不再自动生成source map,需要手动指定这个参数。例: /tmp/groups.map
    --assets-dest [string]       打包时图片资源的存储路径
    --verbose                    显示打包过程
    --reset-cache                移除缓存文件
    --config [string]            命令行的配置文件路径

接下来,就开始我们的离线包打包命令:

 

react-native bundle --entry-file index.js --platform android --dev false --bundle-output ./android/app/src/main/assets/index.android.bundle --assets-dest ./android/app/src/main/res/

打安卓包的话,react-native bundle 可以替换为 react-native unbundle 做到拆分功能 

react-native unbundle --entry-file index.js --platform android --dev false --bundle-output ./android/app/src/main/assets/index.android.bundle --assets-dest ./android/app/src/main/res/

 

 

 

 

翻译成我们能理解的意思就是:入口文件是index.js(0.49以前是index.android.js 注意改一下代码) ,平台是安卓,不显示警告,bundle包输出路径(保存在)react-./android/app/src/main/assets/index.android.bundle  ,图片资源路径是: ./android/app/src/main/res/ .

很容易理解,但是要确保有assets这个文件夹,如果没有请先新建这个文件夹.成功之后会生成index.android.bundle文件.

 

二 生成签名文件

1.命令行方式:

 

你可以用keytool命令生成一个私有密钥。在Windows上keytool命令放在JDK的bin目录中(比如C:\Program Files\Java\jdkx.x.x_x\bin),你可能需要在命令行中先进入那个目录才能执行此命令。

$ keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

这条命令会要求你输入密钥库(keystore)和对应密钥的密码,然后设置一些发行相关的信息。最后它会生成一个叫做my-release-key.keystore的密钥库文件。

在运行上面这条语句之后,密钥库里应该已经生成了一个单独的密钥,有效期为10000天。--alias参数后面的别名是你将来为应用签名时所需要用到的,所以记得记录这个别名。

注意:请记得妥善地保管好你的密钥库文件,不要上传到版本库或者其它的地方。

生成的密钥文件在android目录下,记得要将它保存到android/app目录下. 如图所示:

 

 

2. 用android studio 生成签名文件:

点击android stuido 菜单栏中的 build, 找到“Generate Signed APK”.

然后

”Create new…”新建一个签名文件

Choose existing…”选择一个已经存在的签名文件

如果已经有签名文件,可以直接选择使用,没有的话就新建一个.

点击新建之后会有一个弹窗, 需要写很多信息:

Key store path : 签名文件路径
Password : 签名密码
Confirm : 确认密码
Alias : 别名
Validity ( years ) : 有限期 (年)
First and Last Name : 全名
Organizational Unit : 组织单位
Organization : 组织
City or Locality : 城市或地方
State or Province : 州或省
Country Code(XX) : 国家代码

 

要记住你填写的东西,有一些不是必填的可以不填.

填写完成之后回到上一个页面,将你填好的信息填进去即可生成一个签名文件.

 

 

3 . 设置gradle变量:

编辑~/.gradle/gradle.properties(没有这个文件你就创建一个),添加如下的代码(注意把其中的****替换为相应密码)
注意:~表示用户目录,比如windows上可能是C:\Users\用户名,而mac上可能是/Users/用户名。


MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****
上面的这些会作为全局的gradle变量,我们在后面的步骤中可以用来给应用签名。

如上图所示:

 

添加签名到项目的gradle配置文件

编辑你项目目录下的android/app/build.gradle,添加如下的签名配置:

...
android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            storeFile file(MYAPP_RELEASE_STORE_FILE)
            storePassword MYAPP_RELEASE_STORE_PASSWORD
            keyAlias MYAPP_RELEASE_KEY_ALIAS
            keyPassword MYAPP_RELEASE_KEY_PASSWORD
        }
    }
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release
        }
    }
}
...

 

 

 

 

4.生成签名apk

在RN项目根目录运行:

cd android && ./gradlew assembleRelease     ---生产包

cd android && ./gradlewassembleDebug  ---测试包

cd android表示进入android目录(如果你已经在android目录中了那就不用输入了)。

但是要注意:

./gradlew assembleRelease在macOS、Linux或是windows的PowerShell环境中表示执行当前目录下的名为gradlew的脚本文件,且其运行参数为assembleRelease,注意这个./不可省略;而在windows的传统CMD命令行下则需要去掉./。

Gradle的assembleRelease参数会把所有用到的JavaScript代码都打包到一起,然后内置到APK包中。如果你想调整下这个行为(比如js代码以及静态资源打包的默认文件名或是目录结构等),可以看看android/app/build.gradle文件,然后琢磨下应该怎么修改以满足你的需求。

生成的APK文件位于android/app/build/outputs/apk/app-release.apk,它已经可以用来发布了。

也可以通过命令行直接安装到手机上:

adb install (apk在PC上的路径/)*.apk

 

ok 那么今天打包apk的过程就到此结束了~

 

 

总结就是:
1:
运行: react-native bundle --entry-file index.android.js --platform android --dev false --bundle-output ./android/app/src/main/assets/index.android.bundle --assets-dest ./android/app/src/main/res/
生成bundle包
2:
设置gradle变量, 修改gradle.propertites和build.gradle文件.
3:

Linux&&Mac:
运行: cd android && ./gradlew assembleRelease 生成rease版本的apk 或者

cd android && ./gradlew assembleDebug 生成debug版本的apk

Windows:

运行: cd android && gradlew assembleRelease 生成rease版本的apk 或者

cd android && gradlew assembleDebug 生成debug版本的apk

 

如果有flavor的话,则是 gradlew assemble渠道名Release

 

昨天我升级android studio 3.0之后,Gradle版本也同样升级了,但是升级之后打包出现node_modules_reactnavigation_src_views_assets_backicon.png图片重复问题,这个原因我在Github上面找了好久,才发现是因为Gradle2.3之后,离线打包的路径都会在drawable-xxx-v4中,原版的离线路径在drawable-xxx中,所以导致图片重复问题,怎么解决这个问题呢:

1,修改assetPathUtils.js

assetPathUtils.js文件路径:node_modules\react-native\local-cli\bundle\assetPathUtils.js

修改:getAndroidAssetSuffix方法

修改前:

 function getAndroidAssetSuffix(scale) {
   switch (scale) {
    case 0.75: return 'ldpi';
    case 1: return 'mdpi';
    case 1.5: return 'hdpi';
    case 2: return 'xhdpi';
    case 3: return 'xxhdpi';
    case 4: return 'xxxhdpi';
   }
 }

修改后:

 function getAndroidAssetSuffix(scale) {
   switch (scale) {
     case 0.75: return 'ldpi-v4';
    case 1: return 'mdpi-v4';
    case 1.5: return 'hdpi-v4';
    case 2: return 'xhdpi-v4';
    case 3: return 'xxhdpi-v4';
    case 4: return 'xxxhdpi-v4';
   }
 }