快來跟我一起學(xué) React(Day2?)

簡介

繼續(xù)我們的 React 的學(xué)習(xí)次慢,上一節(jié)我們介紹了什么是 JSX 語法,并且從 Babel 源碼角度分析了 JSX 語法的轉(zhuǎn)換過程俱尼,最后我們還用 CDN 的形式搭建了一個簡單的 React 項目薄榛,這一節(jié)我們研究一下 React 官方提供的腳手架create-react-app

知識點(diǎn)

  • React 官方腳手架(create-react-app)
  • react-scripts
  • react 項目中的 webpack 配置
  • start 命令
  • build 命令

安裝 React

小伙伴可以先看一下官網(wǎng)的描述:

React 的安裝方式有兩種:

  1. CDN 鏈接窘俺。
  2. 使用React 官方腳手架(create-react-app)饲帅。

第一種我們上一節(jié)已經(jīng)使用過了,接下來我們從源碼角度介紹一下 create-react-app

你可以利用以下方式通過腳手架去創(chuàng)建 React 項目:

npx

npx create-react-app my-app

(npx 在 npm 5.2+ 才能使用灶泵,可以看這個 instructions for older npm versions)

npm

npm init react-app my-app

npm init 在 npm 6+ 才能使用

Yarn

yarn create react-app my-appe

yarn create 在 Yarn 0.25+ 才能使用

其實 npm inityarn create 就是 npx 的簡寫(但是在 npmyarn 中可以省略 create 字符串育八,直接 npm init react-appyarn create react-app 就可以了 ),工作流程大概是這樣的:

  1. 首先會判斷你本地有沒有 create-react-app 依賴赦邻,如果沒有的話就會去 npm 官方下載髓棋。
  2. 找到 create-react-app 依賴,執(zhí)行 create-react-app 聲明的 bin 入口文件惶洲。

我們還是來測試一下吧按声。

測試

首先在本地找一個目錄,然后執(zhí)行以下命令(以 npm 為例)恬吕,創(chuàng)建一個叫 react-demo1 的項目:

npm init react-app react-demo1

等執(zhí)行完畢后會看到一個新創(chuàng)建好的文件夾 react-demo1

1-1.png

然后我們在 react-demo1 目錄執(zhí)行 npm start 命令就可以啟動項目了:

npm start
1

可以看到儒喊,一個簡單的 React 項目就被創(chuàng)建完畢并啟動了。

React 官方腳手架(create-react-app)

我們從源碼角度分析一下币呵,當(dāng)我們執(zhí)行:

npm init react-app react-demo1

命令后怀愧,create-react-app 腳手架是如何幫我們創(chuàng)建項目的?

我們直接去官網(wǎng)下一份 create-react-app 的源碼:

create-react-app 源碼地址:https://github.com/facebook/create-react-app

1-3.png

可以看到余赢,create-react-app 是一個用 lerna 管理的項目集合芯义,所以接下來我們先安裝依賴:

lerna bootstrap || yarn 

本地沒有安裝 lerna 的話就直接用 yarn 去安裝。

當(dāng)我們執(zhí)行:

npm init react-app react-demo1

命令后妻柒,首先執(zhí)行的是 packages/create-react-app/index.js 文件(當(dāng)前版本 4.0.3):

...
const { init } = require('./createReactApp');
init();

可以看到扛拨,直接執(zhí)行了 ./createReactApp.js 文件的 init 方法:

function init() {
  const program = new commander.Command(packageJson.name)
    ...
    .action(name => {
      // 獲取傳遞的項目名 react-demo1
      projectName = name;
    })
    ...
    // 開始創(chuàng)建項目
    createApp(
      projectName, // 項目名
      program.verbose, // 是否顯示 npm 安裝具體信息
      program.scriptsVersion, // react-scripts 版本號
      program.template, // 模版名稱
      program.useNpm, // 是否使用 npm
      program.usePnp // 是否使用 pnp
    );
  ...
}

init 方法中獲取了一下傳遞進(jìn)來的項目名,然后調(diào)用了 createApp 方法:

function createApp(name, verbose, version, template, useNpm, usePnp) {
  // 項目根目錄
  const root = path.resolve(name);
  // 項目名
  const appName = path.basename(root);
    // 初始化項目 package.json 文件
  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL
  );
    // 開始創(chuàng)建
  run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp
  );
}

可以看到举塔,初始化了我們項目的 package.json 文件绑警,接著又執(zhí)行了 run 方法:

function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp
) {
  Promise.all([
    // 獲取 react-scripts 依賴基本信息
    getInstallPackage(version, originalDirectory),
    // 獲取項目模版依賴基本信息,默認(rèn)是 cra-templagte 模版
    getTemplateInstallPackage(template, originalDirectory),
  ]).then(([packageToInstall, templateToInstall]) => {
        ...
      .then(({ isOnline, packageInfo, templateInfo }) => {
        // 在項目根目錄安裝 react央渣、react-dom计盒、cra-tamplte 依賴
        return install(
          root,
          useYarn,
          usePnp,
          allDependencies,
          verbose,
          isOnline
        ).then(() => ({
          packageInfo,
          supportsTemplates,
          templateInfo,
        }));
      })
      .then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
       // 執(zhí)行當(dāng)前項目 react-demo1/node_modules/packageName/scripts/init.js 腳本文件
        await executeNodeScript(
          {
            cwd: process.cwd(),
            args: nodeArgs,
          },
          [root, appName, verbose, originalDirectory, templateName],
          `
        var init = require('${packageName}/scripts/init.js');
        init.apply(null, JSON.parse(process.argv[1]));
      `
        );
  });
}

可以看到,run 方法主要是安裝依賴芽丹,這些依賴是:

  • react:react api 基礎(chǔ)庫北启。

  • react-dom:react 核心庫拔第。

  • cra-template:react 項目模版。

    因為我們在創(chuàng)建項目的時候沒有指定項目模版懈涛,所以默認(rèn)是官方的 cra-template 模版,官方中有兩個模版:

    1. cra-template:默認(rèn)項目模版批钠。
    2. cra-tamplate-typescript:ts 項目模版泣港。

    當(dāng)然,還支持你傳遞自己的模版呛每,可以為 filenpm晨横、gitlab 類型,就不具體掩飾了手形。

接著執(zhí)行了當(dāng)前項目 react-demo1/node_modules/packageName/scripts/init.js 腳本文件:

// 初始化 git
function tryGitInit() {
 ...
}

module.exports = function (
  appPath,
  appName,
  verbose,
  originalDirectory,
  templateName
) {
  // 找到 react-demo1/nodule_modules/cra-template 目錄,然后按照規(guī)則 copy 文件到當(dāng)前 react-demo1 項目库糠,最后刪除 react-demo1/nodule_modules/cra-template 目錄
  console.log();
  // 恭喜創(chuàng)建完畢
  console.log('Happy hacking!');
};

到這伙狐,react-demo1 項目就算是創(chuàng)建完畢了。

start 命令

當(dāng)我們在剛創(chuàng)建好的 react-demo1 項目中執(zhí)行 npm start 命令的時候瞬欧,會自動幫我們開啟一個開發(fā)環(huán)境贷屎,并且打開入口頁面:

npm start

ok,我們看一下當(dāng)我們在項目根目錄執(zhí)行 npm start 命令到底干了什么艘虎?

首先是 react-demo1/package.json 文件中的 start 命令:

... 
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
...

可以看到唉侄,執(zhí)行了 react-scripts start 命令。

我們找到 react-scripts start 命令的源碼 create-react-app/packages/react-scripts/scripts/start.js

// 設(shè)置當(dāng)前環(huán)境變量為 development
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

// 開始項目中配置的環(huán)境變量
require('../config/env');
// 校驗 typescript 的配置
verifyTypeScriptSetup();
...
// 校驗入口文件跟入口 html 模版文件是否存在
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}
    ...
    // 創(chuàng)建 webpack 的編譯類
    const compiler = createCompiler({
      appName,
      config,
      devSocket,
      urls,
      useYarn,
      useTypeScript,
      tscCompileOnError,
      webpack,
    });
    // Load proxy config
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );
    // Serve webpack assets generated by the compiler over a web server.
    const serverConfig = createDevServerConfig(
      proxyConfig,
      urls.lanUrlForConfig
    );
        // 創(chuàng)建 WebpackDevServer 開啟 webpack 服務(wù)
    const devServer = new WebpackDevServer(compiler, serverConfig);
   ...
  });

start 命令其實就是利用 webpack-dev-server 開啟了一個 webpack 服務(wù)野建。(對 webpack 不熟的童鞋属划,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場戀愛吧:https://www.lanqiao.cn/courses/2893

build 命令

build 命令就不用說了,直接就是 webpack 的打包操作候生,比如我們在 react-demo1 目錄下執(zhí)行 build 命令:

npm run build
1-4.png

可以看到同眯,在 react-demo1/build 目錄中輸出了 webpack 打包過后的結(jié)果。

startbuild 都是利用的 webpack 進(jìn)行編譯打包操作的唯鸭,只是環(huán)境不同 webpack 的配置也會不同嗽测,下面我們重點(diǎn)看一下在 development 模式與 production 模式中,React 腳手架對 webpack 的配置肿孵。

React 項目中的 Webpack 配置

我們直接找到源碼 create-react-app/packages/react-scripts/config/webpack.config.js 文件:

...
// 是否生成 source-map 文件
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// webpack 客戶端熱載入口文件
const webpackDevClientEntry = require.resolve(
  'react-dev-utils/webpackHotDevClient'
);

// 是否禁止 eslint 警告提示
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
// 是否禁止 eslint
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
// 媒體文件字節(jié)限制唠粥,小于這個限制會打包成 base64 字符串,超出這個限制會導(dǎo)出文件
// 主要是指對 url-loader 的配置
const imageInlineSizeLimit = parseInt(
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

// 是否使用 ts
const useTypeScript = fs.existsSync(paths.appTsConfig);

// 根據(jù)環(huán)境返回不同的 webpack 配置停做,development 或者 production
module.exports = function (webpackEnv) {
  // 開發(fā)環(huán)境
  const isEnvDevelopment = webpackEnv === 'development';
  // 生產(chǎn)環(huán)境
  const isEnvProduction = webpackEnv === 'production';

  // 生成樣式 loaders 主要是 sass晤愧、scss、css
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      // 開發(fā)環(huán)境使用 style-loader(會生成內(nèi)嵌樣式)
      isEnvDevelopment && require.resolve('style-loader'), 
      // 生產(chǎn)環(huán)境使用 MiniCssExtractPlugin.loader (生成外聯(lián)樣式)
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        options: paths.publicUrlOrPath.startsWith('.')
          ? { publicPath: '../../' }
          : {},
      },
      // 配置 css-loader
      {
        loader: require.resolve('css-loader'),
        options: cssOptions,
      },
      // 配置 postcss-loader
      {
        loader: require.resolve('postcss-loader'),
        options: {
          postcssOptions: {
            plugins: [
              // flex 布局兼容插件
              require('postcss-flexbugs-fixes'),
              [
                // postcss env 插件集合
                require('postcss-preset-env'),
                {
                  // 自動添加樣式兼容前綴
                  autoprefixer: {
                    flexbox: 'no-2009',
                  },
                  stage: 3,
                },
              ],
              postcssNormalize(),
            ],
          },
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
    ].filter(Boolean);
    if (preProcessor) {
      // 添加根路徑解析 loader蛉腌,默認(rèn)指向項目 src 目錄
      loaders.push(
        {
          loader: require.resolve('resolve-url-loader'),
          options: {
            sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
            root: paths.appSrc,
          },
        },
        // 添加 sass loader 等樣式預(yù)加載器
        {
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: true,
          },
        }
      );
    }
    return loaders;
  };

  return {
    // 設(shè)置 webpack 的模式
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
        // 設(shè)置 source-map 的生成方式
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? 'source-map'
        : false
      : isEnvDevelopment && 'cheap-module-source-map',
    // 入口文件配置
    entry:
      isEnvDevelopment && !shouldUseReactRefresh
        ? [
            // 測試環(huán)境并且允許熱載刷新頁面的時候
            // 加載熱載刷新入口文件
            webpackDevClientEntry,
            // 加載項目入口文件(默認(rèn) src/index.js)
            paths.appIndexJs,
          ]
        : paths.appIndexJs,
    // 輸出文件設(shè)置
    output: {
      // 輸出目錄(默認(rèn)是項目的 build 目錄)
      path: isEnvProduction ? paths.appBuild : undefined,
        // 開發(fā)環(huán)境打開模塊的 pathinfo 路徑提示
      pathinfo: isEnvDevelopment,
        // 輸出文 chunk官份、assets 名稱設(shè)置
      filename: isEnvProduction
      ...
};

太多了只厘,就不一一分析了,小伙伴自己看一下源碼文件哦(對 webpack 不熟的童鞋舅巷,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場戀愛吧:https://www.lanqiao.cn/courses/2893)羔味。

那有小伙伴要問了,既然 React 腳手架幫我們內(nèi)置了 webpack 的配置赋元,如果我們需要自己修改 webpack 的一些配置該咋辦呢飒房?

比如我們需要修改以下配置:

修改輸出的目錄

從源碼中我們可以知道狠毯,目前項目的輸出文件的目錄為 build,比如我們需要改成 dist嫡良,我們需要怎么做呢皆刺?

我們先看一下目前的配置文件 packages/react-scripts/config/webpack.config.js

  const paths = require('./paths');
  ...
   output: {
      // The build folder.
      path: isEnvProduction ? paths.appBuild : undefined,

可以看到羡蛾,當(dāng)為生產(chǎn)環(huán)境(production)的時候锨亏,path 的值為 paths.appBuild器予。

我們找到 packages/react-scripts/config/paths.js 文件中的 appBuild 變量:

...
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
...
// 默認(rèn)輸出文件目錄路徑
const buildPath = process.env.BUILD_PATH || 'build';
module.exports = {
    ...
  appBuild: resolveApp(buildPath),
  ...

可以看到乾翔,我們可以通過 process.env.BUILD_PATH 變量去修改輸出文件路徑。

那么 process.env.BUILD_PATH 我們該怎么定義呢萌丈?

  1. 利用 cross-env 庫辆雾,在執(zhí)行命令的時候聲明 process.env.BUILD_PATH 變量度迂。

    我們首先在 react-demo1 項目根目錄安裝 cross-env

    yarn add -D cross-env
    

    接著修改一下 package.json 中的 build 命令:

     "scripts": {
        "build": "cross-env BUILD_PATH=dist react-scripts build",
       ...
     }
    

    修改完畢后重新打包測試:

    npm run build
    
1-5.png

可以看到,打包輸出的目錄變成了 dist坛梁。

  1. 利用腳手架提供的環(huán)境變量文件 .env.[NODE_ENV].[local] 來修改划咐,其中 NODE_ENVlocal 可選,表示根據(jù)環(huán)境來加載丈莺。

    我們在 react-demo1 項目根目錄底下創(chuàng)建一個 .env 文件缔俄,這樣不管是 development 模式還是 production 模式,都會加載 .env 文件中聲明的變量:

    touch ./.env
    

    然后在 .env 文件中聲明 BUILD_PATH 變量為 dist

    ## 修改項目的輸出路徑
    BUILD_PATH=dist
    

    修改完畢后重新打包測試蟹略,效果跟上面的一樣挖炬,我就不演示了意敛。

總結(jié)

這一節(jié)我們主要介紹了 React 官方提供的腳手架 create-react-app膛虫,我們直接從源碼的角度來分析了一個 React 項目創(chuàng)建的過程稍刀,其實無非就是對 Webpack 的一些配置而已,所以對 Webpack 不熟悉的小伙伴一定要加油補(bǔ)上哦综膀,從create-react-app 官方文檔上看僧须,并沒有提供 .env 配置文件的說明项炼、怎么去修改 webpack 配置說明等等,還是需要你自己去看源碼的暂论,所以這就是看源碼的重要性取胎,其實從源碼中我們可以知道,并不是所有的 webpack 配置都能修改的匪傍,那項目中我們又需要修改的話役衡,該怎么辦呢薪棒?那就只能拋棄腳手架了俐芯,所以這也算是 React 腳手架的一些不足吧,并沒有像 vue-cli 一樣邮辽,可以隨意修改 webpack 的配置逆巍。

ok锐极,后面我將會帶大家脫離腳手架芳肌,利用 webpack0 開始搭建一個 React 項目亿笤,大家敬請期待吧!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末汪榔,一起剝皮案震驚了整個濱河市痴腌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锦援,老刑警劉巖灵寺,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件略板,死亡現(xiàn)場離奇詭異叮称,居然都是意外死亡胀糜,警方通過查閱死者的電腦和手機(jī)蒂誉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門括堤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绍移,“玉大人蹂窖,你說我怎么就攤上這事『崦模” “怎么了灯蝴?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵穷躁,是天一觀的道長因妇。 經(jīng)常有香客問我猿诸,道長两芳,這世上最難降的妖魔是什么怖辆? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任竖螃,我火速辦了婚禮逗余,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腻格。我一直安慰自己菜职,他們只是感情好旗闽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布适室。 她就那樣靜靜地躺著,像睡著了一般蔬螟。 火紅的嫁衣襯著肌膚如雪旧巾。 梳的紋絲不亂的頭發(fā)上整袁,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天坐昙,我揣著相機(jī)與錄音,去河邊找鬼戈钢。 笑死,一個胖子當(dāng)著我的面吹牛殉了,可吹牛的內(nèi)容都是我干的薪铜。 我是一名探鬼主播隔箍,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蜒滩,長吁一口氣:“原來是場噩夢啊……” “哼奶稠!你這毒婦竟也來了锌订?” 一聲冷哼從身側(cè)響起瀑志,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤污秆,失蹤者是張志新(化名)和其女友劉穎良拼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體常侦,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡聋亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年坡倔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片投蝉。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘩缆,死狀恐怖庸娱,靈堂內(nèi)的尸體忽然破棺而出谐算,到底是詐尸還是另有隱情,我是刑警寧澤臣樱,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布雇毫,位于F島的核電站棚放,受9級特大地震影響飘蚯,放射性物質(zhì)發(fā)生泄漏局骤。R本人自食惡果不足惜暴凑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一现喳、第九天 我趴在偏房一處隱蔽的房頂上張望嗦篱。 院中可真熱鬧灸促,春花似錦狮腿、人聲如沸缘厢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亦渗。三九已至,卻和暖如春多律,著一層夾襖步出監(jiān)牢的瞬間狼荞,已是汗流浹背相味。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工丰涉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留一死,地道東北人输拇。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓策吠,卻偏偏與公主長得像猴抹,于是被迫代替她去往敵國和親蟀给。 傳聞我的和親對象是個殘疾皇子跋理,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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