Научите, как написать строительные леса

внешний интерфейс JavaScript

недавно учусьvue-cliИсходный код, пользы много. Чтобы глубже понять себя, я решил сымитировать его и построить колесо, стараясь добиться как можно большего количества оригинальных функций.

Я разделил это колесо на три версии:

  1. Реализуйте минимальную версию скаффолдинга с минимальным количеством кода.
  2. Добавьте некоторые вспомогательные функции поверх 1, такие как выбор менеджера пакетов, исходников npm и т. д.
  3. Реализовать плагин, который можно свободно расширять. Добавьте функциональность, не затрагивая внутренний исходный код.

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

Рекомендуется, чтобы при чтении этой статьи ее можно было использовать вместе с исходным кодом проекта, и эффект был бы лучше. Это адрес проектаmini-cli. Каждая ветка в проекте соответствует версии, например, ветка git, соответствующая первой версии, — v1. Поэтому при чтении исходного кода не забудьте переключиться на соответствующую ветку.

первая версия v1

Функция первой версии относительно проста, примерно:

  1. Пользователь вводит команду, готовую создать проект.
  2. Скаффолдинг анализирует пользовательские команды и выводит интерактивные операторы, спрашивающие пользователя, какие функции необходимы для создания проекта.
  3. Пользователь выбирает нужные ему функции.
  4. Леса создаются по выбору пользователяpackage.jsonфайл и добавьте соответствующие зависимости.
  5. Скаффолдинг отображает шаблон проекта на основе выбора пользователя, создавая файлы (например,index.html,main.js,App.vueи др. документы).
  6. воплощать в жизньnpm installкоманда для установки зависимостей.

Дерево каталогов проекта:

├─.vscode
├─bin 
│  ├─mvc.js # mvc 全局命令
├─lib
│  ├─generator # 各个功能的模板
│  │  ├─babel # babel 模板
│  │  ├─linter # eslint 模板
│  │  ├─router # vue-router 模板
│  │  ├─vue # vue 模板
│  │  ├─vuex # vuex 模板
│  │  └─webpack # webpack 模板
│  ├─promptModules # 各个模块的交互提示语
│  └─utils # 一系列工具函数
│  ├─create.js # create 命令处理函数
│  ├─Creator.js # 处理交互提示
│  ├─Generator.js # 渲染模板
│  ├─PromptModuleAPI.js # 将各个功能的提示语注入 Creator
└─scripts # commit message 验证脚本 和项目无关 不需关注

Обработка пользовательских команд

Первая функция скаффолдинга — обработка пользовательских команд, что требует использованияcommander.js. Функция этой библиотеки состоит в том, чтобы анализировать команду пользователя, извлекать ввод пользователя и передавать его в леса. Например, этот код:

#!/usr/bin/env node
const program = require('commander')
const create = require('../lib/create')

program
.version('0.1.0')
.command('create <name>')
.description('create a new project')
.action(name => { 
    create(name)
})

program.parse()

Он использует команду для регистрацииcreateкоманда и задает версию и описание скаффолдинга. Я сохраняю этот код в проекте подbinкаталог и названный какmvc.js. затем вpackage.jsonфайл добавьте этот код:

"bin": {
  "mvc": "./bin/mvc.js"
},

повторно выполнитьnpm link, ты сможешьmvcЗарегистрируйтесь как глобальная команда. Его можно использовать в любом месте на компьютереmvcзаказ. На самом деле, используяmvcкоманду вместо выполненияnode ./bin/mvc.js.

Предположим, пользователь вводит в командной строкеmvc create demo(фактически выполняетnode ./bin/mvc.js create demo),commanderРазобрать командуcreateи параметрыdemo. Тогда подмости могут бытьactionПолучить параметры в обратном вызовеname(значение демо).

взаимодействовать с пользователями

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

Например следующий код:

const prompts = [
    {
        "name": "features", // 选项名称
        "message": "Check the features needed for your project:", // 选项提示语
        "pageSize": 10,
        "type": "checkbox", // 选项类型 另外还有 confirm list 等
        "choices": [ // 具体的选项
            {
                "name": "Babel",
                "value": "babel",
                "short": "Babel",
                "description": "Transpile modern JavaScript to older versions (for compatibility)",
                "link": "https://babeljs.io/",
                "checked": true
            },
            {
                "name": "Router",
                "value": "router",
                "description": "Structure the app with dynamic pages",
                "link": "https://router.vuejs.org/"
            },
        ]
    }
]

inquirer.prompt(prompts)

Возникают следующие вопросы и варианты:

тип проблемы"type": "checkbox"даcheckboxОписание с множественным выбором. Если выбраны оба варианта, возвращаемое значение:

{ features: ['babel', 'router'] }

вfeaturesв вопросе вышеnameАтрибуты.featuresЗначения в массиве — это значения в каждом вариантеvalue.

Inquirer.jsТакже могут быть предоставлены соответствующие вопросы, то есть следующий вопрос будет отображаться при выборе указанной опции для предыдущего вопроса. Например следующий код:

{
    name: 'Router',
    value: 'router',
    description: 'Structure the app with dynamic pages',
    link: 'https://router.vuejs.org/',
},
{
    name: 'historyMode',
    when: answers => answers.features.includes('router'),
    type: 'confirm',
    message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
    description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,
    link: 'https://router.vuejs.org/guide/essentials/history-mode.html',
},

Во втором вопросе есть свойствоwhen, значение которого является функциейanswers => answers.features.includes('router'). Когда результат выполнения функцииtrue, отображается второй вопрос. Если вы выбрали в предыдущем вопросеrouter, его результат становитсяtrue. Всплывает второй вопрос: спросить вас, выбран ли режим маршрутизацииhistoryмодель.

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

Какие функции

Давайте посмотрим, какие функции поддерживаются в первой версии:

  • vue
  • vue-router
  • vuex
  • babel
  • webpack
  • linter(eslint)

Поскольку это скаффолдинг, связанный с vue, vue предоставляется по умолчанию и не требует выбора пользователем. Кроме того, веб-пакет инструмента сборки предоставляет среду разработки и функции упаковки, которые также требуются без выбора пользователя. Таким образом, у пользователей есть только 4 функции на выбор:

  • vue-router
  • vuex
  • babel
  • linter

Теперь давайте посмотрим на файлы, связанные с интерактивными подсказками, соответствующими этим 4 функциям. они все положилиlib/promptModulesПод содержанием:

-babel.js
-linter.js
-router.js
-vuex.js

Каждый файл содержит все связанные с ним интерактивные вопросы. Например, пример только что,routerЕсть две взаимосвязанные проблемы. Посмотрите нижеbabel.jsкод:

module.exports = (api) => {
    api.injectFeature({
        name: 'Babel',
        value: 'babel',
        short: 'Babel',
        description: 'Transpile modern JavaScript to older versions (for compatibility)',
        link: 'https://babeljs.io/',
        checked: true,
    })
}

Есть только один вопрос: нужно ли пользователюbabelфункция, по умолчаниюchecked: true, то есть нужно.

проблема с впрыском

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

// craete.js
const creator = new Creator()
// 获取各个模块的交互提示语
const promptModules = getPromptModules()
const promptAPI = new PromptModuleAPI(creator)
promptModules.forEach(m => m(promptAPI))
// 清空控制台
clearConsole()

// 弹出交互提示语并获取用户的选择
const answers = await inquirer.prompt(creator.getFinalPrompts())
    
function getPromptModules() {
    return [
        'babel',
        'router',
        'vuex',
        'linter',
    ].map(file => require(`./promptModules/${file}`))
}

// Creator.js
class Creator {
    constructor() {
        this.featurePrompt = {
            name: 'features',
            message: 'Check the features needed for your project:',
            pageSize: 10,
            type: 'checkbox',
            choices: [],
        }

        this.injectedPrompts = []
    }

    getFinalPrompts() {
        this.injectedPrompts.forEach(prompt => {
            const originalWhen = prompt.when || (() => true)
            prompt.when = answers => originalWhen(answers)
        })
    
        const prompts = [
            this.featurePrompt,
            ...this.injectedPrompts,
        ]
    
        return prompts
    }
}

module.exports = Creator


// PromptModuleAPI.js
module.exports = class PromptModuleAPI {
    constructor(creator) {
        this.creator = creator
    }

    injectFeature(feature) {
        this.creator.featurePrompt.choices.push(feature)
    }

    injectPrompt(prompt) {
        this.creator.injectedPrompts.push(prompt)
    }
}

Логика приведенного выше кода следующая:

  1. Создайтеcreatorобъект
  2. передачаgetPromptModules()Получите интерактивные подсказки для всех функций
  3. позвони сноваPromptModuleAPIВнедрить все интерактивные подсказки вcreatorобъект
  4. пройти черезconst answers = await inquirer.prompt(creator.getFinalPrompts())Откройте интерактивный оператор в консоли и назначьте результат выбора пользователяanswersПеременная.

Если выбраны все функции,answersЗначение:

{
  features: [ 'vue', 'webpack', 'babel', 'router', 'vuex', 'linter' ], // 项目具有的功能
  historyMode: true, // 路由是否使用 history 模式
  eslintConfig: 'airbnb', // esilnt 校验代码的默认规则,可被覆盖
  lintOn: [ 'save' ] // 保存代码时进行校验
}

шаблон проекта

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

// package.json 文件内容
const pkg = {
    name,
    version: '0.1.0',
    dependencies: {},
    devDependencies: {},
}

сначала определитеpkgпеременная для представленияpackage.jsonфайл и установить некоторые значения по умолчанию.

Все шаблоны проектов размещены вlib/generatorПод содержанием:

├─lib
│  ├─generator # 各个功能的模板
│  │  ├─babel # babel 模板
│  │  ├─linter # eslint 模板
│  │  ├─router # vue-router 模板
│  │  ├─vue # vue 模板
│  │  ├─vuex # vuex 模板
│  │  └─webpack # webpack 模板

Каждый шаблон делает одно и то же:

  1. КpkgЗависимости переменных инъекций
  2. Предоставьте файлы шаблонов

внедрить зависимости

НижеbabelСоответствующий код:

module.exports = (generator) => {
    generator.extendPackage({
        babel: {
            presets: ['@babel/preset-env'],
        },
        dependencies: {
            'core-js': '^3.8.3',
        },
        devDependencies: {
            '@babel/core': '^7.12.13',
            '@babel/preset-env': '^7.12.13',
            'babel-loader': '^8.2.2',
        },
    })
}

Как видите, шаблон вызываетgeneratorобъектextendPackage()методpkgвведенная переменнаяbabelВсе связанные зависимости.

extendPackage(fields) {
    const pkg = this.pkg
    for (const key in fields) {
        const value = fields[key]
        const existing = pkg[key]
        if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
            pkg[key] = Object.assign(existing || {}, value)
        } else {
            pkg[key] = value
        }
    }
}

Процесс внедрения зависимостей заключается в обходе всех шаблонов, выбранных пользователем, и вызовеextendPackage()Внедрить зависимости.

шаблон рендеринга

Как строительные леса отображают шаблоны? использоватьvuexНапример, давайте посмотрим на его код:

module.exports = (generator) => {
	// 向入口文件 `src/main.js` 注入代码 import store from './store'
    generator.injectImports(generator.entryFile, `import store from './store'`)
	
    // 向入口文件 `src/main.js` 的 new Vue() 注入选项 store
    generator.injectRootOptions(generator.entryFile, `store`)
	
    // 注入依赖
    generator.extendPackage({
        dependencies: {
            vuex: '^3.6.2',
        },
    })
	
    // 渲染模板
    generator.render('./template', {})
}

Вы можете видеть, что визуализированный кодgenerator.render('./template', {})../templateэто путь к каталогу шаблонов:

Весь код шаблона находится вtemplateПод содержанием,vuexбудет в каталоге, созданном пользователемsrcгенерация каталогаstoreпапка, естьindex.jsдокумент. Его содержание:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    },
})

Вот краткое описаниеgenerator.render()процесс рендеринга.

первый шаг, использоватьglobbyПрочитать все файлы в каталоге шаблона:

const _files = await globby(['**/*'], { cwd: source, dot: true })

второй шаг, перебирает все прочитанные файлы. Если файл является бинарным файлом, он не будет обрабатываться, а файл будет сгенерирован непосредственно во время рендеринга. В противном случае прочитайте содержимое файла, а затем вызовитеejsДля рендеринга:

// 返回文件内容
const template = fs.readFileSync(name, 'utf-8')
return ejs.render(template, data, ejsOptions)

использоватьejsПреимущество заключается в том, что вы можете комбинировать переменные, чтобы решить, следует ли отображать определенный код. НапримерwebpackВ шаблоне есть такой кусок кода:

module: {
      rules: [
          <%_ if (hasBabel) { _%>
          {
              test: /\.js$/,
              loader: 'babel-loader',
              exclude: /node_modules/,
          },
          <%_ } _%>
      ],
  },

ejsможет быть выбран в зависимости от того, является ли пользовательbabelчтобы решить, отображать ли этот код или нет. еслиhasBabelдляfalse, то этот код:

{
    test: /\.js$/,
    loader: 'babel-loader',
    exclude: /node_modules/,
},

не будет рендериться.hasBabelЗначение называетсяrender()При передаче с параметрами:

generator.render('./template', {
    hasBabel: options.features.includes('babel'),
    lintOnSave: options.lintOn.includes('save'),
})

третий шаг, ввести определенный код. Вспомните только чтоvuexсередина:

// 向入口文件 `src/main.js` 注入代码 import store from './store'
generator.injectImports(generator.entryFile, `import store from './store'`)

// 向入口文件 `src/main.js` 的 new Vue() 注入选项 store
generator.injectRootOptions(generator.entryFile, `store`)

Роль этих двух строк кода такова: в файле входа проектаsrc/main.jsввести определенный код.

vuexдаvueБиблиотека управления состоянием дляvueЧлен семейного ковша. Если созданный проект не выбранvuexа такжеvue-router. ноsrc/main.jsКод:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: (h) => h(App),
}).$mount('#app')

если выбраноvuex, он введет две строки кода, упомянутые выше, теперьsrc/main.jsКод становится:

import Vue from 'vue'
import store from './store' // 注入的代码
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  store, // 注入的代码
  render: (h) => h(App),
}).$mount('#app')

Вот краткое описание кода процесса впрыска:

  1. использоватьvue-codemodРазберите код в синтаксическое абстрактное дерево AST.
  2. Затем вставляемый код становится узлом AST и вставляется в упомянутый выше AST.
  3. Наконец, перерисуйте новый AST в код.

извлекатьpackage.jsonНекоторые варианты для

Некоторые элементы конфигурации сторонних библиотек могут быть размещены вpackage.jsonфайл, или вы можете создать файл самостоятельно. Напримерbabelсуществуетpackage.jsonВведенная конфигурация:

babel: {
    presets: ['@babel/preset-env'],
}

мы можем позвонитьgenerator.extractConfigFiles()Извлеките содержимое и сгенерируйтеbabel.config.jsдокумент:

module.exports = {
    presets: ['@babel/preset-env'],
}

makefile

отрендеренный файл шаблона иpackage.jsonФайл в настоящее время все еще находится в памяти и фактически не создается на жестком диске. В этот момент вы можете позвонитьwriteFileTree()Создайте файл:

const fs = require('fs-extra')
const path = require('path')

module.exports = async function writeFileTree(dir, files) {
    Object.keys(files).forEach((name) => {
        const filePath = path.join(dir, name)
        fs.ensureDirSync(path.dirname(filePath))
        fs.writeFileSync(filePath, files[name])
    })
}

Логика этого кода следующая:

  1. Пройдитесь по всем визуализированным файлам и сгенерируйте их один за другим.
  2. При создании файла подтвердите, существует ли его родительский каталог, если нет, сначала создайте родительский каталог.
  3. Записать в файл.

Например, теперь путь к файлуsrc/test.js, при написании в первый раз, так как нетsrcсодержание. Таким образом, он будет сгенерирован первымsrcкаталог, восстановитьtest.jsдокумент.

webpack

Webpack должен предоставлять такие услуги, как горячая загрузка и компиляция в среде разработки, а также услуги по упаковке. В настоящее время код веб-пакета относительно невелик, а функция относительно проста. А в сгенерированном проекте выставлен код конфигурации webpack. Это осталось для версии v3, чтобы улучшить.

Добавить новые функции

Добавление новой функции требует добавления кода в двух местах:lib/promptModulesа такжеlib/generator. существуетlib/promptModulesДобавлена ​​интерактивная подсказка, связанная с этой функцией. существуетlib/generatorДобавлены зависимости и код шаблона, связанные с этой функцией.

Однако не все функции должны добавлять код шаблона, напримерbabelВам не нужно. При добавлении новой функциональности это может повлиять на существующий код шаблона. Например мне нужна поддержка проекта сейчасts. В дополнение к добавлениюtsСвязанные зависимости должны быть вwebpack vue vue-router vuex linterИзмените исходный код шаблона в других функциях.

Например, вvue-router, если поддерживаетсяts, то этот код:

const routes = [ // ... ]

необходимо изменить на:

<%_ if (hasTypeScript) { _%>
const routes: Array<RouteConfig> = [ // ... ]
<%_ } else { _%>
const routes = [ // ... ]
<%_ } _%>

потому чтоtsЗначение имеет тип.

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

Скачать зависимости

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

const execa = require('execa')

module.exports = function executeCommand(command, cwd) {
    return new Promise((resolve, reject) => {
        const child = execa(command, [], {
            cwd,
            stdio: ['inherit', 'pipe', 'inherit'],
        })

        child.stdout.on('data', buffer => {
            process.stdout.write(buffer)
        })

        child.on('close', code => {
            if (code !== 0) {
                reject(new Error(`command failed: ${command}`))
                return
            }

            resolve()
        })
    })
}

// create.js 文件
console.log('\n正在下载依赖...\n')
// 下载依赖
await executeCommand('npm install', path.join(process.cwd(), name))
console.log('\n依赖下载完成! 执行下列命令开始开发:\n')
console.log(`cd ${name}`)
console.log(`npm run dev`)

передачаexecuteCommand()Начать загрузку зависимостей, параметрыnpm installИ путь проекта, созданный пользователями. Чтобы позволить пользователю видеть процесс загрузки зависимостей, нам необходимо использовать следующий код, будет передан на вывод ребенка, который будет передан основным процессом, который выводится в консоль:

child.stdout.on('data', buffer => {
    process.stdout.write(buffer)
})

Ниже я использую движущееся изображение, чтобы продемонстрировать процесс создания версии v1:

Скриншот успешного создания проекта:

Вторая версия v2

Вторая версия добавляет некоторые вспомогательные функции к v1:

  1. При создании проекта оценивается, существует ли он уже, и поддерживается перезапись и создание слияния.
  2. Конфигурация по умолчанию и ручной выбор предоставляются при выборе функций.
  3. Если в среде пользователя существуют и yarn, и npm, пользователю будет предложено использовать менеджер пакетов.
  4. Если исходная скорость npm по умолчанию относительно низкая, пользователю будет предложено переключиться на исходный код Taobao.
  5. Если пользователь выбирает функцию вручную, пользователю будет задан вопрос, хочет ли он сохранить этот выбор в качестве конфигурации по умолчанию после завершения.

Переписать и объединить

При создании проекта сначала определите, существует ли проект заранее:

const targetDir = path.join(process.cwd(), name)
// 如果目标目录已存在,询问是覆盖还是合并
if (fs.existsSync(targetDir)) {
    // 清空控制台
    clearConsole()

    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' },
            ],
        },
    ])

    if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
        await fs.remove(targetDir)
    }
}

Если вы выберетеoverwrite, затем удалитеfs.remove(targetDir).

Конфигурация по умолчанию и ручной режим

Во-первых, заранее в коде пропишите код дефолтной конфигурации:

exports.defaultPreset = {
    features: ['babel', 'linter'],
    historyMode: false,
    eslintConfig: 'airbnb',
    lintOn: ['save'],
}

Эта конфигурация используется по умолчаниюbabelа такжеeslint.

Затем при создании интерактивной подсказки сначала вызовитеgetDefaultPrompts()способ получить конфигурацию по умолчанию.

getDefaultPrompts() {
    const presets = this.getPresets()
    const presetChoices = Object.entries(presets).map(([name, preset]) => {
        let displayName = name

        return {
            name: `${displayName} (${preset.features})`,
            value: name,
        }
    })

    const presetPrompt = {
        name: 'preset',
        type: 'list',
        message: `Please pick a preset:`,
        choices: [
            // 默认配置
            ...presetChoices,
            // 这是手动模式提示语
            {
                name: 'Manually select features',
                value: '__manual__',
            },
        ],
    }

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

    return {
        presetPrompt,
        featurePrompt,
    }
}

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

менеджер пакетов

существуетvue-cliПри создании проекта появляется.vuercфайл, который будет записывать некоторую информацию о конфигурации проекта. Например, какой менеджер пакетов использовать, использует ли исходный код npm исходный код Taobao и т. д. избегать иvue-cliКонфликт, файл конфигурации, сгенерированный этим скаффолдингом,.mvcrc.

это.mvcrcФайл сохраняется в папке пользователяhomeкаталог (разные каталоги операционной системы). Моя операционная система - win10, а каталог для сохраненияC:\Users\bin. получить пользовательскийhomeКаталог можно получить с помощью следующего кода:

const os = require('os')
os.homedir()

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

При создании проекта впервые,.mvcrcфайл не существует. Если в это время у пользователя также установлена ​​пряжа, скаффолдинг подскажет пользователю, какой менеджер пакетов использовать:

// 读取 `.mvcrc` 文件
const savedOptions = loadOptions()
// 如果没有指定包管理器并且存在 yarn
if (!savedOptions.packageManager && hasYarn) {
    const packageManagerChoices = []

    if (hasYarn()) {
        packageManagerChoices.push({
            name: 'Use Yarn',
            value: 'yarn',
            short: 'Yarn',
        })
    }

    packageManagerChoices.push({
        name: 'Use NPM',
        value: 'npm',
        short: 'NPM',
    })

    otherPrompts.push({
        name: 'packageManager',
        type: 'list',
        message: 'Pick the package manager to use when installing dependencies:',
        choices: packageManagerChoices,
    })
}

Когда пользователь выбирает пряжу, команда для загрузки зависимостей становитсяyarn; если выбран npm, команда загрузкиnpm install:

const PACKAGE_MANAGER_CONFIG = {
    npm: {
        install: ['install'],
    },
    yarn: {
        install: [],
    },
}

await executeCommand(
    this.bin, // 'yarn' or 'npm'
    [
        ...PACKAGE_MANAGER_CONFIG[this.bin][command],
        ...(args || []),
    ],
    this.context,
)

переключить источник npm

Когда пользователь выбирает функцию проекта, она сначала вызываетshouldUseTaobao()Способ определения того, необходима необходимость для передачи источника Taobao:

const execa = require('execa')
const chalk = require('chalk')
const request = require('./request')
const { hasYarn } = require('./env')
const inquirer = require('inquirer')
const registries = require('./registries')
const { loadOptions, saveOptions } = require('./options')
  
async function ping(registry) {
    await request.get(`${registry}/vue-cli-version-marker/latest`)
    return registry
}
  
function removeSlash(url) {
    return url.replace(/\/$/, '')
}
  
let checked
let result
  
module.exports = async function shouldUseTaobao(command) {
    if (!command) {
        command = hasYarn() ? 'yarn' : 'npm'
    }
  
    // ensure this only gets called once.
    if (checked) return result
    checked = true
  
    // previously saved preference
    const saved = loadOptions().useTaobaoRegistry
    if (typeof saved === 'boolean') {
        return (result = saved)
    }
  
    const save = val => {
        result = val
        saveOptions({ useTaobaoRegistry: val })
        return val
    }
  
    let userCurrent
    try {
        userCurrent = (await execa(command, ['config', 'get', 'registry'])).stdout
    } catch (registryError) {
        try {
        // Yarn 2 uses `npmRegistryServer` instead of `registry`
            userCurrent = (await execa(command, ['config', 'get', 'npmRegistryServer'])).stdout
        } catch (npmRegistryServerError) {
            return save(false)
        }
    }
  
    const defaultRegistry = registries[command]
    if (removeSlash(userCurrent) !== removeSlash(defaultRegistry)) {
        // user has configured custom registry, respect that
        return save(false)
    }
  
    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)
    }
  
    if (process.env.VUE_CLI_API_MODE) {
        return save(true)
    }
  
    // ask and save preference
    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?`,
            ),
        },
    ])
    
    // 注册淘宝源
    if (useTaobaoRegistry) {
        await execa(command, ['config', 'set', 'registry', registries.taobao])
    }

    return save(useTaobaoRegistry)
}

Логика приведенного выше кода такова:

  1. Сначала определите файл конфигурации по умолчанию.mvcrcЗдесьuseTaobaoRegistryопции. Если есть, верните результат напрямую без осуждения.
  2. Отправьте по одному в источник npm по умолчанию и источник Taobao.getзапрос, черезPromise.race()звонить. Таким образом, более быстрый запрос будет возвращен первым, чтобы быстрее узнать, является ли источник по умолчанию или источник Taobao быстрее.
  3. Если источник Taobao работает быстрее, предложите пользователю переключиться на источник Taobao.
  4. Если пользователь выбирает источник Taobao, то вызываетawait execa(command, ['config', 'set', 'registry', registries.taobao])Измените текущий источник npm на источник Taobao, то естьnpm config set registry https://registry.npm.taobao.org. Если это пряжа, командаyarn config set registry https://registry.npm.taobao.org.

небольшое сомнение

фактическиvue-cliбез этого кода:

// 注册淘宝源
if (useTaobaoRegistry) {
    await execa(command, ['config', 'set', 'registry', registries.taobao])
}

Я добавил это сам. В основном потому, что меня там нетvue-cliНайдите код для явной регистрации исходного кода Taobao в , он просто считывает из файла конфигурации, следует ли использовать исходный код Taobao, или записывает параметр использования исходного кода Taobao в файл конфигурации. Еще один файл конфигурации npm.npmrcМожно изменить источник по умолчанию, если в.npmrcЕсли файл напрямую записывается на зеркальный адрес Taobao, npm будет использовать источник Taobao для загрузки зависимостей. Но npm точно не прочитает.vuercКонфигурация, чтобы решить, использовать ли источник Taobao.

Для этого я не понимаю, поэтому после того, как пользователь выберет источники Taobao, снова вручную вызовите регистр команд.

Сохраните функции проекта к конфигурации по умолчанию

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

Спросите пользователя, следует ли сохранить этот выбор проекта в качестве конфигурации по умолчанию.Если пользователь выберет «да», появится следующее приглашение:

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

Коды, относящиеся к этим двум подсказкам:

const otherPrompts = [
    {
        name: 'save',
        when: isManualMode,
        type: 'confirm',
        message: 'Save this as a preset for future projects?',
        default: false,
    },
    {
        name: 'saveName',
        when: answers => answers.save,
        type: 'input',
        message: 'Save preset as:',
    },
]

Код для сохранения конфигурации:

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 })
}

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

{
  "packageManager": "npm",
  "presets": {
    "test": {
      "features": [
        "babel",
        "linter"
      ],
      "eslintConfig": "airbnb",
      "lintOn": [
        "save"
      ]
    },
    "demo": {
      "features": [
        "babel",
        "linter"
      ],
      "eslintConfig": "airbnb",
      "lintOn": [
        "save"
      ]
    }
  },
  "useTaobaoRegistry": true
}

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

До сих пор было представлено содержимое версии v2.

резюме

из-заvue-cliЯ не читал исходный код плагина, поэтому в этой статье описываются только исходные коды первых двух версий. Версия v3 ждет, пока я закончуvue-cliИсходный код проекта вернется, чтобы заполнить яму, и ожидается, что она будет завершена в начале марта.

Если вы хотите узнать больше о фронтенд-инжиниринге, вы можете прочитать мою статью«Возьмите вас, чтобы начать работу с фронтенд-инжинирингом». Вот полный текстовый каталог:

  1. Выбор технологии: как сделать выбор технологии?
  2. Единообразные нормы: как сформулировать нормы и использовать инструменты для обеспечения их строгого соблюдения?
  3. Компонентизация интерфейса: что такое модульность и компонентизация?
  4. Тестирование: как написать тесты подразделения и E2E (сквозные) тесты?
  5. Инструменты сборки: что такое инструменты сборки? Каковы особенности и преимущества?
  6. Автоматическое развертывание: как использовать Jenkins, Github Actions для автоматизации развертывания проектов?
  7. Интерфейсный мониторинг: объясните принцип внешнего мониторинга и как использовать sentry для мониторинга проекта.
  8. Оптимизация производительности (1): как определить производительность веб-сайта? Каковы некоторые практические правила оптимизации производительности?
  9. Оптимизация производительности (2): как определить производительность сайта? Каковы некоторые практические правила оптимизации производительности?
  10. Refactoring: почему вы рутактически? Каковы методы реконструкции?
  11. Микросервисы: что такое микросервисы? Как построить проект микросервиса?
  12. Serverless: что такое Serverless и как использовать Serverless?

Вторая статья:

использованная литература