React Native 拆包实践1 - bundle server的启动过程

从这周开始,准备启动一个新的专题《React Native拆包实践》,目的是想完成jsbundle的拆分,分为基础包和业务包,从而实现js的按需加载,其一可以提高启动速度,其二可以变相实现一个App中同时容纳多个RN模块。

先来一个预热吧,当我们使用react-native run-ios或通过Xcode启动RN项目时,都会自动启动一个bundle server,用来在dev模式下加载js代码,那么这部分是如何实现的呢?使用Xcode开启RN项目,可以看到在build phases中有一步名为Start Packager的操作,其中有一段shell脚本,如下所示:

export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../node_modules/react-native/scripts/.packager.env"
if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] ; then
  if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
    if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
      echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"
      exit 2
    fi
  else
    open "$SRCROOT/../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
  fi
fi

我们来逐行解释一些:

  1. export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}" 在当前session中声明一个环境变量RCT_METRO_PORT,也就是这个bundle server的端口号,定义为8081
  2. echo... 将这个端口号写入了.packager.env中,用于后面使用这个端口号
  3. if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] 检查是否声明了RCT_NO_LAUNCH_PACKAGER+xxx这个环境变量,-z用于判断这个变量的长度是否为0,如果为0则执行then后的脚本,这个环境变量的声明用于调用方不想要启动这个server,比如在构建production包时
  4. if nc -w 5 -z localhost ${RCT_METRO_PORT} 当步骤3中没有声明那个环境变量时,将做这个检测。nc -w 5 -z localhost 8081:使用Natcat工具,-w 5扫描5秒,-z localhost 8081扫描localhost的8081端口。当这个端口已经被占用时,执行第5步,当没有被占用时,执行第7步
  5. if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running",当端口被占用时,需要检测一下这个端口是不是被之前启动好的bundle server在使用(比如在之前已经运行了npm run start)。检测的方式是向http://localhost:${RCT_METRO_PORT}/status发送一个get请求,再由| grep -q "packager-status:running"看看response中是否包含packager-status:running。当不包含时执行步骤6
  6. echo "Port ${RCT_METRO_PORT} ...",输出一个log,之后就exit 2,此时也将break当前Xcode的build
  7. open "$SRCROOT/../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically" 运行launchPackager.command,如果失败则输出一个log。
  8. launchPackager.command是什么呢?代码如下
#!/bin/bash
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

# Set terminal title
echo -en "\\033]0;Metro Bundler\\a"
clear

THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)

# shellcheck source=/dev/null
. "$THIS_DIR/packager.sh"

if [[ -z "$CI" ]]; then
  echo "Process terminated. Press <enter> to close the window"
  read -r
fi

其实就执行了当前目录下的另一个shell脚本packager.sh:

#!/bin/bash
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

# scripts directory
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
REACT_NATIVE_ROOT="$THIS_DIR/.."
# Application root directory - General use case: react-native is a dependency
PROJECT_ROOT="$THIS_DIR/../../.."

# check and assign NODE_BINARY env
# shellcheck disable=SC1090
source "${THIS_DIR}/node-binary.sh"

# When running react-native tests, react-native doesn't live in node_modules but in the PROJECT_ROOT
if [ ! -d "$PROJECT_ROOT/node_modules/react-native" ];
then
  PROJECT_ROOT="$THIS_DIR/.."
fi
# Start packager from PROJECT_ROOT
cd "$PROJECT_ROOT" || exit
"$NODE_BINARY" "$REACT_NATIVE_ROOT/cli.js" start "$@"

做了一些check后,最后执行:"$NODE_BINARY" "$REACT_NATIVE_ROOT/cli.js" start "$@",替换掉环境变量之后为:node ../cli.js start,$@指定的是传入的所有参数,而在调用packager.sh时没有任何参数。cli.js代码如下,这个cli其实就是Metro的CLI工具了:

#!/usr/bin/env node
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

'use strict';

var cli = require('@react-native-community/cli');

if (require.main === module) {
  cli.run();
}

module.exports = cli;

在这个js文件中仅仅是把@react-native-community/cli这个命令行工具expose出去了,在node_module下找不到这个依赖,具体的实现可查阅https://github.com/react-native-community/cli。具体的路径为./packages/cli/src/commands/index.ts。由于笔者的js功底有限,并未理解require.main === module的含义,有兴趣可以参考stackoverflow的文章https://stackoverflow.com/questions/45136831/node-js-require-main-module
cli.run()的实现大致如下,可以理解他做的事情就是初始化:

async function run() {
  try {
    await setupAndRun();
  } catch (e) {
    handleError(e);
  }
}

...

async function setupAndRun() {
  // config 日志 和 运行环境
  ...
  // detachedCommands 追踪源码其实就是react-native 的 init 命令,用于创建一个rn项目
  for (const command of detachedCommands) {
    // Attaches a new command onto global `commander` instance.
    attachCommand(command);
  }

  try {
    // when we run `config`, we don't want to output anything to the console. We
    // expect it to return valid JSON
    if (process.argv.includes('config')) {
      logger.disable();
    }

    const ctx = loadConfig();

    logger.enable();
    // 继续加载其他的 命令行,只是加载并不执行,其中projectCommands包括了
    // export const projectCommands = [
    //   server,
    //   bundle,
    //   ramBundle,
    //   link,
    //   unlink,
    //   install,
    //   uninstall,
    //   upgrade,
    //   info,
    //   config,
    //   doctor,
    // ] as Command[];
    for (const command of [...projectCommands, ...ctx.commands]) {
      attachCommand(command, ctx);
    }
  } catch (e) {
    logger.enable();
    logger.debug(e.message);
    logger.debug(
      'Failed to load configuration of your project. Only a subset of commands will be available.',
    );
  }

  commander.parse(process.argv);

  if (commander.rawArgs.length === 2) {
    commander.outputHelp();
  }

  // We handle --version as a special case like this because both `commander`
  // and `yargs` append it to every command and we don't want to do that.
  // E.g. outside command `init` has --version flag and we want to preserve it.
  if (commander.args.length === 0 && commander.rawArgs.includes('--version')) {
    console.log(pkgJson.version);
  }
}

回到packager.sh,在这个脚本中,最后执行了:
"$NODE_BINARY" "$REACT_NATIVE_ROOT/cli.js" start "$@",其中这个start在哪定义的呢?
根据上面对cli.run()的分析,其中加载了很多命令全局命令行中,其中未见start命令,其实他藏在server命令中,也就是projectCommands中的一个命令,它的定义如下,它的name就是start,而实现就是这个runServer

import path from 'path';
import runServer from './runServer';

export default {
  name: 'start',
  func: runServer,
  description: 'starts the webserver',
  options: [
    ...
  ],
};

终于在runServer中看到了Metro的身影。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容