React Native 拆包實踐1 - bundle server的啟動過程

從這周開始掂摔,準備啟動一個新的專題《React Native拆包實踐》惶翻,目的是想完成jsbundle的拆分得运,分為基礎包和業(yè)務包膝蜈,從而實現(xiàn)js的按需加載,其一可以提高啟動速度熔掺,其二可以變相實現(xiàn)一個App中同時容納多個RN模塊饱搏。

先來一個預熱吧,當我們使用react-native run-ios或通過Xcode啟動RN項目時置逻,都會自動啟動一個bundle server推沸,用來在dev模式下加載js代碼,那么這部分是如何實現(xiàn)的呢?使用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中聲明一個環(huán)境變量RCT_METRO_PORT宇驾,也就是這個bundle server的端口號倍靡,定義為8081
  2. echo... 將這個端口號寫入了.packager.env中,用于后面使用這個端口號
  3. if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] 檢查是否聲明了RCT_NO_LAUNCH_PACKAGER+xxx這個環(huán)境變量课舍,-z用于判斷這個變量的長度是否為0塌西,如果為0則執(zhí)行then后的腳本,這個環(huán)境變量的聲明用于調用方不想要啟動這個server布卡,比如在構建production包時
  4. if nc -w 5 -z localhost ${RCT_METRO_PORT} 當步驟3中沒有聲明那個環(huán)境變量時雨让,將做這個檢測雇盖。nc -w 5 -z localhost 8081:使用Natcat工具忿等,-w 5掃描5秒,-z localhost 8081掃描localhost的8081端口崔挖。當這個端口已經被占用時贸街,執(zhí)行第5步,當沒有被占用時狸相,執(zhí)行第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發(fā)送一個get請求逸尖,再由| grep -q "packager-status:running"看看response中是否包含packager-status:running。當不包含時執(zhí)行步驟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

其實就執(zhí)行了當前目錄下的另一個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后龄章,最后執(zhí)行:"$NODE_BINARY" "$REACT_NATIVE_ROOT/cli.js" start "$@"吃谣,替換掉環(huán)境變量之后為:node ../cli.js start,$@指定的是傳入的所有參數(shù)做裙,而在調用packager.sh時沒有任何參數(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下找不到這個依賴澜驮,具體的實現(xiàn)可查閱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()的實現(xiàn)大致如下耐量,可以理解他做的事情就是初始化:

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

...

async function setupAndRun() {
  // config 日志 和 運行環(huán)境
  ...
  // detachedCommands 追蹤源碼其實就是react-native 的 init 命令飞蚓,用于創(chuàng)建一個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();
    // 繼續(xù)加載其他的 命令行,只是加載并不執(zhí)行廊蜒,其中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趴拧,在這個腳本中,最后執(zhí)行了:
"$NODE_BINARY" "$REACT_NATIVE_ROOT/cli.js" start "$@"山叮,其中這個start在哪定義的呢著榴?
根據(jù)上面對cli.run()的分析,其中加載了很多命令全局命令行中屁倔,其中未見start命令脑又,其實他藏在server命令中,也就是projectCommands中的一個命令锐借,它的定義如下问麸,它的name就是start,而實現(xiàn)就是這個runServer

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

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

終于在runServer中看到了Metro的身影钞翔。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末严卖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子布轿,更是在濱河造成了極大的恐慌哮笆,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汰扭,死亡現(xiàn)場離奇詭異稠肘,居然都是意外死亡,警方通過查閱死者的電腦和手機东且,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門启具,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人珊泳,你說我怎么就攤上這事鲁冯。” “怎么了色查?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵薯演,是天一觀的道長。 經常有香客問我秧了,道長跨扮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮衡创,結果婚禮上帝嗡,老公的妹妹穿的比我還像新娘。我一直安慰自己璃氢,他們只是感情好哟玷,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著一也,像睡著了一般巢寡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椰苟,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天抑月,我揣著相機與錄音,去河邊找鬼舆蝴。 笑死谦絮,一個胖子當著我的面吹牛,可吹牛的內容都是我干的须误。 我是一名探鬼主播挨稿,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼仇轻,長吁一口氣:“原來是場噩夢啊……” “哼京痢!你這毒婦竟也來了?” 一聲冷哼從身側響起篷店,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤祭椰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疲陕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體方淤,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年蹄殃,在試婚紗的時候發(fā)現(xiàn)自己被綠了携茂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡诅岩,死狀恐怖讳苦,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情吩谦,我是刑警寧澤鸳谜,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站式廷,受9級特大地震影響咐扭,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一蝗肪、第九天 我趴在偏房一處隱蔽的房頂上張望袜爪。 院中可真熱鬧,春花似錦薛闪、人聲如沸饿敲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怀各。三九已至,卻和暖如春术浪,著一層夾襖步出監(jiān)牢的瞬間瓢对,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工胰苏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留硕蛹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓硕并,卻偏偏與公主長得像法焰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倔毙,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容