快来跟我一起学 React(Day2️)

简介

继续我们的 React 的学习,上一节我们介绍了什么是 JSX 语法,并且从 Babel 源码角度分析了 JSX 语法的转换过程,最后我们还用 CDN 的形式搭建了一个简单的 React 项目,这一节我们研究一下 React 官方提供的脚手架create-react-app

知识点

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

安装 React

小伙伴可以先看一下官网的描述:

React 的安装方式有两种:

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

第一种我们上一节已经使用过了,接下来我们从源码角度介绍一下 create-react-app

你可以利用以下方式通过脚手架去创建 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 依赖,执行 create-react-app 声明的 bin 入口文件。

我们还是来测试一下吧。

测试

首先在本地找一个目录,然后执行以下命令(以 npm 为例),创建一个叫 react-demo1 的项目:

npm init react-app react-demo1

等执行完毕后会看到一个新创建好的文件夹 react-demo1

1-1.png

然后我们在 react-demo1 目录执行 npm start 命令就可以启动项目了:

npm start
1

可以看到,一个简单的 React 项目就被创建完毕并启动了。

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

我们从源码角度分析一下,当我们执行:

npm init react-app react-demo1

命令后,create-react-app 脚手架是如何帮我们创建项目的?

我们直接去官网下一份 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 去安装。

当我们执行:

npm init react-app react-demo1

命令后,首先执行的是 packages/create-react-app/index.js 文件(当前版本 4.0.3):

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

可以看到,直接执行了 ./createReactApp.js 文件的 init 方法:

function init() {
  const program = new commander.Command(packageJson.name)
    ...
    .action(name => {
      // 获取传递的项目名 react-demo1
      projectName = name;
    })
    ...
    // 开始创建项目
    createApp(
      projectName, // 项目名
      program.verbose, // 是否显示 npm 安装具体信息
      program.scriptsVersion, // react-scripts 版本号
      program.template, // 模版名称
      program.useNpm, // 是否使用 npm
      program.usePnp // 是否使用 pnp
    );
  ...
}

init 方法中获取了一下传递进来的项目名,然后调用了 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
  );
    // 开始创建
  run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp
  );
}

可以看到,初始化了我们项目的 package.json 文件,接着又执行了 run 方法:

function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp
) {
  Promise.all([
    // 获取 react-scripts 依赖基本信息
    getInstallPackage(version, originalDirectory),
    // 获取项目模版依赖基本信息,默认是 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 }) => {
       // 执行当前项目 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 基础库。

  • react-dom:react 核心库。

  • cra-template:react 项目模版。

    因为我们在创建项目的时候没有指定项目模版,所以默认是官方的 cra-template 模版,官方中有两个模版:

    1. cra-template:默认项目模版。
    2. cra-tamplate-typescript:ts 项目模版。

    当然,还支持你传递自己的模版,可以为 filenpmgitlab 类型,就不具体掩饰了。

接着执行了当前项目 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 目录,然后按照规则 copy 文件到当前 react-demo1 项目,最后删除 react-demo1/nodule_modules/cra-template 目录
  console.log();
  // 恭喜创建完毕
  console.log('Happy hacking!');
};

到这,react-demo1 项目就算是创建完毕了。

start 命令

当我们在刚创建好的 react-demo1 项目中执行 npm start 命令的时候,会自动帮我们开启一个开发环境,并且打开入口页面:

npm start

ok,我们看一下当我们在项目根目录执行 npm start 命令到底干了什么?

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

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

可以看到,执行了 react-scripts start 命令。

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

// 设置当前环境变量为 development
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

// 开始项目中配置的环境变量
require('../config/env');
// 校验 typescript 的配置
verifyTypeScriptSetup();
...
// 校验入口文件跟入口 html 模版文件是否存在
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}
    ...
    // 创建 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
    );
        // 创建 WebpackDevServer 开启 webpack 服务
    const devServer = new WebpackDevServer(compiler, serverConfig);
   ...
  });

start 命令其实就是利用 webpack-dev-server 开启了一个 webpack 服务。(对 webpack 不熟的童鞋,强烈推荐我之前写的文章 来和 webpack 谈场恋爱吧:https://www.lanqiao.cn/courses/2893

build 命令

build 命令就不用说了,直接就是 webpack 的打包操作,比如我们在 react-demo1 目录下执行 build 命令:

npm run build
1-4.png

可以看到,在 react-demo1/build 目录中输出了 webpack 打包过后的结果。

startbuild 都是利用的 webpack 进行编译打包操作的,只是环境不同 webpack 的配置也会不同,下面我们重点看一下在 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';
// 媒体文件字节限制,小于这个限制会打包成 base64 字符串,超出这个限制会导出文件
// 主要是指对 url-loader 的配置
const imageInlineSizeLimit = parseInt(
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

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

// 根据环境返回不同的 webpack 配置,development 或者 production
module.exports = function (webpackEnv) {
  // 开发环境
  const isEnvDevelopment = webpackEnv === 'development';
  // 生产环境
  const isEnvProduction = webpackEnv === 'production';

  // 生成样式 loaders 主要是 sass、scss、css
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      // 开发环境使用 style-loader(会生成内嵌样式)
      isEnvDevelopment && require.resolve('style-loader'), 
      // 生产环境使用 MiniCssExtractPlugin.loader (生成外联样式)
      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,默认指向项目 src 目录
      loaders.push(
        {
          loader: require.resolve('resolve-url-loader'),
          options: {
            sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
            root: paths.appSrc,
          },
        },
        // 添加 sass loader 等样式预加载器
        {
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: true,
          },
        }
      );
    }
    return loaders;
  };

  return {
    // 设置 webpack 的模式
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
        // 设置 source-map 的生成方式
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? 'source-map'
        : false
      : isEnvDevelopment && 'cheap-module-source-map',
    // 入口文件配置
    entry:
      isEnvDevelopment && !shouldUseReactRefresh
        ? [
            // 测试环境并且允许热载刷新页面的时候
            // 加载热载刷新入口文件
            webpackDevClientEntry,
            // 加载项目入口文件(默认 src/index.js)
            paths.appIndexJs,
          ]
        : paths.appIndexJs,
    // 输出文件设置
    output: {
      // 输出目录(默认是项目的 build 目录)
      path: isEnvProduction ? paths.appBuild : undefined,
        // 开发环境打开模块的 pathinfo 路径提示
      pathinfo: isEnvDevelopment,
        // 输出文 chunk、assets 名称设置
      filename: isEnvProduction
      ...
};

太多了,就不一一分析了,小伙伴自己看一下源码文件哦(对 webpack 不熟的童鞋,强烈推荐我之前写的文章 来和 webpack 谈场恋爱吧:https://www.lanqiao.cn/courses/2893)。

那有小伙伴要问了,既然 React 脚手架帮我们内置了 webpack 的配置,如果我们需要自己修改 webpack 的一些配置该咋办呢?

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

修改输出的目录

从源码中我们可以知道,目前项目的输出文件的目录为 build,比如我们需要改成 dist,我们需要怎么做呢?

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

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

可以看到,当为生产环境(production)的时候,path 的值为 paths.appBuild

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

...
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
...
// 默认输出文件目录路径
const buildPath = process.env.BUILD_PATH || 'build';
module.exports = {
    ...
  appBuild: resolveApp(buildPath),
  ...

可以看到,我们可以通过 process.env.BUILD_PATH 变量去修改输出文件路径。

那么 process.env.BUILD_PATH 我们该怎么定义呢?

  1. 利用 cross-env 库,在执行命令的时候声明 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. 利用脚手架提供的环境变量文件 .env.[NODE_ENV].[local] 来修改,其中 NODE_ENVlocal 可选,表示根据环境来加载。

    我们在 react-demo1 项目根目录底下创建一个 .env 文件,这样不管是 development 模式还是 production 模式,都会加载 .env 文件中声明的变量:

    touch ./.env
    

    然后在 .env 文件中声明 BUILD_PATH 变量为 dist

    ## 修改项目的输出路径
    BUILD_PATH=dist
    

    修改完毕后重新打包测试,效果跟上面的一样,我就不演示了。

总结

这一节我们主要介绍了 React 官方提供的脚手架 create-react-app,我们直接从源码的角度来分析了一个 React 项目创建的过程,其实无非就是对 Webpack 的一些配置而已,所以对 Webpack 不熟悉的小伙伴一定要加油补上哦,从create-react-app 官方文档上看,并没有提供 .env 配置文件的说明、怎么去修改 webpack 配置说明等等,还是需要你自己去看源码的,所以这就是看源码的重要性,其实从源码中我们可以知道,并不是所有的 webpack 配置都能修改的,那项目中我们又需要修改的话,该怎么办呢?那就只能抛弃脚手架了,所以这也算是 React 脚手架的一些不足吧,并没有像 vue-cli 一样,可以随意修改 webpack 的配置。

ok,后面我将会带大家脱离脚手架,利用 webpack0 开始搭建一个 React 项目,大家敬请期待吧!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容