Всякий раз, когда вы оказываетесь на стороне большинства людей, пора остановиться и подумать. - Марк Тун
Поскольку эта часть немного сложна, адрес github и адрес объяснения видео публикуются перед объяснением:
Исходный код проекта:GitHub.com/Уокер-Ли Э…
Для видео-объяснения, пожалуйста, выполните поиск в общедоступной учетной записи WeChat.«Полный стек JavaScript»
Я считаю, что каждый имеет следующий опыт работы:
Для разработки нового проекта требуется множество логик, таких как: структура проекта, запрос интерфейса, управление состоянием, интернационализация, скиннинг и т. д., проект уже существует, на данный момент мы выбираем «практический», ctrl + c, ctrl + v Erlian, говори и смейся А пока, когда новый проект будет завершен, останется не что иное, как изменить некоторые файлы и имена пакетов;
При добавлении модуля в проект скопируйте существующий модуль и измените имя, даже если новый модуль создан успешно;
Спецификация проекта должна постоянно упоминаться в ушах коллег, даже если есть документ спецификации, на него все равно нужно обращать внимание.
Использование копипаста имеет следующие недостатки:
Повторяющаяся работа, утомительная и отнимающая много времени
Скопированный шаблон может содержать нерелевантный код
В проекте есть много мест, которые нужно настроить, и некоторые пункты конфигурации легко проигнорировать.
Люди всегда могут ошибаться. При создании нового проекта всегда требуется время, чтобы устранить неполадки.
Фреймворк также будет продолжать итерации.Проекты, созданные вручную, не знают, какой номер последней версии и какие версии зависимостей используются.Легко иметь много ошибок.
Должно быть много студентов, которые испытали вышеперечисленные боли, как решить эти проблемы? Я думаю, что скаффолдинг может избежать многих проблем в работе, потому что скаффолдинг может создавать проекты, определять новые модули, упаковывать, развертывать и т. д. в соответствии с заранее согласованными спецификациями.Из-за стоимости обучения новых сотрудников я рекомендую всем подумайте о строительстве строительных лесов для команды!
Сторонняя библиотека, которую нам нужно использовать для разработки скаффолдинга
Имя библиотеки | описывать |
---|---|
commander | Обработка консольных команд |
chalk | красочная консоль |
semver | Советы по определению версии |
fs-extra | Более дружественная работа fs |
inquirer | консольный запрос |
execa | выполнять команды терминала |
download-git-repo | удаленный репозиторий git |
Ответственность за строительные леса и процесс выполнения
Скаффолдинг может сделать для нас многое, например, создание проекта, добавление модуля проекта, упаковку проекта, унифицированное тестирование проекта, выпуск проекта и т. д. Позвольте мне рассказать вам о самой начальной функции: создании проекта.
На рисунке выше показан общий рабочий процесс создания проекта и создания модуля в проекте.На рисунке ниже более подробно описан процесс создания шаблона на основе шаблона:
Идея очень проста, и далее мы объясним ее подробно на примерах кода.
package.json и запись
Структура проекта показана на рисунке
Укажите в package.json, как ваш пакет запускается через симлинки:bin
Уточните, потому что это пакет package.json, поэтому мы должны обратить внимание на разницу между зависимостями, devDependencies и peerDependencies, я не буду ее здесь раскрывать.
{
"name": "awesome-test-cli",
"version": "1.0.0",
"description": "合一带大家开发脚手架工具",
"main": "index.js",
"bin": {
"awesome-test": "bin/main.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"scaffold",
"efficient",
"react"
],
"author": "walker",
"license": "ISC",
"engines": {
"node": ">=8.9"
},
"dependencies": {
"chalk": "^2.4.2",
"commander": "^3.0.0",
"download-git-repo": "^2.0.0",
"execa": "^2.0.4",
"fs-extra": "^8.1.0",
"import-global": "^0.1.0",
"inquirer": "^6.5.1",
"lru-cache": "^5.1.1",
"minimist": "^1.2.0",
"nunjucks": "^3.2.0",
"ora": "^3.4.0",
"request-promise-native": "^1.0.7",
"semver": "^6.3.0",
"string.prototype.padstart": "^3.0.0",
"valid-filename": "^3.1.0",
"validate-npm-package-name": "^3.0.0"
}
}
Следующая запись/bin/main.js
Входной файл, основная операция - передатьcommander
Обрабатывайте консольные команды и обрабатывайте различную логику в соответствии с разными параметрами.
// 开始处理命令
const program = require('commander')
const minimist = require('minimist')
program
.version(require('../package').version)
.usage('<command> [options]')
// 创建命令
program
.command('create <app-name>')
.description('create a new project')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.action((name, cmd) => {
const options = cleanArgs(cmd)
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n ⚠️ 检测到您输入了多个名称,将以第一个参数为项目名,舍弃后续参数哦'))
}
require('../lib/create')(name, options)
})
создать создать проект
Поместите реальную логику обработки вlib
, так что мы хотим добавить больше команд или операций, чтобы быть более удобными позже. Далее пишемlib/create
Файл, этот файл в основном имеет дело с конфигурацией, такой как определение допустимого имени файла, существует ли файл и т. д., определение правильное, и выполняется логика создания проекта, которую мы помещаем вlib/Creator
обрабатывается в файле.
async function create (projectName, options) {
const cwd = options.cwd || process.cwd()
// 是否在当前目录
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
const result = validatePackageName(name)
// 如果所输入的不是合法npm包名,则退出
if (!result.validForNewPackages) {
console.error(chalk.red(`不合法的项目名: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('❌ ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('⚠️ ' + warn))
})
exit(1)
}
// 检查文件夹是否存在
if (fs.existsSync(targetDir)) {
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 {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `目标文件夹 ${chalk.cyan(targetDir)} 已经存在,请选择:`,
choices: [
{ name: '覆盖', value: 'overwrite' },
{ name: '取消', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
await clearConsole()
// 前面完成准备工作,正式开始创建项目
const creator = new Creator(name, targetDir)
await creator.create(options)
}
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false)
error(err)
})
}
С помощью вышеуказанных операций подготовка перед созданием проекта завершена, а затем создание официально начинается.Операция создания начинается со следующего кода
const creator = new Creator(name, targetDir)
await creator.create(options)
Выносим логику создания в другой файл/lib/Creator
, основные операции, которые мы выполняем в этом файле:
Вытащите удаленный шаблон;
Попросите проект создать соответствующую конфигурацию, такую как: имя проекта, версия проекта, оператор и т. д.;
Скопируйте извлеченный файл шаблона в созданную папку проекта, чтобы создать документ readme;
Зависимости, необходимые для установки проекта;
Создайте репозиторий git и завершите создание проекта.
const chalk = require('chalk')
const execa = require('execa')
const inquirer = require('inquirer')
const EventEmitter = require('events')
const loadRemotePreset = require('../lib/utils/loadRemotePreset')
const writeFileTree = require('../lib/utils/writeFileTree')
const copyFile = require('../lib/utils/copyFile')
const generateReadme = require('../lib/utils/generateReadme')
const {installDeps} = require('../lib/utils/installDeps')
const {
defaults
} = require('../lib/options')
const {
log,
error,
hasYarn,
hasGit,
hasProjectGit,
logWithSpinner,
clearConsole,
stopSpinner,
exit
} = require('../lib/utils/common')
module.exports = class Creator extends EventEmitter {
constructor(name, context) {
super()
this.name = name
this.context = context
this.run = this.run.bind(this)
}
async create(cliOptions = {}, preset = null) {
const { run, name, context } = this
if (cliOptions.preset) {
// awesome-test create foo --preset mobx
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else {
preset = await this.resolvePreset(defaults.presets.default, cliOptions.clone)
}
await clearConsole()
log(chalk.blue.bold(`Awesome-test CLI v${require('../package.json').version}`))
logWithSpinner(`✨`, `正在创建项目 ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating' })
stopSpinner()
// 设置文件名,版本号等
const { pkgVers, pkgDes } = await inquirer.prompt([
{
name: 'pkgVers',
message: `请输入项目版本号`,
default: '1.0.0',
},
{
name: 'pkgDes',
message: `请输入项目简介`,
default: 'project created by awesome-test-cli',
}
])
// 将下载的临时文件拷贝到项目中
const pkgJson = await copyFile(preset.tmpdir, preset.targetDir)
const pkg = Object.assign(pkgJson, {
version: pkgVers,
description: pkgDes
})
// write package.json
log()
logWithSpinner('📄', `生成 ${chalk.yellow('package.json')} 等模板文件`)
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// 包管理
const packageManager = (
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
await writeFileTree(context, {
'README.md': generateReadme(pkg, packageManager)
})
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
logWithSpinner(`🗃`, `初始化Git仓库`)
this.emit('creation', { event: 'git-init' })
await run('git init')
}
// 安装依赖
stopSpinner()
log()
logWithSpinner(`⚙`, `安装依赖`)
// log(`⚙ 安装依赖中,请稍等...`)
await installDeps(context, packageManager, cliOptions.registry)
// commit initial state
let gitCommitFailed = false
if (shouldInitGit) {
await run('git add -A')
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {
await run('git', ['commit', '-m', msg])
} catch (e) {
gitCommitFailed = true
}
}
// log instructions
stopSpinner()
log()
log(`🎉 项目创建成功 ${chalk.yellow(name)}.`)
if (!cliOptions.skipGetStarted) {
log(
`👉 请按如下命令,开始愉快开发吧!\n\n` +
(this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn start' : packageManager === 'pnpm' ? 'pnpm run start' : 'npm start'}`)
)
}
log()
this.emit('creation', { event: 'done' })
if (gitCommitFailed) {
warn(
`因您的git username或email配置不正确,无法为您初始化git commit,\n` +
`请稍后自行git commit。\n`
)
}
}
async resolvePreset (name, clone) {
let preset
logWithSpinner(`Fetching remote preset ${chalk.cyan(name)}...`)
this.emit('creation', { event: 'fetch-remote-preset' })
try {
preset = await loadRemotePreset(name, this.context, clone)
stopSpinner()
} catch (e) {
stopSpinner()
error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
throw e
}
// 默认使用default参数
if (name === 'default' && !preset) {
preset = defaults.presets.default
}
if (!preset) {
error(`preset "${name}" not found.`)
exit(1)
}
return preset
}
run (command, args) {
if (!args) { [command, ...args] = command.split(/\s+/) }
return execa(command, args, { cwd: this.context })
}
shouldInitGit (cliOptions) {
if (!hasGit()) {
return false
}
// --git
if (cliOptions.forceGit) {
return true
}
// --no-git
if (cliOptions.git === false || cliOptions.git === 'false') {
return false
}
// default: true unless already in a git repo
return !hasProjectGit(this.context)
}
}
На этом мы завершили создание проекта, теперь давайте рассмотрим модуль создания проекта.
модуль создания страницы
Вернемся к входному файлу и добавим обработку команды page
// 创建页面命令
program
.command('page <page-name>')
.description('create a new page')
.option('-f, --force', 'Overwrite target directory if it exists')
.action((name, cmd) => {
const options = cleanArgs(cmd)
require('../lib/page')(name, options)
})
Подобно созданию, наша реальная логическая обработка помещается вlib/page
, основной ответственный контент на странице аналогичен созданию, сделайте некоторые приготовления для создания модулей, например, определите, существует ли уже измененный модуль в проекте, и если да, спросите, следует ли перезаписать и другие операции.
const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const PageCreator = require('./PageCreator')
const validFileName = require('valid-filename')
const {error, stopSpinner, exit, clearConsole} = require('../lib/utils/common')
/**
* 创建项目
* @param {*} pageName
* @param {*} options
*/
async function create (pageName, options) {
// 检测文件名是否合规
const result = validFileName(pageName)
// 如果所输入的不是合法npm包名,则退出
if (!result) {
console.error(chalk.red(`不合法的文件名: "${pageName}"`))
exit(1)
}
const cwd = options.cwd || process.cwd()
const pagePath = path.resolve(cwd, './src/pages', (pageName.charAt(0).toUpperCase() + pageName.slice(1).toLowerCase()))
const pkgJsonFile = path.resolve(cwd, 'package.json')
// 如果不存在package.json,说明不再根目录,不能创建
if (!fs.existsSync(pkgJsonFile)) {
console.error(chalk.red(
'\n'+
'⚠️ 请确认您是否在项目根目录下运行此命令\n'
))
return
}
// 如果page已经存在,询问覆盖还是取消
if (fs.existsSync(pagePath)) {
if (options.force) {
await fs.remove(pagePath)
} else {
await clearConsole()
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `已存在 ${chalk.cyan(pageName)} 页面,请选择:`,
choices: [
{name: '覆盖', value: true},
{name: '取消', value: false},
]
}
])
if (!action) {
return
} else {
console.log(`\nRemoving ${chalk.cyan(pagePath)}...`)
await fs.remove(pagePath)
}
}
}
// 前面完成准备工作,正式开始创建页面
const pageCreator = new PageCreator(pageName, pagePath)
await pageCreator.create(options)
}
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false)
error(err)
})
}
После обнаружения выполните логику создания страницы с помощью следующего кода
// 前面完成准备工作,正式开始创建页面
const pageCreator = new PageCreator(pageName, pagePath)
await pageCreator.create(options)
существуетlib/pageCreator
В файле мы генерируем целевой файл, читая предопределенный файл шаблона.Здесь мы используем язык шаблонов - nunjucks, и мы помещаем операцию создания страницы вutils/generatePage
Файл обрабатывается следующим образом:
const chalk = require('chalk')
const path = require('path')
const fs = require('fs-extra')
const nunjucks = require('nunjucks')
const {
log,
error,
logWithSpinner,
stopSpinner,
} = require('./common')
const tempPath = path.resolve(__dirname, '../../temp')
const pageTempPath = path.resolve(tempPath, 'page.js')
const lessTempPath = path.resolve(tempPath, 'page.less')
const ioTempPath = path.resolve(tempPath, 'io.js')
const storeTempPath = path.resolve(tempPath, 'store.js')
async function generatePage(context, {lowerName, upperName}) {
logWithSpinner(`生成 ${chalk.yellow(`${upperName}/${upperName}.js`)}`)
const ioTemp = await fs.readFile(pageTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./${upperName}.js`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateLess(context, {lowerName, upperName}) {
logWithSpinner(`生成 ${chalk.yellow(`${upperName}/${upperName}.less`)}`)
const ioTemp = await fs.readFile(lessTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./${upperName}.less`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateIo(context, {lowerName, upperName}) {
logWithSpinner(`生成 ${chalk.yellow(`${upperName}/io.js`)}`)
const ioTemp = await fs.readFile(ioTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./io.js`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateStore(context, {lowerName, upperName}) {
logWithSpinner(`生成 ${chalk.yellow(`${upperName}/store-${lowerName}.js`)}`)
const ioTemp = await fs.readFile(storeTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./store-${lowerName}.js`), ioContent, {flag: 'a'})
stopSpinner()
}
module.exports = (context, nameObj) => {
Promise.all([
generateIo(context, nameObj),
generatePage(context, nameObj),
generateStore(context, nameObj),
generateLess(context, nameObj)
]).catch(err => {
stopSpinner(false)
error(err)
})
}
Внесите этот файл в PageCreator и запустите его, так будет удобнее дать несколько подсказок.
const chalk = require('chalk')
const EventEmitter = require('events')
const fs = require('fs-extra')
const generatePage = require('./utils/generatePage')
const {
log,
error,
logWithSpinner,
clearConsole,
stopSpinner,
exit
} = require('../lib/utils/common')
module.exports = class PageCreator extends EventEmitter {
constructor(name, context) {
super()
this.name = name
this.context = context
}
async create(cliOptions = {}) {
const fileNameObj = this.getName()
const {context} = this
await clearConsole()
log(chalk.blue.bold(`Awesome-test CLI v${require('../package.json').version}`))
logWithSpinner(`✨`, `正在创建页面...`)
// 创建文件夹
await fs.mkdir(context, { recursive: true })
this.emit('creation', { event: 'creating' })
stopSpinner()
console.log(context)
await generatePage(context, fileNameObj)
}
getName() {
const originName = this.name
const tailName = originName.slice(1)
const upperName = originName.charAt(0).toUpperCase() + tailName
const lowerName = originName.charAt(0).toLowerCase() + tailName
return {
upperName,
lowerName
}
}
}
Ну, вот мы и завершили создание проекта и создание модуля строительных лесов.Я думаю, всем не терпится попробовать.Следуя этой идее, мы можем обогатить функции этих лесов, и мы будем создавать больше и лучшие творения вместе. Иди исследовать!