前言
本身平時喜歡一些工具類的小玩意,因此打算搞一個本身玩的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()`
最後也指望有大神,能出個比較全套的講解,讓我也學習一下,哈哈哈哈哈哈哈哈
複製代碼