当前位置:   article > 正文

实现create-react-app react-scripts中的 start 和 build 命令_react-scripts start

react-scripts start

背景

在之前 实现create-react-app packages 中的 create-react-app 这篇学习记录里头,七七八八实现了 create-react-app 主要流程,再往下就到了 script 命令执行,我们常用的两个命令就是 startbuild 了,这两个命令也有点相似,我们就来看看 “非死不可” 是怎么实现的吧

流程分析

react-scripts/package.json 中注册了一个命令。当我们在终端执行 react-scripts,就会运行这个文件

 "bin": {
    "react-scripts": "./bin/react-scripts.js"
  },
  • 1
  • 2
  • 3

bin/react-scripts.js 的主要实现是这样的,其实也很简单,他相当于一个入口,根据终端提供的命令,直接去找对应的文件执行

#!/usr/bin/env node

// 捕获未处理的异常
process.on('unhandledRejection', err => {
  throw err;
});

const spawn = require('react-dev-utils/crossSpawn');
const args = process.argv.slice(2);

// build,eject,start,test 只有这 4 个指令
const scriptIndex = args.findIndex(
  x => x === 'build' || x === 'eject' || x === 'start' || x === 'test'
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];

if (['build', 'eject', 'start', 'test'].includes(script)) {
  // 开个子进程执行 scripts 文件下对应名称的文件
  // 如: 执行 scripts/build.js
  const result = spawn.sync(
    process.execPath,
    nodeArgs
      .concat(require.resolve('../scripts/' + script))
      .concat(args.slice(scriptIndex + 1)),
    { stdio: 'inherit' }
  );
  // ...省略 SIGKILL 和 SIGTERM 提示,和主要流程无关,暂且不用管他
} else {
  // 未知命令的提示,不用管他
  console.log('Unknown script "' + script + '".');
  console.log('Perhaps you need to update react-scripts?');
  console.log(
    'See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases'
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

流程梳理:注册命令 + 根据命令找相对应的文件执行(一个命令的主要逻辑都封装在一个文件里头)
我们只需要分析对应文件的逻辑就可以了。

start 命令

代码量也不多,主要逻辑就几十行代码,主要得流程就是

  1. 获取配置 configFactory('development')
  2. 创建 compiler const compiler = webpack(config)
  3. 获取 Dev server 配置项 const serverConfig = createDevServerConfig()
  4. 启动服务 const devServer = new WebpackDevServer(serverConfig, compiler);
  5. 最后打开浏览器 openBrowser(urls.localUrlForBrowser);

如果不是很熟悉 webpack 配置的小伙伴,也可以去 react-scripts/config/webpack.config.js 看一下他的 webpack 配置,这里用的是工厂模式,module.exports = function (webpackEnv) {} 根据 webpackEnv 的不用,返回不同的配置信息。稍微整理了一下代码:

process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

// 捕获未处理的异常
process.on('unhandledRejection', err => {
  throw err;
});

const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const { choosePort, createCompiler, prepareProxy, prepareUrls} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');

const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;

// 检查是否有入口文件
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}

const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const { checkBrowsers } = require('react-dev-utils/browsersHelper');

checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    // 有默认端口,如果端口被占用,就用用户提供的端口
    return choosePort(HOST, DEFAULT_PORT);
  })
  .then(port => {
    if (port == null) {
      // We have not found a port.
      return;
    }

    // 有点工厂模式的感觉,development 和 production 生成两个对应的配置
    const config = configFactory('development');
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;

    const useTypeScript = fs.existsSync(paths.appTsConfig);
    const urls = prepareUrls(
      protocol,
      HOST,
      port,
      paths.publicUrlOrPath.slice(0, -1)
    );
    // Create a webpack compiler that is configured with custom messages.
    const compiler = createCompiler({
      appName,
      config,
      urls,
      useYarn,
      useTypeScript,
      webpack,
    });
    // 配置代理服务器
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );
    // 整合配置
    const serverConfig = {
      ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
      host: HOST,
      port,
    };
    // 用 serverConfig 配置 WebpackDevServer 起服务
    const devServer = new WebpackDevServer(serverConfig, compiler);
    devServer.startCallback(() => {
      if (isInteractive) {
        clearConsole();
      }
      // ...版本更新提示
      openBrowser(urls.localUrlForBrowser);
    });
    // ...省略 SIGINT,SIGTERM 处理 和 环境变量处理
  })
  //...省略catch 处理,就是打印错误信息,退出程序
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

build 命令

build 命令和 start 命令有一些相似的地方,我们梳理一下主要的流程:

  1. 获取 webpack 的配置文件
  2. 清空 build 目录
  3. 拷贝 public 下面的静态文件到 build 目录
  4. webpack compiler 直接打包就行了
const path = require('path');
const fs = require('fs-extra');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');

const measureFileSizesBeforeBuild =
  FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);

// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;

const isInteractive = process.stdout.isTTY;

// 同样检查入口
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}

// 获取 webpack 配置
const config = configFactory('production');

// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    return measureFileSizesBeforeBuild(paths.appBuild);
  })
  .then(previousFileSizes => {
    // 清空 build 文件夹
    fs.emptyDirSync(paths.appBuild);
    // 复制 public 文件夹
    copyPublicFolder();
    // 开始打包
    return build(previousFileSizes);
  })
  .then(
    ({ stats, previousFileSizes, warnings }) => {
      // ...省略各种打印信息
      
      // 打印打包后的信息
      printFileSizesAfterBuild(
        stats,
        previousFileSizes,
        paths.appBuild,
        WARN_AFTER_BUNDLE_GZIP_SIZE,
        WARN_AFTER_CHUNK_GZIP_SIZE
      );
      const appPackage = require(paths.appPackageJson);
      const publicUrl = paths.publicUrlOrPath;
      const publicPath = config.output.publicPath;
      const buildFolder = path.relative(process.cwd(), paths.appBuild);
      printHostingInstructions(
        appPackage,
        publicUrl,
        publicPath,
        buildFolder,
        useYarn
      );
    },
    // ...省略错误信息处理
  )

// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
  console.log('Creating an optimized production build...');

  const compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      //...各种 warning 打印,和错误处理
    });
  });
}

function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    // appHtml 有插件 htmlWebpackPlugin 处理
    filter: file => file !== paths.appHtml,
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

总结

只要把 webpack 的配置信息配置好了,其他的就交给 webpackcomplier 打包就行了,更多的可能就是要做交互的信息提示,以及异常处理而已。感觉还是比较简单的,我对后面开发自己的脚手架少了许多迷茫,多了许多信心。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/651750
推荐阅读
相关标签
  

闽ICP备14008679号