create-react-app 脚本分析

scripts 文件夹

start.js

    "use strict";

    // Do this as the first thing so that any code reading it knows the right env.

    /***** 定义环境变量 ***********************/
    process.env.BABEL_ENV = "development";
    process.env.NODE_ENV = "development";

    /*******************************************************************************************************
     * 使脚本在未处理的拒绝时崩溃,而不是沉默
     * 忽略它们。在未来,未处理的承诺拒绝将会使用非零退出码终止Node.js进程。
     */
    // Makes the script crash on unhandled rejections instead of silently
    // ignoring them. In the future, promise rejections that are not handled will
    // terminate the Node.js process with a non-zero exit code.
    process.on("unhandledRejection", (err) => {
      throw err;
    });

    /*******************************************************************************************************
     * Ensure environment variables are read.
     * 确保环境变量被读取,若无环境变量 process·env·NODE_ENV ,则会抛错*
     */

    // Ensure environment variables are read.
    require("../config/env");

    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 semver = require("semver");
    const paths = require("../config/paths");
    const configFactory = require("../config/webpack.config");
    const createDevServerConfig = require("../config/webpackDevServer.config");
    const getClientEnvironment = require("../config/env");
    const react = require(require.resolve("react", { paths: [paths.appPath] }));

    const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

    /*****判断是否使用yarn,依据是否有 yarn.lock 文件 ***********************/
    const useYarn = fs.existsSync(paths.yarnLockFile);

    /***** 确定  Nodejs  是否在终端上下文中运行的首选方法是检查 process·stdout·isTTY 属性的值是否为true ***********************/
    const isInteractive = process.stdout.isTTY;

    /***** 判断 public - index·html 与  src - index 文件是否存在***********************/
    // Warn and crash if required files are missing
    if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
      process.exit(1);
    }

    /***** 端口号与host ***********************/
    // Tools like Cloud9 rely on this.
    const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
    const HOST = process.env.HOST || "0.0.0.0";

    if (process.env.HOST) {
      console.log(
        chalk.cyan(
          `Attempting to bind to HOST environment variable: ${chalk.yellow(
            chalk.bold(process.env.HOST)
          )}`
        )
      );
      console.log(
        `If this was unintentional, check that you haven't mistakenly set it in your shell.`
      );
      console.log(
        `Learn more here: ${chalk.yellow("https://cra.link/advanced-config")}`
      );
      console.log();
    }

    /*******************************************************************************************************
     * 我们要求你明确地设置浏览器,不要返回到browserslist默认配置
     */
    // We require that you explicitly set browsers and do not fall back to
    // browserslist defaults.
    const { checkBrowsers } = require("react-dev-utils/browsersHelper");

    /*******************************************************************************************************
    * 获取浏览器兼容性配置列表,例如:
    "development": [
          "last 1 chrome version",
          "last 1 firefox version",
          "last 1 safari version"
      ]
    * 
    */

    checkBrowsers(paths.appPath, isInteractive)
      .then(() => {
        /*******************************************************************************************************
         * 我们尝试使用默认端口,但如果它很忙,我们提供给用户
         * 在另一个端口上运行 choosePort() 承诺解析到下一个空闲端口
         * 使用webpack的工具方法choosePort,设置主机与端口号
         */
        // We attempt to use the default port but if it is busy, we offer the user to
        // run on a different port. `choosePort()` Promise resolves to the next free port.
        return choosePort(HOST, DEFAULT_PORT);
      })
      .then((port) => {
        if (port == null) {
          // We have not found a port.
          return;
        }

        /*******************************************************************************************************
         * configFactory:获取 webpack·config·js 开发模式的配置
         * appName:获取 package·json 的app名称
         * useTypeScript:查看文件中是否包含 tsconfig·json 文件
         */
        const config = configFactory("development");
        const protocol = process.env.HTTPS === "true" ? "https" : "http";
        const appName = require(paths.appPackageJson).name;
        const useTypeScript = fs.existsSync(paths.appTsConfig);

        /*******************************************************************************************************
        * urls = {
            lanUrlForConfig:'192.168.20.65'
            lanUrlForTerminal:'http://192.168.20.65:8899'
            localUrlForBrowser:'http://localhost:8899'
            localUrlForTerminal:'http://localhost:8899'
          }
        */
        const urls = prepareUrls(
          protocol,
          HOST,
          port,
          paths.publicUrlOrPath.slice(0, -1)
        );

        /*******************************************************************************************************
         * 创建一个配置了自定义消息的webpack编译器
         */
        // Create a webpack compiler that is configured with custom messages.
        const compiler = createCompiler({
          appName,
          config,
          urls,
          useYarn,
          useTypeScript,
          webpack,
        });

        /*******************************************************************************************************
         * 加载代理配置
         * proxySetting:获取package·json的proxy
         * publicUrlOrPath:优先获取 process·env·PUBLIC_URL,再获取 package·json 的homepage
         */
        // Load proxy config
        const proxySetting = require(paths.appPackageJson).proxy;
        const proxyConfig = prepareProxy(
          proxySetting,
          paths.appPublic,
          paths.publicUrlOrPath
        );

        /*******************************************************************************************************
         * 通过web服务器提供由编译器生成的webpack资源。
         * serverConfig:生成webpack·devServer的服务器配置
         * devServer:通过webpack·devServer的服务器配置与编译器compiler生成一个开发服务器devServer的实例
         */
        // Serve webpack assets generated by the compiler over a web server.
        const serverConfig = {
          ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
          host: HOST,
          port,
        };

        const devServer = new WebpackDevServer(serverConfig, compiler);

        /*******************************************************************************************************
         * 启动webpack·devServer
         */
        // Launch WebpackDevServer.
        devServer.startCallback(() => {
          // 如果在终端,清除终端控制台
          if (isInteractive) {
            clearConsole();
          }

          // react版本大于16.10才能进行快速刷新
          if (env.raw.FAST_REFRESH && semver.lt(react.version, "16.10.0")) {
            console.log(
              chalk.yellow(
                `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
              )
            );
          }

          // 打印启动开发服务器
          console.log(chalk.cyan("Starting the development server...\n"));

          // 打开浏览器进入localUrlForBrowser地址,localUrlForBrowser:'http://localhost:8899'
          openBrowser(urls.localUrlForBrowser);
        });

        /*******************************************************************************************************
         * SIGINT
         * 产生方式: 键盘Ctrl+C
         * 产生结果: 只对当前前台进程,和他的所在的进程组的每个进程都发送SIGINT信号,之后这些进程会执行信号处理程序再终止.
         *
         * SIGTERM
         * 产生方式: 和任何控制字符无关,用kill函数发送
         * 本质: 相当于shell> kill不加-9时 pid.
         * 产生结果: 当前进程会收到信号,而其子进程不会收到.如果当前进程被kill(即收到SIGTERM),则其子进程的父进程将为init,即pid为1的进程.
         */

        ["SIGINT", "SIGTERM"].forEach(function (sig) {
          process.on(sig, function () {
            devServer.close();
            process.exit();
          });
        });

        if (process.env.CI !== "true") {
          // 当stdin结束时优雅的退出
          // Gracefully exit when stdin ends
          process.stdin.on("end", function () {
            devServer.close();
            process.exit();
          });
        }
      })
      .catch((err) => {
        if (err && err.message) {
          console.log(err.message);
        }
        process.exit(1);
      });
Contributors: masecho