前言
本身平时喜欢一些工具类的小玩意,因此打算搞一个本身玩的cli!
比较了下仍是vue-cli比较合适,因此本文以vue-cli为模板进行拆分.
网上查阅了大量资料,但资料大部分都是vue-cli2的,简单的看了下,比较简单,就不打算深究了!
本文着重vue-cli3的实现思路
复制代码
依赖工具包
semver // 语义化版本控制规范
fs-extra // 二次封装的fs模块
didyoumean //一个简单的JavaScript匹配引擎
execa // 是能够调用shell和本地外部程序的javascript封装。
commander.js,能够自动的解析命令和参数,用于处理用户输入的命令。富有表现力和强大的命令行框架.
download-git-repo,下载并提取 git 仓库,用于下载项目模板。
Inquirer.js,通用的命令行用户界面集合,用于和用户进行交互。
handlebars.js,模板引擎,将用户提交的信息动态填充到文件中。
ora,下载过程久的话,能够用于显示下载中的动画效果。
chalk,能够给终端的字体加上颜色。
log-symbols,能够在终端上显示出 √ 或 × 等的图标。
loglevel JavaScript的最小轻量级简单日志记录
prompt //一个漂亮的命令行提示符
slash //用于转换 Windows 反斜杠路径转换为正斜杠路径 \ => /
minimist // 轻量级的命令行参数解析引擎
validate-npm-package-name // 验证npm包名
复制代码
正文
// vue.js
...
program
.command('create <app-name>') // app-name 必需输入
.description('create a new project')
.action((name, cmd) => {
// console.log(cmd)
const options = cleanArgs(cmd)
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.red('\n 信息:您提供了多个参数。第一个将用做应用程序的名称,其他的将被忽略。'))
}
// --git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
require('../lib/create')(name, options) // 这里去读取了create文件
})
...
// 解析参数很是重要
program.parse(process.argv)
复制代码
// create.js
async function create (projectName, options) {
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}
// 当前工做目录
const cwd = options.cwd ||process.cwd()
// 是否相等
const inCurrent = projectName === '.'
//
const name = inCurrent ? path.relative('../', cwd) : projectName
// 目标路径
const targetDir = path.resolve(cwd, projectName || '.')
// 获取包名返回结果
// 为true 说明不能注册,反之--
const result = validateProjectName(name)
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
// process.exit(1)
}
// 若是路径存在,进行如下操做
if(fs.existsSync(targetDir)){
// 若是存在 -f 参数 ,把当前路径删除
if(options.force) {
await fs.remove(targetDir)
}else {
...
...
if(!inCurrent) {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `目标目录 ${chalk.cyan(targetDir)} 已经存在,请选择一个选项:`,
choices: [
{ name: '覆盖', value: 'overwrite' },
{ name: '合并', value: 'merge' },
{ name: '取消', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`\n正在删除 ${chalk.cyan(targetDir)}`)
await fs.remove(targetDir)
}
}
}
}
const creator = new Creator(name, targetDir, getPromptModules()) // 这里调用了Creator函数 //
// getPromptModules() 这个函数读取了promptModules这个目录下的每一个模块
await creator.create(options)
}
复制代码
exports.getPromptModules = () => {
return [
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
复制代码
module.exports = class Creator extends EventEmitter {
constructor (name, targetDir, promptModules) {
super()
this.name = name
this.context = process.env.VUE_CLI_CONTEXT = targetDir
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() // 这个函数解析命令行交互(1)
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts() // 一些配置暂时无论
this.injectedPrompts = []
this.promptCompleteCbs = []
this.createCompleteCbs = []
this.run = this.run.bind(this)
const promptAPI = new PromptModuleAPI(this) // 这个函数是一些操做模块的方法(2)
promptModules.forEach(m => m(promptAPI)) // 把promptAPI模块传进去,进行操做(3)
}
async promptAndResolvePreset (answers = null) {
...
...
}
...
...
}
复制代码
- resolveIntroPrompts.js (上面1)
// 解析命令行交互
resolveIntroPrompts () {
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: [], // 这里规则在(3)进行了替换(见 PromptModuleAPI.js)
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
复制代码
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
复制代码
- (上面3 ,在promptModules目录下,这里列举一个Babel.js)
module.exports = cli => {
cli.injectFeature({
name: 'Babel',
value: 'babel',
short: 'Babel',
description: 'Transpile modern JavaScript to older versions (for compatibility)',
link: 'https://babeljs.io/',
checked: true
})
cli.onPromptComplete((answers, options) => {
if (answers.features.includes('ts')) {
if (!answers.useTsWithBabel) {
return
}
} else if (!answers.features.includes('babel')) {
return
}
options.plugins['@vue/cli-plugin-babel'] = {}
})
}
复制代码
// create.js
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options) // 这里调用了Creator类下面的create方法
复制代码
// Creator.js
...
...
async create (cliOptions = {}, preset = null){
const { run, name, context, createCompleteCbs } = this;
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG // 判断是不是测试或debug
if (!preset) {
if (cliOptions.preset) {
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() // 调用全部选项函数 (4)
}
}
// 给preset添加plugins
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
// 这个是vue-cli的共享工具里的函数,添加下载动画
logWithSpinner(`✨`, `建立的项目在 ${chalk.cyan(context)}.`)
this.emit('creation', { event: 'creating' })
// get latest CLI version
const { latest } = await getVersions() // 返回当前用户.vuerc下的版本 || 检查版本 (5)
// semver包
// semver包.major(v):返回主版本号。
// semver包.minor(v):返回次要版本号。
// 这里个人.vuerc目录下版本是3.9.3,这里进行了拼接latestMinor
const latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0`
// generate package.json with plugin dependencies
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {}
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
if (preset.plugins[dep]._isPreset) {
return
}
pkg.devDependencies[dep] = (
preset.plugins[dep].version ||
((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
)
})
// 写入 package.json
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// 在安装开发以来以前,下载git仓库,以便vue- clip -service能够设置git挂钩。
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
logWithSpinner(`🗃`, `Initializing git repository...`)
this.emit('creation', { event: 'git-init' })
await run('git init')
}
// install plugins
stopSpinner()
log(`⚙ Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install' })
if (isTestOrDebug) {
// in development, avoid installation process
await require('./util/setupDevProject')(context)
} else {
// 进入这里,这个函数是下载package依赖的方法 (3)
await installDeps(context, packageManager, cliOptions.registry)
}
// run generator
log(`🚀 Invoking generators...`)
this.emit('creation', { event: 'invoking-generators' })
const plugins = await this.resolvePlugins(preset.plugins)
// ?????? 这个Generator比较复杂暂时没搞懂 ,(脑瓜疼...)
// const generator = new Generator(context, {
// pkg,
// plugins,
// completeCbs: createCompleteCbs
// })
// await generator.generate({
// extractConfigFiles: preset.useConfigFiles
// })
// install additional deps (injected by generators)
log(`📦 Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install' })
log()
if (!isTestOrDebug) {
await installDeps(context, packageManager, cliOptions.registry) // 到这里就是下载新生成的package.json里的依赖
}
// run complete cbs if any (injected by generators)
logWithSpinner('⚓', `Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' })
...
...
}
复制代码
- promptAndResolvePreset() (上面4)
// 调用全部选项
async promptAndResolvePreset (answers = null) {
// prompt
if (!answers) {
answers = await inquirer.prompt(this.resolveFinalPrompts()) 交互选项 (6)
}
let preset
if (answers.preset && answers.preset !== '__manual__') {
preset = await this.resolvePreset(answers.preset)
} else {
// manual
preset = {
useConfigFiles: answers.useConfigFiles === 'files',
plugins: {}
}
answers.features = answers.features || []
// console.log(answers.features)
// run cb registered by prompt modules to finalize the preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
}
// console.log(preset,'preset');
return preset
}
复制代码
...
...
module.exports = async function getVersions () {
if (sessionCached) {
return sessionCached
}
let latest
const local = require(`../../package.json`).version
if (process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG) {
return (sessionCached = {
current: local,
latest: local
})
}
const { latestVersion = local, lastChecked = 0 } = loadOptions()
const cached = latestVersion
const daysPassed = (Date.now() - lastChecked) / (60 * 60 * 1000 * 24)
if (daysPassed > 1) {
// 在继续以前,若是咱们一天没有检查新版本,等待检查
latest = await getAndCacheLatestVersion(cached)
} else {
// Otherwise, do a check in the background. If the result was updated,
// it will be used for the next 24 hours.
getAndCacheLatestVersion(cached)
latest = cached
}
return (sessionCached = {
current: local,
latest
})
}
...
...
复制代码
- resolveFinalPrompts() (上面6)
// 交互全部选项
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
]
return prompts
}
复制代码
结尾
着陆是不可能的了,哈哈,本人技术有限,若有不对的地方,欢迎指出,敬请谅解!!,后续继续补充 `Generator()`
最后也指望有大神,能出个比较全套的讲解,让我也学习一下,哈哈哈哈哈哈哈哈
复制代码