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

簡(jiǎn)介

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

知識(shí)點(diǎn)

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

安裝 React

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

React 的安裝方式有兩種:

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

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

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

npx

npx create-react-app my-app

(npx 在 npm 5.2+ 才能使用,可以看這個(gè) 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+ 才能使用

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

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

我們還是來測(cè)試一下吧。

測(cè)試

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

npm init react-app react-demo1

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

1-1.png

然后我們?cè)?react-demo1 目錄執(zhí)行 npm start 命令就可以啟動(dòng)項(xiàng)目了:

npm start
1

可以看到,一個(gè)簡(jiǎn)單的 React 項(xiàng)目就被創(chuàng)建完畢并啟動(dòng)了。

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

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

npm init react-app react-demo1

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

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

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

1-3.png

可以看到,create-react-app 是一個(gè)用 lerna 管理的項(xiàng)目集合,所以接下來我們先安裝依賴:

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 => {
      // 獲取傳遞的項(xiàng)目名 react-demo1
      projectName = name;
    })
    ...
    // 開始創(chuàng)建項(xiàng)目
    createApp(
      projectName, // 項(xiàng)目名
      program.verbose, // 是否顯示 npm 安裝具體信息
      program.scriptsVersion, // react-scripts 版本號(hào)
      program.template, // 模版名稱
      program.useNpm, // 是否使用 npm
      program.usePnp // 是否使用 pnp
    );
  ...
}

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

function createApp(name, verbose, version, template, useNpm, usePnp) {
  // 項(xiàng)目根目錄
  const root = path.resolve(name);
  // 項(xiàng)目名
  const appName = path.basename(root);
    // 初始化項(xiàng)目 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
  );
}

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

function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp
) {
  Promise.all([
    // 獲取 react-scripts 依賴基本信息
    getInstallPackage(version, originalDirectory),
    // 獲取項(xiàng)目模版依賴基本信息,默認(rèn)是 cra-templagte 模版
    getTemplateInstallPackage(template, originalDirectory),
  ]).then(([packageToInstall, templateToInstall]) => {
        ...
      .then(({ isOnline, packageInfo, templateInfo }) => {
        // 在項(xiàng)目根目錄安裝 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)前項(xià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 項(xiàng)目模版。

    因?yàn)槲覀冊(cè)趧?chuàng)建項(xiàng)目的時(shí)候沒有指定項(xiàng)目模版,所以默認(rèn)是官方的 cra-template 模版,官方中有兩個(gè)模版:

    1. cra-template:默認(rèn)項(xiàng)目模版。
    2. cra-tamplate-typescript:ts 項(xiàng)目模版。

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

接著執(zhí)行了當(dāng)前項(xià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 項(xiàng)目,最后刪除 react-demo1/nodule_modules/cra-template 目錄
  console.log();
  // 恭喜創(chuàng)建完畢
  console.log('Happy hacking!');
};

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

start 命令

當(dāng)我們?cè)趧倓?chuàng)建好的 react-demo1 項(xiàng)目中執(zhí)行 npm start 命令的時(shí)候,會(huì)自動(dòng)幫我們開啟一個(gè)開發(fā)環(huán)境,并且打開入口頁面:

npm start

ok,我們看一下當(dāng)我們?cè)陧?xià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';

// 開始項(xiàng)目中配置的環(huán)境變量
require('../config/env');
// 校驗(yàn) typescript 的配置
verifyTypeScriptSetup();
...
// 校驗(yàn)入口文件跟入口 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 命令其實(shí)就是利用 webpack-dev-server 開啟了一個(gè) webpack 服務(wù)。(對(duì) webpack 不熟的童鞋,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場(chǎng)戀愛吧:https://www.lanqiao.cn/courses/2893

build 命令

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

npm run build
1-4.png

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

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

React 項(xiàng)目中的 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é)限制,小于這個(gè)限制會(huì)打包成 base64 字符串,超出這個(gè)限制會(huì)導(dǎo)出文件
// 主要是指對(duì) 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(會(huì)生成內(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'),
                {
                  // 自動(dòng)添加樣式兼容前綴
                  autoprefixer: {
                    flexbox: 'no-2009',
                  },
                  stage: 3,
                },
              ],
              postcssNormalize(),
            ],
          },
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
    ].filter(Boolean);
    if (preProcessor) {
      // 添加根路徑解析 loader,默認(rèn)指向項(xiàng)目 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
        ? [
            // 測(cè)試環(huán)境并且允許熱載刷新頁面的時(shí)候
            // 加載熱載刷新入口文件
            webpackDevClientEntry,
            // 加載項(xiàng)目入口文件(默認(rèn) src/index.js)
            paths.appIndexJs,
          ]
        : paths.appIndexJs,
    // 輸出文件設(shè)置
    output: {
      // 輸出目錄(默認(rèn)是項(xiàng)目的 build 目錄)
      path: isEnvProduction ? paths.appBuild : undefined,
        // 開發(fā)環(huán)境打開模塊的 pathinfo 路徑提示
      pathinfo: isEnvDevelopment,
        // 輸出文 chunk、assets 名稱設(shè)置
      filename: isEnvProduction
      ...
};

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

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

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

修改輸出的目錄

從源碼中我們可以知道,目前項(xiàng)目的輸出文件的目錄為 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)的時(shí)候,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 我們?cè)撛趺炊x呢?

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

    我們首先在 react-demo1 項(xiàng)目根目錄安裝 cross-env

    yarn add -D cross-env
    

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

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

    修改完畢后重新打包測(cè)試:

    npm run build
    
1-5.png

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

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

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

    touch ./.env
    

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

    ## 修改項(xiàng)目的輸出路徑
    BUILD_PATH=dist
    

    修改完畢后重新打包測(cè)試,效果跟上面的一樣,我就不演示了。

總結(jié)

這一節(jié)我們主要介紹了 React 官方提供的腳手架 create-react-app,我們直接從源碼的角度來分析了一個(gè) React 項(xiàng)目創(chuàng)建的過程,其實(shí)無非就是對(duì) Webpack 的一些配置而已,所以對(duì) Webpack 不熟悉的小伙伴一定要加油補(bǔ)上哦,從create-react-app 官方文檔上看,并沒有提供 .env 配置文件的說明、怎么去修改 webpack 配置說明等等,還是需要你自己去看源碼的,所以這就是看源碼的重要性,其實(shí)從源碼中我們可以知道,并不是所有的 webpack 配置都能修改的,那項(xiàng)目中我們又需要修改的話,該怎么辦呢?那就只能拋棄腳手架了,所以這也算是 React 腳手架的一些不足吧,并沒有像 vue-cli 一樣,可以隨意修改 webpack 的配置。

ok,后面我將會(huì)帶大家脫離腳手架,利用 webpack0 開始搭建一個(gè) React 項(xiàng)目,大家敬請(qǐng)期待吧!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容