当前位置:

React Native启动脚本解读、优化

访客 2024-04-23 245 0

前言

ReactNative是一个跨平台的使用JavaScript编写最终生成移动端原生控件应用的开源框架。下面是根据ReactNative在iOS端的情形下,对项目启动时,所运行的脚本进行解读。当我们使用ReactNative的命令行工具创建一个项目(react-nativeinitmyProject),在启动项目项目(执行react-nativestart命令或使用Xcode打开ios项目,CMDR运行项目)时,ReactNative会默认帮我们启动一个名为Packager的Node服务,同时在iOS项目编译过程中还会执行react-natvie-xcode.sh的脚本。下面我们将对这两部分的脚本进行一个一个地解读。

启动Packager服务
  1. Packager的意义

    它是一个本地的Node服务,我们在开发过程中(DEBUG模下)获取的js代码其实是这个服务器返回的。我们在修改js代码时,Packager服务会动态的同步我们的代码,然后App再去请求Packager服务获取我们修改的最新代码。这一过程就避免了我们在开发过程中,频繁进行打包的过程。Packager为我们提供了一个高效的代码动态加载方式。

  2. 脚本启动入口

    1. 使用命令行启动如果我们启动的方式是使用命令行方式启动的,那么在这个命令背后做的工作之一就是启动Packager。启动命令如下:

      react-nativerun-ios
    2. 在iOS项目中启动如果我们是通过XCode打开iOS项目,然后在执行快捷键CMDR命令运行项目,此时也会自动的启动Packager,那么启动Packager的入口在哪里呢?参见Xcode界面的截图:

      通过上面的截图标注的路径,我们可以看到通过Xcode在编译React这个工程时,会去执行一段shell脚本来启动我们的Packager服务。
    3. shell脚本解读

      1. RCT_METRO_PORT变量为Packager的服务的端口号,将这个变量赋值8081
      2. RCT_METRO_PORT=8081这段字符串写入到node_modules/react-native/scripts/.packager.env路径下
      3. 判断指定的端口号(8081)是否被占用了:
        1. 如果被占用了:则向这个端口号发送请求(请求的url:http://localhost:8081/status/),如果返回字符串为packager-status:running则说明Packager已经启动了。否则就退出脚本,返回退出码2。(注意:在编译过程中,执行的脚本退出码为非零,则编译就会退出并报错)。
        2. 如果没有被占用:则执行node_modules/react-native/scripts/launchPackager.command路径下的脚本。为了更直观的感受script文件夹下都有哪些东西,可见下图:
      • launchPackager.command脚本其实只做了非常简单的事情,只是将打印形式以及终端标题修改,然后再执行同级目录下的packager.sh脚本。
      • packager.sh脚本也仅有几行代码。在读取前面讲到的.packager.env文件,获取端口号之后,通过执行Node命令执行与scripts文件夹同级的local-cli文件夹下的cli.js文件,并传入当前脚本的所有参数。最终达到了与执行命令行时的效果———启动Packager服务。
  3. 小结通过iOS项目启动Packager服务的脚本解读,我们知道了在iOS项目中,启动脚本的入口在哪,以及层层递进找到了真正启动Packager服务的文件在哪里。在iOS项目中的这些设置,是ReactNative的命令行在生成项目时,为我们做的工作。如果我们是在原生项目中集成RN的话,我们就不得不自己手动做这些操作了。大多数情况下我们会使用CocoaPods来管理第三方的依赖库(当然也包括ReactNative)。因此对于启动Packager的脚本放置的位置应该是在Pods工程下了。

执行打包操作

不管我们是通过命令行启动App还是通过Xcode启动App,最终都会执行编译的过程。而在编译我们项目的Target时,仍会执行ReactNative帮我们插入的脚本。脚本的入口如下图:

我们可以看到,在这段脚本中,主要执行了react-native-xcode.sh文件中的脚本。下面就针对脚本的执行逻辑进行解读:

  1. 获取项目编译的路径,即编译时生成.app包的路径
  2. 根据项目配置(CONFIGURATION)判断是否是DEBUG模式,同时判断运行的平台(PLATFORM_NAME)是否不是模拟器(即是否在真机上)。若两个条件同时满足,则获取本机在当前局域网下的ip地址,并将这个ip地址写入到.app包路径下的ip.txt文件中。说明:这个ip.txt会用在真机调试下。在App启动时,在RCTBundleURLProvider对象寻找jsbundle路径时用到。此时返回Packager的URL的主机名不再是localhost,而是从ip.txt中获取的ip地址
  3. 判断环境变量SKIP_BUNDLING是否有值,如果有值,则退出脚本。不再进行后续的操作了。说明:这个参数在ReactNative初始化项目时,并没有给我们在iOS项目中显式的表达出来。需要我们自己指定,隐藏的很深啊!
  4. 针对环境变量CONFIGURATION的值,进行以下逻辑判断:
    1. 值为DEBUG;判断运行的平台是否是模拟器。如果是模拟器,则再判断环境变量FORCE_BUNDLING是否有值,如果有值则继续走打包的流程;如果没值,则退出脚本。这样做的目的是想要判断是否在模拟器下也进行打包操作(通过FORCE_BUNDLING来判断)。
    2. 值为空"";说明该脚本并不是在Xcode编译时运行的,因此也没必要再执行后续的操作了,停止指定脚本,退出码1。
    3. 其他值;说明此时的编译配置是Release模式,是需要打包成AdHoc包或上传到AppStore的。此时将脚本中的变量DEBUG的值设置为false(在其他分支上DEBUG都为true),这个值在后面执行打包操作时,会作为参数传给打包命令。
  5. 获取在node_modules文件夹下的reactnative文件夹的目录,以及我们创建的项目的根目录,并且切换路径(cd)到根目录。
  6. 获取根目录下的入口文件名,这个文件名可能是index.js(在较高RN版本项目中),也可能是index.ios.js(在较低RN版本项目中)
  7. 判断node环境是否存在,如果不存在的话,就退出脚本,退出码2,编译报错。
  8. 获取执行打包命令的文件cli.js路径,即是/reactnative/cli.js路径
  9. 获取打包的命令;即是bundle命令
  10. 执行打包命令;即运行node,执行cli文件,将之前准备的参数拼接好传递进去。打出来的main.jsbundle以及asserts文件夹都会放在编译生成的.app文件夹下,也即是放入到App包内了。

小结:

通过以上流程,我们知道了在iOS项目中,执行打包脚本的入口在哪里,以及打包脚本react-native-xcode.sh内部做了哪些操作。其实归根结底也就执行了我们日常使用命令行工具打包的bundle命令,打出来的jsbundle和asserts文件直接放入到了.app包内了。

react-nativebundle--entry-fileindex.js--platformios--devfalse--bundle-output"导出路径/main.jsbundle"--assets-dest"导出路径"

这一打包脚本中,值得我们关注的几个变量:

  1. SKIP_BUNDLING:是否跳过打包操作。我们在真机调试的过程中,一般都会将手机的wifi连接到与电脑同一局域网下(也可以说是同一wifi下),这样手机才能访问到Packager服务,进而获取服务器的js代码。如果调试的手机的网络和Packager服务(通常是运行项目的电脑)不是在同一局域网下,那么运行App时,由于访问不到Packager服务器,在请求失败后,App就会加载我们在编译过程生成的js包,从而避免崩溃。这套流程非常完美的解决了我们在真机调试下,不管手机处于什么网络,都能够正常的启动App了。尽管ReactNative尽心尽力的帮我们解决在开发中我们做得不规范操作所导致的App运行不起来的原因。但是这样也给我们带来了额外的开销。在大多数开发情况下,我们进行真机调试时,都会意识到将手机的网络设置成与电脑的网络处于同一局域网下(通俗点讲是:要设置成同一wifi下)。我们明确我们的目标是让App接连到Packager服务,而并非本地的jsbundle代码。但是我们却意外的承受了打包jsbundle所带来的额外时间开销。从另一方面来说,假设我们的手机处于与Packager服务不同的局域网下,在App启动时,会发送请求去验证Packager服务是否正常运行,在等待相应的过程中是异常的漫长,默认情况下我们需要等待60秒后(请求超时),返回结果失败了,才会去加载本地的jsbundle代码,App才能启动。此时我们花费的等待时间就太漫长了。如果开发者不愿意承受这种额外的开销。那么SKIP_BUNDLING变量就给了我们避免额外开销的一种解决方式。我们只需要在iOS项目执行打包脚本的入口将SKIP_BUNDLING的值设置为true即可。代码如下:
exportSKIP_BUNDLING=true

最终入口的截图:

2.FORCE_BUNDLING:该变量用来表明在模拟器上运行时,是否也要进行打包操作。我们已经知道App启动时,去哪里加载js代码的规则。有些时候,我们想在在模拟器上直接运行我们的App,但是我们又想启动Packager服务。那么我们就可以使用这个变量来让我们在编译App时,也需要将js代码打包到模拟器的.app包内。就可以达到这样的目的了设置的方法如同前面介绍的方法一样。只需要将exportFORCE_BUNDLING=true代码放入到iOS项目执行打包脚本的入口即可。

总结

我们介绍了ReactNative在iOS平台下,在启动App时所执行的脚本。对于日常开发具有哪些指导意义呢?

  1. 在原生项目想要集成RN时,我们介绍了应该如何去完善自己手动集成RN时,缺失的那部分脚本设置。
  2. 在优化编译速度以及我们的开发效率上,提出了几点意见。
  3. 对于了解RN的底层逻辑,方便我们日后问题排查和个性化定制提供基础。

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~