create-react-app анализ исходного кода реагирующих скриптов

Node.js внешний интерфейс JavaScript React.js

Ссылка на эту статью:JSON в 1993.GitHub.IO/2018/05/пока горячо…

Предыдущая статьяМы говорили оcreate-react-appсоздать внутриpackage.jsonТакие шаги, как установка зависимостей и копирование работоспособной демоверсии.портал

Давайте поговорим об этомcreate-react-appСлужбы запуска и другие части внутри обычно после установки зависимостей запускают службы разработки:npm start. В этой части слишком много всего о вебпаке и настройке, а в первой статье чувствуется, что описание слишком избыточно~ Так что эта статья не будет вдаваться в подробности, а примерно распишет логическую идею его работы, и конкретный исходный код будут предоставлять порталы.

Всем рекомендую прочитать первую частьИнициализация проектаа такжеотладка точки остановачасть, которая не будет здесь повторяться.Раздел отладки точки останова инициализации проекта портала

Этап подготовки

Версия приложения create-реагировать, которую мы здесь обсуждаем, по-прежнемуv1.1.4

Поскольку эта статья в основном посвященаcreate-react-appВнутри сервиса webpack мы должны сначала создать новый проект.

  • npm install create-react-app -gУстановить приложение create-реагировать глобально
  • create-react-app my-react-projectСоздайте новый проект с помощью приложения create-реагировать
  cd my-react-project
  yarn start

После создания нового терминал предлагает нам напрямую войти в проект и запустить пряжу (npm), чтобы начать разработку. мы открытыpackage.jsonВы можете видеть, что команда, запускаемая запуском пряжи,"react-scripts start"

Итак, что это за команда реагирования-скриптов?

Обычно пишут вpackage.json=> scriptsкоманда, пойдет первымproject_path(项目目录)/node_modules/.binПоиск, не могу найти, а затем найти пакет, установленный глобально.

Так откуда же взялись файлы в node_modules/.bin? Если мы добавим package.json пакетаbinполе, npm автоматически сопоставит его с node_modules/.bin для насПортал документации npm bin

мы открываемся напрямуюnode_modules/react-scripts/package.jsonможно увидеть эту строку"react-scripts": "./bin/react-scripts.js", направить командуnode_modules/react-scripts/.bin/react-scripts.js, что также подтверждает нашу точку зрения.

Помните предыдущий пост, мы вcreate-react-app/packagesНайтиreact-scripts. На самом деле это происходит из того же, поэтому следующие шаги очень понятны, просто используйте старый метод, измените конфигурацию, а затем используйте vscode для запуска отладки точки останова и чтенияproject_path/node_modules/react-scripts/.bin/react-scripts.jsИсходный код .

vscode launch.json

Здесь мы передаем start в качестве параметра для имитации работы в проекте.yarn startЭффект.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "program": "${workspaceFolder}/node_modules/react-scripts/bin/react-scripts.js", //调试的文件路径
      "args": [
        "start" // 传入 start 做为参数
      ]
    }
  ]
}

пс: нижеreact-scriptsЕсли не указано иное, все представляютproject_path/node_modules/react-scriptsКаталоги легко читаются

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

файловый порталЗдесь мы все еще по-старому, не смотрите сначала на зависимости, сначала посмотрите на основной процесс, чтобы понять, мы видим, что этот файл также является файлом входа, очень кратким.

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

Сначала обработайте входящие параметры, используйтеscriptПеременные для получения того, какую команду мы запускаем, есть ['build', 'eject', 'start', 'test'], которые соответствуют командам сборки, конфигурации экспозиции, разработки и тестирования соответственно. Затем передайте вместе другие параметры, напримерnpm testКоманда принимает дополнительный параметр--env=jsdom.

switch (script) {
  case 'build':
  case 'eject':
  case 'start':
  case 'test': {
    // 用 cross-spawn 去跑一个同步的命令
    // 根据传入的命令来拼接对应的路径 用node去跑
    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') {
        // 输出错误提醒日志
      } else if (result.signal === 'SIGTERM') {
        // 输出错误提醒日志
      }
      process.exit(1); // 退出进程, 传1代表有错误
    }
    process.exit(result.status);
    break;
  }
  default:
    // 这里输出匹配不到对应的命令
    break;
}

Затем по полученной команде, соответствующейreact-scripts/scriptsСледующий файл для запуска, напримерreact-scripts startбудет работатьreact-scripts/scripts/start.js.

Вставьте сюда несколько предложений, чтобы рассказать о наиболее распространенных методах разделения библиотек классов в проекте, которые мы можем увидеть здесь.spawnЦитируетсяreact-dev-utils/crossSpawn. пока вreact-dev-utils/corssSpawnВ нем всего несколько простых предложений, вводящихcross-spawnпоставь это сноваcross-spawnнезащищенный. Но запись таким образом может сыграть роль разделения библиотеки классов.Например, в будущем, если в этой библиотеке будут серьезные ошибки или прекратится обслуживание, вы можете напрямую изменить этот файл, чтобы ввести другие библиотеки классов и другой код, который ссылается на этот файл менять не надо.

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

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

module.exports = crossSpawn;

react-scripts/scripts/start.js

файловый портал

Люди, прочитавшие первую статью, должны быть знакомы с этой папкой.create-react-appпосле установкиreactПосле ожидания зависимостей он будет работать в этой папкеinit.jsчтобы скопировать файл шаблона, изменитеpackage.jsonи так далее.

Теперь, когда мы знаем, что он хочет выполнить start.js, давайте изменим отладочный файл vscode на файл start.js."program": "${workspaceFolder}/node_modules/react-scripts/scripts/start.js",Причина модификации в том, что он не ссылается на файл 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;
});

Две переменные окружения установлены в самом начале файла, потому что start используется для запуска разработки, поэтому переменные окружения здесьdevelopment, а затем датьprocessПривяжите функцию прослушивания ошибок, этот прослушиватель ошибок в основном используется для мониторингаНекоторые обещания, которые не являются .catch.Подробнее см. в документации по узлу., про Обещание можно прочитать в статье которую я писал ранееСтатья, знакомящая с Promise, включает в себя как принципы использования, так и принципы реализации.

Затем введите../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')];

env.jsфайл импортируется./paths.jsПосле этого сразу удалить из кеша, чтобы в следующий раз при введении других модулейpaths.js, он не будет получен из кеша, что гарантированоpaths.jsЛогика выполнения внутри будет использовать самые последние переменные среды.

var dotenvFiles = [
  // 举个例子:第一个元素在我的电脑路径是这样的 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);

Затем получите другие переменные среды в соответствии с адресом, заданным путями, здесьpaths.jsРазличные пути будут даны в соответствии с различными ситуациями Мы обсуждаем нормальную ситуацию создания проектов. Несколько других ситуаций:

  1. Запускаем на уже созданном проектеnpm(yarn) eject,В этот моментreact-scriptsвыставит конфигурацию дляproject_path/configНам удобно модифицировать конфигурацию по проекту, эта операция необратима.
  2. Обычно мы создаем проект и запускаем его напрямую, в это время конфигурация сохраняется вproject/node_modules/react-scripts.
  3. Разработчики используют собственную отладку, на этот раз конфигурация хранится вcreate-react/packages/react-scripts/config.

После сборки пути используйтеdotenv-expandа такжеdotenvДля загрузки переменных окружения в файл эта общая сцена не используется.

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]);
      return env;
    }, {}),
  };
  return { raw, stringified };
}

затем вернутьgetClientEnvironmentфункция, которая возвращает переменные окружения клиента после выполнения.

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;

После загрузки различных переменных среды мы возвращаемся кreact-scripts/scripts/start.js, старое правило, ряд зависимостей сначала пропускается, а потом используется. помнишь, мы былиenv.jsУдалите node.catch внутри, здесьconts paths = require('../config/paths)Он не будет взят из кеша, а перезагружен.

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

Сначала оцените, существуют ли наши два входных файла, ониproject_path/public/index.htmlа такжеproject_path/src/index.js, если он не существует, дайте запрос на завершение программы.

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

Затем установите порт и хост по умолчанию. Если у вас есть особые требования, вы можете изменить их в переменной среды. Если нет, будет использоваться порт по умолчанию.3000порт.

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

После установки порта и хоста по умолчанию начинайте судить, занят ли этот порт другими процессами, и если да, то он предоставит следующий доступный порт, следуемchoosePortПерейдите к заголовку файла, чтобы найти зависимость, и обнаружите, что метод находится в зависимостиreact-dev-utils/WebpackDevServerUtils.

function choosePort(host, defaultPort) {
  return detect(defaultPort, host).then(
    port =>
      new Promise(resolve => {
        if (port === defaultPort) {
          return resolve(port);
        }
        const message =
          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 => {
      // 输出错误日志
    }
  );
}

choosePortиспользуется вdetect-port-altЧтобы определить занятость порта, если он занят, он вернет ближайший доступный порт в возрастающем направлении, например, порт 3000 занят, а 3001 будет возвращен, если он не занят. Если обнаруживается, что возвращенный доступный порт не является портом по умолчанию, дается интерактивная команда, чтобы спросить пользователя, следует ли изменить порт для доступа.Интерактивная команда используетinquirerэта сумка. Здесь, если вы используете vsCode для отладки,process.stdout.isTTYВозвращаемое значениеundefined. Поэтому, если вы хотите протестировать эту интерактивную команду, вы можете вернуться только к терминалу системы для отладки~

交互式命令询问是否切换端口
Интерактивная команда с запросом на переключение портов

файловый порталПосле проверки доступных портов вернитесь кstart.js.

Внешний интерфейс обрабатывает множество переменных среды и загружает множество конфигураций, все из которых используются здесь. Здесь главное собрать переменные окружения и конфигурацию, и запустить локальную службу отладки webpack. Главное, что нужно сделать:

  1. Если доступный порт не найден, вернитесь напрямую и не продолжайте.
  2. Определите, следует ли включать или нет на основе переменных средыhttps, по умолчаниюhttp.
  3. Соберите серию URL-адресов в соответствии с хостом, протоколом, портом, включаяBrowserURL сTerminalURL.
  4. передачаcreateCompilerПередайте webpack, конфигурацию webpack, appName, URL-адрес, полученный на третьем шаге, и укажите, следует ли использовать Yarn и другие параметры для создания webpackCompiler. Вещи, за которые отвечает createCompiler: 4.1 Судя по параметрам окружающей среды, есть ли необходимость в дымовом тесте, если есть, добавить одинhandleCompile, прервите программу, как только возникнет ошибка. 4.2 Создайте webpackCompiler с входящей конфигурацией и handleCompile 4.2 ДополнениеinvalidХук, как только измененный файл обнаружен, и это интерактивный терминал, сначала очистите консоль, а затем выведите лог 4.3 ДополнениеdoneКрюк для организации и унификации выходного журнала веб-пакета
  5. Создайте конфигурацию службы разработки. Конкретный код конфигурации помещается вwebpackDevServer.config.js
  6. кинуть 4 и 5 вWebpackDevServer, который создает локальную службу разработки webpack
  7. Готово, очистите экран, откройте отладочное соединение.

Соответствующее выполнение кода написано в комментариях, говорить о каждой конфигурации метода нет возможности... Иначе место будет очень длинным, и многие моменты здесь могут быть точкой знаний.

choosePort(HOST, DEFAULT_PORT)
  .then(port => {
    // 没有找到可用端口,直接return
    if (port == null) {
      return;
    }
    // 根据环境变量判断是否要用https
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;
    // 获取当前的 host, port, protocol 生成一系列url
    const urls = prepareUrls(protocol, HOST, port);
    // 创建一个webpack compiler
    const compiler = createCompiler(webpack, config, appName, urls, useYarn);
    // 加载代理的配置,在 project_path/package.json 里面加载配置
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
    // 生成 webpack dev server 的配置
    const serverConfig = createDevServerConfig(
      proxyConfig,
      urls.lanUrlForConfig
    );
    const devServer = new WebpackDevServer(compiler, serverConfig);
    // 监听 devServer
    devServer.listen(port, HOST, err => {
      // 一些日志输出
      // 自动用默认浏览器打开调试链接
      openBrowser(urls.localUrlForBrowser);
    });
  })
  .catch(err => {
    // 错误处理
  });

react-dev-utils/WebpackDevServerUtils.js

function createCompiler(webpack, config, appName, urls, useYarn) {
  let compiler;
  try {
    compiler = webpack(config, handleCompile); // handleCompile为冒烟测试的对应处理
  } catch (err) {
    // 错误提示
  }

  compiler.plugin('invalid', () => {
    // invalid 钩子,如果当前处于TTY终端,那么先清除控制台再输出 Compiling...
    if (isInteractive) {
      clearConsole();
    }
    console.log('Compiling...');
  });

  let isFirstCompile = true;

  compiler.plugin('done', stats => {
    // 监听了 done 事件,对输出的日志做了格式化输出
    // 正常情况下会直接输出 `Compiled successfully!`
    // 如果有错误则输出错误信息,这里对错误信息做一些处理,让其输出比较友好
  });
  return compiler;
}

输出日志格式化处理
Унифицированное форматирование выходных журналов

Два последних слова

Мне всегда было любопытно, как эти леса очищают экран нашего терминала. смотреть вcreate-react-appкогда я увидел такой файлreact-dev-utils/clearConsole.js. Этот файл очень короткий, а основной код состоит всего из одного предложения:

react-dev-utils/clearConsole.js

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

Тогда мне стало очень любопытно, я не знал, что означают последние две строки, и я продолжал искать и не мог найти ответ, который хотел.

Я спросил своего коллегу и сказал, что оно шестнадцатеричное, но в моем узком познании я всегда думал, что шестнадцатеричное можно преобразовать только в числа.... Но присмотревшись повнимательнее, естьJЯвно не в шестнадцатеричном формате.

Большой парень в женской одежде сказал мне, что это ASCII-код, Baidu посмотрел на ASCII-код и прочитал его.\x1BASCII соответствуетESC. но последний[2J [3J [HЯ до сих пор не знаю, что это значит... Большой парень позже сказал мне, что это может бытьКоды управления Linux ANSIПотребовалось много времени, чтобы найти и бросить, прежде чем тайна была раскрыта~

Эти команды примерно означают:[2Jочистить консоль[Hпереместите курсор в самый верх[3JДо сих пор не нашел, это должна быть более продвинутая клиринговая консоль системного уровня.

Дайте несколько веб-сайтов с информацией об управляющем коде Linux ANSI.Если вам интересно, вы можете узнать об этом самостоятельно в качестве резерва знаний.

Справочная страница Ubuntu: код терминала управления — управляющие последовательности терминала управления Linux

Код терминала управления — Экранирование терминала управления Linux и управляющие последовательности (Turn) — Papaya Brain — Blog Park

В конце концов, многие из фронтенд-приятелей не из таких крупных, как я, и мне действительно нужно больше работать, чтобы компенсировать некоторые базовые или системные знания компьютера ~