從這周開始掂摔,準備啟動一個新的專題《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
我們來逐行解釋一些:
-
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
在當前session中聲明一個環(huán)境變量RCT_METRO_PORT
宇驾,也就是這個bundle server的端口號倍靡,定義為8081 -
echo...
將這個端口號寫入了.packager.env
中,用于后面使用這個端口號 -
if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ]
檢查是否聲明了RCT_NO_LAUNCH_PACKAGER+xxx
這個環(huán)境變量课舍,-z
用于判斷這個變量的長度是否為0塌西,如果為0則執(zhí)行then后的腳本,這個環(huán)境變量的聲明用于調用方不想要啟動這個server布卡,比如在構建production包時 -
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步 -
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 -
echo "Port ${RCT_METRO_PORT} ..."
瘸右,輸出一個log娇跟,之后就exit 2
,此時也將break當前Xcode的build -
open "$SRCROOT/../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
運行launchPackager.command
太颤,如果失敗則輸出一個log苞俘。 -
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的身影钞翔。