[Front-end Engineering Foundation — CLI] Как реализован Vue CLI

внешний интерфейс Vue.js
[Front-end Engineering Foundation — CLI] Как реализован Vue CLI

Серия статей [Front-end Engineering Foundation — CLI], постоянно обновляемая:

Обратите внимание на публичный аккаунтпрограммист играет с камерой, читать последние статьи в первый раз.

Оригинальная ссылкаaxuetemple.com/articles/share…Пожалуйста, свяжитесь для перепечатки.


Vue CLI — это полная система для быстрой разработки на основе Vue.js, предоставляющая инструменты командной строки терминала, скаффолдинг с нулевой конфигурацией, систему подключаемых модулей, графический интерфейс управления и т. д. В этой статье анализируются толькоИнициализация проектачасть, то есть реализация инструмента командной строки терминала.

0. Использование

Использование простое, и каждый CLI похож:

npm install -g @vue/cli
vue create vue-cli-test

В настоящее время Vue CLI поддерживает создание проектов Vue 2 и Vue 3 (конфигурация по умолчанию).

Выше приведена конфигурация по умолчанию, предоставляемая Vue CLI для быстрого создания проекта. Кроме того, вы также можете настроить конфигурацию проекта проекта в соответствии с вашими требованиями к проекту (использовать ли Babel, использовать ли TS и т. д.), что будет более гибким.

После завершения выбора нажмите Enter, чтобы начать выполнение таких команд, как установка зависимостей, копирование шаблонов и т. д.

Если вы видите Successfully, инициализация проекта прошла успешно.

vue createКоманда поддерживает некоторые конфигурации параметров, которые можно настроить с помощьюvue create --helpПолучить подробную документацию:

用法:create [options] <app-name>

选项:
  -p, --preset <presetName>       忽略提示符并使用已保存的或远程的预设选项
  -d, --default                   忽略提示符并使用默认预设选项
  -i, --inlinePreset <json>       忽略提示符并使用内联的 JSON 字符串预设选项
  -m, --packageManager <command>  在安装依赖时使用指定的 npm 客户端
  -r, --registry <url>            在安装依赖时使用指定的 npm registry
  -g, --git [message]             强制 / 跳过 git 初始化,并可选的指定初始化提交信息
  -n, --no-git                    跳过 git 初始化
  -f, --force                     覆写目标目录可能存在的配置
  -c, --clone                     使用 git clone 获取远程预设选项
  -x, --proxy                     使用指定的代理创建项目
  -b, --bare                      创建项目时省略默认组件中的新手指导信息
  -h, --help                      输出使用帮助信息

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

1. Входной файл

в этой статьеvue cliВерсия4.5.9. Выход при чтении этой статьиbreak change, вам может понадобиться понять

Согласно нормальной логике, мыpackage.jsonВходной файл находится в:

{
  "bin": {
    "vue": "bin/vue.js"
  }
}

bin/vue.jsКода в нем много, не более чем вvueзарегистрирован наcreate / add / uiи другие команды, в этой статье анализируются толькоcreateраздел, найдите эту часть кода (после удаления кода, не относящегося к основному процессу):

// 检查 node 版本
checkNodeVersion(requiredVersion, '@vue/cli');

// 挂载 create 命令
program.command('create <app-name>').action((name, cmd) => {
  // 获取额外参数
  const options = cleanArgs(cmd);
  // 执行 create 方法
  require('../lib/create')(name, options);
});

cleanArgsэто получитьvue createпроходить позади-Входящие параметры черезvue create --helpМожно получить список параметров для выполнения.

После получения параметров выполните реальныйcreateметод, подождите и осторожно разверните.

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

2. Команда ввода неверна, угадывая намерение пользователя

Одно из самых интересных мест в Vue CLI, если пользователь вводит в терминалеvue creat xxxвместоvue create xxx, что случится? Теоретически должна быть ошибка.

Если это просто ошибка, то я не буду упоминать об этом. Проверьте результаты:

На терминал выводится очень важная строка информацииDid you mean create, интерфейс командной строки Vue, кажется, знает, что пользователь хочет использоватьcreateНо скорость руки слишком высока, и печатается неправильное слово.

Как это делается? Ищем ответ в исходном коде:

const leven = require('leven');

// 如果不是当前已挂载的命令,会猜测用户意图
program.arguments('<command>').action(cmd => {
  suggestCommands(cmd);
});

// 猜测用户意图
function suggestCommands(unknownCommand) {
  const availableCommands = program.commands.map(cmd => cmd._name);

  let suggestion;

  availableCommands.forEach(cmd => {
    const isBestMatch =
      leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand);
    if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
      suggestion = cmd;
    }
  });

  if (suggestion) {
    console.log(`  ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`));
  }
}

используется в кодеlevenЭтот пакет представляет собой реализацию JS для вычисления алгоритма расстояния редактирования строки.Vue CLI использует этот пакет здесь для вычисления команд ввода и примеров редактирования всех в настоящее время смонтированных команд, чтобы угадать, что пользователь на самом деле хочет ввести. команда.

Небольшая, но красивая функция, которая значительно улучшает пользовательский опыт.

3. Проверка версии узла

3.1 Ожидаемая версия узла

а такжеcreate-react-appТочно так же Vue CLI также проверяет, соответствует ли текущая версия Node требованиям:

  • Текущая версия узла:process.version
  • Желаемая версия узла:require("../package.json").engines.node

Например, в настоящее время я использую Node v10.20.1, а версия Node, необходимая для @vue/cli 4.5.9,>=8.9, значит соответствует требованиям.

3.2 Рекомендуемая версия Node LTS

существуетbin/vue.jsВот этот фрагмент кода, который также проверяет версию Node:

const EOL_NODE_MAJORS = ['8.x', '9.x', '11.x', '13.x'];
for (const major of EOL_NODE_MAJORS) {
  if (semver.satisfies(process.version, major)) {
    console.log(
      chalk.red(
        `You are using Node ${process.version}.\n` +
          `Node.js ${major} has already reached end-of-life and will not be supported in future major releases.\n` +
          `It's strongly recommended to use an active LTS version instead.`
      )
    );
  }
}

Может не все понимают его функцию, тут немного научности.

Проще говоря, основные версии Node делятся настранная версияа такжечетная версия. Каждый релиз продлится шесть месяцев, по истечении шести месяцев нечетные релизы станутEOLстатус, а четные версии получают статус Active LTS и долгосрочную поддержку. Поэтому, когда мы используем Node в производственной среде, мы должны попытаться использовать его LTS-версию вместоEOLверсия.

Версия EOL: версия Node с окончанием срока службы. LTS-версия: долгосрочная поддерживаемая версия Node.

Это случай текущей общей версии Node:

Объясните несколько состояний на рисунке:

  • CURRENT: исправит ошибки, добавьте новые функции и постоянно улучшаются
  • ACTIVE: Долгосрочная стабильная версия
  • MAINTENANCE: только исправление ошибок, новые функции добавляться не будут.
  • EOL: Когда индикатор выполнения будет завершен, эта версия больше не будет поддерживаться и поддерживаться.

На изображении выше видно, что Node 8.x был выпущен в 2020 году.EOL, Node 12.x также перейдет в состояние **MAINTENANCE** в 2021 г., а Node 10.x — в апреле и мае 2021 г.EOL.

Текущая версия Node оценивается в Vue CLI.Если вы используете версию EOL, рекомендуется использовать версию LTS. То есть в ближайшее время здесь должен состояться еще один суд.10.x, не спешите отправлять PR (ручную собачью голову) в Vue CLI.

4. Определите, находится ли он в текущем пути

в исполненииvue create, необходимо указатьapp-name, иначе будет сообщено об ошибке:Missing required argument <app-name>.

Что делать, если пользователь создал каталог и хочет создать проект в текущем пустом каталоге? Конечно, Vue CLI также поддерживается, выполнитеvue create .Ничего страшного.

lib/create.jsВ , который имеет дело с этой логикой, есть соответствующий код.

async function create(projectName, options) {
  // 判断传入的 projectName 是否是 .
  const inCurrent = projectName === '.';
  // path.relative 会返回第一个参数到第二个参数的相对路径
  // 这里就是用来获取当前目录的目录名
  const name = inCurrent ? path.relative('../', cwd) : projectName;
  // 最终初始化项目的路径
  const targetDir = path.resolve(cwd, projectName || '.');
}

Если вам нужно реализовать CLI, эту логику можно использовать «из коробки».

5. Проверьте имя приложения

Vue CLI пройдетvalidate-npm-package-nameЭтот пакет проверяет вводprojectNameсоответствовать спецификации.

const result = validateProjectName(name);
if (!result.validForNewPackages) {
  console.error(chalk.red(`Invalid project name: "${name}"`));
  exit(1);
}

соответствующийnpmСоглашение об именах можно найти по адресу:Naming Rules

6. Если целевая папка уже существует, следует ли перезаписать

Этот код относительно прост, судитьtargetСуществует ли каталог, а затем в интерактивном режиме спросите пользователя, следует ли перезаписать (соответствующая операция — удалить исходный каталог):

// 是否 vue create -m
if (fs.existsSync(targetDir) && !options.merge) {
  // 是否 vue create -f
  if (options.force) {
    await fs.remove(targetDir);
  } else {
    await clearConsole();
    // 如果是初始化在当前路径,就只是确认一下是否在当前目录创建
    if (inCurrent) {
      const { ok } = await inquirer.prompt([
        {
          name: 'ok',
          type: 'confirm',
          message: `Generate project in current directory?`,
        },
      ]);
      if (!ok) {
        return;
      }
    } else {
      // 如果有目标目录,则询问如何处理:Overwrite / Merge / Cancel
      const { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: `Target directory ${chalk.cyan(
            targetDir
          )} already exists. Pick an action:`,
          choices: [
            { name: 'Overwrite', value: 'overwrite' },
            { name: 'Merge', value: 'merge' },
            { name: 'Cancel', value: false },
          ],
        },
      ]);
      // 如果选择 Cancel,则直接中止
      // 如果选择 Overwrite,则先删除原目录
      // 如果选择 Merge,不用预处理啥
      if (!action) {
        return;
      } else if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
        await fs.remove(targetDir);
      }
    }
  }
}

7. Общий отлов ошибок

существуетcreateСамый внешний слой метода, положитьcatchметод, который перехватывает все внутренние ошибки и преобразует текущийspinnerСтатус остановлен, выход из процесса.

module.exports = (...args) => {
  return create(...args).catch(err => {
    stopSpinner(false); // do not persist
    error(err);
    if (!process.env.VUE_CLI_TEST) {
      process.exit(1);
    }
  });
};

8. Класс создателя

существуетlib/create.jsВ конце метода выполняются следующие две строки кода:

const creator = new Creator(name, targetDir, getPromptModules());
await creator.create(options);

Кажется, что самый важный код все еще вCreatorв этом классе.

ОткрытьCreator.jsДокументация, приятель, более 500 строк кода и 12 импортированных модулей. Конечно, в этой статье речь пойдет не об этих 500 строках кода и 12 модулях, это не обязательно, если вам интересно, зайдите и посмотрите сами.

Эта статья предназначена для того, чтобы разобраться с основным процессом и некоторыми интересными функциями.

8.1 конструктор конструктор

Первый взглядCreatorКонструктор класса:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    super();

    this.name = name;
    this.context = process.env.VUE_CLI_CONTEXT = context;
    // 获取了 preset 和 feature 的 交互选择列表,在 vue create 的时候提供选择
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
    this.presetPrompt = presetPrompt;
    this.featurePrompt = featurePrompt;

    // 交互选择列表:是否输出一些文件
    this.outroPrompts = this.resolveOutroPrompts();

    this.injectedPrompts = [];
    this.promptCompleteCbs = [];
    this.afterInvokeCbs = [];
    this.afterAnyInvokeCbs = [];

    this.run = this.run.bind(this);

    const promptAPI = new PromptModuleAPI(this);
    // 将默认的一些配置注入到交互列表中
    promptModules.forEach(m => m(promptAPI));
  }
};

Функция конструктора в основном предназначена для инициализации некоторых переменных. Логика здесь в основном заключена вresolveIntroPrompts / resolveOutroPrompts а такжеPromptModuleAPIиз этих методов.

В основном смотреть наPromptModuleAPIЧто делает этот класс.

module.exports = class PromptModuleAPI {
  constructor(creator) {
    this.creator = creator;
  }
  // 在 promptModules 里用
  injectFeature(feature) {
    this.creator.featurePrompt.choices.push(feature);
  }
  // 在 promptModules 里用
  injectPrompt(prompt) {
    this.creator.injectedPrompts.push(prompt);
  }
  // 在 promptModules 里用
  injectOptionForPrompt(name, option) {
    this.creator.injectedPrompts
      .find(f => {
        return f.name === name;
      })
      .choices.push(option);
  }
  // 在 promptModules 里用
  onPromptComplete(cb) {
    this.creator.promptCompleteCbs.push(cb);
  }
};

Здесь мы также кратко скажем,promptModulesВозвращаются все модули, используемые для терминального взаимодействия, которые вызовутinjectFeatureа такжеinjectPromptчтобы вставить конфигурацию взаимодействия, и пройдетonPromptCompleteЗарегистрируйте обратный звонок.

onPromptCompleteФорма обратного звонка для регистрацииpromptCompleteCbsв этом массивеpushУчитывая переданный метод, можно предположить, что обратный вызов должен вызываться после завершения всех взаимодействий в форме:

this.promptCompleteCbs.forEach(cb => cb(answers, preset));

Посмотрите на этот код:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    const promptAPI = new PromptModuleAPI(this);
    promptModules.forEach(m => m(promptAPI));
  }
};

существуетCreatorв конструкторе создает экземплярpromptAPIобъект и пройтиprmptModulesпрошел этот объектpromptModules, описание создаетсяCreatorПри , вся конфигурация, используемая для взаимодействия, будет зарегистрирована.

Здесь мы замечаем, что в конструкторе есть четыреprompt:presetPrompt,featurePrompt,injectedPrompts,outroPrompts, в чем разница? Это подробно описано ниже.

8.2 Модуль событий EventEmitter

первый,CreatorКлассы наследуются от Node.jsEventEmitterДобрый. Как мы все знаем,events— самый важный модуль в Node.js, иEventEmitterКласс является его основой и представляет собой инкапсуляцию запуска и мониторинга событий в Node.js.

это здесь,Creatorунаследовано отEventEmitter, это должно быть для удобстваcreateв процессеemitНекоторые события, отсортированные, в основном следующие 8 событий:

this.emit('creation', { event: 'creating' }); // 创建
this.emit('creation', { event: 'git-init' }); // 初始化 git
this.emit('creation', { event: 'plugins-install' }); // 安装插件
this.emit('creation', { event: 'invoking-generators' }); // 调用 generator
this.emit('creation', { event: 'deps-install' }); // 安装额外的依赖
this.emit('creation', { event: 'completion-hooks' }); // 完成之后的回调
this.emit('creation', { event: 'done' }); // create 流程结束
this.emit('creation', { event: 'fetch-remote-preset' }); // 拉取远程 preset

мы знаем событиеemitдолжно бытьonгде это находится? Я искал исходный код, он находится в пакете @vue/cli-ui, то есть в сценарии инструментов командной строки терминала эти события не будут срабатывать, вот краткое понимание:

const creator = new Creator('', cwd.get(), getPromptModules());
onCreationEvent = ({ event }) => {
  progress.set({ id: PROGRESS_ID, status: event, info: null }, context);
};
creator.on('creation', onCreationEvent);

Проще говоря, поvue uiПри запуске графического интерфейса для инициализации проектаserverСвязь между терминалом и терминалом есть.serverНекоторые события монтируются на стороне клиента, и на каждом этапе создания эти события запускаются из методов в cli.

9. Предустановка (по умолчанию)

CreatorУчебный пример методаcreateпринимает два параметра:

  • cliOptions: параметры, передаваемые в командной строке терминала.
  • preset: Пресеты для Vue CLI

9.1 Что такое предустановка

PresetЧто тогда? официальное объяснениеэто объект JSON, содержащий предопределенные параметры и плагины, необходимые для создания нового проекта без необходимости их выбора пользователем в командной строке.. Например:

{
  "useConfigFiles": true,
  "cssPreprocessor": "sass",
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb",
      "lintOn": ["save", "commit"]
    }
  },
  "configs": {
    "vue": {...},
    "postcss": {...},
    "eslintConfig": {...},
    "jest": {...}
  }
}

В интерфейсе командной строки разрешены как локальные, так и удаленные предустановки.

9.2 prompt

использовалinquirerСлово «подсказка» не должно быть незнакомо моим друзьям.input / checkboxДругой тип — это взаимодействие между пользователем и терминалом.

Давайте оглянемся наCreatorметод вgetPromptModules, Буквально этот метод заключается в том, чтобы получить какие-то модули для взаимодействия, давайте посмотрим:

exports.getPromptModules = () => {
  return [
    'vueVersion',
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e',
  ].map(file => require(`../promptModules/${file}`));
};

Вроде получается серия модулей и возвращается массив. Я просмотрел несколько модулей, перечисленных здесь, и формат кода в основном унифицирован:

module.exports = cli => {
  cli.injectFeature({
    name: '',
    value: '',
    short: '',
    description: '',
    link: '',
    checked: true,
  });

  cli.injectPrompt({
    name: '',
    when: answers => answers.features.includes(''),
    message: '',
    type: 'list',
    choices: [],
    default: '2',
  });

  cli.onPromptComplete((answers, options) => {});
};

смотреть в одиночествеinjectFeatureа такжеinjectPromptЯвляется объектом иinquirerЕсть немного сходства? Да, это всего лишь некоторые параметры конфигурации для взаимодействия с пользователем. В чем разница между Feature и Prompt?

Feature: параметры верхнего уровня для Vue CLI при выборе пользовательской конфигурации:

Prompt: выберите дополнительный параметр, соответствующий определенной функции, например, выборChoose Vue versionЭта функция попросит пользователя выбрать 2.x или 3.x:

onPromptCompleteМетод обратного вызова регистрируется для выполнения после завершения взаимодействия.

Кажется, наша догадка была верна,getPromptModulesСпособ состоит в том, чтобы получить некоторыемодули для взаимодействия с пользователями,Например:

  • babel: Выберите, следует ли использовать Babel
  • cssPreprocessors: Выберите препроцессор для CSS (Sass, Less, Stylus)
  • ...

Давайте сначала поговорим об этом, а затем в главе о загрузке пользовательской конфигурации мы представим все используемые Vue CLI.prompt.

9.3 Получить пресеты

Давайте подробно рассмотрим логику, связанную с получением пресетов. Эта часть кода находится вcreateВ методе экземпляра:

// Creator.js
module.exports = class Creator extends EventEmitter {
  async create(cliOptions = {}, preset = null) {
    const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG;
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this;

    if (!preset) {
      if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone);
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets.default;
      } else if (cliOptions.inlinePreset) {
        // vue create foo --inlinePreset {...}
        try {
          preset = JSON.parse(cliOptions.inlinePreset);
        } catch (e) {
          error(
            `CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`
          );
          exit(1);
        }
      } else {
        preset = await this.promptAndResolvePreset();
      }
    }
  }
};

Как видите, код обрабатывает несколько ситуаций:

  • Параметр cli настраивается с --preset
  • Параметр cli настроен с --default
  • Параметр cli настраивается с --inlinePreset
  • cli не снабжен соответствующими параметрами, он получен по умолчаниюPresetповедение

Первые три случая обсуждаться не будем, давайте рассмотрим четвертый случай, который является взаимодействием по умолчанию.promptполучитьPresetЛогика, то естьpromptAndResolvePresetметод.

Давайте посмотрим, как это выглядит на практике:

Мы можем предположить, что этоconst answers = await inquirer.prompt([])код.

 async promptAndResolvePreset(answers = null) {
    // prompt
    if (!answers) {
      await clearConsole(true);
      answers = await inquirer.prompt(this.resolveFinalPrompts());
    }
    debug("vue-cli:answers")(answers);
 }

 resolveFinalPrompts() {
    this.injectedPrompts.forEach((prompt) => {
      const originalWhen = prompt.when || (() => true);
      prompt.when = (answers) => {
        return isManualMode(answers) && originalWhen(answers);
      };
    });

    const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts,
    ];
    debug("vue-cli:prompts")(prompts);
    return prompts;
 }

Да, мы угадали, будетthis.resolveFinalPromptsвзаимодействовать с конфигурацией в , аthis.resolveFinalPromptsМетод на самом деле дляCreatorте, которые инициализированы в конструктореpromptsвместе. Как было сказано выше, существует четыреprompt, которые будут представлены в следующем разделе. **

9.4 Сохранить предустановку

В конце Vue CLI пользователю будет предложено выбратьsave this as a preset for future?, если пользователь выбираетYes, будет выполнена соответствующая логика для сохранения этого результата взаимодействия. Эта часть логики также находится вpromptAndResolvePresetсередина.

async promptAndResolvePreset(answers = null)  {
  if (
    answers.save &&
    answers.saveName &&
    savePreset(answers.saveName, preset)
  ) {
    log();
    log(
      `🎉  Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(
        rcPath
      )}`
    );
  }
}

вызовsavePresetПресеты будут разобраны, проверены и т.д. до этого, поэтому я не буду их расширять, давайте посмотрим непосредственноsavePresetметод:

exports.saveOptions = toSave => {
  const options = Object.assign(cloneDeep(exports.loadOptions()), toSave);
  for (const key in options) {
    if (!(key in exports.defaults)) {
      delete options[key];
    }
  }
  cachedOptions = options;
  try {
    fs.writeFileSync(rcPath, JSON.stringify(options, null, 2));
    return true;
  } catch (e) {
    error(
      `Error saving preferences: ` +
        `make sure you have write access to ${rcPath}.\n` +
        `(${e.message})`
    );
  }
};

exports.savePreset = (name, preset) => {
  const presets = cloneDeep(exports.loadOptions().presets || {});
  presets[name] = preset;
  return exports.saveOptions({ presets });
};

Код очень прост, сначала сделайте глубокую копию пресета (прямо здесь используется клонирование lodash), а затем введите некоторыеmergeпосле операцииwriteFileSyncВышеупомянутого.vuercфайл.

10. Загрузка пользовательской конфигурации

эти четыреpromptОни соответствуют предустановленным параметрам, выбору пользовательских функций, конкретным параметрам функций и другим параметрам, и между ними существует взаимосвязанная и прогрессивная связь. объединить эти четыреprompt, то есть Vue CLI предоставляет пользователю все взаимодействия, включая загрузку пользовательских конфигураций.

10.1 presetPrompt: предустановленные параметры

То есть какие три варианта видны на начальном скриншоте, выбрать Vue2 или Vue3 или пользовательскийfeature:

если выбраноVue2 илиVue3, затем следуйте дальшеpreset всеpromptпрекратится.

10.2 featurePrompt: параметры пользовательских функций

** если вpresetPromptВыбраноManually, будет продолжать выбиратьfeature:

featurePrompt— это сохраненный список, и соответствующий код выглядит следующим образом:

const isManualMode = answers => answers.preset === '__manual__';

const featurePrompt = {
  name: 'features',
  when: isManualMode,
  type: 'checkbox',
  message: 'Check the features needed for your project:',
  choices: [],
  pageSize: 10,
};

Как видно из кода, вisManualModeЭто взаимодействие появится, когда .

10.3 InjectedPrompts: параметры конкретных функций

featurePromptПросто предоставляет список первого уровня, когда пользователь выбираетVue Version / Babel / TypeScriptПосле ожидания вариантов появится новое взаимодействие, напримерChoose Vue version:

injectedPromptsСохраняется ли список этих конкретных опций, то есть, как указано выше, черезgetPromptModulesметод вpromptModulesТе, что получены из каталогаpromptмодуль:

Соответствующий код можно просмотреть еще раз:

cli.injectPrompt({
  name: 'vueVersion',
  when: answers => answers.features.includes('vueVersion'),
  message: 'Choose a version of Vue.js that you want to start the project with',
  type: 'list',
  choices: [
    {
      name: '2.x',
      value: '2',
    },
    {
      name: '3.x (Preview)',
      value: '3',
    },
  ],
  default: '2',
});

Видно, что вanswers => answers.features.includes('vueVersion'), то есть,featurePromptРезультат взаимодействия if содержитvueVersionПоявятся конкретные вариантыVue Versionвзаимодействие.

10.4 outroPrompts: другие варианты

** Здесь хранится то, что некоторые параметры в дополнение к трем вышеупомянутым типам параметров в настоящее время включают три:

**Где вы предпочитаете размещать конфиг для Babel, ESLint и т.д.?**Как хранить Babel, ESLint и другие конфигурационные файлы?

  • In dedicated config files. хранятся отдельно в соответствующих файлах конфигурации.
  • In package.json. Единое хранилище в package.json.

**Сохранить как пресет для будущих проектов?**Сохранять ли этот пресет для непосредственного использования позже.

Если вы выберете Да, будет другое взаимодействие:Save preset as Введите название пресета.

10.5 Резюме: поток взаимодействия Vue CLI

Вот краткое изложение общего взаимодействия Vue CLI, котороеpromptреализация.

То есть, как было сказано в начале статьи, Vue CLI поддерживает не только конфигурацию по умолчанию, но и пользовательскую конфигурацию (Babel, TS и т.д.), как реализован такой интерактивный процесс.

Vue CLI делит все взаимодействия на четыре широкие категории:

От предустановленных параметров до конкретных параметров функций, они представляют собойслой за слоемотношения, разное время и выбор вызовут разные взаимодействия.

Дизайн структуры кода Vue CLI здесь стоит изучить, и каждое взаимодействие поддерживается в разных модулях через унифицированный.prmoptAPIэкземпляр вCreatorКогда экземпляр инициализируется, он вставляется в другойpromptи зарегистрировать соответствующие функции обратного вызова. Это предназначено дляpromptС точки зрения полной развязки удалить пунктpromptВлияние на контекст незначительно.

Ну вот, базовый разбор предустановки (Preset) и взаимодействия (Prompt) здесь завершен, а остальные подробности раскрывать не буду.

Соответствующие файлы исходного кода, задействованные здесь, вы можете посмотреть сами:

  • Creator.js
  • PromptModuleAPI.js
  • utils/createTools.js
  • promptModules
  • ...

11. Инициализируйте базовый файл проекта

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

11.1 Инициализируйте файл package.json

По параметрам пользователя будут монтироваться соответствующиеvue-cli-plugin, Затем используется для генерацииpackage.jsonЗависимостьdevDependencies,Например@vue/cli-service / @vue/cli-plugin-babel / @vue/cli-plugin-eslint Ждать.

Vue CLI теперь напишет базовыйpackage.json:

{
  "name": "a",
  "version": "0.1.0",
  "private": true,
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0"
  }
}

11.2 Инициализация Git

В соответствии с входящими параметрами и рядом суждений среда Git будет инициализирована в целевой директории, которую просто выполнить.git init:

await run('git init');

Необходимость инициализации среды Git определяется следующим образом:

shouldInitGit(cliOptions) {
  // 如果全局没安装 Git,则不初始化
  if (!hasGit()) {
    return false;
  }
  // 如果 CLI 有传入 --git 参数,则初始化
  if (cliOptions.forceGit) {
    return true;
  }
  // 如果 CLI 有传入 --no-git,则不初始化
  if (cliOptions.git === false || cliOptions.git === "false") {
    return false;
  }
  // 如果当前目录下已经有 Git 环境,就不初始化
  return !hasProjectGit(this.context);
}

11.3 Инициализация README.md

ПроэктREADME.mdБудет генерироваться динамически в соответствии с контекстом, а не жестко запрограммированным документом:

function generateReadme(pkg, packageManager) {
  return [
    `# ${pkg.name}\n`,
    '## Project setup',
    '```',
    `${packageManager} install`,
    '```',
    printScripts(pkg, packageManager),
    '### Customize configuration',
    'See [Configuration Reference](https://cli.vuejs.org/config/).',
    '',
  ].join('\n');
}

Создано Vue CLIREADME.mdбудет информировать пользователя о том, как использовать элемент, за исключениемnpm installКроме того, согласноpackage.json внутреннийscriptsпараметр для динамического создания документации по использованию, например, как разрабатывать, создавать и тестировать:

const descriptions = {
  build: 'Compiles and minifies for production',
  serve: 'Compiles and hot-reloads for development',
  lint: 'Lints and fixes files',
  'test:e2e': 'Run your end-to-end tests',
  'test:unit': 'Run your unit tests',
};

function printScripts(pkg, packageManager) {
  return Object.keys(pkg.scripts || {})
    .map(key => {
      if (!descriptions[key]) return '';
      return [
        `\n### ${descriptions[key]}`,
        '```',
        `${packageManager} ${packageManager !== 'yarn' ? 'run ' : ''}${key}`,
        '```',
        '',
      ].join('\n');
    })
    .join('');
}

Некоторые читатели могут спросить здесь, почему бы просто не скопировать одинREADME.mdГде документы?

  • Во-первых, Vue CLI поддерживает различное управление пакетами, а также разные сценарии установки, запуска и сборки, которые необходимо генерировать динамически;
  • Во-вторых, динамическая генерация более свободна, и соответствующие документы могут формироваться по желанию пользователя, а не каждого.

11.4 Зависит от установки

передачаProjectManageизinstallМетод устанавливает зависимости, и код не сложный:

 async install () {
   if (this.needsNpmInstallFix) {
     // 读取 package.json
     const pkg = resolvePkg(this.context)
     // 安装 dependencies
     if (pkg.dependencies) {
       const deps = Object.entries(pkg.dependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', deps)
     }
     // 安装 devDependencies
     if (pkg.devDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-dev'])
     }
     // 安装 optionalDependencies
     if (pkg.optionalDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-optional'])
     }
     return
   }
   return await this.runCommand('install', this.needsPeerDepsFix ? ['--legacy-peer-deps'] : [])
 }

просто читатьpackage.jsonЗатем установить отдельноnpmразличные зависимости.

这里的逻辑深入进去感觉还是挺复杂的,我也没仔细深入看,就不展开说了。 . .

11.4.1 Автоматическое определение источника NPM

Интересным моментом здесь является источник репозитория npm, используемый для установки зависимостей.Если пользователь не укажет источник установки, Vue CLI автоматически определит, следует ли использовать источник установки Taobao NPM., и угадайте как?

function shouldUseTaobao() {
  let faster
  try {
    faster = await Promise.race([
      ping(defaultRegistry),
      ping(registries.taobao)
    ])
  } catch (e) {
    return save(false)
  }

  if (faster !== registries.taobao) {
    // default is already faster
    return save(false)
  }

  const { useTaobaoRegistry } = await inquirer.prompt([
    {
      name: 'useTaobaoRegistry',
      type: 'confirm',
      message: chalk.yellow(
        ` Your connection to the default ${command} registry seems to be slow.\n` +
          `   Use ${chalk.cyan(registries.taobao)} for faster installation?`
      )
    }
  ])
  return save(useTaobaoRegistry);
}

Vue CLI пройдетPromise.raceзапроситьИсточник установки по умолчаниюа такжеИсточник установки Таобао: **

  • Если сначала будет возвращен источник установки Taobao, пользователю будет предложено подтвердить, следует ли использовать источник установки Taobao.
  • Если сначала возвращается источник установки по умолчанию, он будет использоваться напрямую.

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

12. Генератор генерирует код

КромеCreatorКроме того, вторым по важности классом всего Vue CLI являетсяGenerator, отвечающий за генерацию кода проекта, давайте посмотрим, что у нас получилось.

12.1 Инициализация плагина

существуетgenerateметод, первым выполняетсяinitPluginsметод, код выглядит следующим образом:

async initPlugins () {
  for (const id of this.allPluginIds) {
    const api = new GeneratorAPI(id, this, {}, rootOptions)
    const pluginGenerator = loadModule(`${id}/generator`, this.context)

    if (pluginGenerator && pluginGenerator.hooks) {
      await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
    }
  }
}

здесь даст каждомуpackage.jsonПлагин в файле инициализируетGeneratorAPIЭкземпляр, передайте экземпляр в соответствующий плагинgeneratorметод и выполнить, например.@vue/cli-plugin-babel/generator.js.

12.2 Класс GeneratorAPI

Vue CLI использует архитектуру на основе плагинов. Если вы проверите package.json только что созданного проекта, вы обнаружите, что все зависимости начинаются с @vue/cli-plugin-. Плагины могут изменять внутреннюю конфигурацию веб-пакета, а также могут вводить команды в vue-cli-service. При создании проекта большинство перечисленных возможностей реализуются через плагины.

Только что упомянутый, он будет идти к каждому подключаемому модулюgeneratorвходящийGeneratorAPI, чтобы увидеть, что предоставляет этот класс.

12.2.1 Пример: @vue/cli-plugin-babel

Чтобы быть менее абстрактным, возьмем сначала@vue/cli-plugin-babelПосмотрите, этот плагин относительно прост:

module.exports = api => {
  delete api.generator.files['babel.config.js'];

  api.extendPackage({
    babel: {
      presets: ['@vue/cli-plugin-babel/preset'],
    },
    dependencies: {
      'core-js': '^3.6.5',
    },
  });
};

здесьapiэто одинGeneratorAPIПример, вотextendPackageметод:

// GeneratorAPI.js
// 删减部分代码,只针对 @vue/cli-plugin-babel 分析
extendPackage (fields, options = {}) {
  const pkg = this.generator.pkg
  const toMerge = isFunction(fields) ? fields(pkg) : fields
  // 遍历传入的参数,这里是 babel 和 dependencies 两个对象
  for (const key in toMerge) {
    const value = toMerge[key]
    const existing = pkg[key]
    // 如果 key 的名称是 dependencies 和 devDependencies
    // 就通过 mergeDeps 方法往 package.json 合并依赖
    if (isObject(value) && (key === 'dependencies' || key === 'devDependencies')) {
      pkg[key] = mergeDeps(
        this.id,
        existing || {},
        value,
        this.generator.depSources,
        extendOptions
      )
    } else if (!extendOptions.merge || !(key in pkg)) {
      pkg[key] = value
    }
  }
}

В это время по умолчаниюpackage.jsonстановится:

{
  "babel": {
    "presets": ["@vue/cli-plugin-babel/preset"]
  },
  "dependencies": {
    "core-js": "^3.6.5"
  },
  "devDependencies": {},
  "name": "test",
  "private": true,
  "version": "0.1.0"
}

Прочитав этот пример, дляGeneratorAPIВозможно, у вас есть некоторое представление о том, что делает экземпляр ', поэтому давайте подробно рассмотрим экземпляр этого класса.

12.2.2 Важные методы экземпляра

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

  • extendPackage: расширить конфигурацию package.json
  • render: визуализировать файлы шаблонов через ejs
  • onCreateComplete: Обратный вызов после записи регистрационного файла на жесткий диск
  • genJSConfig: вывод json-файла в js-файл
  • injectImports: добавить импорт в файл
  • ...

12.3 @vue/cli-service

Как уже видел@vue/cli-plugin-babelПлагин, это немного похоже на архитектуру плагинов для Vue CLI? Я также узнал более важноеGeneratorAPIКласс, некоторые функции в плагине для изменения конфигурации являются методами экземпляра этого класса.

Далее, давайте посмотрим на более важный плагин@vue/cli-service, этот плагин является основным плагином для Vue CLI, иcreate react app изreact-scriptsТочно так же с помощью этого плагина мы сможем глубже понятьGeneratorAPIИ как реализована архитектура плагинов Vue CLI.

посмотри@vue/cli-serviceпод этим пакетомgenerator/index.jsфайл, здесь для удобства анализа исходный код разобран на несколько сегментов, собственно он и вызывается отдельноGeneratorAPIРазличные методы экземпляра:

12.3.1 Шаблоны визуализации

api.render('./template', {
  doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
});

БудуtemplateФайлы в каталоге передаются черезrenderВоспроизведите на память, вотejsкак механизм рендеринга шаблонов.

12.3.2 Написание package.json

пройти черезextendPackageпрошлоеpacakge.jsonнаписать вVueСоответствующие зависимости:

if (options.vueVersion === '3') {
  api.extendPackage({
    dependencies: {
      vue: '^3.0.0',
    },
    devDependencies: {
      '@vue/compiler-sfc': '^3.0.0',
    },
  });
} else {
  api.extendPackage({
    dependencies: {
      vue: '^2.6.11',
    },
    devDependencies: {
      'vue-template-compiler': '^2.6.11',
    },
  });
}

пройти черезextendPackageпрошлоеpacakge.jsonнаписать вscripts:

api.extendPackage({
  scripts: {
    serve: 'vue-cli-service serve',
    build: 'vue-cli-service build',
  },
  browserslist: ['> 1%', 'last 2 versions', 'not dead'],
});

пройти черезextendPackageпрошлоеpacakge.jsonнаписать вCSSПараметры предварительной обработки:

if (options.cssPreprocessor) {
  const deps = {
    sass: {
      sass: '^1.26.5',
      'sass-loader': '^8.0.2',
    },
    'node-sass': {
      'node-sass': '^4.12.0',
      'sass-loader': '^8.0.2',
    },
    'dart-sass': {
      sass: '^1.26.5',
      'sass-loader': '^8.0.2',
    },
    less: {
      less: '^3.0.4',
      'less-loader': '^5.0.0',
    },
    stylus: {
      stylus: '^0.54.7',
      'stylus-loader': '^3.0.2',
    },
  };

  api.extendPackage({
    devDependencies: deps[options.cssPreprocessor],
  });
}

12.3.3 Вызов подключаемого модуля маршрутизатора и подключаемого модуля vuex

// for v3 compatibility
if (options.router && !api.hasPlugin('router')) {
  require('./router')(api, options, options);
}

// for v3 compatibility
if (options.vuex && !api.hasPlugin('vuex')) {
  require('./vuex')(api, options, options);
}

Не очень ли просто пройтиGeneratorAPIПредоставленные методы экземпляра могут быть очень удобными для изменения и настройки проекта в подключаемом модуле.

13. Извлеките отдельные файлы конфигурации

упоминалось выше, черезextendPackageвернитесьpackage.jsonНапишите какую-нибудь конфигурацию в . Однако существует также упомянутое выше взаимодействие, котороеWhere do you prefer placing config for Babel, ESLint, etc.?То есть конфигурация извлекается в отдельный файл.generate внутреннийextractConfigFilesМетод выполняет эту логику.

extractConfigFiles(extractAll, checkExisting) {
  const configTransforms = Object.assign(
    {},
    defaultConfigTransforms,
    this.configTransforms,
    reservedConfigTransforms
  );
  const extract = (key) => {
    if (
      configTransforms[key] &&
      this.pkg[key] &&
      !this.originalPkg[key]
    ) {
      const value = this.pkg[key];
      const configTransform = configTransforms[key];
      const res = configTransform.transform(
        value,
        checkExisting,
        this.files,
        this.context
      );
      const { content, filename } = res;
      this.files[filename] = ensureEOL(content);
      delete this.pkg[key];
    }
  };
  if (extractAll) {
    for (const key in this.pkg) {
      extract(key);
    }
  } else {
    extract("babel");
  }
}

здесьconfigTransformsВот некоторые конфигурации, которые необходимо извлечь:

еслиextractAll Даtrue, то есть, если в приведенном выше взаимодействии выбрано Да,package.jsonвсе вkey configTransformsСравните и, если оба существуют, извлеките конфигурацию в отдельный файл.

14. Вывод файла из памяти на жесткий диск

упомянутый выше,api.renderФайл шаблона будет преобразован EJS в строку и помещен в память. казненgenerateВедь по логике уже есть различные файлы, которые нужно вывести в память, поместить вthis.files внутри.generateПоследний шаг – позвонитьwriteFileTreeПишет все файлы в память на жесткий диск.

сюдаgenerateЛогика в основном закончена, и часть генерации кода Vue CLI также закончена.

15. Резюме

В целом код Vue CLI по-прежнему относительно сложен, а общая структура относительно ясна, но больше всего впечатляют два момента:

Во-первых, монтирование общего интерактивного процесса. Логика взаимодействия каждого модуля поддерживается экземпляром класса, а время выполнения и успешный обратный вызов также хорошо продуманы.

Во-вторых, механизм плагина очень важен.插件机制将功能和脚手架进行解耦。

Кажется, что будь то приложение create-react-app или Vue CLI, они будут стараться учитывать их при разработке.Механизм плагина, чтобы открыть возможности, а затем интегрировать функции, будь то основные функции самого Vue CLI или для разработчиков сообщества, у него достаточно открытости и масштабируемости.

Глядя на общий код, наиболее важными являются две концепции:

  • Preset: предустановка, включая общий процесс взаимодействия (подсказка).
  • Plugin: Плагины, общая система плагинов

Вокруг этих двух понятий, этих классов в коде:Creator,PromptModuleAPI,Generator,GeneratorAPIявляется ядром.

Кратко о процессе:

  1. воплощать в жизньvue create
  2. инициализацияCreatorпримерcreator, смонтировать всю интерактивную конфигурацию
  3. передачаcreatorметод экземпляраcreate
  4. Запросить пользовательскую конфигурацию
  5. инициализацияGeneratorпримерgenerator
  6. Инициализировать различные плагины
  7. Запустите плагинgeneratorлогика, пишиpackage.json, шаблоны рендеринга и т. д.
  8. записать файл на жесткий диск

Жизненный цикл такого CLI завершен, и проект инициализирован.

Приложение: Инструменты и методы, которые можно использовать непосредственно в Vue CLI

Прочитав исходный код Vue CLI, я не только пожаловался на этот сложный дизайн, но и обнаружил множество инструментов и методов, которые можно использовать «из коробки», когда мы реализуем собственный CLI, кратко изложенный здесь.

Получить параметры интерфейса командной строки

Разобрать CLI через--входящие параметры.

const program = require('commander');

function camelize(str) {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''));
}

function cleanArgs(cmd) {
  const args = {};
  cmd.options.forEach(o => {
    const key = camelize(o.long.replace(/^--/, ''));
    // if an option is not present and Command has a method with the same name
    // it should not be copied
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key];
    }
  });
  return args;
}

Проверить версию узла

пройти черезsemver.satisfiesСравните две версии Node:

  • process.version: версия узла текущей среды выполнения.
  • wanted: package.jsonВерсия узла настроена в
const requiredVersion = require('../package.json').engines.node;

function checkNodeVersion(wanted, id) {
  if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
    console.log(
      chalk.red(
        'You are using Node ' +
          process.version +
          ', but this version of ' +
          id +
          ' requires Node ' +
          wanted +
          '.\nPlease upgrade your Node version.'
      )
    );
    process.exit(1);
  }
}

checkNodeVersion(requiredVersion, '@vue/cli');

читать package.json

const fs = require('fs');
const path = require('path');

function getPackageJson(cwd) {
  const packagePath = path.join(cwd, 'package.json');

  let packageJson;
  try {
    packageJson = fs.readFileSync(packagePath, 'utf-8');
  } catch (err) {
    throw new Error(`The package.json file at '${packagePath}' does not exist`);
  }

  try {
    packageJson = JSON.parse(packageJson);
  } catch (err) {
    throw new Error('The package.json is malformed');
  }

  return packageJson;
}

сортировка объектов

Основная цель здесь — сначала отсортировать объекты вывода при выводе package.json, что более красиво. .

module.exports = function sortObject(obj, keyOrder, dontSortByUnicode) {
  if (!obj) return;
  const res = {};

  if (keyOrder) {
    keyOrder.forEach(key => {
      if (obj.hasOwnProperty(key)) {
        res[key] = obj[key];
        delete obj[key];
      }
    });
  }

  const keys = Object.keys(obj);

  !dontSortByUnicode && keys.sort();
  keys.forEach(key => {
    res[key] = obj[key];
  });

  return res;
};

вывод файла на жесткий диск

Это на самом деле ничего, это три шага:

  • fs.unlink удалить файл
  • fs.ensureDirSync создает каталог
  • fs.writeFileSync записывает файл
const fs = require('fs-extra');
const path = require('path');

// 删除已经存在的文件
function deleteRemovedFiles(directory, newFiles, previousFiles) {
  // get all files that are not in the new filesystem and are still existing
  const filesToDelete = Object.keys(previousFiles).filter(
    filename => !newFiles[filename]
  );

  // delete each of these files
  return Promise.all(
    filesToDelete.map(filename => {
      return fs.unlink(path.join(directory, filename));
    })
  );
}

// 输出文件到硬盘
module.exports = async function writeFileTree(dir, files, previousFiles) {
  if (previousFiles) {
    await deleteRemovedFiles(dir, files, previousFiles);
  }
  // 主要就是这里
  Object.keys(files).forEach(name => {
    const filePath = path.join(dir, name);
    fs.ensureDirSync(path.dirname(filePath));
    fs.writeFileSync(filePath, files[name]);
  });
};

Определите, инициализировал ли проект git

На самом деле он выполняется в каталогеgit statusПосмотрите, не сообщается ли об ошибке.

const hasProjectGit = cwd => {
  let result;
  try {
    execSync('git status', { stdio: 'ignore', cwd });
    result = true;
  } catch (e) {
    result = false;
  }
  return result;
};

метод получения объекта

Вы можете использовать lodash, теперь вы можете использовать a?.b?.c напрямую

function get(target, path) {
  const fields = path.split('.');
  let obj = target;
  const l = fields.length;
  for (let i = 0; i < l - 1; i++) {
    const key = fields[i];
    if (!obj[key]) {
      return undefined;
    }
    obj = obj[key];
  }
  return obj[fields[l - 1]];
}

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