React Native入门指南(下)

image

前言

Learn once, write anywhere: Build mobile apps with React

FacebookReact.js Conf 2015年会议上发布了React Native(以下简称RN )。RN 是一个使用React构建Native App的框架,支持Android 4.1+iOS 8+平台。它充分利用了Facebook现有的轮子,成为前端和客户端技术的集大成者。
RN兼备H5的动态性和Native的性能,完美弥补了手机浏览器性能和Native硬编码的弊端。

RN 的前身是React,这是一个为了解决项目越来越复杂导致DOM频繁操作和界面重绘,而诞生的高性能渲染框架。
它只描述了View层,Facebook 2014年发布Flux,以及Redux等模式框架和React配套使用。

RN 社区活跃度非常高,有完善的文档和众多贡献者支持,不用担心没资料而苦恼,这是RN 能从同类框架(主要指Weex)脱颖而出的优势之一。但是也侧面反映了RN 还不稳定,至今依然没有发布1.0版本。目前RN每两周会发布一个小版本,并提供了完善的升级支持。

在正式开始阅读文章之前,希望你具备以下知识:
- JavaScript 基本知识
- 对 Android 或 iOS 平台开发有了解
- 阅读完上篇

与原生代码交互

有时候我们需要访问Android或者iOS系统API,来完成一些功能,比如使用Camera拍照。又或者你有现成的模块,而你不想在JS中再实现一遍,都可以使用RN的原生代码交互能力进行实现。
但你需要清楚的知道程序是否必须使用原生代码实现,使用原生代码意味着抛弃跨平台,动态化的优势,你需要在AndroidiOS上分别实现一套程序逻辑。

使用SharePres进行存储

这部分我们为RN添加本地存储了能力,提供原生的getsave方法给JS使用。
这里使用这个这个案例仅仅因为简单易懂,实际上RN已经提供了AsyncStorage组件对存储能力进行支持。

1. 创建Module

首先我们需要创建一个继承自ReactContextBaseJavaModuleJava类。这就是一个原生Module,它可以实现JS所需的Native功能。

public class SharedPresDroidModule extends ReactContextBaseJavaModule {

    public static final String KEY_MODE_APPEND = "MODE_APPEND";
    public static final String KEY_MODE_PRIVATE = "MODE_PRIVATE";

    public SharedPresDroidModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "SharedPresDroid";
    }

    @Nullable
    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> map = new HashMap<>();
        map.put(KEY_MODE_APPEND, Context.MODE_APPEND);
        map.put(KEY_MODE_PRIVATE, Context.MODE_PRIVATE);
        return map;
    }

    @ReactMethod
    public void save(int mode, String key, String value) {
        SharedPreferences preferences = this.getReactApplicationContext()
                .getSharedPreferences("_list", mode);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString(key, value);
        edit.apply();
    }

    @ReactMethod
    public void get(int mode, String key, Callback callback) {
        SharedPreferences preferences = getReactApplicationContext()
                .getSharedPreferences("_list", mode);
        callback.invoke(preferences.getString(key, ""));
    }
}

上面的代码使用SharePres API实现了基本的访问和存储,使用@ReactMethod注解标识出提供给JS调用的方法,我们无需关心JSJava参数类型映射,这些RN帮我们处理了。定义Module时必须重写getName方法并返一个名字,重写getConstants方法并在MAP中定义一些常量。

这些定义的方法和常量可以通过下面的方式在JS中访问:

SharedPresDroid.save(SharedPresDroid.MODE_PRIVATE, 'title', 'test');

2. 注册Module

为了让Module能在JS中访问到,还需要通过下面两步将SharedPresDroidModule注册到RN

public class SharePresDroidPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Arrays.<NativeModule>asList(new SharedPresDroidModule(reactContext));
    }
    ...
}

上面的代码通过继承ReactPackage并重写createNativeModules方法,将SharedPresDroidModule类添加到Package中。

public class MainApplication extends Application implements ReactApplication {
    @Override
    protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
                new MainReactPackage(),
                new SharePresDroidPackage()
        );
    }
     ...
}

最终重写MainApplicationgetPackages方法,将我们的SharePresDroidPackage类注册到了RN
由此可见,一个应用可以注册多个Package,每个Package还可以包含多个Module功能。

3. 导出JS模块

为了让JS访问更方便,我们将SharedPresDroid导出,当然这步是可选的。

export default NativeModules.SharedPresDroid;

4. 使用原生代码

SharedPresDroid.save(SharedPresDroid.MODE_PRIVATE,
    'title', 'test');

SharedPresDroid.get(SharedPresDroid.MODE_PRIVATE,
    'title', (title) => {
        ToastAndroid.show(title, ToastAndroid.SHORT);
});

上面的代码通过SharedPresDroid调用get,save方法,通过SharedPresDroid.MODE_PRIVATE调用SharedPresDroidModule定义的才常量。到此我们已经实现了访问原生代码功能。

案例

到这里所有的章节都已经结束,现在我们在“音乐详情页”导航栏右边添加“保存”按钮,点击保存到SharedPres
详情参考Demo 2

安装包和更新

安装包

App发布到App Store之前需要打上带签名的release包。
使用RN开发的应用本质上是将JS和资源文件打包成bundle文件,放到原生应用工程中进行初始化->渲染。
所以我们可以使用AndroidiOS各自平台的方式进行打包:
Android平台上使用Gradle进行打包,iOS平台直接使用XCode打包上传AppStore

1. 生成bundle和资源文件

cd E:\ReactNativeExample

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

在命令行窗口进入项目根目录,执行react-native bundle命令生成bundle文件,参数如下:
- --entry-file:入口JS文件,iOSindex.ios.jsAndroidindex.android.js
- --bundle-output : bundle输出的目录,确保目录是已存在的。
- --platform : 目标系统平台。
- --assets-dest : 图片资源的输出目录。
- --dev : 是否为开发版本,正式打包将其改为false

2. 生成安装包

生成bundle和图片资源文件之后,Android平台将其拷贝到/app/src/main/assets目录,iOS平台将其拷贝到项目根目录。

Android

确保assets文件夹下bundle文件名为index.android.bundle
然后就可以使用Android Studio或者gradlew命令生成APK文件了。

iOS

打开AppDelegate.m文件,确保index.ios和刚才生成的bundle文件名一致。

- (BOOL)application:(UIApplication *)application 
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] 
        jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
     ...

  return YES;
}

然后就和往常一样更改项目的证书,生成Ad hocIPA包或者直接上传App Store

热更新

热更新指的是不需要重新安装的前提下更新功能模块。对于服务端可以使用微软提供的热更新React Native的服务——
CodePush(它作为一个中央仓库,开发者可以推送更新,应用可以从客户端SDK里面查询更新),也可以自己实现。
对于客户端RN资源的更新和App更新方式很像。

1. 更新流程

Server

将要更新的ZIPbundle和图片资源)包上传到Server

Client

  1. 进入APP检查是否有更新;
  2. 有更新则下载ZIP包;
  3. 下载完成后解压得到bundle和图片资源文件;
  4. 打开RN页面时加载新bundle文件。
public class MainApplication extends Application implements ReactApplication {

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {

       ...

        @Nullable
        @Override
        protected String getJSBundleFile() {
            File file = new File (JS_BUNDLE_LOCAL_PATH);  
            if(file != null && file.exists()) {  
                return JS_BUNDLE_LOCAL_PATH;  
            } else {  
                return super.getJSBundleFile();  
            }
        }
    };

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

打开<ProjectName>\android\app\src\main\java\com\reactsample\MainApplication.java,覆盖getJSBundleFile方法。
该方法默认返回null,这时会去加载assets目录下的bundle文件。上面代码判断是否存在新的bundle文件,有就去加载SDcard下的文件,没有加载assets下的文件。
文件的下载和解压可以使用原生代码实现,也可以使用JS代码实现,具体根据项目情况而定。

2. 资源大小优化

流程很简单,但是bundle文件和图片资源包太大了,一个不使用任何本地资源的简单页面还有2M大小。
下面我们进行一点点优化,减少更新资源文件的大小。

为了减少图片资源大小,我们打的ZIPbundle和图片资源)更新包,希望只包含修改的和增加的。
我们从图片渲染入手,打开image.android.js,找到渲染方法:

render: function() {
    const source = resolveAssetSource(this.props.source);
    const loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
    ...
}

原来是调用resolveAssetSource获取资源的,找到resolveAssetSource的实现:

function resolveAssetSource(source: any): ?ResolvedAssetSource {
  ...
  const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
  ...
  return resolver.defaultAsset();
}

最终是通过getBundleSourcePath()方法判断从什么位置读取资源。下面我们通过改动该方法实现只加载修改和新增的图片资源:

function getBundleSourcePath(): ?string {
    if (_bundleSourcePath === undefined) {
        const scriptURL = NativeModules.SourceCode.scriptURL;
        if (!scriptURL) {
            // scriptURL is falsy, we have nothing to go on here
            _bundleSourcePath = null;
            return _bundleSourcePath;
        }

        if (scriptURL.startsWith('assets://')) {
            // running from within assets, no offline path to use
            _bundleSourcePath = null;
            return _bundleSourcePath;
        }
        if (scriptURL.startsWith('file://')) {

            if (global.patchList) {
                for (let i = 0; i < global.patchList.length; i++) {
                    if (scriptURL.endsWith(global.patchList[i])) {
                        return 'file://' + scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
                    }
                }
            }
            _bundleSourcePath = null;
            return _bundleSourcePath;
            // cut off the protocol
            // _bundleSourcePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
        } else {
            _bundleSourcePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
        }
    }

    return _bundleSourcePath;
}

上面代码global.patchList是我们定义的数组,里面放的是自安装包版本以来所有修改过和新增的图片名。如果访问的图片名在这个数组中就离线文件目录里寻找图片资源,否则还是从assets中寻找图片资源。这样我们的资源就小多了。
但这个方案的缺点也很明显,每次RN升级都需要改动源码。现在Bundle文件依然很大,只能使用差异化更新解决这个问题。

3. 增量更新

google-diff-match-patchgoogle开源的增量更新方案,他能对比两个文件插件生成体积很小的补丁,也能对其进行合并。

Server

使用diff-match-patch和初始index.android.bundle文件生成差异文件,上传到Server

Client

  1. 进入APP检查是否有更新;
  2. 有更新则下载ZIP包,
  3. 下载完成后解压得到bundle差异化补丁和图片资源文件;
  4. 使用diff_match_patch合并原始bundlepatch文件;
  5. 打开RN页面时加载合并后的bundle文件。

这里只是提供了一个实现思路,具体实现不仅要学习google-diff-match-patch的使用,要考虑更多细节。如增加版本标记,解决bundle被删除问题。当然你也可以将初始ZIP包放到SDCard,这样就可以对整个ZIP做差异化处理了。

结束语

看到这里基本已经了解React Native技术全貌了,了解了它的优点和它暂时的不足。
希望这几篇文章能作为一个引子,能帮助你了解:怎么深入去学习RN
当然学习一门技术更重要是了解其原理,不必要在API使用上花太多功夫,需要的时候去看就可以了。

现在如果你是追求稳定的团队,建议RN只做技术储备就可以;
如果你是追求稳定,但又想有所突破的团队,可以使用RN实现部分小功能;
如果你追求前瞻技术,可以使用RN功能实现大部分功能;
如果你正好是新APP,并且急需动态化,那么RN非常适合你。

PC时代Web以免安装动态更新的优点替代了大部分的窗口程序,而移动互联网时代H5犹如当年一样,蚕食着APP的市场,RN正是解决了移动浏览器弊端,中和了H5Native的产物。技术的发展无疑不是为了解决用户问题,我们有理由相信RN所代表的这类技术会越走越远,直至它肖声觅迹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

返回主页看更多
狠狠的抽打博主 支付宝 扫一扫