RN实战经验总结


title: RN实战经验总结

前言

在草稿箱中发现了许久之前写的这篇文章,虽然不搞RN已经大半年了,但是之前写过的东西还是还出来作个纪念。如果能帮到别人那再好不过了。

Android中集成RN

这个需要说的都在这里说了,见在原有Android项目中快速集成React Native

关于RN在项目中Android端的预加载

此前曾根据网上的做法并结合最新的RN源码做了一个RN的预加载库,不过在后来发现会出现内存泄漏问题。在集成到项目Android端此着手解决了这一问题。
然而在实际的开发中,几乎有大半的页面是用RN开发,如果全部页面都使用预加载,那么对内存会有很大的压力,而且也没有这个必要。
首先说一下目前项目的页面组织结构,其实就是目前主流的主Activity(带四个Fragment)+其他Activity,主Activity在应用运行期间是一直存在的,这就为预加载提供了一个绝佳的基础。
最终使用预加载的是主Activity【我的】Fragment页面。在RN中加载Fragment并不难,在Android中加载RN,无论是在Activity还是Fragment,加载的都只是一个View而已。而给Fragment设置View,只需要Fragment的onCreateView返回RN的View即可。
具体见:在Android中预加载React Native jsBundle

优化非预加载初始化属性传递

在原本的ReactActivity中传递启动属性可以用以下方式

public class C3RNActivity extends ReactActivity {
    public static final String MAIN_COMPONENT_NAME = C3RNActivity.class.getSimpleName();

    protected @Nullable
    String getMainComponentName() {
        return MAIN_COMPONENT_NAME;
    }

    @Override
    protected ReactActivityDelegate createReactActivityDelegate() {
        return new ReactActivityDelegate(this, getMainComponentName()) {
            @Nullable
            @Override
            protected Bundle getLaunchOptions() {
                Bundle bundle=new Bundle();
                //往bundle中添加启动属性键值对
                bundle.putString("key","value");
                return bundle;
            }
        };
    }
}

这种方式传递是完全没问题的,但是有点局限性。查看ReactActivity的源码,createReactActivityDelegate是在ReactActivity的构造方法调用(在OnCreate之前)。但这样一来就无法在OnCreate通过getItent获取别的Activity传递过来的参数,因此我们需要对原本的ReactActivity进行改造。将createReactActivityDelegate方法调用从ReactActivity移到onCreate方法中,但是在ReactActivityDelegate的onCreate方法之前。这样我们需要重写ReactActivity而不是直接通过继承创建满足我们要求的ReactActivity。

public class MyReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

    private MyReactActivityDelegate mDelegate;

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     * e.g. "MoviesApp"
     */
    protected @Nullable
    String getMainComponentName() {
        return null;
    }

    /**
     * Called at construction time, override if you have a custom delegate implementation.
     */
    protected MyReactActivityDelegate createReactActivityDelegate(final Intent intent) {
        return new MyReactActivityDelegate(this, getMainComponentName()){
            @Nullable
            @Override
            protected Bundle getLaunchOptions() {
                Bundle bundle=new Bundle();
                //在这里将intent参数放入bundle,作为RN的页面启动参数
                //例如:
                bundle.putString("key",intent.getStringExtra("xxx"));
                return bundle;
            }
        };
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent=getIntent();
        mDelegate = createReactActivityDelegate(intent);
        mDelegate.onCreate(savedInstanceState);
    }

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

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

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

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        mDelegate.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
    }

    @Override
    public void onBackPressed() {
        if (!mDelegate.onBackPressed()) {
            super.onBackPressed();
        }
    }

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

    @Override
    public void onNewIntent(Intent intent) {
        if (!mDelegate.onNewIntent(intent)) {
            super.onNewIntent(intent);
        }
    }

    @Override
    public void requestPermissions(
            String[] permissions,
            int requestCode,
            PermissionListener listener) {
        mDelegate.requestPermissions(permissions, requestCode, listener);
    }

    @Override
    public void onRequestPermissionsResult(
            int requestCode,
            String[] permissions,
            int[] grantResults) {
        mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    protected final ReactNativeHost getReactNativeHost() {
        return mDelegate.getReactNativeHost();
    }

    protected final ReactInstanceManager getReactInstanceManager() {
        return mDelegate.getReactInstanceManager();
    }

}

其中,MyReactActivityDelegate 是直接继承ReactActivityDelegate,因为在ReactActivityDelegate中,onCreate,onPause,onDestroy等方法是protect修饰,无法在其他包中引用,所以需要对其复写,实现中只需要调用父类方法即可。

多入口

在本次项目中使用的是多注册方式实现RN的多入口,实际上通过启动属性传递需要打开的RN页面参数也是可以的。不过因为使用多注册实现多入口还是踩了一些坑。在多注册方式下,RN的全局变量在iOS客户端是无效的。也就是说,在一个根组件中给一个全局变量赋值,在另外一个根组件中读取到的全局变量值是空的。而在Android端是没有这个问题。

网络图片加载过渡

这里不得不提这是RN的一个坑,最新的RN都发布到0.5x了,在Android中依然没有支持默认占位图,默认加载错误图,以及加载进度方法。这三个都只有在iOS端有效,在Android端则需要自己手动实现。

网络状况判断

这里不得不提这又是RN的一个坑。在RN中官网推荐判断网络是否可用的方法如下:
NetInfo.isConnected.fetch().done(
(isConnected) => { this.setState({isConnected}); }
);
然而实际上在iOS端,isConnected返回的永远是false。

官方文档说上面这个方法是Android和iOS平台通用的,然而你实际使用的时候在iOS端就会发现问题,即使到了0.51,这个问题仍然存在.....
其实这个解决办法很多,
具体可见:Github issue:iOS: NetInfo.isConnected returns always false
其中一种解决办法如下:

function handleFirstConnectivityChange(isConnected) {
    if (!sConnected) {
     // do action
    } 
    NetInfo.isConnected.removeEventListener('change', handleFirstConnectivityChange);
}

if (Platform.OS === 'ios') {
    NetInfo.isConnected.addEventListener('change', handleFirstConnectivityChange); 
} else {
    NetInfo.isConnected.fetch().then(isConnected => {
    if (!sConnected) {
        // do action
    } 
}

Linking模块在Android release模式下getInitialURL返回为null

这也是一个大坑。在RN中,如果你的应用被其注册过的外部url调起,则可以在任何组件内这样获取和处理它:

componentDidMount() {
Linking.getInitialURL().then((url) => {
if (url) {
console.log('Initial url is: ' + url);
}
}).catch(err => console.error('An error occurred', err));
}
然而在Android端打成Release包时,返回的url偶尔会为空,对,是偶尔,并且概率还比较大,原因暂时未知。所以要通过外部链接和Linking模块来打开RN的话,这种做法是不靠谱的。解决办法是用Android原生的老办法,在Activity的onCreate方法中获取外部链接以及相关参数,并作为启动参数传递给RN。为此,需要重写ReactActivity和ReactActivityDelegate。具体参考:优化非预加载初始化属性传递一节。

总结:

从17年4月份开始接触RN,至今如有大半年时间,在这大半年时间里,从入门学习到实际动手写出一个完整的仿实际产品的App出来花了一个月时间,与当初学习Android相比这个时间短得太多了。到17年6月份在我们公司的天翼云iOS客户端其中一个页面试点使用RN,然后前后花了一个月时间,但实际动手接入项目中与从零开始一个RN项目有很大的不同,期间踩了好几个坑,还好都能及时解决。到17年10月份,在我们公司的产品两个客户端都接入RN并且是重度使用,大概有50%~60%页面是使用RN开发。在这一次接近2个月的开发过程中,对RN简直又爱又恨,踩了大大小小好多个坑,看到了RN的许多不足,也看到了原生与RN无法比拟的一些优势。
先来说一下切身体会的优势:

  • 上手快,即使不懂JS,入门也不用太长时间,半个月时间其实就足够了。上手之后,开发效率其实可以很高。
  • 跨平台,这是一个巨大的优势,虽然RN的代码不能做到100%两个端复用,但是90%还是没问题的。而自然地,可以节省一定的人力成本。
  • 热更新,这一功能在Android中实现比较简单,但是因为苹果爸爸禁用了JSPatch,因此在iOS端能用的热更新方法不多了,而热更新则是其中一个。
  • 更新快,这其实是一个优点也可以说是缺点,说它是优点因为勤快地更新则说明RN加了某些新特性或者修复了一些历史遗留的bug,说它是缺点则是因为更新太快,说不定某些API哪天突然就不能用了,代码的写法又不一样了,RN版本升级的时候也是一件比较痛苦的事情。
  • 调试方便:可以在谷歌浏览器上面单步调试JS代码,双击R(或摇一摇或CMD+R)就能快速reload代码

不足:

  • 开发过程会时不时就踩到坑,RN作为一个还没正式发布1.0版本的框架,有一些bug是必然的。
  • 列表控件性能仍不能满足要求,在快速滑动时会看到一些空白项。
  • 图片缓存:官方没有很好的支持,第三方库也没有找到比较满意的方案。
  • 动画效果不佳:这个众所周知,动画效果需要自己做优化。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,389评论 25 707
  • 诗歌翻译 波德莱尔 Correspondances Correspondances La Nature est u...
    月明西湖阅读 954评论 0 4
  • 励志小故事:《再试一次》 有个年轻人去微软公司应聘,而该公司并没有刊登过招聘广告。见总经理疑惑不解,年轻人用不太娴...
    艺趣书苑阅读 753评论 0 1
  • “你为什么拒绝我 不喜欢我吗” “不 喜欢 非常喜欢 就因为太喜欢 所以才不知所措”
    喵喵小仙女阅读 181评论 0 0
  • 从上个学期频繁去医院,看到人来人往的人们,焦虑的,失望的。 激素六项检查一直拖拖拖,不敢去医院,不敢面对自己脆弱的...
    黄莯洱阅读 328评论 0 0