недавно учусьvue-cliИсходный код, пользы много. Чтобы глубже понять себя, я решил сымитировать его и построить колесо, стараясь добиться как можно большего количества оригинальных функций.
Я разделил это колесо на три версии:
- Реализуйте минимальную версию скаффолдинга с минимальным количеством кода.
- Добавьте некоторые вспомогательные функции поверх 1, такие как выбор менеджера пакетов, исходников npm и т. д.
- Реализовать плагин, который можно свободно расширять. Добавьте функциональность, не затрагивая внутренний исходный код.
Некоторые люди могут не понимать, что такое строительные леса. Насколько я понимаю, строительные леса должны помочь вам установить основную полку проекта. Например, зависимости проекта, шаблоны, инструменты сборки и т. д. Позвольте вам максимально быстро заняться развитием бизнеса без необходимости настройки проекта с нуля.
Рекомендуется, чтобы при чтении этой статьи ее можно было использовать вместе с исходным кодом проекта, и эффект был бы лучше. Это адрес проектаmini-cli. Каждая ветка в проекте соответствует версии, например, ветка git, соответствующая первой версии, — v1. Поэтому при чтении исходного кода не забудьте переключиться на соответствующую ветку.
первая версия v1
Функция первой версии относительно проста, примерно:
- Пользователь вводит команду, готовую создать проект.
- Скаффолдинг анализирует пользовательские команды и выводит интерактивные операторы, спрашивающие пользователя, какие функции необходимы для создания проекта.
- Пользователь выбирает нужные ему функции.
- Леса создаются по выбору пользователя
package.json
файл и добавьте соответствующие зависимости. - Скаффолдинг отображает шаблон проекта на основе выбора пользователя, создавая файлы (например,
index.html
,main.js
,App.vue
и др. документы). - воплощать в жизнь
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)
}
}
Логика приведенного выше кода следующая:
- Создайте
creator
объект - передача
getPromptModules()
Получите интерактивные подсказки для всех функций - позвони снова
PromptModuleAPI
Внедрить все интерактивные подсказки вcreator
объект - пройти через
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 模板
Каждый шаблон делает одно и то же:
- К
pkg
Зависимости переменных инъекций - Предоставьте файлы шаблонов
внедрить зависимости
Ниже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')
Вот краткое описание кода процесса впрыска:
- использоватьvue-codemodРазберите код в синтаксическое абстрактное дерево AST.
- Затем вставляемый код становится узлом AST и вставляется в упомянутый выше AST.
- Наконец, перерисуйте новый 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])
})
}
Логика этого кода следующая:
- Пройдитесь по всем визуализированным файлам и сгенерируйте их один за другим.
- При создании файла подтвердите, существует ли его родительский каталог, если нет, сначала создайте родительский каталог.
- Записать в файл.
Например, теперь путь к файлу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:
- При создании проекта оценивается, существует ли он уже, и поддерживается перезапись и создание слияния.
- Конфигурация по умолчанию и ручной выбор предоставляются при выборе функций.
- Если в среде пользователя существуют и yarn, и npm, пользователю будет предложено использовать менеджер пакетов.
- Если исходная скорость npm по умолчанию относительно низкая, пользователю будет предложено переключиться на исходный код Taobao.
- Если пользователь выбирает функцию вручную, пользователю будет задан вопрос, хочет ли он сохранить этот выбор в качестве конфигурации по умолчанию после завершения.
Переписать и объединить
При создании проекта сначала определите, существует ли проект заранее:
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)
}
Логика приведенного выше кода такова:
- Сначала определите файл конфигурации по умолчанию
.mvcrc
ЗдесьuseTaobaoRegistry
опции. Если есть, верните результат напрямую без осуждения. - Отправьте по одному в источник npm по умолчанию и источник Taobao.
get
запрос, черезPromise.race()
звонить. Таким образом, более быстрый запрос будет возвращен первым, чтобы быстрее узнать, является ли источник по умолчанию или источник Taobao быстрее. - Если источник Taobao работает быстрее, предложите пользователю переключиться на источник Taobao.
- Если пользователь выбирает источник 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
Исходный код проекта вернется, чтобы заполнить яму, и ожидается, что она будет завершена в начале марта.
Если вы хотите узнать больше о фронтенд-инжиниринге, вы можете прочитать мою статью«Возьмите вас, чтобы начать работу с фронтенд-инжинирингом». Вот полный текстовый каталог:
- Выбор технологии: как сделать выбор технологии?
- Единообразные нормы: как сформулировать нормы и использовать инструменты для обеспечения их строгого соблюдения?
- Компонентизация интерфейса: что такое модульность и компонентизация?
- Тестирование: как написать тесты подразделения и E2E (сквозные) тесты?
- Инструменты сборки: что такое инструменты сборки? Каковы особенности и преимущества?
- Автоматическое развертывание: как использовать Jenkins, Github Actions для автоматизации развертывания проектов?
- Интерфейсный мониторинг: объясните принцип внешнего мониторинга и как использовать sentry для мониторинга проекта.
- Оптимизация производительности (1): как определить производительность веб-сайта? Каковы некоторые практические правила оптимизации производительности?
- Оптимизация производительности (2): как определить производительность сайта? Каковы некоторые практические правила оптимизации производительности?
- Refactoring: почему вы рутактически? Каковы методы реконструкции?
- Микросервисы: что такое микросервисы? Как построить проект микросервиса?
- Serverless: что такое Serverless и как использовать Serverless?