This paper links: jsonz1993. Making. IO / 2018/05 / cre…

In create-react-app, we covered the steps of creating package.json installation dependencies and copying a working demo. portal

In create-react-app, NPM start is used to install dependencies on the create-react-app. This piece involves too much about Webpack and configuration things, plus the first feel that the description is too redundant ~ so this article will not be very detailed, just about the logic of his operation to write out, the specific source will provide a portal.

I recommend that you read the section on project initialization and breakpoint debugging in the first article. The portal project initializes the breakpoint debugging section

Preparation stage

The create-React-app version we’re talking about here is still the samev1.1.4

Since this article focuses on the WebPack service in create-React-app, we will definitely need to create a new project first.

  • npm install create-react-app -gInstall create-react-app globally
  • create-react-app my-react-projectCreate a new project with create-react-app
  cd my-react-project
  yarn start
Copy the code

After the creation, the terminal prompts us to directly enter the project, run YARN (NPM) start to develop. If we open package.json, we can see that yarn start is “react-scripts start”.

So which one is this react-scripts command?

Json => scripts will go to project_path(project directory) /node_modules/.bin.

Where does node_modules/.bin come from? If we add the bin field to package package.json, NPM will automatically map us to the NPM bin document portal in node_modules/.bin

If we open node_modules/react-scripts/package.json, we can see “react-scripts”: Node_modules /react-scripts/.bin/react-scripts.js “./bin/react-scripts.

Remember last time we found a react-scripts in create-react-app/ Packages. It’s the same thing, so the next step is pretty clear, just do it the old-fashioned way, change the configuration, Use VScode to run breakpoint debugging and read project_path/node_modules/react-scripts/.bin/react-scripts.

vscode launch.json

Here we pass start as a parameter to simulate the effect of running YARN Start in the project.

{" version ":" 0.2.0, "" configurations: [{" type" : "node", "request" : "launch", "name" : "start program", "the program" : ${workspaceFolder}/node_modules/react-scripts/bin/react-scripts.js", // debug file path "args": ["start" // pass start as argument]}]}Copy the code

Ps: the followingreact-scriptsNo special instructions, all representproject_path/node_modules/react-scriptsTable of contents is easy to read

react-scripts/.bin/react-scripts.js

File portal we’re going to do it the old-fashioned way, so instead of looking at dependencies and looking at the main process understanding, we can see that this file is also an entry file, very brief.

const args = process.argv.slice(2);

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) : [];
Copy the code

We first process the parameters passed in and use the script variable to get which command we are running. There are [‘build’, ‘eject’, ‘start’, ‘test’] for build, expose configuration, development, and test commands. The NPM test command takes an additional argument –env=jsdom.

switch (script) {
  case 'build':
  case 'eject':
  case 'start':
  case 'test': {
    // run a synchronous command with cross-spawn
    // Use node to join the corresponding path according to the incoming command
    const result = spawn.sync(
      'node',
      nodeArgs
        .concat(require.resolve('.. /scripts/' + script))
        .concat(args.slice(scriptIndex + 1)),
      { stdio: 'inherit'});if (result.signal) {
      if (result.signal === 'SIGKILL') {
        // Outputs error notification logs
      } else if (result.signal === 'SIGTERM') {
        // Outputs error notification logs
      }
      process.exit(1); // Exit the process. Pass 1 to indicate an error
    }
    process.exit(result.status);
    break;
  }
  default:
    // The output cannot match the corresponding command
    break;
}
Copy the code

React-scripts /scripts start will run react-scripts/scripts/start.js.

A few words about common library decoupling on a project: spawn refers to react-dev-utils/crossSpawn. In react-dev-utils/corssSpawn, it’s just a few lines introducing cross-spawn and exposing cross-spawn. However, this can be used to decouple the library, if the library is exposed to major bugs or maintenance is stopped, the file can be directly modified to introduce other libraries, other references to the file of the code does not need to change.

// react-dev-utils/corssSpawn
'use strict';

var crossSpawn = require('cross-spawn');

module.exports = crossSpawn;
Copy the code

react-scripts/scripts/start.js

File transfer gate

After installing dependencies like react, create-react-app will run init.js to copy template files and modify package.json.

Now that we know that it will execute start.js, let’s change the vscode debug file to start.js file “program”: ${workspaceFolder}/node_modules/react-scripts/scripts/start.js” / {workspaceFolder}/node_modules/react-scripts/scripts/start.js

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

// 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;
});
Copy the code

The file starts with two environment variables, both of which are development variables because start is used to run development, and then binds an error listener to process, which essentially listens for promises that are not caught. For details, see the Node documentation. For more information on Promises, see the previous article that introduced promises in terms of usage and implementation principles

Then bring in a… /config/env: /config/env: /config/env

react-scripts/config/env.js

const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
Copy the code

The env.js file is removed from the cache immediately after the introduction of./paths.js, so that the next time another module introduces paths.js, it does not fetch it from the cache, ensuring that the execution logic in paths.js uses the latest environment variables.

var dotenvFiles = [
  / /, for example: the first element in the path is my computer so that the Users/jsonz/Documents/my - react - project/env. Development. Local. Js
  `${paths.dotenv}.${NODE_ENV}.local`.`${paths.dotenv}.${NODE_ENV}`, NODE_ENV ! = ='test' && `${paths.dotenv}.local`,
  paths.dotenv,
].filter(Boolean);
Copy the code

Then go to the other environment variables according to the address given by Paths. Here paths.js will give different paths for different cases. We are talking about normal project creation. Other cases include:

  1. We ran in the project we had creatednpm(yarn) ejectThis time,react-scriptsIt exposes the configurationproject_path/configIt is convenient for us to modify the configuration according to the project. This operation is irreversible.
  2. We create the project normally and run the project directly, at which point the configuration is storedproject/node_modules/react-scripts.
  3. Developers debug for their own use, while the configuration is storedcreate-react/packages/react-scripts/config.

After the path is assembled, use dotenv-expand and dotenv to load the environment variables in the file, which is not used in normal scenarios.

function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter(key= > REACT_APP.test(key))
    .reduce(
      (env, key) = > {
        env[key] = process.env[key];
        return env;
      },
      {
        NODE_ENV: process.env.NODE_ENV || 'development'});const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) = > {
      env[key] = JSON.stringify(raw[key]);
      returnenv; }, {})};return { raw, stringified };
}
Copy the code

It then returns a getClientEnvironment function, which returns the client’s environment variables.

react-scripts/scripts/start.js(2)

const fs = require('fs');
const chalk = require('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 config = require('.. /config/webpack.config.dev');
const createDevServerConfig = require('.. /config/webpackDevServer.config');

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

After loading the various environment variables, we go back to react-scripts/scripts/start.js. As usual, a series of dependencies will be skipped and used later. Conts paths = require(‘.. /config/paths) will not be retrieved from the cache but reloaded.

if(! checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { process.exit(1);
}
Copy the code

Check to see if the two entry files project_path/public/index.html and project_path/ SRC /index.js exist. If not, terminate the program.

const DEFAULT_PORT = parseInt(process.env.PORT, 10) | |3000;
const HOST = process.env.HOST || '0.0.0.0';
Copy the code

Then set the default port and host. If there are special requirements, you can change it from the environment variable. If there are no environment variables, the default port 3000 will be used.

choosePort(HOST, DEFAULT_PORT).then(...) // @return Promise
Copy the code

After set the default port and host, began to judge the ports have occupied by other processes, there are words will provide the next available port, we follow the file header choosePort going to find a rely on, to find the method in reliance on the react – dev – utils/WebpackDevServerUtils.

function choosePort(host, defaultPort) {
  return detect(defaultPort, host).then(
    port= >
      new Promise(resolve= > {
        if (port === defaultPort) {
          return resolve(port);
        }
        constmessage = process.platform ! = ='win32' && defaultPort < 1024 && !isRoot()
            ? `Admin permissions are required to run a server on a port below 1024.`
            : `Something is already running on port ${defaultPort}. `;
        if (isInteractive) {
          clearConsole();
          const existingProcess = getProcessForPort(defaultPort);
          const question = {
            type: 'confirm'.name: 'shouldChangePort'.message:
              chalk.yellow(
                message +
                  `${existingProcess ? ` Probably:\n  ${existingProcess}` : ' '}`
              ) + '\n\nWould you like to run the app on another port instead? '.default: true}; inquirer.prompt(question).then(answer= > {
            if (answer.shouldChangePort) {
              resolve(port);
            } else {
              resolve(null); }}); }else {
          console.log(chalk.red(message));
          resolve(null);
        }
      }),
    err => {
      // Outputs error logs}); }Copy the code

ChoosePort uses detect-port-alt to detect port occupancy. If the port is occupied, return a port with the nearest available increment direction, such as port 3000 if occupied and port 3001 if not occupied. If it is found that the available port returned is not the default port, an interactive command is given to ask the user whether to switch ports for access. The interactive command uses the inquirer package. If vsCode is used for debugging, process.stdout.isTTY returns undefined. So if you want to test this piece of interactive command, you have to switch back to the system terminal to debug ~

The interactive command asks whether to switch ports

After the file portal detects the available ports, return to start.js.

The front end handles a bunch of environment variables and loads a bunch of configuration, all for this one. The main thing to do here is to assemble the environment variables and configuration and open a WebPack local debugging service. The main things to do are:

  1. If no available port is found, the execution does not continue
  2. Determine whether to enable the function based on environment variableshttpsThe default ishttp.
  3. Assemble a series of urls according to host, protocol, port, includingBrowserThe url toTerminalThe url.
  4. callcreateCompilerPass in webpack, webpack configuration, appName, the URL obtained in step 3, and whether to use Yarn to generate a Webpack Compiler. CreateCompiler is responsible for the following: 4.1 Determine if there is a smoke test requirement based on environment variables, if there is onehandleCompileInterrupt the program at the first error. 4.2 Added a Webpackage Compiler using the passed configuration and handleCompileinvalidOnce a change file is detected, and it is an interactive terminal, empty the console first, then output log 4.3 addeddoneWebpack hooks for unified output of webpack output logs
  5. The specific configuration code for creating the development service configuration is placed inwebpackDevServer.config.js
  6. I throw the four and the fiveWebpackDevServerGenerate a Webpack local development service
  7. Once you’re done, clear the screen and open the debug connection

The relevant code execution is written in the comments, it is not possible to show every method configuration. Otherwise the length will be very long, which a lot of points can be a knowledge point.

choosePort(HOST, DEFAULT_PORT)
  .then(port= > {
    // Return if no available port is found
    if (port == null) {
      return;
    }
    // Determine whether to use HTTPS based on the environment variable
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;
    // Get the current host, port, protocol to generate a series of urls
    const urls = prepareUrls(protocol, HOST, port);
    // Create a Webpack Compiler
    const compiler = createCompiler(webpack, config, appName, urls, useYarn);
    // Load the proxy configuration from project_path/package.json
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
    // Generate the webPack Dev Server configuration
    const serverConfig = createDevServerConfig(
      proxyConfig,
      urls.lanUrlForConfig
    );
    const devServer = new WebpackDevServer(compiler, serverConfig);
    / / to monitor devServer
    devServer.listen(port, HOST, err => {
      // Some log output
      // Automatically opens the debug link with the default browser
      openBrowser(urls.localUrlForBrowser);
    });
  })
  .catch(err= > {
    // Error handling
  });
Copy the code

react-dev-utils/WebpackDevServerUtils.js

function createCompiler(webpack, config, appName, urls, useYarn) {
  let compiler;
  try {
    compiler = webpack(config, handleCompile); // handleCompile corresponds to the smoke test
  } catch (err) {
    // Error message
  }

  compiler.plugin('invalid', () = > {If you are on a TTY terminal, clear the console and output the Compiling...
    if (isInteractive) {
      clearConsole();
    }
    console.log('Compiling... ');
  });

  let isFirstCompile = true;

  compiler.plugin('done', stats => {
    // Listen for the done event and format the output log
    // 正常情况下会直接输出 `Compiled successfully!`
    // If there is an error, output error message, here to do some processing error message, make it more friendly output
  });
  return compiler;
}
Copy the code

The output logs are formatted in a unified manner

One last word

I’ve been wondering how these scaffoldings clean up our terminal screens. React-dev-utils /clearConsole.js: create-react-app: react-dev-utils/clearConsole.js The file is very short, and the core code is just this:

react-dev-utils/clearConsole.js

process.stdout.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H');
Copy the code

Then my curiosity was particularly heavy, I did not know what the meaning of the two strings was, and I did not find the answer I wanted.

Asked the colleagues around, saying that hexadecimal, and in my narrow cognition has always thought hexadecimal can only be turned into a number…. But if you look at it, there’s a J that’s clearly not hexadecimal.

One of the big ladies told me it was ASCII, googled ASCII and saw \x1B ASCII corresponding to ESC. But the back of [2J [3J [H is what meaning or not clear… the back of the big guy and I said to find may be Linux ANSI control code to find to do STH over and over again after a long time to uncover the mysterious veil ~

[2J clear console [H moves cursor to top [3J still not found, should be more advanced system level clear console

Given several Linux ANSI control code information sites interested in their own knowledge as a reserve

Ubuntu Manpage: Control terminal code – Linux control terminal escape and control sequence

Control terminal code – Linux control terminal escape and control sequence (turn) – Papaya head – Blog garden

Finally, a lot of front-end partners and I are not trained, really need to make up for some computer more rational or more close to the system level of knowledge ~