打造你的React Native腳手架


隨著對(duì)React Native的使用逐漸深入,團(tuán)隊(duì)積累了一些應(yīng)用上的最佳實(shí)踐,同時(shí)整理了一批基礎(chǔ)組件框咙。為了幫助其他團(tuán)隊(duì)快速使用React Native完成需求嚷辅、替換已有業(yè)務(wù)簿姨,同時(shí)也為了幫助新人快速上手、規(guī)范項(xiàng)目開(kāi)發(fā)簸搞,準(zhǔn)備對(duì)React Native進(jìn)行定制化改造扁位,最終形成GFRN。
首先的第一步是了解React Native腳手架工具趁俊,根據(jù)需要定制個(gè)性化腳手架域仇。

React Native Cli 分析

1. react-native-cli

React Native被發(fā)布為兩個(gè)npm包,分別為react-native-cli和react-native寺擂。其中暇务,react-native-cli需要被全局安裝,作為腳手架在命令行工具中使用怔软。
react-native-cli本身很輕量垦细,他的工作只是初始化項(xiàng)目目錄刮刑,本地安裝react-native表牢,并且將所有命令交給本地的react-native執(zhí)行。
react-native-cli 代碼地址涎显,分析其主要代碼

var cli;
//本地react-native目錄下的cli.js挚瘟,node_modules/react-native/cli.js
var cliPath = CLI_MODULE_PATH();
if (fs.existsSync(cliPath)) {
  cli = require(cliPath);
}

var commands = options._;
//本地react-native目錄下cli.js存在時(shí)叹谁,即已經(jīng)執(zhí)行過(guò)init完成初始化,執(zhí)行node_modules/react-native/cli.js的run方法
if (cli) {
  cli.run();
} else {
//本地react-native目錄下cli.js不存在乘盖,進(jìn)行參數(shù)校驗(yàn)焰檩。必須項(xiàng)為init命令和項(xiàng)目名
...
init(name,options)
}

//init方法檢查同名項(xiàng)目是否存在并提示,然后進(jìn)入createProject方法订框,在createProject方法中析苫,創(chuàng)建項(xiàng)目目錄,生成項(xiàng)目的package.json文件,進(jìn)入run(root, projectName, options)方法
function run(root, projectName, options) {
//檢查本地環(huán)境衩侥,判斷使用yarn或者npm
//調(diào)用getInstallPackage獲取準(zhǔn)備安裝的react-native版本国旷,默認(rèn)安裝最新穩(wěn)定版,腳手架支持-v參數(shù)指定版本
...
try {
    //安裝react-native
    execSync(installCommand, {stdio: 'inherit'});
  } catch (err) {
    console.error(err);
    console.error('Command `' + installCommand + '` failed.');
    process.exit(1);
  }
}
...
cli = require(CLI_MODULE_PATH());
//執(zhí)行node_modules/react-native/cli.js的init方法
cli.init(root, projectName);

上述示例中看到茫死,在執(zhí)行react-native init myApp初始化項(xiàng)目時(shí)跪但,會(huì)在本地生成項(xiàng)目目錄,安裝本地react-native后峦萎,執(zhí)行本地react-native目錄下cli.js init方法繼續(xù)完成初始化工作屡久。執(zhí)行react-native start等其他命令時(shí),會(huì)將命令交由本地react-native目錄下cli.js run方法完成命令執(zhí)行工作爱榔。

2. react-native init myApp 執(zhí)行過(guò)程

繼續(xù)閱讀node_modules/react-native/cli.js被环,發(fā)現(xiàn)其代碼很簡(jiǎn)單

module.exports = require('./local-cli/cli.js');

/local-cli/cli.js作為local-cli入口文件代碼也很簡(jiǎn)單,代碼邏輯是在/local-cli/cliEntry.js详幽,他輸出了前文提到的init和run方法

module.exports = {
  run: run,
  init: init,
};

init指向/local-cli/init/init.js,在init.js init方法中筛欢,調(diào)用generateProject()方法來(lái)創(chuàng)建項(xiàng)目。generateProject方法中唇聘,關(guān)鍵步驟是調(diào)用/local-cli/generator/templates.js createProjectFromTemplate()方法來(lái)根據(jù)模板生成初始模板代碼文件悴能,其代碼如下

function createProjectFromTemplate(destPath, newProjectName, template, yarnVersion) {
  // 以templates/HelloWorld項(xiàng)目為模板,將node_modules/react-native/local-cli/templates/HelloWorld文件拷貝到項(xiàng)目根目錄
  copyProjectTemplateAndReplace(
    path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'),
    destPath,
    newProjectName
  );

  if (template === undefined) {
    // 不指定模板參數(shù)template時(shí)雳灾,直接以HelloWorld項(xiàng)目作為模板項(xiàng)目
    return;
  }

//下段英文注釋為源碼中原有注釋漠酿,解釋了提供template參數(shù)時(shí),根據(jù)template參數(shù)創(chuàng)建模板項(xiàng)目時(shí)的做飯谎亩,提到了對(duì)模板項(xiàng)目的結(jié)構(gòu)要求
  // Keep the files from the 'HelloWorld' template, and overwrite some of them
  // with the specified project template.
  // The 'HelloWorld' template contains the native files (these are used by
  // all templates) and every other template only contains additional JS code.
  // Reason:
  // This way we don't have to duplicate the native files in every template.
  // If we duplicated them we'd make RN larger and risk that people would
  // forget to maintain all the copies so they would go out of sync.
  const builtInTemplateName = builtInTemplates[template];
  //node_modules/react-native/local-cli/templates/路徑下內(nèi)置模板
  if (builtInTemplateName) {
    createFromBuiltInTemplate(builtInTemplateName, destPath, newProjectName, yarnVersion);
  } else {
    // npm庫(kù)中模板炒嘲,template is e.g. 'ignite',
    // use the template react-native-template-ignite from npm
    createFromRemoteTemplate(template, destPath, newProjectName, yarnVersion);
  }
}

以上分析可知,react-native init myApp命令最終會(huì)根據(jù)node_modules/react-native/local-cli/templates/路徑下內(nèi)置模板來(lái)生成模板文件匈庭,如下即為模板項(xiàng)目的示例


3. react-native start 分析

前文提到夫凸,執(zhí)行react-native start等其他命令時(shí),會(huì)將命令交由本地react-native目錄下cli.js run方法完成命令執(zhí)行工作阱持。
繼續(xù)來(lái)看/local-cli/cliEntry.js

function run() {
  const setupEnvScript = /^win/.test(process.platform)
    ? 'setup_env.bat'
    : 'setup_env.sh';

  childProcess.execFileSync(path.join(__dirname, setupEnvScript));
  //遍歷commands,在addCommand方法中夭拌,通過(guò)commander庫(kù)注冊(cè)該命令
  commands.forEach(cmd => addCommand(cmd, config));
//通過(guò)commander庫(kù)解析當(dāng)前輸入
  commander.parse(process.argv);

  const isValidCommand = commands.find(cmd => cmd.name.split(' ')[0] === process.argv[2]);

  if (!isValidCommand) {
    printUnknownCommand(process.argv[2]);
    return;
  }

  if (!commander.args.length) {
    commander.help();
  }
}

上述代碼中,會(huì)首先注冊(cè)所有的命令衷咽,再根據(jù)當(dāng)前輸入匹配命中的命令去執(zhí)行鸽扁。
這里的commands來(lái)自/local-cli/commands.js,其中定義了當(dāng)前項(xiàng)目中已有的命令



觀察可知镶骗,在documentedCommands中列出的桶现,即我們常用的命令,這里的每個(gè)命令鼎姊,其結(jié)構(gòu)的定義如下

export type CommandT = {
  name: string,
  description?: string,
  usage?: string,
  func: (argv: Array<string>, config: RNConfig, args: Object) => ?Promise<void>,
  options?: Array<{
    command: string,
    description?: string,
    parse?: (val: string) => any,
    default?: ((config: RNConfig) => mixed) | mixed,
  }>,
  examples?: Array<{
    desc: string,
    cmd: string,
  }>,
  pkg?: {
    version: string,
    name: string,
  },
};

其中react-native start命令骡和,對(duì)應(yīng)引入的./server/server相赁。查看/local-cli/server/server.js可知,其輸出為上述結(jié)構(gòu)

module.exports = {
  name: 'start',
  func: server,
  description: 'starts the webserver',
  options: [{
    command: '--port [number]',
    default: 8081,
    parse: (val: string) => Number(val),
  }, ...//各種其他的option],
};

至此react-native start執(zhí)行過(guò)程已經(jīng)明確慰于,其他命令的執(zhí)行過(guò)程也是同樣钮科。

Create React Native App Cli 分析

了解了react-native-cli,再來(lái)看下當(dāng)前社區(qū)熱門的腳手架工具create-react-native-app婆赠。
create-react-native-app項(xiàng)目下包含兩個(gè)子項(xiàng)目绵脯,create-react-native-app和react-native-scripts,其中,create-react-native-app為命令行工具页藻。
create-react-native-app腳手架主要代碼

async function createApp(name: string, verbose: boolean, version: ?string): Promise<void> {
  //參數(shù)校驗(yàn)
//packageToInstall = 'react-native-scripts'
  const packageToInstall = getInstallPackage(version);
//生成項(xiàng)目目錄
  if (!await pathExists(name)) {
    await fse.mkdir(root);
  } else if (!await isSafeToCreateProjectIn(root)) {
    console.log(`The directory \`${name}\` contains file(s) that could conflict. Aborting.`);
    process.exit(1);
  }
//生成項(xiàng)目package.json文件
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  await fse.writeFile(path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2));

//調(diào)用run方法
  await run(root, appName, version, verbose, packageToInstall, packageName);
}

async function run(
  root: string,
  appName: string,
  version: ?string,
  verbose: boolean,
  packageToInstall: string,
  packageName: string
): Promise<void> {
//本地安裝react-native-scripts
  install(packageToInstall, verbose, async (code: number, command: string, args: Array<string>) => {
    ...
  const scriptsPath = path.resolve(
      process.cwd(),
      'node_modules',
      packageName,
      'build',
      'scripts',
      'init.js'
    );

    const init = require(scriptsPath);
//執(zhí)行node_modules/build/scripts/init.js init方法
    await init(root, appName, verbose, cwd);
  });
}

上述主要代碼邏輯看出,create-react-native-app腳手架代碼也很簡(jiǎn)單植兰,在生成項(xiàng)目目錄后份帐,主要工作就是安裝react-native-scripts并且調(diào)用react-native-scripts 的init.js完成初始化。而init.js中的主要代碼邏輯楣导,是將其template目錄下模板文件拷貝到項(xiàng)目根目錄废境,補(bǔ)充package.json文件并且安裝依賴,其支持的命令筒繁,配置在package.json scripts中噩凹。

React Native 腳手架定制

在我們的實(shí)際需求中,需要通過(guò)腳手架工具毡咏,生成的模板項(xiàng)目中驮宴,包含我們封裝的基礎(chǔ)庫(kù)和組件庫(kù),示例項(xiàng)目采用更有實(shí)際意義的demo呕缭,并且支持個(gè)性化的命令堵泽。

通過(guò)對(duì)react-native-cli, create-react-native-app源碼的分析發(fā)現(xiàn),實(shí)際上我們不需要重頭開(kāi)始開(kāi)發(fā)我們的腳手架工具恢总。

react-native-cli本身是具有可擴(kuò)展性的迎罗,在使用react-native init命令初始化項(xiàng)目時(shí),可以通過(guò)-template參數(shù)指定模板項(xiàng)目片仿,對(duì)于定制化方案來(lái)講纹安,可以將抽離的組件、基礎(chǔ)庫(kù)和demo代碼發(fā)布為一個(gè)npm包砂豌,以這樣的形式來(lái)初始化項(xiàng)目厢岂,是可以滿足生成定制化模板項(xiàng)目的需求的。

在分析react-native-cli時(shí)阳距,其命令是通過(guò)commands.js定義的咪笑,其中 ,documentedCommands是其內(nèi)置指令娄涩,如需自定義窗怒,一種方式可以再加一個(gè)組合customerCommands映跟,然后引入自定義指令,這種方式侵入到node_modules下進(jìn)行源碼級(jí)別的修改扬虚,這是一個(gè)react-native-cli擴(kuò)展性的不夠的地方努隙;另一種方式,是從getProjectCommands方法入手辜昵,這一方法會(huì)讀取一些配置項(xiàng)作為初始時(shí)的命令荸镊,這為我們提供了一個(gè)hook的方式,可以通過(guò)定義rn-cli.config.js文件堪置,并在其中覆蓋內(nèi)置的方法躬存,返回我們的自定義命令。才有這一方式舀锨,需要對(duì)hook的方法充分理解岭洲,避免產(chǎn)生意外的沖突。

基于react-native-cli已經(jīng)可以滿足定制化模板項(xiàng)目的腳手架需求坎匿,而create-react-native-app之所以能夠流行起來(lái)盾剩,是因?yàn)槠浜?jiǎn)化了react-native的環(huán)境配置。如果你的業(yè)務(wù)僅包含js代碼替蔬,無(wú)需額外原生依賴告私,可以使用配套的expo直接掃描本地server生成的二維碼來(lái)運(yùn)行代碼,同時(shí)承桥,expo-sdk也為開(kāi)發(fā)者提供了一套方便的組件驻粟。這種掃碼運(yùn)行簡(jiǎn)單代碼,快速查看效果的方式凶异,也可以集成到模板項(xiàng)目中格嗅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市唠帝,隨后出現(xiàn)的幾起案子屯掖,更是在濱河造成了極大的恐慌,老刑警劉巖襟衰,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贴铜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡瀑晒,警方通過(guò)查閱死者的電腦和手機(jī)绍坝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)苔悦,“玉大人轩褐,你說(shuō)我怎么就攤上這事【料辏” “怎么了把介?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵勤讽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拗踢,道長(zhǎng)脚牍,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任巢墅,我火速辦了婚禮诸狭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘君纫。我一直安慰自己驯遇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布蓄髓。 她就那樣靜靜地躺著叉庐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪双吆。 梳的紋絲不亂的頭發(fā)上眨唬,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天会前,我揣著相機(jī)與錄音好乐,去河邊找鬼。 笑死瓦宜,一個(gè)胖子當(dāng)著我的面吹牛蔚万,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播临庇,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼反璃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了假夺?” 一聲冷哼從身側(cè)響起淮蜈,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎已卷,沒(méi)想到半個(gè)月后梧田,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侧蘸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年裁眯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片讳癌。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡穿稳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晌坤,到底是詐尸還是另有隱情逢艘,我是刑警寧澤旦袋,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站埋虹,受9級(jí)特大地震影響猜憎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搔课,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一胰柑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爬泥,春花似錦柬讨、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至境输,卻和暖如春蔗牡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嗅剖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工辩越, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人信粮。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓黔攒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親强缘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子督惰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容