快来跟我一起学 React(Day2)

Posted vv_小虫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快来跟我一起学 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

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

npm start

可以看到,一个简单的 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

可以看到,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

可以看到,在 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
    

可以看到,打包输出的目录变成了 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 项目,大家敬请期待吧!

欢迎关注我的公众号,每天都有技术文章推送。

以上是关于快来跟我一起学 React(Day2)的主要内容,如果未能解决你的问题,请参考以下文章

快来跟我一起学 React(Day5)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day8)

快来跟我一起学 React(Day7)

快来跟我一起学 React(Day7)